mirror of
https://github.com/anchore/syft.git
synced 2026-10-24 12:02:22 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8cb0bd80 | ||
|
|
8535ee5b2e | ||
|
|
a689fb1213 | ||
|
|
4903b4d73f | ||
|
|
7de5e1288f | ||
|
|
474ed682bc | ||
|
|
b7979dbc7d | ||
|
|
cc20a8f341 | ||
|
|
006ba9b557 | ||
|
|
a27907659d | ||
|
|
727b84ce0d | ||
|
|
85ac5bcbf8 | ||
|
|
3d8c16649d | ||
|
|
ecbfbe6896 | ||
|
|
d3804d1a82 | ||
|
|
3a10ca7e6a | ||
|
|
f98868b55e | ||
|
|
ab9fe53ff2 | ||
|
|
da0b17b719 | ||
|
|
c7752f0f6c | ||
|
|
7a359dc16b | ||
|
|
7957294b26 | ||
|
|
5374a1dc6f |
@@ -338,7 +338,7 @@ hash_sha256_verify() {
|
||||
return 1
|
||||
fi
|
||||
BASENAME=${TARGET##*/}
|
||||
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
want=$(grep "${BASENAME}\$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
if [ -z "$want" ]; then
|
||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||
return 1
|
||||
|
||||
@@ -100,7 +100,8 @@ bootstrap-tools: $(TEMPDIR)
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
|
||||
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v1.2.2
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
|
||||
|
||||
### Join our community meetings!
|
||||
|
||||
- Calendar: https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t
|
||||
- Agenda: https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing (join [this group](https://groups.google.com/g/anchore-oss-community) for write access)
|
||||
- All are welcome!
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
@@ -87,6 +93,23 @@ file:path/to/yourproject/file read directly from a path on disk (any si
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
### Excluding file paths
|
||||
|
||||
Syft can exclude files and paths from being scanned within a source by using glob expressions
|
||||
with one or more `--exclude` parameters:
|
||||
```
|
||||
syft <source> --exclude './out/**/*.json' --exclude /etc
|
||||
```
|
||||
**Note:** in the case of _image scanning_, since the entire filesystem is scanned it is
|
||||
possible to use absolute paths like `/etc` or `/usr/**/*.txt` whereas _directory scans_
|
||||
exclude files _relative to the specified directory_. For example: scanning `/usr/foo` with
|
||||
`--exclude ./package.json` would exclude `/usr/foo/package.json` and `--exclude '**/package.json'`
|
||||
would exclude all `package.json` files under `/usr/foo`. For _directory scans_,
|
||||
it is required to begin path expressions with `./`, `*/`, or `**/`, all of which
|
||||
will be resolved _relative to the specified scan directory_. Keep in mind, your shell
|
||||
may attempt to expand wildcards, so put those parameters in single quotes, like:
|
||||
`'**/*.json'`.
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
@@ -98,7 +121,8 @@ syft packages <image> -o <format>
|
||||
Where the `formats` available are:
|
||||
- `json`: Use this to get as much information out of Syft as possible!
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.3 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.3 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `table`: A columnar summary (default).
|
||||
@@ -212,6 +236,12 @@ file: ""
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# exclude:
|
||||
# - '/etc/**'
|
||||
# - './out/**/*.json'
|
||||
exclude:
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
package:
|
||||
cataloger:
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
func checkForApplicationUpdate() {
|
||||
if appConfig.CheckForAppUpdate {
|
||||
log.Debugf("checking if new vesion of %s is available", internal.ApplicationName)
|
||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
||||
if err != nil {
|
||||
// this should never stop the application
|
||||
|
||||
+36
-2
@@ -1,17 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
@@ -28,6 +30,8 @@ func init() {
|
||||
initAppConfig,
|
||||
initLogging,
|
||||
logAppConfig,
|
||||
checkForApplicationUpdate,
|
||||
logAppVersion,
|
||||
initEventBus,
|
||||
)
|
||||
}
|
||||
@@ -110,3 +114,33 @@ func initEventBus() {
|
||||
stereoscope.SetBus(eventBus)
|
||||
syft.SetBus(eventBus)
|
||||
}
|
||||
|
||||
func logAppVersion() {
|
||||
versionInfo := version.FromBuild()
|
||||
log.Infof("syft version: %s", versionInfo.Version)
|
||||
|
||||
var fields map[string]interface{}
|
||||
bytes, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(bytes, &fields)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(fields))
|
||||
for k := range fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for idx, field := range keys {
|
||||
value := fields[field]
|
||||
branch := "├──"
|
||||
if idx == len(fields)-1 {
|
||||
branch = "└──"
|
||||
}
|
||||
log.Debugf(" %s %s: %s", branch, field, value)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-4
@@ -132,6 +132,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
"include dockerfile for upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.StringArrayP(
|
||||
"exclude", "", nil,
|
||||
"exclude paths from being scanned using a glob expression",
|
||||
)
|
||||
|
||||
flags.Bool(
|
||||
"overwrite-existing-image", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
@@ -158,6 +163,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
@@ -251,11 +260,9 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
}
|
||||
if cleanup != nil {
|
||||
|
||||
+1
-3
@@ -111,9 +111,7 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ module github.com/anchore/syft
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.4.0
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
@@ -12,7 +13,7 @@ require (
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89
|
||||
github.com/anchore/stereoscope v0.0.0-20211222141827-6e663afeef5d
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
|
||||
@@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.4.0 h1:Wz4QZ9B4RXGWIWTypVLEOVJgOdFfy5mcS5PGNzUkZxU=
|
||||
github.com/CycloneDX/cyclonedx-go v0.4.0/go.mod h1:rmRcf//gT7PIzovatusbWi377xqCg1FS4jyST0GH20E=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@@ -109,8 +111,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89 h1:pcMFN7wjIQIxdZm8NA0R3JiRRFn5Eyx9mhagHOVS4Bw=
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89/go.mod h1:FNm1rtauEjkC3elA3jr3bJkU+/4QiovApwJdPnHQ9x0=
|
||||
github.com/anchore/stereoscope v0.0.0-20211222141827-6e663afeef5d h1:ggdC5SRb8rAXmBkwxah8gfSxwpoAVcpp12tQ+XbDi20=
|
||||
github.com/anchore/stereoscope v0.0.0-20211222141827-6e663afeef5d/go.mod h1:FNm1rtauEjkC3elA3jr3bJkU+/4QiovApwJdPnHQ9x0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
@@ -138,6 +140,8 @@ github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQm
|
||||
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
|
||||
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
|
||||
+6
-4
@@ -1,7 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Code generated by godownloader on 2020-08-10T20:55:46Z.
|
||||
#
|
||||
|
||||
usage() {
|
||||
this=$1
|
||||
@@ -23,7 +21,7 @@ EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
#BINDIR is ./bin unless set be ENV
|
||||
# BINDIR is ./bin unless set be ENV
|
||||
# over-ridden by flag below
|
||||
|
||||
BINDIR=${BINDIR:-./bin}
|
||||
@@ -67,6 +65,7 @@ execute() {
|
||||
}
|
||||
get_binaries() {
|
||||
case "$PLATFORM" in
|
||||
darwin/arm64) BINARIES="syft" ;;
|
||||
darwin/amd64) BINARIES="syft" ;;
|
||||
linux/amd64) BINARIES="syft" ;;
|
||||
windows/amd64) BINARIES="syft" ;;
|
||||
@@ -97,6 +96,9 @@ adjust_format() {
|
||||
darwin) FORMAT=dmg ;;
|
||||
windows) FORMAT=zip ;;
|
||||
esac
|
||||
case "${PLATFORM}" in
|
||||
darwin/arm64) FORMAT=zip ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
adjust_os() {
|
||||
@@ -244,7 +246,7 @@ extract_from_dmg() {
|
||||
dmg_file=$1
|
||||
mount_point="/Volumes/tmp-dmg"
|
||||
hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}"
|
||||
cp -fR "${mount_point}/" ./
|
||||
cp -fR "${mount_point}/." ./
|
||||
hdiutil detach -quiet -force "${mount_point}"
|
||||
}
|
||||
http_download_curl() {
|
||||
|
||||
@@ -44,6 +44,7 @@ type Application struct {
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
|
||||
@@ -6,5 +6,5 @@ const (
|
||||
|
||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "2.0.0"
|
||||
JSONSchemaVersion = "2.0.2"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
cdxBOM := cyclonedx.NewBOM()
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
// NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid):
|
||||
// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
|
||||
// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
cdxBOM.SerialNumber = uuid.New().URN()
|
||||
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source)
|
||||
|
||||
packages := s.Artifacts.PackageCatalog.Sorted()
|
||||
components := make([]cyclonedx.Component, len(packages))
|
||||
for i, p := range packages {
|
||||
components[i] = toComponent(p)
|
||||
}
|
||||
cdxBOM.Components = &components
|
||||
|
||||
return cdxBOM
|
||||
}
|
||||
|
||||
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
||||
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata {
|
||||
return &cyclonedx.Metadata{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tools: &[]cyclonedx.Tool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
Component: toBomDescriptorComponent(srcMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
func toComponent(p pkg.Package) cyclonedx.Component {
|
||||
return cyclonedx.Component{
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
Licenses: toLicenses(p.Licenses),
|
||||
}
|
||||
}
|
||||
|
||||
func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
return &cyclonedx.Component{
|
||||
Type: cyclonedx.ComponentTypeContainer,
|
||||
Name: srcMetadata.ImageMetadata.UserInput,
|
||||
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
||||
}
|
||||
case source.DirectoryScheme, source.FileScheme:
|
||||
return &cyclonedx.Component{
|
||||
Type: cyclonedx.ComponentTypeFile,
|
||||
Name: srcMetadata.Path,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toLicenses(ls []string) *cyclonedx.Licenses {
|
||||
if len(ls) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lc := make(cyclonedx.Licenses, len(ls))
|
||||
for i, licenseName := range ls {
|
||||
lc[i] = cyclonedx.LicenseChoice{
|
||||
License: &cyclonedx.License{
|
||||
Name: licenseName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &lc
|
||||
}
|
||||
@@ -87,6 +87,8 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Logf("len: %d\nexpected: %s", len(expected), expected)
|
||||
t.Logf("len: %d\nactual: %s", len(actual), actual)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package cyclonedx12xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
enc := xml.NewEncoder(output)
|
||||
enc.Indent("", " ")
|
||||
|
||||
_, err := output.Write([]byte(xml.Header))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enc.Encode(toFormatModel(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = output.Write([]byte("\n"))
|
||||
return err
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Source: https://cyclonedx.org/ext/bom-descriptor/
|
||||
|
||||
// BomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
|
||||
type BomDescriptor struct {
|
||||
XMLName xml.Name `xml:"metadata"`
|
||||
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
|
||||
Tools []BomDescriptorTool `xml:"tools>tool"` // The tool used to create the BOM.
|
||||
Component *BomDescriptorComponent `xml:"component"` // The component that the BOM describes.
|
||||
}
|
||||
|
||||
// BomDescriptorTool represents the tool that created the BOM report.
|
||||
type BomDescriptorTool struct {
|
||||
XMLName xml.Name `xml:"tool"`
|
||||
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
|
||||
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
|
||||
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
|
||||
// TODO: hashes, author, manufacture, supplier
|
||||
// TODO: add user-defined fields for the remaining build/version parameters
|
||||
}
|
||||
|
||||
// BomDescriptorComponent represents the software/package being cataloged.
|
||||
type BomDescriptorComponent struct {
|
||||
XMLName xml.Name `xml:"component"`
|
||||
Component
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package model
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Component represents a single element in the CycloneDX BOM
|
||||
type Component struct {
|
||||
XMLName xml.Name `xml:"component"`
|
||||
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
|
||||
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
|
||||
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
|
||||
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
|
||||
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
|
||||
Name string `xml:"name"` // Required; The name of the component as defined by the project
|
||||
Version string `xml:"version"` // Required; The version of the component as defined by the project
|
||||
Description string `xml:"description,omitempty"` // A description of the component
|
||||
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
|
||||
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
|
||||
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
||||
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Source: https://github.com/CycloneDX/specification
|
||||
|
||||
// Document represents a CycloneDX BOM Document.
|
||||
type Document struct {
|
||||
XMLName xml.Name `xml:"bom"`
|
||||
XMLNs string `xml:"xmlns,attr"`
|
||||
Version int `xml:"version,attr"`
|
||||
SerialNumber string `xml:"serialNumber,attr"`
|
||||
BomDescriptor *BomDescriptor `xml:"metadata"` // The BOM descriptor extension
|
||||
Components []Component `xml:"components>component"` // The BOM contents
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// License represents a single software license for a Component
|
||||
type License struct {
|
||||
XMLName xml.Name `xml:"license"`
|
||||
ID string `xml:"id,omitempty"` // A valid SPDX license ID
|
||||
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package cyclonedx12xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx12xml/model"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// toFormatModel creates and populates a new in-memory representation of a CycloneDX 1.2 document
|
||||
func toFormatModel(s sbom.SBOM) model.Document {
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
doc := model.Document{
|
||||
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
|
||||
Version: 1,
|
||||
SerialNumber: uuid.New().URN(),
|
||||
BomDescriptor: toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source),
|
||||
}
|
||||
|
||||
// attach components
|
||||
for _, p := range s.Artifacts.PackageCatalog.Sorted() {
|
||||
doc.Components = append(doc.Components, toComponent(p))
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func toComponent(p pkg.Package) model.Component {
|
||||
return model.Component{
|
||||
Type: "library", // TODO: this is not accurate
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
Licenses: toLicenses(p.Licenses),
|
||||
}
|
||||
}
|
||||
|
||||
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
||||
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *model.BomDescriptor {
|
||||
return &model.BomDescriptor{
|
||||
XMLName: xml.Name{},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tools: []model.BomDescriptorTool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
Component: toBomDescriptorComponent(srcMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
func toBomDescriptorComponent(srcMetadata source.Metadata) *model.BomDescriptorComponent {
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
return &model.BomDescriptorComponent{
|
||||
Component: model.Component{
|
||||
Type: "container",
|
||||
Name: srcMetadata.ImageMetadata.UserInput,
|
||||
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
||||
},
|
||||
}
|
||||
case source.DirectoryScheme, source.FileScheme:
|
||||
return &model.BomDescriptorComponent{
|
||||
Component: model.Component{
|
||||
Type: "file",
|
||||
Name: srcMetadata.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toLicenses(licenses []string) *[]model.License {
|
||||
if len(licenses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []model.License
|
||||
for _, licenseName := range licenses {
|
||||
result = append(result, model.License{
|
||||
Name: licenseName,
|
||||
})
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
bom := cyclonedxhelpers.ToFormatModel(s)
|
||||
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON)
|
||||
enc.SetPretty(true)
|
||||
|
||||
err := enc.Encode(bom)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||
)
|
||||
|
||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
||||
|
||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func cycloneDxRedactor(s []byte) []byte {
|
||||
serialPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
|
||||
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
|
||||
|
||||
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} {
|
||||
s = pattern.ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxJSONOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:a81dc685-cf22-48e0-bda5-65ea1a8bca5b",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2021-12-03T13:17:26-08:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"type": "file",
|
||||
"name": "/some/path",
|
||||
"version": ""
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"licenses": [
|
||||
{
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-2"
|
||||
},
|
||||
{
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"purl": "a-purl-2"
|
||||
}
|
||||
]
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:2156ac1f-c838-4e93-8dc5-a3874ffeb967",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2021-12-03T13:17:26-08:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"type": "container",
|
||||
"name": "user-image-input",
|
||||
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"licenses": [
|
||||
{
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-1"
|
||||
},
|
||||
{
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"purl": "a-purl-2"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
package cyclonedx13xml
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
bom := cyclonedxhelpers.ToFormatModel(s)
|
||||
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML)
|
||||
enc.SetPretty(true)
|
||||
|
||||
err := enc.Encode(bom)
|
||||
return err
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cyclonedx12xml
|
||||
package cyclonedx13xml
|
||||
|
||||
import (
|
||||
"flag"
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package cyclonedx12xml
|
||||
package cyclonedx13xml
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxOption,
|
||||
format.CycloneDxXMLOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
@@ -0,0 +1,4 @@
|
||||
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
||||
FROM scratch
|
||||
ADD file-1.txt /somefile-1.txt
|
||||
ADD file-2.txt /somefile-2.txt
|
||||
@@ -0,0 +1 @@
|
||||
this file has contents
|
||||
@@ -0,0 +1 @@
|
||||
file-2 contents!
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:7b1c3b1d-ea3b-4022-9dcc-80f4b4cbce36" version="1">
|
||||
<metadata>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -31,4 +31,4 @@
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
</bom>
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:66bda3e1-888a-4d43-b906-7fd96d428753" version="1">
|
||||
<metadata>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -31,4 +31,4 @@
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
</bom>
|
||||
BIN
Binary file not shown.
@@ -3,7 +3,8 @@ package formats
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx12xml"
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx13json"
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
|
||||
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
@@ -17,7 +18,8 @@ func All() []format.Format {
|
||||
return []format.Format{
|
||||
syftjson.Format(),
|
||||
table.Format(),
|
||||
cyclonedx12xml.Format(),
|
||||
cyclonedx13xml.Format(),
|
||||
cyclonedx13json.Format(),
|
||||
spdx22json.Format(),
|
||||
spdx22tagvalue.Format(),
|
||||
text.Format(),
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-12-01T15:08:29.469369Z",
|
||||
"created": "2021-12-20T19:12:47.869816Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
@@ -11,10 +11,10 @@
|
||||
"licenseListVersion": "3.15"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/dir/some/path-f4586501-2da6-4541-a8e9-232b32f25e9a",
|
||||
"documentNamespace": "https://anchore.com/syft/dir/some/path-4b896ded-7852-4e31-b764-136b53bdf346",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-2a115ac97d018a0e",
|
||||
"SPDXID": "SPDXRef-1d97af55efe9512f",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
@@ -36,7 +36,7 @@
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-5e920b2bece2c3ae",
|
||||
"SPDXID": "SPDXRef-ad3d1c4abd84bf75",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-12-01T15:08:29.476498Z",
|
||||
"created": "2021-12-20T19:13:07.647486Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
@@ -11,10 +11,10 @@
|
||||
"licenseListVersion": "3.15"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input-e3b7637c-9b2f-4005-a683-58e60f979082",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input-174da656-1824-4bd3-8604-28919f8a65bc",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-888661d4f0362f02",
|
||||
"SPDXID": "SPDXRef-d16127444133b5c1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
@@ -36,7 +36,7 @@
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-4068ff5e8926b305",
|
||||
"SPDXID": "SPDXRef-24907357f3705420",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
|
||||
BIN
Binary file not shown.
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
doc := ToFormatModel(s)
|
||||
doc := toFormatModel(s)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "2a115ac97d018a0e",
|
||||
"id": "1d97af55efe9512f",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5e920b2bece2c3ae",
|
||||
"id": "ad3d1c4abd84bf75",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -83,7 +83,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "962403cfb7be50d7",
|
||||
"id": "d9a7c58726ab4bef",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b11f44847bba0ed1",
|
||||
"id": "ac462e450060da2c",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -62,8 +62,8 @@
|
||||
],
|
||||
"artifactRelationships": [
|
||||
{
|
||||
"parent": "962403cfb7be50d7",
|
||||
"child": "b11f44847bba0ed1",
|
||||
"parent": "",
|
||||
"child": "",
|
||||
"type": "ownership-by-file-overlap",
|
||||
"metadata": {
|
||||
"file": "path"
|
||||
@@ -179,7 +179,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "888661d4f0362f02",
|
||||
"id": "d16127444133b5c1",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -9,7 +9,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-1.txt",
|
||||
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||
"layerID": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4068ff5e8926b305",
|
||||
"id": "24907357f3705420",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -40,7 +40,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-2.txt",
|
||||
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||
"layerID": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
|
||||
}
|
||||
],
|
||||
"licenses": [],
|
||||
@@ -67,7 +67,7 @@
|
||||
"type": "image",
|
||||
"target": {
|
||||
"userInput": "user-image-input",
|
||||
"imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
|
||||
"imageID": "sha256:9624b89704d23fa5f61b427379d172dac91dc7a508c4d7dea7aed0e04a4cf39e",
|
||||
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"tags": [
|
||||
@@ -77,17 +77,17 @@
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
|
||||
"digest": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab",
|
||||
"size": 22
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
|
||||
"digest": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67",
|
||||
"size": 16
|
||||
}
|
||||
],
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1Njo5NjI0Yjg5NzA0ZDIzZmE1ZjYxYjQyNzM3OWQxNzJkYWM5MWRjN2E1MDhjNGQ3ZGVhN2FlZDBlMDRhNGNmMzllIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjoxNmU2NDU0MWYyZGRmNTlhOTAzOTFjZTdiYjhhZjkwMzEzZjdkMzczZjIxMDVkODhmM2QzMjY3YjcyZTBlYmFiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmRlNmMyMzVmNzZlYTI0Yzg1MDNlYzA4ODkxNDQ1YjVkNmE4YmRmODI0OTExN2VkOGQ4YjBiNmZiM2ViZTRmNjcifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTItMDFUMTI6MTM6NDMuNDAxMzkxNFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMi0wMVQxMjoxMzo0My4zNDM2MzQyWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMi0wMVQxMjoxMzo0My40MDEzOTE0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MTZlNjQ1NDFmMmRkZjU5YTkwMzkxY2U3YmI4YWY5MDMxM2Y3ZDM3M2YyMTA1ZDg4ZjNkMzI2N2I3MmUwZWJhYiIsInNoYTI1NjpkZTZjMjM1Zjc2ZWEyNGM4NTAzZWMwODg5MTQ0NWI1ZDZhOGJkZjgyNDkxMTdlZDhkOGIwYjZmYjNlYmU0ZjY3Il19fQ==",
|
||||
"repoDigests": []
|
||||
}
|
||||
},
|
||||
@@ -104,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -19,8 +19,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// TODO: this is exported for the use of the power-user command (temp)
|
||||
func ToFormatModel(s sbom.SBOM) model.Document {
|
||||
func toFormatModel(s sbom.SBOM) model.Document {
|
||||
src, err := toSourceModel(s.Source)
|
||||
if err != nil {
|
||||
log.Warnf("unable to create syft-json source object: %+v", err)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
bom.xml
|
||||
bom.xml
|
||||
bom.json
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
.PHONY: validate-schema
|
||||
validate-schema:
|
||||
go run ../../main.go ubuntu:latest -vv -o cyclonedx > bom.xml
|
||||
xmllint --noout --schema ./cyclonedx.xsd bom.xml
|
||||
xmllint --noout --schema ./cyclonedx.xsd bom.xml
|
||||
go run ../../main.go ubuntu:latest -vv -o cyclonedx-json > bom.json
|
||||
../../.tmp/yajsv -s bom-1.3.schema.json bom.json
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+269
-56
@@ -16,13 +16,13 @@ limitations under the License.
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
||||
xmlns:bom="http://cyclonedx.org/schema/bom/1.2"
|
||||
xmlns:bom="http://cyclonedx.org/schema/bom/1.3"
|
||||
xmlns:spdx="http://cyclonedx.org/schema/spdx"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://cyclonedx.org/schema/bom/1.2"
|
||||
targetNamespace="http://cyclonedx.org/schema/bom/1.3"
|
||||
vc:minVersion="1.0"
|
||||
vc:maxVersion="1.1"
|
||||
version="1.2">
|
||||
version="1.3">
|
||||
|
||||
<xs:import namespace="http://cyclonedx.org/schema/spdx" schemaLocation="spdx.xsd"/>
|
||||
|
||||
@@ -32,9 +32,6 @@ limitations under the License.
|
||||
<url>https://cyclonedx.org/</url>
|
||||
<license uri="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
version="2.0">Apache License, Version 2.0</license>
|
||||
<authors>
|
||||
<author>Steve Springett</author>
|
||||
</authors>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
@@ -71,15 +68,23 @@ limitations under the License.
|
||||
<xs:documentation>The component that the BOM describes.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="manufacture" type="bom:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="manufacture" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The organization that manufactured the component that the BOM describes.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The organization that supplied the component that the BOM describes. The
|
||||
supplier may often be the manufacture, but may also be a distributor or repackager.</xs:documentation>
|
||||
supplier may often be the manufacturer, but may also be a distributor or repackager.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document properties in a key/value store.
|
||||
This provides flexibility to include data not officially supported in the standard
|
||||
without having to use additional namespaces or create extensions.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
@@ -181,14 +186,14 @@ limitations under the License.
|
||||
<xs:documentation>The name of the contact</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The email address of the contact. Multiple email addresses are allowed.</xs:documentation>
|
||||
<xs:documentation>The email address of the contact.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The phone number of the contact. Multiple phone numbers are allowed.</xs:documentation>
|
||||
<xs:documentation>The phone number of the contact.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
@@ -231,7 +236,7 @@ limitations under the License.
|
||||
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The organization that supplied the component. The supplier may often
|
||||
be the manufacture, but may also be a distributor or repackager.</xs:documentation>
|
||||
be the manufacturer, but may also be a distributor or repackager.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="author" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
@@ -282,19 +287,7 @@ limitations under the License.
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:choice>
|
||||
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A valid SPDX license expression.
|
||||
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="copyright" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>An optional copyright notice informing users of the underlying claims to
|
||||
@@ -349,6 +342,13 @@ limitations under the License.
|
||||
component or to the project the component describes.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document properties in a key/value store.
|
||||
This provides flexibility to include data not officially supported in the standard
|
||||
without having to use additional namespaces or create extensions.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="components" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
@@ -370,6 +370,11 @@ limitations under the License.
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="evidence" type="bom:componentEvidenceType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document evidence collected through various forms of extraction or analysis.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
@@ -512,45 +517,45 @@ limitations under the License.
|
||||
<xs:simpleType name="classification">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="application">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A software application. Refer to https://en.wikipedia.org/wiki/Application_software
|
||||
<xs:annotation>
|
||||
<xs:documentation>A software application. Refer to https://en.wikipedia.org/wiki/Application_software
|
||||
for information about applications.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="framework">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A software framework. Refer to https://en.wikipedia.org/wiki/Software_framework
|
||||
for information on how frameworks vary slightly from libraries.</xs:documentation>
|
||||
for information on how frameworks vary slightly from libraries.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="library">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A software library. Refer to https://en.wikipedia.org/wiki/Library_(computing)
|
||||
for information about libraries. All third-party and open source reusable components will likely
|
||||
be a library. If the library also has key features of a framework, then it should be classified
|
||||
as a framework. If not, or is unknown, then specifying library is recommended.</xs:documentation>
|
||||
for information about libraries. All third-party and open source reusable components will likely
|
||||
be a library. If the library also has key features of a framework, then it should be classified
|
||||
as a framework. If not, or is unknown, then specifying library is recommended.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="container">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A packaging and/or runtime format, not specific to any particular technology,
|
||||
which isolates software inside the container from software outside of a container through
|
||||
virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization</xs:documentation>
|
||||
which isolates software inside the container from software outside of a container through
|
||||
virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="operating-system">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A software operating system without regard to deployment model
|
||||
(i.e. installed on physical hardware, virtual machine, image, etc) Refer to
|
||||
https://en.wikipedia.org/wiki/Operating_system</xs:documentation>
|
||||
(i.e. installed on physical hardware, virtual machine, image, etc) Refer to
|
||||
https://en.wikipedia.org/wiki/Operating_system</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="device">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A hardware device such as a processor, or chip-set. A hardware device
|
||||
containing firmware should include a component for the physical hardware itself, and another
|
||||
component of type 'firmware' or 'operating-system' (whichever is relevant), describing
|
||||
information about the software running on the device.</xs:documentation>
|
||||
containing firmware should include a component for the physical hardware itself, and another
|
||||
component of type 'firmware' or 'operating-system' (whichever is relevant), describing
|
||||
information about the software running on the device.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="firmware">
|
||||
@@ -562,7 +567,7 @@ limitations under the License.
|
||||
<xs:enumeration value="file">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A computer file. Refer to https://en.wikipedia.org/wiki/Computer_file
|
||||
for information about files.</xs:documentation>
|
||||
for information about files.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
@@ -782,6 +787,13 @@ limitations under the License.
|
||||
<xs:documentation xml:lang="en">An optional comment describing the external reference</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="hash" type="bom:hashType"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="type" type="bom:externalReferenceType" use="required">
|
||||
<xs:annotation>
|
||||
@@ -927,7 +939,7 @@ limitations under the License.
|
||||
<xs:enumeration value="backport">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A patch which takes code from a newer version of software and applies
|
||||
it to older versions of the same software. Refer to https://en.wikipedia.org/wiki/Backporting</xs:documentation>
|
||||
it to older versions of the same software. Refer to https://en.wikipedia.org/wiki/Backporting</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="cherry-pick">
|
||||
@@ -1255,24 +1267,19 @@ limitations under the License.
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:choice>
|
||||
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A valid SPDX license expression.
|
||||
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="externalReferences" type="bom:externalReferences" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document external references related to the service.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document properties in a key/value store.
|
||||
This provides flexibility to include data not officially supported in the standard
|
||||
without having to use additional namespaces or create extensions.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="services" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
@@ -1349,6 +1356,199 @@ limitations under the License.
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="licenseChoiceType">
|
||||
<xs:choice>
|
||||
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A valid SPDX license expression.
|
||||
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="copyrightsType">
|
||||
<xs:sequence>
|
||||
<xs:element name="text" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="componentEvidenceType">
|
||||
<xs:sequence>
|
||||
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="copyright" type="bom:copyrightsType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Allows any undeclared elements as long as the elements are placed in a different namespace.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##any" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="compositionsType">
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="composition" type="bom:compositionType"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Allows any undeclared elements as long as the elements are placed in a different namespace.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##any" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="compositionType">
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="aggregate" type="bom:aggregateType" default="not_specified">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies an aggregate type that describe how complete a relationship is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="assemblies" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
The bom-ref identifiers of the components or services being described. Assemblies refer to
|
||||
nested relationships whereby a constituent part may include other constituent parts. References
|
||||
do not cascade to child parts. References are explicit for the specified constituent part only.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="assembly" type="bom:bomReferenceType"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Allows any undeclared elements as long as the elements are placed in a different namespace.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="dependencies" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
The bom-ref identifiers of the components or services being described. Dependencies refer to a
|
||||
relationship whereby an independent constituent part requires another independent constituent
|
||||
part. References do not cascade to transitive dependencies. References are explicit for the
|
||||
specified dependency only.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="dependency" type="bom:bomReferenceType"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Allows any undeclared elements as long as the elements are placed in a different namespace.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="aggregateType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="complete">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship is complete. No further relationships including constituent components, services, or dependencies exist.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="incomplete">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship is incomplete. Additional relationships exist and may include constituent components, services, or dependencies.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="incomplete_first_party_only">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="incomplete_third_party_only">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="unknown">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship may be complete or incomplete. This usually signifies a 'best-effort' to obtain constituent components, services, or dependencies but the completeness is inconclusive.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="not_specified">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The relationship completeness is not specified.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="bomReferenceType">
|
||||
<xs:attribute name="ref" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>References a component or service by the its bom-ref attribute</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:anyAttribute namespace="##other" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="propertiesType">
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="property" type="bom:propertyType"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Allows any undeclared elements as long as the elements are placed in a different namespace.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##any" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="propertyType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies an individual property with a name and value.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:normalizedString">
|
||||
<xs:attribute name="name" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the property. Duplicate names are allowed, each potentially having a different value.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:element name="bom">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
@@ -1378,6 +1578,19 @@ limitations under the License.
|
||||
<xs:documentation>Provides the ability to document dependency relationships.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="compositions" type="bom:compositionsType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides the ability to document properties in a name-value store.
|
||||
This provides flexibility to include data not officially supported in the standard
|
||||
without having to use additional namespaces or create extensions. Unlike key-value
|
||||
stores, properties support duplicate names, each potentially having different values.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
|
||||
+1226
-1146
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ type artifactMetadataContainer struct {
|
||||
Python pkg.PythonPackageMetadata
|
||||
Rpm pkg.RpmdbMetadata
|
||||
Cargo pkg.CargoPackageMetadata
|
||||
Go pkg.GolangBinMetadata
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -13,7 +13,7 @@ type Identifiable interface {
|
||||
ID() ID
|
||||
}
|
||||
|
||||
func IDFromHash(obj interface{}) (ID, error) {
|
||||
func IDByHash(obj interface{}) (ID, error) {
|
||||
f, err := hashstructure.Hash(obj, hashstructure.FormatV2, &hashstructure.HashOptions{
|
||||
ZeroNil: true,
|
||||
SlicesAsSets: true,
|
||||
|
||||
@@ -6,10 +6,9 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
hashiVer "github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
hashiVer "github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -128,6 +127,11 @@ func TestIdentifyDistro(t *testing.T) {
|
||||
Type: RockyLinux,
|
||||
Version: "8.4.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/almalinux",
|
||||
Type: AlmaLinux,
|
||||
Version: "8.4.0",
|
||||
},
|
||||
}
|
||||
|
||||
observedDistros := internal.NewStringSet()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
NAME="AlmaLinux"
|
||||
VERSION="8.4 (Electric Cheetah)"
|
||||
ID="almalinux"
|
||||
ID_LIKE="rhel centos fedora"
|
||||
VERSION_ID="8.4"
|
||||
PLATFORM_ID="platform:el8"
|
||||
PRETTY_NAME="AlmaLinux 8.4 (Electric Cheetah)"
|
||||
ANSI_COLOR="0;34"
|
||||
CPE_NAME="cpe:/o:almalinux:almalinux:8.4:GA"
|
||||
HOME_URL="https://almalinux.org/"
|
||||
DOCUMENTATION_URL="https://wiki.almalinux.org/"
|
||||
BUG_REPORT_URL="https://bugs.almalinux.org/"
|
||||
|
||||
ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8"
|
||||
ALMALINUX_MANTISBT_PROJECT_VERSION="8.4"
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
Windows Type = "windows"
|
||||
Mariner Type = "mariner"
|
||||
RockyLinux Type = "rockylinux"
|
||||
AlmaLinux Type = "almalinux"
|
||||
)
|
||||
|
||||
// All contains all Linux distribution options
|
||||
@@ -42,6 +43,7 @@ var All = []Type{
|
||||
Windows,
|
||||
Mariner,
|
||||
RockyLinux,
|
||||
AlmaLinux,
|
||||
}
|
||||
|
||||
// IDMapping connects a distro ID like "ubuntu" to a Distro type
|
||||
@@ -62,6 +64,7 @@ var IDMapping = map[string]Type{
|
||||
"windows": Windows,
|
||||
"mariner": Mariner,
|
||||
"rocky": RockyLinux,
|
||||
"almalinux": AlmaLinux,
|
||||
}
|
||||
|
||||
// String returns the string representation of the given Linux distribution.
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-libpython3.7.so",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/libpython3.7.so",
|
||||
location: "libpython3.7.so",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "python-binary",
|
||||
@@ -32,7 +32,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-python3.6",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/python3.6",
|
||||
location: "python3.6",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "python-binary",
|
||||
@@ -46,7 +46,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-patchlevel.h",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/patchlevel.h",
|
||||
location: "patchlevel.h",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "cpython-source",
|
||||
@@ -60,7 +60,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-go",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/go",
|
||||
location: "go",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "go-binary",
|
||||
@@ -74,7 +74,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-go-hint",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/VERSION",
|
||||
location: "VERSION",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "go-binary-hint",
|
||||
@@ -88,7 +88,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
{
|
||||
name: "positive-busybox",
|
||||
fixtureDir: "test-fixtures/classifiers/positive",
|
||||
location: "test-fixtures/classifiers/positive/busybox",
|
||||
location: "busybox",
|
||||
expected: []Classification{
|
||||
{
|
||||
Class: "busybox-binary",
|
||||
|
||||
+10
-4
@@ -7,7 +7,8 @@ const (
|
||||
JSONOption Option = "json"
|
||||
TextOption Option = "text"
|
||||
TableOption Option = "table"
|
||||
CycloneDxOption Option = "cyclonedx"
|
||||
CycloneDxXMLOption Option = "cyclonedx"
|
||||
CycloneDxJSONOption Option = "cyclonedx-json"
|
||||
SPDXTagValueOption Option = "spdx-tag-value"
|
||||
SPDXJSONOption Option = "spdx-json"
|
||||
)
|
||||
@@ -16,7 +17,8 @@ var AllOptions = []Option{
|
||||
JSONOption,
|
||||
TextOption,
|
||||
TableOption,
|
||||
CycloneDxOption,
|
||||
CycloneDxXMLOption,
|
||||
CycloneDxJSONOption,
|
||||
SPDXTagValueOption,
|
||||
SPDXJSONOption,
|
||||
}
|
||||
@@ -31,8 +33,12 @@ func ParseOption(userStr string) Option {
|
||||
return TextOption
|
||||
case string(TableOption):
|
||||
return TableOption
|
||||
case string(CycloneDxOption), "cyclone", "cyclone-dx":
|
||||
return CycloneDxOption
|
||||
case string(CycloneDxXMLOption), "cyclone", "cyclone-dx", "cyclone-dx-xml", "cyclone-xml":
|
||||
// NOTE(jonasagx): setting "cyclone" to XML by default for retro-compatibility.
|
||||
// If we want to show no preference between XML and JSON please remove it.
|
||||
return CycloneDxXMLOption
|
||||
case string(CycloneDxJSONOption), "cyclone-json", "cyclone-dx-json":
|
||||
return CycloneDxJSONOption
|
||||
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
|
||||
return SPDXTagValueOption
|
||||
case string(SPDXJSONOption), "spdxjson":
|
||||
|
||||
@@ -9,8 +9,8 @@ type CargoPackageMetadata struct {
|
||||
}
|
||||
|
||||
// Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata.
|
||||
func (p CargoPackageMetadata) Pkg() Package {
|
||||
return Package{
|
||||
func (p CargoPackageMetadata) Pkg() *Package {
|
||||
return &Package{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Language: Rust,
|
||||
|
||||
+11
-2
@@ -51,6 +51,7 @@ func (c *Catalog) Package(id artifact.ID) *Package {
|
||||
log.Warnf("unable to copy package id=%q name=%q: %+v", id, v.Name, err)
|
||||
return nil
|
||||
}
|
||||
p.id = v.id
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -75,8 +76,12 @@ func (c *Catalog) Add(p Package) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// note: since we are capturing the ID, we cannot modify the package being added from this point forward
|
||||
id := p.ID()
|
||||
if id == "" {
|
||||
log.Warnf("found package with empty ID while adding to the catalog: %+v", p)
|
||||
p.SetID()
|
||||
id = p.ID()
|
||||
}
|
||||
|
||||
// store by package ID
|
||||
c.byID[id] = p
|
||||
@@ -103,6 +108,10 @@ func (c *Catalog) Enumerate(types ...Type) <-chan Package {
|
||||
channel := make(chan Package)
|
||||
go func() {
|
||||
defer close(channel)
|
||||
if c == nil {
|
||||
// we should allow enumerating from a catalog that was never created (which will result in no packages enumerated)
|
||||
return
|
||||
}
|
||||
for ty, ids := range c.idsByType {
|
||||
if len(types) != 0 {
|
||||
found := false
|
||||
@@ -138,7 +147,7 @@ func (c *Catalog) Sorted(types ...Type) (pkgs []Package) {
|
||||
sort.SliceStable(pkgs, func(i, j int) bool {
|
||||
if pkgs[i].Name == pkgs[j].Name {
|
||||
if pkgs[i].Version == pkgs[j].Version {
|
||||
if pkgs[i].Type == pkgs[j].Type {
|
||||
if pkgs[i].Type == pkgs[j].Type && len(pkgs[i].Locations) > 0 && len(pkgs[j].Locations) > 0 {
|
||||
return pkgs[i].Locations[0].String() < pkgs[j].Locations[0].String()
|
||||
}
|
||||
return pkgs[i].Type < pkgs[j].Type
|
||||
|
||||
+33
-27
@@ -3,28 +3,13 @@ package pkg
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var catalogAddAndRemoveTestPkgs = []Package{
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/a/path", "/another/path"),
|
||||
source.NewVirtualLocation("/b/path", "/bee/path"),
|
||||
},
|
||||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/c/path", "/another/path"),
|
||||
source.NewVirtualLocation("/d/path", "/another/path"),
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
}
|
||||
|
||||
type expectedIndexes struct {
|
||||
byType map[Type]*strset.Set
|
||||
byPath map[string]*strset.Set
|
||||
@@ -32,18 +17,38 @@ type expectedIndexes struct {
|
||||
|
||||
func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
var pkgs = []Package{
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/a/path", "/another/path"),
|
||||
source.NewVirtualLocation("/b/path", "/bee/path"),
|
||||
},
|
||||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/c/path", "/another/path"),
|
||||
source.NewVirtualLocation("/d/path", "/another/path"),
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
p.SetID()
|
||||
}
|
||||
|
||||
fixtureID := func(i int) string {
|
||||
return string(catalogAddAndRemoveTestPkgs[i].ID())
|
||||
return string(pkgs[i].ID())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []Package
|
||||
expectedIndexes expectedIndexes
|
||||
}{
|
||||
{
|
||||
name: "vanilla-add",
|
||||
pkgs: catalogAddAndRemoveTestPkgs,
|
||||
expectedIndexes: expectedIndexes{
|
||||
byType: map[Type]*strset.Set{
|
||||
RpmPkg: strset.New(fixtureID(0)),
|
||||
@@ -63,7 +68,7 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCatalog(test.pkgs...)
|
||||
c := NewCatalog(pkgs...)
|
||||
|
||||
assertIndexes(t, c, test.expectedIndexes)
|
||||
|
||||
@@ -73,9 +78,7 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
|
||||
// assert path index
|
||||
if len(c.idsByPath) != len(expectedIndexes.byPath) {
|
||||
t.Errorf("unexpected path index length: %d != %d", len(c.idsByPath), len(expectedIndexes.byPath))
|
||||
}
|
||||
assert.Len(t, c.idsByPath, len(expectedIndexes.byPath), "unexpected path index length")
|
||||
for path, expectedIds := range expectedIndexes.byPath {
|
||||
actualIds := strset.New()
|
||||
for _, p := range c.PackagesByPath(path) {
|
||||
@@ -88,9 +91,7 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
|
||||
}
|
||||
|
||||
// assert type index
|
||||
if len(c.idsByType) != len(expectedIndexes.byType) {
|
||||
t.Errorf("unexpected type index length: %d != %d", len(c.idsByType), len(expectedIndexes.byType))
|
||||
}
|
||||
assert.Len(t, c.idsByType, len(expectedIndexes.byType), "unexpected type index length")
|
||||
for ty, expectedIds := range expectedIndexes.byType {
|
||||
actualIds := strset.New()
|
||||
for p := range c.Enumerate(ty) {
|
||||
@@ -144,3 +145,8 @@ func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCatalog_EnumerateNilCatalog(t *testing.T) {
|
||||
var c *Catalog
|
||||
assert.Empty(t, c.Enumerate())
|
||||
}
|
||||
|
||||
@@ -21,14 +21,25 @@ import (
|
||||
// integrity check
|
||||
var _ common.ParserFn = parseApkDB
|
||||
|
||||
func newApkDBPackage(d *pkg.ApkMetadata) *pkg.Package {
|
||||
return &pkg.Package{
|
||||
Name: d.Package,
|
||||
Version: d.Version,
|
||||
Licenses: strings.Split(d.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: *d,
|
||||
}
|
||||
}
|
||||
|
||||
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
|
||||
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
|
||||
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
// larger capacity for the scanner.
|
||||
const maxScannerCapacity = 1024 * 1024
|
||||
// a new larger buffer for the scanner
|
||||
bufScan := make([]byte, maxScannerCapacity)
|
||||
packages := make([]pkg.Package, 0)
|
||||
packages := make([]*pkg.Package, 0)
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(bufScan, maxScannerCapacity)
|
||||
@@ -52,14 +63,7 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relations
|
||||
return nil, nil, err
|
||||
}
|
||||
if metadata != nil {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: strings.Split(metadata.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: *metadata,
|
||||
})
|
||||
packages = append(packages, newApkDBPackage(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -647,11 +647,11 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
func TestMultiplePackages(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected []pkg.Package
|
||||
expected []*pkg.Package
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/multiple",
|
||||
expected: []pkg.Package{
|
||||
expected: []*pkg.Package{
|
||||
{
|
||||
Name: "libc-utils",
|
||||
Version: "0.7.2-r0",
|
||||
|
||||
@@ -49,9 +49,10 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
|
||||
|
||||
// perform analysis, accumulating errors for each failed analysis
|
||||
var errs error
|
||||
for _, theCataloger := range catalogers {
|
||||
for _, c := range catalogers {
|
||||
// find packages from the underlying raw data
|
||||
packages, relationships, err := theCataloger.Catalog(resolver)
|
||||
log.Debugf("cataloging with %q", c.Name())
|
||||
packages, relationships, err := c.Catalog(resolver)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
@@ -59,14 +60,14 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
|
||||
|
||||
catalogedPackages := len(packages)
|
||||
|
||||
log.Debugf("package cataloger %q discovered %d packages", theCataloger.Name(), catalogedPackages)
|
||||
log.Debugf("discovered %d packages", catalogedPackages)
|
||||
packagesDiscovered.N += int64(catalogedPackages)
|
||||
|
||||
for _, p := range packages {
|
||||
// generate CPEs
|
||||
// generate CPEs (note: this is excluded from package ID, so is safe to mutate)
|
||||
p.CPEs = cpe.Generate(p)
|
||||
|
||||
// generate PURL
|
||||
// generate PURL (note: this is excluded from package ID, so is safe to mutate)
|
||||
p.PURL = generatePackageURL(p, theDistro)
|
||||
|
||||
// create file-to-package relationships for files owned by the package
|
||||
|
||||
@@ -45,22 +45,23 @@ func (c *GenericCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package,
|
||||
contentReader, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
// TODO: fail or log?
|
||||
return nil, nil, fmt.Errorf("unable to fetch contents for location=%v : %w", location, err)
|
||||
return nil, nil, fmt.Errorf("unable to fetch contents at location=%v: %w", location, err)
|
||||
}
|
||||
|
||||
discoveredPackages, discoveredRelationships, err := parser(location.RealPath, contentReader)
|
||||
internal.CloseAndLogError(contentReader, location.VirtualPath)
|
||||
if err != nil {
|
||||
// TODO: should we fail? or only log?
|
||||
log.Warnf("cataloger '%s' failed to parse entries (location=%+v): %+v", c.upstreamCataloger, location, err)
|
||||
log.Warnf("cataloger '%s' failed to parse entries at location=%+v: %+v", c.upstreamCataloger, location, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range discoveredPackages {
|
||||
p.FoundBy = c.upstreamCataloger
|
||||
p.Locations = append(p.Locations, location)
|
||||
p.SetID()
|
||||
|
||||
packages = append(packages, p)
|
||||
packages = append(packages, *p)
|
||||
}
|
||||
|
||||
relationships = append(relationships, discoveredRelationships...)
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func parser(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parser(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
contents, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return []pkg.Package{
|
||||
return []*pkg.Package{
|
||||
{
|
||||
Name: string(contents),
|
||||
},
|
||||
|
||||
@@ -8,4 +8,4 @@ import (
|
||||
)
|
||||
|
||||
// ParserFn standardizes a function signature for parser functions that accept the virtual file path (not usable for file reads) and contents and return any discovered packages from that file
|
||||
type ParserFn func(string, io.Reader) ([]pkg.Package, []artifact.Relationship, error)
|
||||
type ParserFn func(string, io.Reader) ([]*pkg.Package, []artifact.Relationship, error)
|
||||
|
||||
@@ -68,6 +68,8 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
|
||||
|
||||
// fetch additional data from the copyright file to derive the license information
|
||||
addLicenses(resolver, dbLocation, p)
|
||||
|
||||
p.SetID()
|
||||
}
|
||||
|
||||
allPackages = append(allPackages, pkgs...)
|
||||
@@ -138,7 +140,7 @@ func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Lo
|
||||
if conffilesReader != nil && conffilesLocation != nil {
|
||||
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgConffileInfo(md5Reader)...)
|
||||
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
locations = append(locations, *conffilesLocation)
|
||||
|
||||
@@ -20,6 +20,16 @@ var (
|
||||
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||
)
|
||||
|
||||
func newDpkgPackage(d pkg.DpkgMetadata) pkg.Package {
|
||||
return pkg.Package{
|
||||
Name: d.Package,
|
||||
Version: d.Version,
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: d,
|
||||
}
|
||||
}
|
||||
|
||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
buffedReader := bufio.NewReader(reader)
|
||||
@@ -37,13 +47,7 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
}
|
||||
|
||||
if entry.Package != "" {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: entry.Package,
|
||||
Version: entry.Version,
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: entry,
|
||||
})
|
||||
packages = append(packages, newDpkgPackage(entry))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,12 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
|
||||
for _, location := range fileMatches {
|
||||
r, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
return pkgs, nil, fmt.Errorf("failed to resolve file contents by location: %w", err)
|
||||
return pkgs, nil, fmt.Errorf("failed to resolve file contents by location=%q: %w", location.RealPath, err)
|
||||
}
|
||||
|
||||
goPkgs, err := parseGoBin(location, r)
|
||||
goPkgs, err := parseGoBin(location, r, openExe)
|
||||
if err != nil {
|
||||
log.Warnf("could not parse possible go binary: %+v", err)
|
||||
log.Warnf("could not parse possible go binary at %q: %+v", location.RealPath, err)
|
||||
}
|
||||
|
||||
internal.CloseAndLogError(r, location.RealPath)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"debug/pe"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
|
||||
@@ -24,6 +25,9 @@ type exe interface {
|
||||
// ReadData reads and returns up to size byte starting at virtual address addr.
|
||||
ReadData(addr, size uint64) ([]byte, error)
|
||||
|
||||
// ArchName returns a string that represents the CPU architecture of the executable.
|
||||
ArchName() string
|
||||
|
||||
// DataStart returns the writable data segment start address.
|
||||
DataStart() uint64
|
||||
}
|
||||
@@ -32,7 +36,7 @@ type exe interface {
|
||||
// we changed this signature from accpeting a string
|
||||
// to a ReadCloser so we could adapt the code to the
|
||||
// stereoscope api. We removed the file open methods.
|
||||
func openExe(file io.ReadCloser) (exe, error) {
|
||||
func openExe(file io.ReadCloser) ([]exe, error) {
|
||||
/*
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
@@ -57,7 +61,7 @@ func openExe(file io.ReadCloser) (exe, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &elfExe{file, e}, nil
|
||||
return []exe{&elfExe{file, e}}, nil
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(data, []byte("MZ")) {
|
||||
@@ -65,7 +69,7 @@ func openExe(file io.ReadCloser) (exe, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &peExe{file, e}, nil
|
||||
return []exe{&peExe{file, e}}, nil
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
|
||||
@@ -73,7 +77,20 @@ func openExe(file io.ReadCloser) (exe, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &machoExe{file, e}, nil
|
||||
return []exe{&machoExe{file, e}}, nil
|
||||
}
|
||||
|
||||
// adding macho multi-architecture support (both for 64bit and 32 bit)... this case is not in the stdlib yet
|
||||
if bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBE")) || bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBF")) {
|
||||
fatExe, err := macho.NewFatFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var exes []exe
|
||||
for _, arch := range fatExe.Arches {
|
||||
exes = append(exes, &machoExe{file, arch.File})
|
||||
}
|
||||
return exes, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unrecognized executable format")
|
||||
@@ -90,6 +107,14 @@ func (x *elfExe) Close() error {
|
||||
return x.os.Close()
|
||||
}
|
||||
|
||||
func (x *elfExe) ArchName() string {
|
||||
return cleanElfArch(x.f.Machine)
|
||||
}
|
||||
|
||||
func cleanElfArch(machine elf.Machine) string {
|
||||
return strings.TrimPrefix(strings.ToLower(machine.String()), "em_")
|
||||
}
|
||||
|
||||
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
|
||||
for _, prog := range x.f.Progs {
|
||||
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
|
||||
@@ -132,6 +157,56 @@ func (x *peExe) Close() error {
|
||||
return x.os.Close()
|
||||
}
|
||||
|
||||
func (x *peExe) ArchName() string {
|
||||
// from: debug/pe/pe.go
|
||||
switch x.f.Machine {
|
||||
case pe.IMAGE_FILE_MACHINE_AM33:
|
||||
return "amd33"
|
||||
case pe.IMAGE_FILE_MACHINE_AMD64:
|
||||
return "amd64"
|
||||
case pe.IMAGE_FILE_MACHINE_ARM:
|
||||
return "arm"
|
||||
case pe.IMAGE_FILE_MACHINE_ARMNT:
|
||||
return "armnt"
|
||||
case pe.IMAGE_FILE_MACHINE_ARM64:
|
||||
return "arm64"
|
||||
case pe.IMAGE_FILE_MACHINE_EBC:
|
||||
return "ebc"
|
||||
case pe.IMAGE_FILE_MACHINE_I386:
|
||||
return "i386"
|
||||
case pe.IMAGE_FILE_MACHINE_IA64:
|
||||
return "ia64"
|
||||
case pe.IMAGE_FILE_MACHINE_M32R:
|
||||
return "m32r"
|
||||
case pe.IMAGE_FILE_MACHINE_MIPS16:
|
||||
return "mips16"
|
||||
case pe.IMAGE_FILE_MACHINE_MIPSFPU:
|
||||
return "mipsfpu"
|
||||
case pe.IMAGE_FILE_MACHINE_MIPSFPU16:
|
||||
return "mipsfpu16"
|
||||
case pe.IMAGE_FILE_MACHINE_POWERPC:
|
||||
return "ppc"
|
||||
case pe.IMAGE_FILE_MACHINE_POWERPCFP:
|
||||
return "ppcfp"
|
||||
case pe.IMAGE_FILE_MACHINE_R4000:
|
||||
return "r4000"
|
||||
case pe.IMAGE_FILE_MACHINE_SH3:
|
||||
return "sh3"
|
||||
case pe.IMAGE_FILE_MACHINE_SH3DSP:
|
||||
return "sh3dsp"
|
||||
case pe.IMAGE_FILE_MACHINE_SH4:
|
||||
return "sh4"
|
||||
case pe.IMAGE_FILE_MACHINE_SH5:
|
||||
return "sh5"
|
||||
case pe.IMAGE_FILE_MACHINE_THUMB:
|
||||
return "thumb"
|
||||
case pe.IMAGE_FILE_MACHINE_WCEMIPSV2:
|
||||
return "wcemipsv2"
|
||||
default:
|
||||
return fmt.Sprintf("unknown-pe-machine-%d", x.f.Machine)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *peExe) imageBase() uint64 {
|
||||
switch oh := x.f.OptionalHeader.(type) {
|
||||
case *pe.OptionalHeader32:
|
||||
@@ -193,6 +268,14 @@ func (x *machoExe) Close() error {
|
||||
return x.os.Close()
|
||||
}
|
||||
|
||||
func (x *machoExe) ArchName() string {
|
||||
return cleanMachoArch(x.f.Cpu)
|
||||
}
|
||||
|
||||
func cleanMachoArch(cpu macho.Cpu) string {
|
||||
return strings.TrimPrefix(strings.ToLower(cpu.String()), "cpu")
|
||||
}
|
||||
|
||||
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
|
||||
for _, load := range x.f.Loads {
|
||||
seg, ok := load.(*macho.Segment)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_cleanElfArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
machine elf.Machine
|
||||
want string
|
||||
}{
|
||||
{
|
||||
machine: elf.EM_X86_64,
|
||||
want: "x86_64",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.machine.String(), func(t *testing.T) {
|
||||
assert.Equalf(t, test.want, cleanElfArch(test.machine), "cleanElfArch(%v)", test.machine)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cleanMachoArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
cpu macho.Cpu
|
||||
want string
|
||||
}{
|
||||
{
|
||||
cpu: macho.CpuAmd64,
|
||||
want: "amd64",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.cpu.String(), func(t *testing.T) {
|
||||
assert.Equalf(t, test.want, cleanMachoArch(test.cpu), "cleanMachoArch(%v)", test.cpu)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package golang
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@@ -14,19 +15,54 @@ const (
|
||||
replaceIdentifier = "=>"
|
||||
)
|
||||
|
||||
func parseGoBin(location source.Location, reader io.ReadCloser) ([]pkg.Package, error) {
|
||||
// Identify if bin was compiled by go
|
||||
x, err := openExe(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
type exeOpener func(file io.ReadCloser) ([]exe, error)
|
||||
|
||||
func newGoBinaryPackage(name, version, h1Digest, goVersion, architecture string, location source.Location) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: []source.Location{
|
||||
location,
|
||||
},
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goVersion,
|
||||
H1Digest: h1Digest,
|
||||
Architecture: architecture,
|
||||
},
|
||||
}
|
||||
|
||||
goVersion, mod := findVers(x)
|
||||
p.SetID()
|
||||
|
||||
return buildGoPkgInfo(location, mod, goVersion), nil
|
||||
return p
|
||||
}
|
||||
|
||||
func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Package {
|
||||
func parseGoBin(location source.Location, reader io.ReadCloser, opener exeOpener) (pkgs []pkg.Package, err error) {
|
||||
var exes []exe
|
||||
// it has been found that there are stdlib paths within openExe that can panic. We want to prevent this behavior
|
||||
// bubbling up and halting execution. For this reason we try to recover from any panic and return an error.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("recovered from panic while parse go binary at %q: %+v", location.RealPath, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Identify if bin was compiled by go
|
||||
exes, err = opener(reader)
|
||||
if err != nil {
|
||||
return pkgs, err
|
||||
}
|
||||
|
||||
for _, x := range exes {
|
||||
goVersion, mod := findVers(x)
|
||||
pkgs = append(pkgs, buildGoPkgInfo(location, mod, goVersion, x.ArchName())...)
|
||||
}
|
||||
return pkgs, err
|
||||
}
|
||||
|
||||
func buildGoPkgInfo(location source.Location, mod, goVersion, arch string) []pkg.Package {
|
||||
pkgsSlice := make([]pkg.Package, 0)
|
||||
scanner := bufio.NewScanner(strings.NewReader(mod))
|
||||
|
||||
@@ -40,20 +76,10 @@ func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Packa
|
||||
}
|
||||
|
||||
if fields[0] == packageIdentifier || fields[0] == replaceIdentifier {
|
||||
pkgsSlice = append(pkgsSlice, pkg.Package{
|
||||
Name: fields[1],
|
||||
Version: fields[2],
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: []source.Location{
|
||||
location,
|
||||
},
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goVersion,
|
||||
H1Digest: fields[3],
|
||||
},
|
||||
})
|
||||
name := fields[1]
|
||||
version := fields[2]
|
||||
h1Digest := fields[3]
|
||||
pkgsSlice = append(pkgsSlice, newGoBinaryPackage(name, version, h1Digest, goVersion, arch, location))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -8,9 +9,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const goCompiledVersion = "1.17"
|
||||
|
||||
func TestBuildGoPkgInfo(t *testing.T) {
|
||||
const (
|
||||
goCompiledVersion = "1.17"
|
||||
archDetails = "amd64"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
mod string
|
||||
@@ -43,6 +46,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goCompiledVersion,
|
||||
Architecture: archDetails,
|
||||
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
|
||||
},
|
||||
},
|
||||
@@ -62,6 +66,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goCompiledVersion,
|
||||
Architecture: archDetails,
|
||||
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
|
||||
},
|
||||
},
|
||||
@@ -92,6 +97,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goCompiledVersion,
|
||||
Architecture: archDetails,
|
||||
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
|
||||
},
|
||||
},
|
||||
@@ -111,6 +117,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goCompiledVersion,
|
||||
Architecture: archDetails,
|
||||
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
|
||||
},
|
||||
},
|
||||
@@ -130,6 +137,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goCompiledVersion,
|
||||
Architecture: archDetails,
|
||||
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
|
||||
},
|
||||
},
|
||||
@@ -137,17 +145,42 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for i := range test.expected {
|
||||
p := &test.expected[i]
|
||||
p.SetID()
|
||||
}
|
||||
location := source.Location{
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "/a-path",
|
||||
FileSystemID: "layer-id",
|
||||
},
|
||||
}
|
||||
pkgs := buildGoPkgInfo(location, tt.mod, goCompiledVersion)
|
||||
assert.Equal(t, tt.expected, pkgs)
|
||||
pkgs := buildGoPkgInfo(location, test.mod, goCompiledVersion, archDetails)
|
||||
assert.Equal(t, test.expected, pkgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseGoBin_recoversFromPanic(t *testing.T) {
|
||||
freakOut := func(file io.ReadCloser) ([]exe, error) {
|
||||
panic("baaahhh!")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
wantPkgs []pkg.Package
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "recovers from panic",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pkgs, err := parseGoBin(source.NewLocation("some/path"), nil, freakOut)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pkgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// parseGoMod takes a go.mod and lists all packages discovered.
|
||||
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make(map[string]pkg.Package)
|
||||
func parseGoMod(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make(map[string]*pkg.Package)
|
||||
|
||||
contents, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
@@ -26,7 +26,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
}
|
||||
|
||||
for _, m := range file.Require {
|
||||
packages[m.Mod.Path] = pkg.Package{
|
||||
packages[m.Mod.Path] = &pkg.Package{
|
||||
Name: m.Mod.Path,
|
||||
Version: m.Mod.Version,
|
||||
Language: pkg.Go,
|
||||
@@ -36,7 +36,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
|
||||
// remove any old packages and replace with new ones...
|
||||
for _, m := range file.Replace {
|
||||
packages[m.New.Path] = pkg.Package{
|
||||
packages[m.New.Path] = &pkg.Package{
|
||||
Name: m.New.Path,
|
||||
Version: m.New.Version,
|
||||
Language: pkg.Go,
|
||||
@@ -49,7 +49,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
delete(packages, m.Mod.Path)
|
||||
}
|
||||
|
||||
pkgsSlice := make([]pkg.Package, len(packages))
|
||||
pkgsSlice := make([]*pkg.Package, len(packages))
|
||||
idx := 0
|
||||
for _, p := range packages {
|
||||
pkgsSlice[idx] = p
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
diffs := deep.Equal(a, e)
|
||||
diffs := deep.Equal(a, &e)
|
||||
if len(diffs) > 0 {
|
||||
t.Errorf("diffs found for %q", a.Name)
|
||||
for _, d := range diffs {
|
||||
|
||||
@@ -35,7 +35,7 @@ type archiveParser struct {
|
||||
}
|
||||
|
||||
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
|
||||
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parseJavaArchive(virtualPath string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
|
||||
// note: even on error, we should always run cleanup functions
|
||||
defer cleanupFn()
|
||||
@@ -81,8 +81,8 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
|
||||
}
|
||||
|
||||
// parse the loaded archive and return all packages found.
|
||||
func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []pkg.Package
|
||||
func (j *archiveParser) parse() ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []*pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
// find the parent package from the java manifest
|
||||
@@ -110,7 +110,7 @@ func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error)
|
||||
|
||||
// lastly, add the parent package to the list (assuming the parent exists)
|
||||
if parentPkg != nil {
|
||||
pkgs = append([]pkg.Package{*parentPkg}, pkgs...)
|
||||
pkgs = append([]*pkg.Package{parentPkg}, pkgs...)
|
||||
}
|
||||
|
||||
return pkgs, relationships, nil
|
||||
@@ -158,12 +158,12 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
||||
// parent package, returning all listed Java packages found for each pom
|
||||
// properties discovered and potentially updating the given parentPkg with new
|
||||
// data.
|
||||
func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
||||
func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([]*pkg.Package, error) {
|
||||
if parentPkg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
var pkgs []*pkg.Package
|
||||
|
||||
properties, err := pomPropertiesByParentPath(j.archivePath, j.fileManifest.GlobMatch(pomPropertiesGlob), j.virtualPath)
|
||||
if err != nil {
|
||||
@@ -183,7 +183,7 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
|
||||
|
||||
pkgFromPom := newPackageFromMavenData(propertiesObj, pomProject, parentPkg, j.virtualPath)
|
||||
if pkgFromPom != nil {
|
||||
pkgs = append(pkgs, *pkgFromPom)
|
||||
pkgs = append(pkgs, pkgFromPom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +192,8 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
|
||||
|
||||
// discoverPkgsFromNestedArchives finds Java archives within Java archives, returning all listed Java packages found and
|
||||
// associating each discovered package to the given parent package.
|
||||
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []pkg.Package
|
||||
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []*pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
// search and parse pom.properties files & fetch the contents
|
||||
|
||||
@@ -257,7 +257,7 @@ func TestParseJar(t *testing.T) {
|
||||
var parent *pkg.Package
|
||||
for _, a := range actual {
|
||||
if strings.Contains(a.Name, "example-") {
|
||||
parent = &a
|
||||
parent = a
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ func TestParseJar(t *testing.T) {
|
||||
// write censored data back
|
||||
a.Metadata = metadata
|
||||
|
||||
diffs := deep.Equal(e, a)
|
||||
diffs := deep.Equal(&e, a)
|
||||
if len(diffs) > 0 {
|
||||
t.Errorf("diffs found for %q", a.Name)
|
||||
for _, d := range diffs {
|
||||
@@ -527,7 +527,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
|
||||
actualNameVersionPairSet := internal.NewStringSet()
|
||||
for _, a := range actual {
|
||||
key := makeKey(&a)
|
||||
key := makeKey(a)
|
||||
actualNameVersionPairSet.Add(key)
|
||||
if !expectedNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("extra package: %s", a)
|
||||
@@ -545,7 +545,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
actualKey := makeKey(&a)
|
||||
actualKey := makeKey(a)
|
||||
|
||||
metadata := a.Metadata.(pkg.JavaMetadata)
|
||||
if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
|
||||
|
||||
@@ -19,48 +19,66 @@ const manifestGlob = "/META-INF/MANIFEST.MF"
|
||||
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
|
||||
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
var manifest pkg.JavaManifest
|
||||
sections := []map[string]string{
|
||||
make(map[string]string),
|
||||
var sections []map[string]string
|
||||
|
||||
currentSection := func() int {
|
||||
return len(sections) - 1
|
||||
}
|
||||
currentSection := 0
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
var lastKey string
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// empty lines denote section separators
|
||||
if strings.TrimSpace(line) == "" {
|
||||
currentSection++
|
||||
// we don't want to allocate a new section map that wont necessarily be used, do that once there is
|
||||
// we don't want to allocate a new section map that won't necessarily be used, do that once there is
|
||||
// a non-empty line to process
|
||||
|
||||
// do not process line continuations after this
|
||||
lastKey = ""
|
||||
|
||||
continue
|
||||
} else if currentSection >= len(sections) {
|
||||
sections = append(sections, make(map[string]string))
|
||||
}
|
||||
|
||||
if line[0] == ' ' {
|
||||
// this is a continuation
|
||||
|
||||
if lastKey == "" {
|
||||
return nil, fmt.Errorf("found continuation with no previous key (%s)", line)
|
||||
}
|
||||
sections[currentSection][lastKey] += strings.TrimSpace(line)
|
||||
} else {
|
||||
// this is a new key-value pair
|
||||
idx := strings.Index(line, ":")
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf("unable to split java manifest key-value pairs: %q", line)
|
||||
log.Warnf("java manifest %q: found continuation with no previous key: %q", path, line)
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[0:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
sections[currentSection][key] = value
|
||||
sections[currentSection()][lastKey] += strings.TrimSpace(line)
|
||||
|
||||
// keep track of key for potential future continuations
|
||||
lastKey = key
|
||||
continue
|
||||
}
|
||||
|
||||
// this is a new key-value pair
|
||||
idx := strings.Index(line, ":")
|
||||
if idx == -1 {
|
||||
log.Warnf("java manifest %q: unable to split java manifest key-value pairs: %q", path, line)
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[0:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
|
||||
if key == "" {
|
||||
// don't attempt to add new keys or sections unless there is a non-empty key
|
||||
continue
|
||||
}
|
||||
|
||||
if lastKey == "" {
|
||||
// we're entering a new section
|
||||
sections = append(sections, make(map[string]string))
|
||||
}
|
||||
|
||||
sections[currentSection()][key] = value
|
||||
|
||||
// keep track of key for potential future continuations
|
||||
lastKey = key
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -75,7 +93,7 @@ func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error)
|
||||
name, ok := s["Name"]
|
||||
if !ok {
|
||||
// per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
|
||||
// this should never happen. If it does we want to know about it, but not necessarily stop
|
||||
// this should never happen. If it does, we want to know about it, but not necessarily stop
|
||||
// cataloging entirely... for this reason we only log.
|
||||
log.Warnf("java manifest section found without a name: %s", path)
|
||||
name = strconv.Itoa(i)
|
||||
@@ -115,16 +133,40 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
|
||||
}
|
||||
|
||||
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
var version string
|
||||
switch {
|
||||
case filenameObj.version != "":
|
||||
version = filenameObj.version
|
||||
case manifest.Main["Implementation-Version"] != "":
|
||||
version = manifest.Main["Implementation-Version"]
|
||||
case manifest.Main["Specification-Version"] != "":
|
||||
version = manifest.Main["Specification-Version"]
|
||||
case manifest.Main["Plugin-Version"] != "":
|
||||
version = manifest.Main["Plugin-Version"]
|
||||
if v := filenameObj.version; v != "" {
|
||||
return v
|
||||
}
|
||||
return version
|
||||
|
||||
if manifest == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fieldNames := []string{
|
||||
"Implementation-Version",
|
||||
"Specification-Version",
|
||||
"Plugin-Version",
|
||||
"Bundle-Version",
|
||||
}
|
||||
|
||||
for _, fieldName := range fieldNames {
|
||||
if v := fieldValueFromManifest(*manifest, fieldName); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func fieldValueFromManifest(manifest pkg.JavaManifest, fieldName string) string {
|
||||
if value := manifest.Main[fieldName]; value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
for _, section := range manifest.NamedSections {
|
||||
if value := section[fieldName]; value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
@@ -56,6 +58,30 @@ func TestParseJavaManifest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/manifest/extra-empty-lines",
|
||||
expected: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
"Archiver-Version": "Plexus Archiver",
|
||||
"Created-By": "Apache Maven 3.6.3",
|
||||
},
|
||||
NamedSections: map[string]map[string]string{
|
||||
"thing-1": {
|
||||
"Built-By": "?",
|
||||
},
|
||||
"thing-2": {
|
||||
"Built-By": "someone!",
|
||||
},
|
||||
"2": {
|
||||
"Other": "things",
|
||||
},
|
||||
"3": {
|
||||
"Last": "item",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/manifest/continuation",
|
||||
expected: pkg.JavaManifest{
|
||||
@@ -145,5 +171,94 @@ func TestSelectName(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest pkg.JavaManifest
|
||||
archive archiveFilename
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Get name from Implementation-Version",
|
||||
archive: archiveFilename{},
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Implementation-Version": "1.8.2",
|
||||
},
|
||||
},
|
||||
expected: "1.8.2",
|
||||
},
|
||||
{
|
||||
name: "Implementation-Version takes precedence over Specification-Version",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Implementation-Version": "1.8.2",
|
||||
"Specification-Version": "1.0",
|
||||
},
|
||||
},
|
||||
expected: "1.8.2",
|
||||
},
|
||||
{
|
||||
name: "Implementation-Version found outside the main section",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
"Ant-Version": "Apache Ant 1.8.2",
|
||||
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
|
||||
},
|
||||
NamedSections: map[string]map[string]string{
|
||||
"org/apache/tools/ant/taskdefs/optional/": {
|
||||
"Implementation-Version": "1.8.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "1.8.2",
|
||||
},
|
||||
{
|
||||
name: "Implementation-Version takes precedence over Specification-Version in subsequent section",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
"Ant-Version": "Apache Ant 1.8.2",
|
||||
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
|
||||
"Specification-Version": "2.0",
|
||||
},
|
||||
NamedSections: map[string]map[string]string{
|
||||
"org/apache/tools/ant/taskdefs/optional/": {
|
||||
"Specification-Version": "1.8",
|
||||
},
|
||||
"some-other-section": {
|
||||
"Implementation-Version": "1.8.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "1.8.2",
|
||||
},
|
||||
{
|
||||
name: "Implementation-Version takes precedence over Specification-Version in subsequent section",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
"Ant-Version": "Apache Ant 1.8.2",
|
||||
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
|
||||
},
|
||||
NamedSections: map[string]map[string]string{
|
||||
"some-other-section": {
|
||||
"Bundle-Version": "1.11.28",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "1.11.28",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := selectVersion(&test.manifest, test.archive)
|
||||
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Manifest-Version: 1.0
|
||||
Archiver-Version: Plexus Archiver
|
||||
Created-By: Apache Maven 3.6.3
|
||||
|
||||
|
||||
Name: thing-1
|
||||
Built-By: ?
|
||||
|
||||
.
|
||||
|
||||
Name: thing-2
|
||||
Built-By: someone!
|
||||
|
||||
|
||||
:
|
||||
|
||||
Other: things
|
||||
junk
|
||||
|
||||
:
|
||||
|
||||
Last: item
|
||||
@@ -50,7 +50,6 @@ type Repository struct {
|
||||
// ---> name: "Isaac Z. Schlueter" email: "i@izs.me" url: "http://blog.izs.me"
|
||||
var authorPattern = regexp.MustCompile(`^\s*(?P<name>[^<(]*)(\s+<(?P<email>.*)>)?(\s\((?P<url>.*)\))?\s*$`)
|
||||
|
||||
// Exports Author.UnmarshalJSON interface to help normalize the json structure.
|
||||
func (a *Author) UnmarshalJSON(b []byte) error {
|
||||
var authorStr string
|
||||
var fields map[string]string
|
||||
@@ -135,7 +134,7 @@ func licenseFromJSON(b []byte) (string, error) {
|
||||
return "", errors.New("unable to unmarshal license field as either string or object")
|
||||
}
|
||||
|
||||
func licensesFromJSON(p PackageJSON) ([]string, error) {
|
||||
func (p PackageJSON) licensesFromJSON() ([]string, error) {
|
||||
if p.License == nil && p.Licenses == nil {
|
||||
// This package.json doesn't specify any licenses whatsoever
|
||||
return []string{}, nil
|
||||
@@ -163,8 +162,8 @@ func licensesFromJSON(p PackageJSON) ([]string, error) {
|
||||
}
|
||||
|
||||
// parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
|
||||
func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var packages []pkg.Package
|
||||
func parsePackageJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
var packages []*pkg.Package
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
for {
|
||||
@@ -180,35 +179,46 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.Rel
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
licenses, err := licensesFromJSON(p)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse package.json file: %w", err)
|
||||
}
|
||||
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Licenses: licenses,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: p.Author.AuthorString(),
|
||||
Homepage: p.Homepage,
|
||||
URL: p.Repository.URL,
|
||||
Licenses: licenses,
|
||||
},
|
||||
})
|
||||
packages = append(packages, newPackageJSONPackage(p))
|
||||
}
|
||||
|
||||
return packages, nil, nil
|
||||
}
|
||||
|
||||
func newPackageJSONPackage(p PackageJSON) *pkg.Package {
|
||||
licenses, err := p.licensesFromJSON()
|
||||
if err != nil {
|
||||
log.Warnf("unable to extract licenses from javascript package.json: %+v", err)
|
||||
}
|
||||
|
||||
return &pkg.Package{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Licenses: licenses,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: p.Author.AuthorString(),
|
||||
Homepage: p.Homepage,
|
||||
URL: p.Repository.URL,
|
||||
Licenses: licenses,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p PackageJSON) hasNameAndVersionValues() bool {
|
||||
return p.Name != "" && p.Version != ""
|
||||
}
|
||||
|
||||
func pathContainsNodeModulesDirectory(path string) bool {
|
||||
isNodeModulesPath, _ := regexp.MatchString("[\\/]node_modules[\\/]", path)
|
||||
return isNodeModulesPath
|
||||
// this supports both windows and unix paths
|
||||
var filepathSeparator = regexp.MustCompile(`[\\/]`)
|
||||
|
||||
func pathContainsNodeModulesDirectory(p string) bool {
|
||||
for _, subPath := range filepathSeparator.Split(p, -1) {
|
||||
if subPath == "node_modules" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParsePackageJSON(t *testing.T) {
|
||||
@@ -135,7 +136,7 @@ func TestParsePackageJSON(t *testing.T) {
|
||||
t.Fatalf("unexpected package count: %d!=1", len(actual))
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(actual[0], test.ExpectedPkg) {
|
||||
for _, d := range deep.Equal(actual[0], &test.ExpectedPkg) {
|
||||
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
@@ -160,3 +161,50 @@ func TestParsePackageJSON_Partial(t *testing.T) { // see https://github.com/anch
|
||||
t.Errorf("no packages should've been returned (but got %d packages)", actualCount)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pathContainsNodeModulesDirectory(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
// positive
|
||||
{
|
||||
path: "something/node_modules/package",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "node_modules/package",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "something/node_modules",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "\\something\\node_modules\\",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "\\something\\node_modules",
|
||||
expected: true,
|
||||
},
|
||||
// negative
|
||||
{
|
||||
path: "something/node_bogus_modules",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
path: "something/node_modules_bogus",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
path: "something/node_bogus_modules/package",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.path, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, pathContainsNodeModulesDirectory(test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ type Dependency struct {
|
||||
}
|
||||
|
||||
// parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages.
|
||||
func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parsePackageLock(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
// in the case we find package-lock.json files in the node_modules directories, skip those
|
||||
// as the whole purpose of the lock file is for the specific dependencies of the root project
|
||||
if pathContainsNodeModulesDirectory(path) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var packages []pkg.Package
|
||||
var packages []*pkg.Package
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
for {
|
||||
@@ -47,7 +47,7 @@ func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, []artifact.
|
||||
return nil, nil, fmt.Errorf("failed to parse package-lock.json file: %w", err)
|
||||
}
|
||||
for name, pkgMeta := range lock.Dependencies {
|
||||
packages = append(packages, pkg.Package{
|
||||
packages = append(packages, &pkg.Package{
|
||||
Name: name,
|
||||
Version: pkgMeta.Version,
|
||||
Language: pkg.JavaScript,
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) {
|
||||
func assertPkgsEqual(t *testing.T, actual []*pkg.Package, expected map[string]pkg.Package) {
|
||||
t.Helper()
|
||||
if len(actual) != len(expected) {
|
||||
for _, a := range actual {
|
||||
@@ -18,26 +20,11 @@ func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg
|
||||
|
||||
for _, a := range actual {
|
||||
expectedPkg, ok := expected[a.Name]
|
||||
if !ok {
|
||||
t.Errorf("unexpected package found: '%s'", a.Name)
|
||||
}
|
||||
|
||||
if expectedPkg.Version != a.Version {
|
||||
t.Errorf("%s : unexpected package version: '%s', expected: '%s'", a.Name, a.Version, expectedPkg.Version)
|
||||
}
|
||||
|
||||
if a.Language != expectedPkg.Language {
|
||||
t.Errorf("%s : bad language: '%+v', expected: '%+v'", a.Name, a.Language, expectedPkg.Language)
|
||||
}
|
||||
|
||||
if a.Type != expectedPkg.Type {
|
||||
t.Errorf("%s : bad package type: %+v, expected: %+v", a.Name, a.Type, expectedPkg.Type)
|
||||
}
|
||||
|
||||
if len(a.Licenses) < len(expectedPkg.Licenses) {
|
||||
t.Errorf("%s : bad package licenses count: '%+v'", a.Name, a.Licenses)
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, expectedPkg.Version, a.Version, "bad version")
|
||||
assert.Equal(t, expectedPkg.Language, a.Language, "bad language")
|
||||
assert.Equal(t, expectedPkg.Type, a.Type, "bad type")
|
||||
assert.Equal(t, expectedPkg.Licenses, a.Licenses, "bad license count")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ const (
|
||||
noVersion = ""
|
||||
)
|
||||
|
||||
func parseYarnLock(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parseYarnLock(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
// in the case we find yarn.lock files in the node_modules directories, skip those
|
||||
// as the whole purpose of the lock file is for the specific dependencies of the project
|
||||
if pathContainsNodeModulesDirectory(path) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var packages []pkg.Package
|
||||
var packages []*pkg.Package
|
||||
scanner := bufio.NewScanner(reader)
|
||||
parsedPackages := internal.NewStringSet()
|
||||
currentPackage := noPackage
|
||||
@@ -106,8 +106,8 @@ func findPackageVersion(line string) string {
|
||||
return noVersion
|
||||
}
|
||||
|
||||
func newYarnLockPackage(name, version string) pkg.Package {
|
||||
return pkg.Package{
|
||||
func newYarnLockPackage(name, version string) *pkg.Package {
|
||||
return &pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.JavaScript,
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
|
||||
func TestPackageURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
distro *distro.Distro
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "golang",
|
||||
pkg: pkg.Package{
|
||||
Name: "github.com/anchore/syft",
|
||||
Version: "v0.1.0",
|
||||
@@ -23,14 +25,38 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "pip with vcs url",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.PythonPkg,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{
|
||||
VCS: "git",
|
||||
URL: "https://github.com/test/test.git",
|
||||
CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0?vcs_url=git+https:%2F%2Fgithub.com%2Ftest%2Ftest.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
name: "pip without vcs url",
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.PythonPkg,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "gem",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
@@ -39,6 +65,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:gem/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "npm",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
@@ -47,6 +74,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:npm/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "deb with arch",
|
||||
distro: &distro.Distro{
|
||||
Type: distro.Ubuntu,
|
||||
},
|
||||
@@ -63,6 +91,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64",
|
||||
},
|
||||
{
|
||||
name: "deb with epoch",
|
||||
distro: &distro.Distro{
|
||||
Type: distro.CentOS,
|
||||
},
|
||||
@@ -81,6 +110,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:rpm/centos/name@0.1.0-3?arch=amd64&epoch=2",
|
||||
},
|
||||
{
|
||||
name: "deb with nil epoch",
|
||||
distro: &distro.Distro{
|
||||
Type: distro.CentOS,
|
||||
},
|
||||
@@ -99,6 +129,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:rpm/centos/name@0.1.0-3?arch=amd64",
|
||||
},
|
||||
{
|
||||
name: "deb with unknown distro",
|
||||
distro: &distro.Distro{
|
||||
Type: distro.UnknownDistroType,
|
||||
},
|
||||
@@ -110,6 +141,7 @@ func TestPackageURL(t *testing.T) {
|
||||
expected: "pkg:deb/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "cargo",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
@@ -120,7 +152,7 @@ func TestPackageURL(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.pkg.Type)+"|"+test.expected, func(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := generatePackageURL(test.pkg, test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||
)
|
||||
|
||||
type ComposerLock struct {
|
||||
@@ -21,12 +20,9 @@ type Dependency struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// integrity check
|
||||
var _ common.ParserFn = parseComposerLock
|
||||
|
||||
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
|
||||
func parseComposerLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]pkg.Package, 0)
|
||||
func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]*pkg.Package, 0)
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
for {
|
||||
@@ -39,7 +35,7 @@ func parseComposerLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Re
|
||||
for _, pkgMeta := range lock.Packages {
|
||||
version := pkgMeta.Version
|
||||
name := pkgMeta.Name
|
||||
packages = append(packages, pkg.Package{
|
||||
packages = append(packages, &pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.PHP,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestParseComposerFileLock(t *testing.T) {
|
||||
expected := []pkg.Package{
|
||||
expected := []*pkg.Package{
|
||||
{
|
||||
Name: "adoy/fastcgi-client",
|
||||
Version: "1.0.2",
|
||||
|
||||
@@ -40,8 +40,8 @@ func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
|
||||
var _ common.ParserFn = parseComposerLock
|
||||
|
||||
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
|
||||
func parseInstalledJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]pkg.Package, 0)
|
||||
func parseInstalledJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]*pkg.Package, 0)
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
for {
|
||||
@@ -54,7 +54,7 @@ func parseInstalledJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.R
|
||||
for _, pkgMeta := range lock.Packages {
|
||||
version := pkgMeta.Version
|
||||
name := pkgMeta.Name
|
||||
packages = append(packages, pkg.Package{
|
||||
packages = append(packages, &pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.PHP,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestParseInstalledJsonComposerV1(t *testing.T) {
|
||||
expected := []pkg.Package{
|
||||
expected := []*pkg.Package{
|
||||
{
|
||||
Name: "asm89/stack-cors",
|
||||
Version: "1.3.0",
|
||||
@@ -41,7 +41,7 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseInstalledJsonComposerV2(t *testing.T) {
|
||||
expected := []pkg.Package{
|
||||
expected := []*pkg.Package{
|
||||
{
|
||||
Name: "asm89/stack-cors",
|
||||
Version: "1.3.0",
|
||||
|
||||
@@ -2,7 +2,9 @@ package python
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -74,7 +76,7 @@ func (c *PackageCataloger) catalogEggOrWheel(resolver source.FileResolver, metad
|
||||
licenses = []string{metadata.License}
|
||||
}
|
||||
|
||||
return &pkg.Package{
|
||||
p := &pkg.Package{
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
FoundBy: c.Name(),
|
||||
@@ -84,7 +86,11 @@ func (c *PackageCataloger) catalogEggOrWheel(resolver source.FileResolver, metad
|
||||
Type: pkg.PythonPkg,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Metadata: *metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// fetchRecordFiles finds a corresponding RECORD file for the given python package metadata file and returns the set of file records contained.
|
||||
@@ -148,6 +154,40 @@ func (c *PackageCataloger) fetchTopLevelPackages(resolver source.FileResolver, m
|
||||
return pkgs, sources, nil
|
||||
}
|
||||
|
||||
func (c *PackageCataloger) fetchDirectURLData(resolver source.FileResolver, metadataLocation source.Location) (d *pkg.PythonDirectURLOriginInfo, sources []source.Location, err error) {
|
||||
parentDir := filepath.Dir(metadataLocation.RealPath)
|
||||
directURLPath := filepath.Join(parentDir, "direct_url.json")
|
||||
directURLLocation := resolver.RelativeFileByPath(metadataLocation, directURLPath)
|
||||
|
||||
if directURLLocation == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
sources = append(sources, *directURLLocation)
|
||||
|
||||
directURLContents, err := resolver.FileContentsByLocation(*directURLLocation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer internal.CloseAndLogError(directURLContents, directURLLocation.VirtualPath)
|
||||
|
||||
buffer, err := ioutil.ReadAll(directURLContents)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var directURLJson pkg.DirectURLOrigin
|
||||
if err := json.Unmarshal(buffer, &directURLJson); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &pkg.PythonDirectURLOriginInfo{
|
||||
URL: directURLJson.URL,
|
||||
CommitID: directURLJson.VCSInfo.CommitID,
|
||||
VCS: directURLJson.VCSInfo.VCS,
|
||||
}, sources, nil
|
||||
}
|
||||
|
||||
// assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from.
|
||||
func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*pkg.PythonPackageMetadata, []source.Location, error) {
|
||||
var sources = []source.Location{metadataLocation}
|
||||
@@ -179,5 +219,13 @@ func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver source.FileResolv
|
||||
sources = append(sources, s...)
|
||||
metadata.TopLevelPackages = p
|
||||
|
||||
// attach any direct-url package data found for the given wheel/egg installation
|
||||
d, s, err := c.fetchDirectURLData(resolver, metadataLocation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sources = append(sources, s...)
|
||||
metadata.DirectURLOrigin = d
|
||||
|
||||
return &metadata, sources, nil
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
|
||||
"test-fixtures/dist-info/METADATA",
|
||||
"test-fixtures/dist-info/RECORD",
|
||||
"test-fixtures/dist-info/top_level.txt",
|
||||
"test-fixtures/dist-info/direct_url.json",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Pygments",
|
||||
@@ -82,6 +83,7 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
|
||||
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
|
||||
},
|
||||
TopLevelPackages: []string{"pygments", "something_else"},
|
||||
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,8 +38,8 @@ type Dependency struct {
|
||||
var _ common.ParserFn = parsePipfileLock
|
||||
|
||||
// parsePipfileLock is a parser function for Pipfile.lock contents, returning "Default" python packages discovered.
|
||||
func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]pkg.Package, 0)
|
||||
func parsePipfileLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make([]*pkg.Package, 0)
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
for {
|
||||
@@ -51,7 +51,7 @@ func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Rel
|
||||
}
|
||||
for name, pkgMeta := range lock.Default {
|
||||
version := strings.TrimPrefix(pkgMeta.Version, "==")
|
||||
packages = append(packages, pkg.Package{
|
||||
packages = append(packages, &pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.Python,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
var _ common.ParserFn = parsePoetryLock
|
||||
|
||||
// parsePoetryLock is a parser function for poetry.lock contents, returning all python packages discovered.
|
||||
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parsePoetryLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
tree, err := toml.LoadReader(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to load poetry.lock for parsing: %v", err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user