Compare commits

...
14 Commits
Author SHA1 Message Date
Alex Goodman 52bac6e2fd Add enterprise upload capability (#285)
* add support to upload results to enterprise

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add package sbom upload

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add dockerfile support

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add manifest, index, and dockerfile import functions

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* schema version to json output + enhance json schema generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* modify package SBOM shape to be entire syft document + add etui updates

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add import image config and manifest support

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add config options for import to enterprise

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* incorporate final stereoscope and client-go deps

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-12-09 22:20:53 -05:00
Toure Dunnon 2d0c127419 Merge pull request #282 from anchore/issue_270
Corrected syft cyclonedx generated bom to adhere to the specifications found in CycloneDX 1.2
2020-12-04 13:48:55 -05:00
Toure Dunnon 1a124bd77b added: regenerated new test fixtures to reflect change in specification.
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-12-04 11:48:20 -05:00
Toure Dunnon a5fd83b21d added: correct the bom descriptor to meet the cyclonedx 1.2
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-12-04 11:48:20 -05:00
Alex Goodman f87c59b4eb Merge pull request #286 from anchore/rm-tree-catalog-sync-check
Bump stereoscope to remove tree-catalog syft check
2020-12-03 16:32:24 -05:00
Alex Goodman 3ce7eabc98 bump stereoscope to remove tree-catalog syft check
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-12-03 16:20:17 -05:00
Alex Goodman 6f9ded60ed Merge pull request #279 from anchore/enhance-java-cpe-by-group-id
Include CPEs with elements from POM GroupId fields
2020-12-02 07:50:31 -05:00
Dan Luhring 65cbacd135 Clarify python wheel parsing process (#281)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-12-01 16:30:30 -05:00
Alex Goodman 2989d3d975 include CPEs with elementds from POM GroupId fields
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-12-01 08:00:49 -05:00
Dan Luhring ae71b8832d Update stereoscope version to fix opaque directory merge issue (#278)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-30 11:37:44 -05:00
Alex Goodman c42b036f46 Merge pull request #276 from anchore/generate-json-schema
Generate json schema from struct definitions
2020-11-23 11:22:33 -05:00
Alex Goodman 8a17bfb69f generate json schema from struct definitions
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-20 15:47:10 -05:00
Alex Goodman 0ed30138c4 Merge pull request #271 from anchore/cpe-generation
Move package PURL and CPEs to Package definition
2020-11-19 12:38:20 -05:00
Alex Goodman 3aaa0e5566 move package purl and cpes (identities) to pkg.Package
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-19 11:25:51 -05:00
76 changed files with 3078 additions and 1130 deletions
+1
View File
@@ -3,6 +3,7 @@ CHANGELOG.md
/snapshot
.server/
.vscode/
.history/
*.fingerprint
*.tar
*.jar
+5 -17
View File
@@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 72
COVERAGE_THRESHOLD := 68
## Build variables
DISTDIR=./dist
@@ -153,17 +153,9 @@ fixtures:
cd syft/cataloger/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: clean-json-schema-examples integration ## Generate a new json schema for the json presenter, derived from integration test cases
docker run \
-i \
--rm \
-v $(shell pwd)/schema/json:/work \
-w /work \
python:3.8 \
bash -x -c "\
pip install -r requirements.txt && \
python generate.py \
"
generate-json-schema: ## Generate a new json schema
cd schema/json
go run generate.go
.PHONY: clear-test-cache
clear-test-cache: ## Delete all test cache (built docker image tars)
@@ -288,7 +280,7 @@ release: clean-dist ci-bootstrap-mac changelog-release ## Build and publish fina
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
.PHONY: clean
clean: clean-dist clean-snapshot clean-json-schema-examples ## Remove previous builds and result reports
clean: clean-dist clean-snapshot ## Remove previous builds and result reports
rm -rf $(RESULTSDIR)/*
.PHONY: clean-snapshot
@@ -298,7 +290,3 @@ clean-snapshot:
.PHONY: clean-dist
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-json-schema-examples
clean-json-schema-examples:
rm -f schema/json/examples/*
+37 -9
View File
@@ -81,30 +81,58 @@ Configuration search paths:
Configuration options (example values are the default):
```yaml
# same as -o ; the output format of the SBOM report (options: table, text, json)
# the output format of the SBOM report (options: table, text, json)
# same as -o ; SYFT_OUTPUT env var
output: "table"
# same as -s ; the search space to look for packages (options: all-layers, squashed)
# the search space to look for packages (options: all-layers, squashed)
# same as -s ; SYFT_SCOPE env var
scope: "squashed"
# same as -q ; suppress all output (except for the SBOM report)
# suppress all output (except for the SBOM report)
# same as -q ; SYFT_QUIET env var
quiet: false
# enable/disable checking for application updates on startup
# same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true
log:
# use structured logging
# same as SYFT_LOG_STRUCTURED env var
structured: false
# the log level; note: detailed logging suppress the ETUI
# same as SYFT_LOG_LEVEL env var
level: "error"
# location to write the log file (default is not to have a log file)
# same as SYFT_LOG_FILE env var
file: ""
# enable/disable checking for application updates on startup
check-for-app-update: true
```
anchore:
# (feature-preview) enable uploading of results to Anchore Enterprise automatically (supported on Enterprise 3.0+)
# same as SYFT_ANCHORE_UPLOAD_ENABLED env var
upload-enabled: false
## Future plans
# (feature-preview) the Anchore Enterprise Host or URL to upload results to (supported on Enterprise 3.0+)
# same as -H ; SYFT_ANCHORE_HOST env var
host: ""
The following areas of potential development are currently being investigated:
- Establish a stable interchange format w/Grype
# (feature-preview) the path after the host to the Anchore External API (supported on Enterprise 3.0+)
# same as SYFT_ANCHORE_PATH env var
path: ""
# (feature-preview) the username to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
# same as -u ; SYFT_ANCHORE_USERNAME env var
username: ""
# (feature-preview) the password to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
# same as -p ; SYFT_ANCHORE_PASSWORD env var
password: ""
# (feature-preview) path to dockerfile to be uploaded with the syft results to Anchore Enterprise (supported on Enterprise 3.0+)
# same as -d ; SYFT_ANCHORE_DOCKERFILE env var
dockerfile: ""
```
+57 -17
View File
@@ -4,21 +4,17 @@ import (
"fmt"
"os"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"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/syft"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
"gopkg.in/yaml.v2"
)
var appConfig *config.Application
@@ -57,8 +53,13 @@ func setGlobalCliOptions() {
os.Exit(1)
}
setGlobalFormatOptions()
setGlobalUploadOptions()
}
func setGlobalFormatOptions() {
// output & formatting options
flag = "output"
flag := "output"
rootCmd.Flags().StringP(
flag, "o", string(presenter.TablePresenter),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
@@ -81,12 +82,57 @@ func setGlobalCliOptions() {
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
}
func setGlobalUploadOptions() {
flag := "host"
rootCmd.Flags().StringP(
flag, "H", "",
"the hostname or URL of the Anchore Engine/Enterprise instance to upload to",
)
if err := viper.BindPFlag("anchore.host", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "username"
rootCmd.Flags().StringP(
flag, "u", "",
"the username to authenticate against Anchore Engine/Enterprise",
)
if err := viper.BindPFlag("anchore.username", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "password"
rootCmd.Flags().StringP(
flag, "p", "",
"the password to authenticate against Anchore Engine/Enterprise",
)
if err := viper.BindPFlag("anchore.password", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "dockerfile"
rootCmd.Flags().StringP(
flag, "d", "",
"include dockerfile for upload to Anchore Engine/Enterprise",
)
if err := viper.BindPFlag("anchore.dockerfile", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '#{flag}': #{err}")
os.Exit(1)
}
}
func initAppConfig() {
cfg, err := config.LoadConfigFromFile(viper.GetViper(), &cliOpts)
cfgVehicle := viper.GetViper()
wasHostnameSet := rootCmd.Flags().Changed("host")
cfg, err := config.LoadApplicationConfig(cfgVehicle, cliOpts, wasHostnameSet)
if err != nil {
fmt.Printf("failed to load application config: \n\t%+v\n", err)
os.Exit(1)
}
appConfig = cfg
}
@@ -107,13 +153,7 @@ func initLogging() {
}
func logAppConfig() {
appCfgStr, err := yaml.Marshal(&appConfig)
if err != nil {
log.Debugf("Could not display application config: %+v", err)
} else {
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(string(appCfgStr)))
}
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
}
func initEventBus() {
+65 -1
View File
@@ -3,10 +3,18 @@ package cmd
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
@@ -23,7 +31,7 @@ import (
var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A tool for generating a Software Bill Of Materials (SBOM) from container images and filesystems",
Short: "A tool for generating a Software Bill Of Materials (PackageSBOM) from container images and filesystems",
Long: internal.Tprintf(`
Supports the following image sources:
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
@@ -98,6 +106,13 @@ func startWorker(userInput string) <-chan error {
return
}
if appConfig.Anchore.UploadEnabled {
if err := doImport(src, src.Metadata, catalog, distro); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, src.Metadata, catalog, distro),
@@ -106,6 +121,55 @@ func startWorker(userInput string) <-chan error {
return errs
}
func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
// TODO: ETUI element for this
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
return fmt.Errorf("unable to upload results: only images are supported")
}
var dockerfileContents []byte
if appConfig.Anchore.Dockerfile != "" {
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
fh, err := os.Open(appConfig.Anchore.Dockerfile)
if err != nil {
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
dockerfileContents, err = ioutil.ReadAll(fh)
if err != nil {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
}
var scheme string
var hostname = appConfig.Anchore.Host
urlFields := strings.Split(hostname, "://")
if len(urlFields) > 1 {
scheme = urlFields[0]
hostname = urlFields[1]
}
c, err := anchore.NewClient(anchore.Configuration{
Hostname: hostname,
Username: appConfig.Anchore.Username,
Password: appConfig.Anchore.Password,
Scheme: scheme,
})
if err != nil {
return fmt.Errorf("failed to create anchore client: %+v", err)
}
if err := c.Import(context.Background(), src.Image.Metadata, s, catalog, d, dockerfileContents); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
}
return nil
}
func doRunCmd(_ *cobra.Command, args []string) error {
userInput := args[0]
errs := startWorker(userInput)
+5 -1
View File
@@ -4,13 +4,16 @@ go 1.14
require (
github.com/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20201106140100-12e75c48f409
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e
github.com/bmatcuk/doublestar v1.3.3
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.7
github.com/google/uuid v1.1.1
github.com/gookit/color v1.2.7
@@ -21,6 +24,7 @@ require (
github.com/olekukonko/tablewriter v0.0.4
github.com/package-url/packageurl-go v0.1.0
github.com/pelletier/go-toml v1.8.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.6.0
github.com/spf13/afero v1.2.2
+23
View File
@@ -119,11 +119,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/anchore/client-go v0.0.0-20201120223920-9f812673f4d6/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74 h1:9kkKTIyXJC+/syUcY6KWxFoJZJ+GWwrIscF+gBY067k=
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12 h1:xbeIbn5F52JVx3RUIajxCj8b0y+9lywspql4sFhcxWQ=
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12/go.mod h1:juoyWXIj7sJ1IDl4E/KIfyLtovbs5XQVSIdaQifFQT8=
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
@@ -132,7 +137,17 @@ 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/stereoscope v0.0.0-20201106140100-12e75c48f409 h1:xKSpDRjmYrEFrdMeDh4AuSUAFc99pdro6YFBKxy2um0=
github.com/anchore/stereoscope v0.0.0-20201106140100-12e75c48f409/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
github.com/anchore/stereoscope v0.0.0-20201130153727-b3f1fad856b0 h1:wa0hdnvBeCpI+rmzDbPG7k5SKlsGkot7aZ8Az1i/vws=
github.com/anchore/stereoscope v0.0.0-20201130153727-b3f1fad856b0/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
github.com/anchore/stereoscope v0.0.0-20201203153145-3f9a05a624d7 h1:G3LnRqHL/IIeQZTAMtDOJNYfSYsXLNCZX4DCiS0R0FY=
github.com/anchore/stereoscope v0.0.0-20201203153145-3f9a05a624d7/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
github.com/anchore/stereoscope v0.0.0-20201203222654-09e79bf5fef4 h1:XDuCqOWKyQQlKhd9kEDnyKbvSCwShKBDCsyBmD/ALYs=
github.com/anchore/stereoscope v0.0.0-20201203222654-09e79bf5fef4/go.mod h1:/dHAFjYflH/1tzhdHAcnMCjprMch+YzHJKi59m/1KCM=
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e h1:vHUqHTvH9/oxdDDh1fxS9Ls9gWGytKO7XbbzcQ9MBwI=
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e/go.mod h1:/dHAFjYflH/1tzhdHAcnMCjprMch+YzHJKi59m/1KCM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
@@ -254,9 +269,12 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/facebookincubator/nvdtools v0.1.4 h1:x1Ucw9+bSkMd8DJJN4jNQ1Lk4PSFlJarGOxp9D6WUMo=
github.com/facebookincubator/nvdtools v0.1.4/go.mod h1:0/FIVnSEl9YHXLq3tKBPpKaI0iUceDhdSHPlIwIX44Y=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -499,6 +517,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@@ -718,6 +738,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE=
github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989 h1:rq2/kILQnPtq5oL4+IAjgVOjh5e2yj2aaCYi7squEvI=
@@ -785,6 +807,7 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+58
View File
@@ -0,0 +1,58 @@
package anchore
import (
"context"
"fmt"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
)
type Configuration struct {
Hostname string
Username string
Password string
UserAgent string
Scheme string
}
type Client struct {
config Configuration
client *external.APIClient
}
func NewClient(cfg Configuration) (*Client, error) {
if cfg.UserAgent == "" {
versionInfo := version.FromBuild()
// format: product / product-version comment
cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform)
}
if cfg.Scheme == "" {
cfg.Scheme = "https"
}
return &Client{
config: cfg,
client: external.NewAPIClient(&external.Configuration{
Host: cfg.Hostname,
UserAgent: cfg.UserAgent,
Scheme: cfg.Scheme,
}),
}, nil
}
func (c *Client) newRequestContext(parentContext context.Context) context.Context {
if parentContext == nil {
parentContext = context.Background()
}
return context.WithValue(
parentContext,
external.ContextBasicAuth,
external.BasicAuth{
UserName: c.config.Username,
Password: c.config.Password,
},
)
}
+128
View File
@@ -0,0 +1,128 @@
package anchore
import (
"context"
"errors"
"fmt"
"time"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
func importProgress(source string) (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{
// this is the number of stages to expect; start + individual endpoints + stop
Total: 6,
}
bus.Publish(partybus.Event{
Type: event.ImportStarted,
Source: source,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: progress.Stager(stage),
Progressable: prog,
}),
})
return stage, prog
}
// nolint:funlen
func (c *Client) Import(ctx context.Context, imageMetadata image.Metadata, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, dockerfile []byte) error {
stage, prog := importProgress(imageMetadata.ID)
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
authedCtx := c.newRequestContext(ctxWithTimeout)
stage.Current = "starting session"
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
if err != nil {
var detail = "no details given"
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
detail = string(openAPIErr.Body())
}
return fmt.Errorf("unable to start import session: %w: %s", err, detail)
}
prog.N++
sessionID := startOperation.Uuid
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, s, catalog, d, stage)
if err != nil {
return fmt.Errorf("failed to import Package SBOM: %w", err)
}
prog.N++
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawManifest, stage)
if err != nil {
return fmt.Errorf("failed to import Manifest: %w", err)
}
prog.N++
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawConfig, stage)
if err != nil {
return fmt.Errorf("failed to import Config: %w", err)
}
prog.N++
dockerfileDigest, err := importDockerfile(authedCtx, c.client.ImportsApi, sessionID, dockerfile, stage)
if err != nil {
return fmt.Errorf("failed to import Dockerfile: %w", err)
}
prog.N++
stage.Current = "finalizing"
imageModel := addImageModel(imageMetadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID)
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, nil)
if err != nil {
var detail = "no details given"
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
detail = string(openAPIErr.Body())
}
return fmt.Errorf("unable to complete import session=%q: %w: %s", sessionID, err, detail)
}
prog.N++
stage.Current = ""
prog.SetCompleted()
return nil
}
func addImageModel(imageMetadata image.Metadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID string) external.ImageAnalysisRequest {
var tags = make([]string, len(imageMetadata.Tags))
for i, t := range imageMetadata.Tags {
tags[i] = t.String()
}
return external.ImageAnalysisRequest{
Source: external.ImageSource{
Import: &external.ImageImportManifest{
Contents: external.ImportContentDigests{
Packages: packageDigest,
Manifest: manifestDigest,
Dockerfile: dockerfileDigest,
ImageConfig: configDigest,
},
Tags: tags,
Digest: imageMetadata.ManifestDigest,
LocalImageId: imageMetadata.ID,
OperationUuid: sessionID,
},
},
}
}
+43
View File
@@ -0,0 +1,43 @@
// nolint:dupl
package anchore
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/wagoodman/go-progress"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/log"
)
type configImportAPI interface {
ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
}
func importConfig(ctx context.Context, api configImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
if len(manifest) > 0 {
log.Debug("importing image config")
stage.Current = "image config"
response, httpResponse, err := api.ImportImageConfig(ctx, sessionID, string(manifest))
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
log.Errorf("api response: %+v", string(openAPIErr.Body()))
}
return "", fmt.Errorf("unable to import Config: %w", err)
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode != 200 {
return "", fmt.Errorf("unable to import Config: %s", httpResponse.Status)
}
return response.Digest, nil
}
return "", nil
}
+123
View File
@@ -0,0 +1,123 @@
package anchore
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/wagoodman/go-progress"
"github.com/anchore/client-go/pkg/external"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep"
)
type mockConfigImportAPI struct {
sessionID string
model string
httpResponse *http.Response
err error
ctx context.Context
responseDigest string
wasCalled bool
}
func (m *mockConfigImportAPI) ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
m.wasCalled = true
m.model = contents
m.sessionID = sessionID
m.ctx = ctx
if m.httpResponse == nil {
m.httpResponse = &http.Response{}
}
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
}
func TestConfigImport(t *testing.T) {
sessionID := "my-session"
tests := []struct {
name string
manifest string
api *mockConfigImportAPI
expectsError bool
expectsCall bool
}{
{
name: "Go case: import works",
manifest: "the-manifest-contents!",
api: &mockConfigImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
},
expectsCall: true,
},
{
name: "No manifest provided",
manifest: "",
api: &mockConfigImportAPI{},
expectsCall: false,
},
{
name: "API returns an error",
manifest: "the-manifest-contents!",
api: &mockConfigImportAPI{
err: fmt.Errorf("api error, something went wrong"),
},
expectsError: true,
expectsCall: true,
},
{
name: "API HTTP-level error",
manifest: "the-manifest-contents!",
api: &mockConfigImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
expectsError: true,
expectsCall: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importConfig(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
t.Fatalf("did not expect an error, but got: %+v", err)
} else if err == nil && test.expectsError {
t.Fatalf("did expect an error, but got none")
}
if !test.api.wasCalled && test.expectsCall {
t.Fatalf("was not called!")
} else if test.api.wasCalled && !test.expectsCall {
t.Fatalf("should not have been called")
}
if !test.expectsCall {
return
}
if digest != test.api.responseDigest {
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
}
// validating that the mock got the right parameters
if test.api.sessionID != sessionID {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(test.api.model, test.manifest) {
t.Errorf("model difference: %s", d)
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
// nolint:dupl
package anchore
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/internal/log"
"github.com/anchore/client-go/pkg/external"
)
type dockerfileImportAPI interface {
ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
}
func importDockerfile(ctx context.Context, api dockerfileImportAPI, sessionID string, dockerfile []byte, stage *progress.Stage) (string, error) {
if len(dockerfile) > 0 {
log.Debug("importing dockerfile")
stage.Current = "dockerfile"
response, httpResponse, err := api.ImportImageDockerfile(ctx, sessionID, string(dockerfile))
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
log.Errorf("api response: %+v", string(openAPIErr.Body()))
}
return "", fmt.Errorf("unable to import Dockerfile: %w", err)
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode != 200 {
return "", fmt.Errorf("unable to import Dockerfile: %s", httpResponse.Status)
}
return response.Digest, nil
}
return "", nil
}
+124
View File
@@ -0,0 +1,124 @@
package anchore
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/wagoodman/go-progress"
"github.com/docker/docker/pkg/ioutils"
"github.com/anchore/client-go/pkg/external"
"github.com/go-test/deep"
)
type mockDockerfileImportAPI struct {
sessionID string
model string
httpResponse *http.Response
err error
ctx context.Context
responseDigest string
wasCalled bool
}
func (m *mockDockerfileImportAPI) ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
m.wasCalled = true
m.model = contents
m.sessionID = sessionID
m.ctx = ctx
if m.httpResponse == nil {
m.httpResponse = &http.Response{}
}
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
}
func TestDockerfileImport(t *testing.T) {
sessionID := "my-session"
tests := []struct {
name string
dockerfile string
api *mockDockerfileImportAPI
expectsError bool
expectsCall bool
}{
{
name: "Go case: import works",
dockerfile: "the-manifest-contents!",
api: &mockDockerfileImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
},
expectsCall: true,
},
{
name: "No manifest provided",
dockerfile: "",
api: &mockDockerfileImportAPI{},
expectsCall: false,
},
{
name: "API returns an error",
dockerfile: "the-manifest-contents!",
api: &mockDockerfileImportAPI{
err: fmt.Errorf("api error, something went wrong"),
},
expectsError: true,
expectsCall: true,
},
{
name: "API HTTP-level error",
dockerfile: "the-manifest-contents!",
api: &mockDockerfileImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
expectsError: true,
expectsCall: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importDockerfile(context.TODO(), test.api, sessionID, []byte(test.dockerfile), &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
t.Fatalf("did not expect an error, but got: %+v", err)
} else if err == nil && test.expectsError {
t.Fatalf("did expect an error, but got none")
}
if !test.api.wasCalled && test.expectsCall {
t.Fatalf("was not called!")
} else if test.api.wasCalled && !test.expectsCall {
t.Fatalf("should not have been called")
}
if !test.expectsCall {
return
}
if digest != test.api.responseDigest {
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
}
// validating that the mock got the right parameters
if test.api.sessionID != sessionID {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(test.api.model, test.dockerfile) {
t.Errorf("model difference: %s", d)
}
})
}
}
+43
View File
@@ -0,0 +1,43 @@
// nolint: dupl
package anchore
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/wagoodman/go-progress"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/log"
)
type manifestImportAPI interface {
ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
}
func importManifest(ctx context.Context, api manifestImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
if len(manifest) > 0 {
log.Debug("importing image manifest")
stage.Current = "image manifest"
response, httpResponse, err := api.ImportImageManifest(ctx, sessionID, string(manifest))
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
log.Errorf("api response: %+v", string(openAPIErr.Body()))
}
return "", fmt.Errorf("unable to import Manifest: %w", err)
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode != 200 {
return "", fmt.Errorf("unable to import Manifest: %s", httpResponse.Status)
}
return response.Digest, nil
}
return "", nil
}
+123
View File
@@ -0,0 +1,123 @@
package anchore
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/wagoodman/go-progress"
"github.com/anchore/client-go/pkg/external"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep"
)
type mockManifestImportAPI struct {
sessionID string
model string
httpResponse *http.Response
err error
ctx context.Context
responseDigest string
wasCalled bool
}
func (m *mockManifestImportAPI) ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
m.wasCalled = true
m.model = contents
m.sessionID = sessionID
m.ctx = ctx
if m.httpResponse == nil {
m.httpResponse = &http.Response{}
}
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
}
func TestManifestImport(t *testing.T) {
sessionID := "my-session"
tests := []struct {
name string
manifest string
api *mockManifestImportAPI
expectsError bool
expectsCall bool
}{
{
name: "Go case: import works",
manifest: "the-manifest-contents!",
api: &mockManifestImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
},
expectsCall: true,
},
{
name: "No manifest provided",
manifest: "",
api: &mockManifestImportAPI{},
expectsCall: false,
},
{
name: "API returns an error",
manifest: "the-manifest-contents!",
api: &mockManifestImportAPI{
err: fmt.Errorf("api error, something went wrong"),
},
expectsError: true,
expectsCall: true,
},
{
name: "API HTTP-level error",
manifest: "the-manifest-contents!",
api: &mockManifestImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
expectsError: true,
expectsCall: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importManifest(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
t.Fatalf("did not expect an error, but got: %+v", err)
} else if err == nil && test.expectsError {
t.Fatalf("did expect an error, but got none")
}
if !test.api.wasCalled && test.expectsCall {
t.Fatalf("was not called!")
} else if test.api.wasCalled && !test.expectsCall {
t.Fatalf("should not have been called")
}
if !test.expectsCall {
return
}
if digest != test.api.responseDigest {
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
}
// validating that the mock got the right parameters
if test.api.sessionID != sessionID {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(test.api.model, test.manifest) {
t.Errorf("model difference: %s", d)
}
})
}
}
+69
View File
@@ -0,0 +1,69 @@
package anchore
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/wagoodman/go-progress"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/source"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
type packageSBOMImportAPI interface {
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
}
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
pres := jsonPresenter.NewPresenter(catalog, s, d)
err := pres.Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
}
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
var model external.ImagePackageManifest
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
return nil, fmt.Errorf("unable to convert JSON presenter output to import model: %w", err)
}
return &model, nil
}
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
log.Debug("importing package SBOM")
stage.Current = "package SBOM"
model, err := packageSbomModel(s, catalog, d)
if err != nil {
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
}
response, httpResponse, err := api.ImportImagePackages(ctx, sessionID, *model)
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
log.Errorf("api response: %+v", string(openAPIErr.Body()))
}
return "", fmt.Errorf("unable to import PackageSBOM: %w", err)
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode != 200 {
return "", fmt.Errorf("unable to import PackageSBOM: %s", httpResponse.Status)
}
return response.Digest, nil
}
@@ -0,0 +1,254 @@
package anchore
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/wagoodman/go-progress"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
"github.com/anchore/syft/syft/distro"
"github.com/docker/docker/pkg/ioutils"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/go-test/deep"
)
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
// this test is tailored towards the assumption that the import doc shape and the syft json shape are the same.
// TODO: replace this as the document shapes diverge.
func TestPackageSbomToModel(t *testing.T) {
m := source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Scope: "scope!",
Layers: []source.LayerMetadata{
{
MediaType: "layer-metadata-type!",
Digest: "layer-digest",
Size: 20,
},
},
Size: 10,
ManifestDigest: "sha256:digest!",
MediaType: "mediatype!",
Tags: nil,
},
}
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
p := pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: []source.Location{
{
Path: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
}
c := pkg.NewCatalog(p)
model, err := packageSbomModel(m, c, &d)
if err != nil {
t.Fatalf("unable to generate model from source material: %+v", err)
}
var modelJSON []byte
modelJSON, err = json.Marshal(&model)
if err != nil {
t.Fatalf("unable to marshal model: %+v", err)
}
var buf bytes.Buffer
pres := jsonPresenter.NewPresenter(c, m, &d)
if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err)
}
// unmarshal expected result
var expectedDoc jsonPresenter.Document
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
// unmarshal actual result
var actualDoc jsonPresenter.Document
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
for _, d := range deep.Equal(actualDoc, expectedDoc) {
t.Errorf("diff: %+v", d)
}
}
type mockPackageSBOMImportAPI struct {
sessionID string
model external.ImagePackageManifest
httpResponse *http.Response
err error
ctx context.Context
responseDigest string
}
func (m *mockPackageSBOMImportAPI) ImportImagePackages(ctx context.Context, sessionID string, model external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error) {
m.model = model
m.sessionID = sessionID
m.ctx = ctx
if m.httpResponse == nil {
m.httpResponse = &http.Response{}
}
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
}
func TestPackageSbomImport(t *testing.T) {
catalog := pkg.NewCatalog(pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: []source.Location{
{
Path: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "p-name",
Version: "p-version",
License: "p-license",
Author: "p-author",
AuthorEmail: "p-email",
Platform: "p-platform",
Files: []pkg.PythonFileRecord{
{
Path: "p-path",
Digest: &pkg.PythonFileDigest{
Algorithm: "p-alg",
Value: "p-digest",
},
Size: "p-size",
},
},
SitePackagesRootPath: "p-site-packages-root",
TopLevelPackages: []string{"top-level"},
},
})
m := source.Metadata{
Scheme: "a-schema",
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Scope: "scope!",
Layers: nil,
Size: 10,
ManifestDigest: "sha256:digest!",
MediaType: "mediatype!",
Tags: nil,
},
}
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
theModel, err := packageSbomModel(m, catalog, &d)
if err != nil {
t.Fatalf("could not get sbom model: %+v", err)
}
sessionID := "my-session"
tests := []struct {
name string
api *mockPackageSBOMImportAPI
expectsError bool
}{
{
name: "Go case: import works",
api: &mockPackageSBOMImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
},
},
{
name: "API returns an error",
api: &mockPackageSBOMImportAPI{
err: fmt.Errorf("API error, something went wrong."),
},
expectsError: true,
},
{
name: "API HTTP-level error",
api: &mockPackageSBOMImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
expectsError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
t.Fatalf("did not expect an error, but got: %+v", err)
} else if err == nil && test.expectsError {
t.Fatalf("did expect an error, but got none")
}
if digest != test.api.responseDigest {
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
}
// validating that the mock got the right parameters (api.ImportImagePackages)
if test.api.sessionID != sessionID {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(&test.api.model, theModel) {
t.Errorf("model difference: %s", d)
}
})
}
}
+79 -36
View File
@@ -12,66 +12,71 @@ import (
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
type CliOnlyOptions struct {
ConfigPath string
Verbosity int
}
// Application is the main syft application configuration.
type Application struct {
ConfigPath string
PresenterOpt presenter.Option
Output string `mapstructure:"output"`
ScopeOpt source.Scope
Scope string `mapstructure:"scope"`
Quiet bool `mapstructure:"quiet"`
Log Logging `mapstructure:"log"`
CliOptions CliOnlyOptions
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
ConfigPath string `yaml:",omitempty"` // the location where the application config was read from (either from -c or discovered while loading)
PresenterOpt presenter.Option `yaml:"-"` // -o, the native Presenter.Option to use for report formatting
Output string `yaml:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
ScopeOpt source.Scope `yaml:"-"` // -s, the native source.Scope option to use for how to catalog the container image
Scope string `yaml:"scope" mapstructure:"scope"` // -s, the source.Scope string hint for how to catalog the container image
Quiet bool `yaml:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
Log logging `yaml:"log" mapstructure:"log"` // all logging-related options
CliOptions CliOnlyOptions `yaml:"-"` // all options only available through the CLI (not via env vars or config)
CheckForAppUpdate bool `yaml:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Anchore anchore `yaml:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
}
type Logging struct {
Structured bool `mapstructure:"structured"`
LevelOpt logrus.Level
Level string `mapstructure:"level"`
FileLocation string `mapstructure:"file"`
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
type CliOnlyOptions struct {
ConfigPath string // -c. where the read config is on disk
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
}
func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("log.level", "")
v.SetDefault("log.file", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
// logging contains all logging-related configuration options available to the user via the application config.
type logging struct {
Structured bool `yaml:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
LevelOpt logrus.Level `yaml:"level"` // the native log level object used by the logger
Level string `yaml:"-" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" mapstructure:"file"` // the file path to write logs to
}
func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, error) {
type anchore struct {
// upload options
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
}
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions, wasHostnameSet bool) (*Application, error) {
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
setNonCliDefaultValues(v)
if cliOpts != nil {
_ = readConfig(v, cliOpts.ConfigPath)
} else {
_ = readConfig(v, "")
}
_ = readConfig(v, cliOpts.ConfigPath)
config := &Application{
CliOptions: *cliOpts,
CliOptions: cliOpts,
}
err := v.Unmarshal(config)
if err != nil {
if err := v.Unmarshal(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
config.ConfigPath = v.ConfigFileUsed()
err = config.Build()
if err != nil {
if err := config.build(v, wasHostnameSet); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return config, nil
}
func (cfg *Application) Build() error {
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
func (cfg *Application) build(v *viper.Viper, wasHostnameSet bool) error {
// set the presenter
presenterOption := presenter.ParseOption(cfg.Output)
if presenterOption == presenter.UnknownPresenter {
@@ -115,10 +120,40 @@ func (cfg *Application) Build() error {
}
}
}
// check if upload should be done relative to the CLI and config behavior
if !v.IsSet("anchore.upload-enabled") && wasHostnameSet {
// we know the user didn't specify to upload in the config file and a --hostname option was provided (so set upload)
cfg.Anchore.UploadEnabled = true
}
if !cfg.Anchore.UploadEnabled && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
return nil
}
func (cfg Application) String() string {
// redact sensitive information
if cfg.Anchore.Username != "" {
cfg.Anchore.Username = "********"
}
if cfg.Anchore.Password != "" {
cfg.Anchore.Password = "********"
}
// yaml is pretty human friendly (at least when compared to json)
appCfgStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}
return string(appCfgStr)
}
// readConfig attempts to read the given config path from disk or discover an alternate store location
func readConfig(v *viper.Viper, configPath string) error {
v.AutomaticEnv()
v.SetEnvPrefix(internal.ApplicationName)
@@ -174,3 +209,11 @@ func readConfig(v *viper.Viper, configPath string) error {
return fmt.Errorf("application config not found")
}
// setNonCliDefaultValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value)
func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("log.level", "")
v.SetDefault("log.file", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
}
+8 -2
View File
@@ -1,4 +1,10 @@
package internal
// ApplicationName is the non-capitalized name of the application (do not change this)
const ApplicationName = "syft"
const (
// ApplicationName is the non-capitalized name of the application (do not change this)
ApplicationName = "syft"
// 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 = "1.0.0"
)
-9
View File
@@ -1,9 +0,0 @@
## Updating the JSON schema
Today the JSON schema is generated from integration test data. Specifically, when integration tests are run, the `/schema/json/examples` directory is populated with syft JSON output data. This examples directory is used to drive automatically generating the JSON schema.
The caveats with this approach is:
1) the JSON schema is only as good as the examples provided
2) there is an integration test that ensures that the JSON schema is valid relative to what the code currently generates.
This means to update the JSON schema you need to
1) Open up `test/integration/json_schema_test.go` and comment out invocations of the `validateAgainstV1Schema` function.
2) From the root of the repo run `generate-json-schema`. Now there should be a new schema generated at `/schema/json/schema.json`
3) Uncomment the `validateAgainstV1Schema` function.
-183
View File
@@ -1,183 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
CycloneDX BOM Descriptor Extension
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
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.1"
xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
vc:minVersion="1.0"
vc:maxVersion="1.1"
version="1.0">
<xs:annotation>
<xs:documentation>
<name>CycloneDX BOM Descriptor Extension</name>
<url>https://cyclonedx.org/ext/bom-descriptor</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>
<xs:import namespace="http://cyclonedx.org/schema/bom/1.1" schemaLocation="http://cyclonedx.org/schema/bom/1.1"/>
<xs:complexType name="metadata">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="timestamp" type="xs:dateTime" minOccurs="0">
<xs:annotation>
<xs:documentation>The date and time (timestamp) when the document was created.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tool" minOccurs="0" type="bd:toolType">
<xs:annotation>
<xs:documentation>The tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="authors" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The person(s) who created the BOM. Authors are common in BOMs created through
manual processes. BOMs created through automated means may not have authors.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="author" type="bd:organizationalPerson"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="component" type="bom:component" minOccurs="0">
<xs:annotation>
<xs:documentation>The component that the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="manufacture" type="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<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="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<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>
</xs:annotation>
</xs:element>
</xs:sequence>
<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="organizationalEntity">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the organization</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="url" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The URL of the organization. Multiple URLs are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="contact" type="bd:organizationalPerson" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>A contact person at the organization. Multiple contacts are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<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="toolType">
<xs:annotation>
<xs:documentation>Specifies a tool (manual or automated).</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="vendor" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The vendor of the tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The name of the tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="version" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The version of the tool used to create the BOM.</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: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="organizationalPerson">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the person</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The email address of the person. Multiple email addresses are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The phone number of the person. Multiple phone numbers are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<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:element name="metadata" type="bd:metadata">
<xs:annotation>
<xs:documentation>Provides additional information about a BOM.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:schema>
-1
View File
@@ -1 +0,0 @@
examples/
+39
View File
@@ -0,0 +1,39 @@
# JSON Schema
This is the JSON schema for output from the JSON presenter (`syft <img> -o json`). The required inputs for defining the JSON schema are as follows:
- the value of `internal.JSONSchemaVersion` that governs the schema filename
- the `Document` struct definition within `syft/presenters/json/document.go` that governs the overall document shape
- the `metadataContainer` struct definition within `schema/json/generate.go` that governs the allowable shapes of `pkg.Package.Metadata`
With regard to testing the JSON schema, integration test cases provided by the developer are used as examples to validate that JSON output from Syft is always valid relative to the `schema/json/schema-$VERSION.json` file.
## Versioning
Versioning the JSON schema must be done manually by changing the `JSONSchemaVersion` constant within `internal/constants.go`.
This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.
Given a version number format `MODEL.REVISION.ADDITION`:
- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data
- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data
- `ADDITION`: increment when you make a schema change that is compatible with all historical data
## Adding a New `pkg.*Metadata` Type
When adding a new `pkg.*Metadata` that is assigned to the `pkg.Package.Metadata` struct field it is important that a few things
are done:
- a new integration test case is added to `test/integration/pkg_cases_test.go` that exercises the new package type with the new metadata
- the new metadata struct is added to the `metadataContainer` struct within `schema/json/generate.go`
## Generating a New Schema
Create the new schema by running `cd schema/json && go run generate.go` (note you must be in the `schema/json` dir while running this):
- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/json/schema-$VERSION.json`
- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
+125
View File
@@ -0,0 +1,125 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/alecthomas/jsonschema"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
)
/*
This method of creating the JSON schema only captures strongly typed fields for the purpose of integrations between syft
JSON output and integrations. The downside to this approach is that any values and types used on weakly typed fields
are not captured (empty interfaces). This means that pkg.Package.Metadata is not validated at this time. This approach
can be extended to include specific package metadata struct shapes in the future.
*/
// This should represent all possible metadatas represented in the pkg.Package.Metadata field (an interface{}).
// When a new package metadata definition is created it will need to be manually added here. The variable name does
// not matter as long as it is exported.
type metadataContainer struct {
Apk pkg.ApkMetadata
Dpkg pkg.DpkgMetadata
Gem pkg.GemMetadata
Java pkg.JavaMetadata
Npm pkg.NpmPackageJSONMetadata
Python pkg.PythonPackageMetadata
Rpm pkg.RpmdbMetadata
}
// nolint:funlen
func main() {
metadataSchema := jsonschema.Reflect(&metadataContainer{})
documentSchema := jsonschema.Reflect(&jsonPresenter.Document{})
// TODO: inject source definitions
// inject the definitions of all metadatas into the schema definitions
var metadataNames []string
for name, definition := range metadataSchema.Definitions {
if name == "metadataContainer" {
// ignore the definition for the fake container
continue
}
documentSchema.Definitions[name] = definition
if strings.HasSuffix(name, "Metadata") {
metadataNames = append(metadataNames, name)
}
}
// ensure the generated list of names is stable between runs
sort.Strings(metadataNames)
var metadataTypes = []map[string]string{
// allow for no metadata to be provided
{"type": "null"},
}
for _, name := range metadataNames {
metadataTypes = append(metadataTypes, map[string]string{
"$ref": fmt.Sprintf("#/definitions/%s", name),
})
}
// set the "anyOf" field for Package.Metadata to be a conjunction of several types
documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{
"anyOf": metadataTypes,
})
filename := fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)
var newSchemaBuffer = new(bytes.Buffer)
enc := json.NewEncoder(newSchemaBuffer)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(&documentSchema)
if err != nil {
panic(err)
}
if _, err := os.Stat(filename); !os.IsNotExist(err) {
// check if the schema is the same...
existingFh, err := os.Open(filename)
if err != nil {
panic(err)
}
existingSchemaBytes, err := ioutil.ReadAll(existingFh)
if err != nil {
panic(err)
}
if bytes.Equal(existingSchemaBytes, newSchemaBuffer.Bytes()) {
// the generated schema is the same, bail with no error :)
fmt.Println("No change to the existing schema!")
os.Exit(0)
}
// the generated schema is different, bail with error :(
fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the scheam/json/README.md for how to increment\n", filename)
os.Exit(1)
}
fh, err := os.Create(filename)
if err != nil {
panic(err)
}
_, err = fh.Write(newSchemaBuffer.Bytes())
if err != nil {
panic(err)
}
defer fh.Close()
fmt.Printf("wrote new schema to %q\n", filename)
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/env/bin python3
import os
import glob
import json
from genson import SchemaBuilder
EXAMPLES_DIR = "examples/"
OUTPUT = "schema.json"
def main():
builder = SchemaBuilder()
print("Generating new Syft json schema...")
for filepath in glob.glob(os.path.join(EXAMPLES_DIR, '*.json')):
with open(filepath, 'r') as f:
print(f" adding {filepath}")
builder.add_object(json.loads(f.read()))
print("Building schema...")
new_schema = builder.to_schema()
with open(OUTPUT, 'w') as f:
f.write(json.dumps(new_schema, sort_keys=True, indent=4))
print(f"New schema written to '{OUTPUT}'")
if __name__ == "__main__":
main()
-1
View File
@@ -1 +0,0 @@
genson
+678
View File
@@ -0,0 +1,678 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Document",
"definitions": {
"ApkFileRecord": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"ownerUid": {
"type": "string"
},
"ownerGid": {
"type": "string"
},
"permissions": {
"type": "string"
},
"checksum": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"ApkMetadata": {
"required": [
"package",
"originPackage",
"maintainer",
"version",
"license",
"architecture",
"url",
"description",
"size",
"installedSize",
"pullDependencies",
"pullChecksum",
"gitCommitOfApkPort",
"files"
],
"properties": {
"package": {
"type": "string"
},
"originPackage": {
"type": "string"
},
"maintainer": {
"type": "string"
},
"version": {
"type": "string"
},
"license": {
"type": "string"
},
"architecture": {
"type": "string"
},
"url": {
"type": "string"
},
"description": {
"type": "string"
},
"size": {
"type": "integer"
},
"installedSize": {
"type": "integer"
},
"pullDependencies": {
"type": "string"
},
"pullChecksum": {
"type": "string"
},
"gitCommitOfApkPort": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/ApkFileRecord"
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object"
},
"Descriptor": {
"required": [
"name",
"version"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Distribution": {
"required": [
"name",
"version",
"idLike"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"idLike": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Document": {
"required": [
"artifacts",
"source",
"distro",
"descriptor",
"schema"
],
"properties": {
"artifacts": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Package"
},
"type": "array"
},
"source": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Source"
},
"distro": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Distribution"
},
"descriptor": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Descriptor"
},
"schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Schema"
}
},
"additionalProperties": false,
"type": "object"
},
"DpkgFileRecord": {
"required": [
"path",
"md5"
],
"properties": {
"path": {
"type": "string"
},
"md5": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"DpkgMetadata": {
"required": [
"package",
"source",
"version",
"architecture",
"maintainer",
"installedSize",
"files"
],
"properties": {
"package": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
},
"architecture": {
"type": "string"
},
"maintainer": {
"type": "string"
},
"installedSize": {
"type": "integer"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/DpkgFileRecord"
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object"
},
"GemMetadata": {
"required": [
"name",
"version"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"authors": {
"items": {
"type": "string"
},
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"homepage": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"JavaManifest": {
"properties": {
"main": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"namedSections": {
"patternProperties": {
".*": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
},
"additionalProperties": false,
"type": "object"
},
"JavaMetadata": {
"required": [
"virtualPath"
],
"properties": {
"virtualPath": {
"type": "string"
},
"manifest": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/JavaManifest"
},
"pomProperties": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PomProperties"
}
},
"additionalProperties": false,
"type": "object"
},
"Location": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"layerID": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"NpmPackageJSONMetadata": {
"required": [
"author",
"licenses",
"homepage",
"description",
"url"
],
"properties": {
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"author": {
"type": "string"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"homepage": {
"type": "string"
},
"description": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Package": {
"required": [
"name",
"version",
"type",
"foundBy",
"locations",
"licenses",
"language",
"cpes",
"purl",
"metadataType",
"metadata"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"type": {
"type": "string"
},
"foundBy": {
"type": "string"
},
"locations": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Location"
},
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"language": {
"type": "string"
},
"cpes": {
"items": {
"type": "string"
},
"type": "array"
},
"purl": {
"type": "string"
},
"metadataType": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/ApkMetadata"
},
{
"$ref": "#/definitions/DpkgMetadata"
},
{
"$ref": "#/definitions/GemMetadata"
},
{
"$ref": "#/definitions/JavaMetadata"
},
{
"$ref": "#/definitions/NpmPackageJSONMetadata"
},
{
"$ref": "#/definitions/PythonPackageMetadata"
},
{
"$ref": "#/definitions/RpmdbMetadata"
}
]
}
},
"additionalProperties": false,
"type": "object"
},
"PomProperties": {
"required": [
"path",
"name",
"groupId",
"artifactId",
"version",
"extraFields"
],
"properties": {
"path": {
"type": "string"
},
"name": {
"type": "string"
},
"groupId": {
"type": "string"
},
"artifactId": {
"type": "string"
},
"version": {
"type": "string"
},
"extraFields": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
}
},
"additionalProperties": false,
"type": "object"
},
"PythonFileDigest": {
"required": [
"algorithm",
"value"
],
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"PythonFileRecord": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"digest": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PythonFileDigest"
},
"size": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"PythonPackageMetadata": {
"required": [
"name",
"version",
"license",
"author",
"authorEmail",
"platform",
"sitePackagesRootPath"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"license": {
"type": "string"
},
"author": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"platform": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PythonFileRecord"
},
"type": "array"
},
"sitePackagesRootPath": {
"type": "string"
},
"topLevelPackages": {
"items": {
"type": "string"
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object"
},
"RpmdbFileRecord": {
"required": [
"path",
"mode",
"size",
"sha256"
],
"properties": {
"path": {
"type": "string"
},
"mode": {
"type": "integer"
},
"size": {
"type": "integer"
},
"sha256": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"RpmdbMetadata": {
"required": [
"name",
"version",
"epoch",
"architecture",
"release",
"sourceRpm",
"size",
"license",
"vendor",
"files"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"epoch": {
"type": "integer"
},
"architecture": {
"type": "string"
},
"release": {
"type": "string"
},
"sourceRpm": {
"type": "string"
},
"size": {
"type": "integer"
},
"license": {
"type": "string"
},
"vendor": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/RpmdbFileRecord"
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object"
},
"Schema": {
"required": [
"version",
"url"
],
"properties": {
"version": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Source": {
"required": [
"type",
"target"
],
"properties": {
"type": {
"type": "string"
},
"target": {
"additionalProperties": true
}
},
"additionalProperties": false,
"type": "object"
}
}
}
-455
View File
@@ -1,455 +0,0 @@
{
"$schema": "http://json-schema.org/schema#",
"properties": {
"artifacts": {
"items": {
"properties": {
"foundBy": {
"type": "string"
},
"language": {
"type": "string"
},
"licenses": {
"anyOf": [
{
"type": "null"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"locations": {
"items": {
"properties": {
"layerID": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
},
"type": "array"
},
"metadata": {
"properties": {
"architecture": {
"type": "string"
},
"author": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"description": {
"type": "string"
},
"epoch": {
"type": "integer"
},
"files": {
"anyOf": [
{
"type": "null"
},
{
"items": {
"anyOf": [
{
"type": "string"
},
{
"properties": {
"checksum": {
"type": "string"
},
"digest": {
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"algorithm",
"value"
],
"type": "object"
},
"ownerGid": {
"type": "string"
},
"ownerUid": {
"type": "string"
},
"path": {
"type": "string"
},
"permissions": {
"type": "string"
},
"size": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
]
},
"type": "array"
}
]
},
"gitCommitOfApkPort": {
"type": "string"
},
"homepage": {
"type": "string"
},
"installedSize": {
"type": "integer"
},
"license": {
"type": "string"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"maintainer": {
"type": "string"
},
"manifest": {
"properties": {
"main": {
"properties": {
"Archiver-Version": {
"type": "string"
},
"Build-Jdk": {
"type": "string"
},
"Built-By": {
"type": "string"
},
"Created-By": {
"type": "string"
},
"Extension-Name": {
"type": "string"
},
"Group-Id": {
"type": "string"
},
"Hudson-Version": {
"type": "string"
},
"Implementation-Title": {
"type": "string"
},
"Implementation-Version": {
"type": "string"
},
"Jenkins-Version": {
"type": "string"
},
"Long-Name": {
"type": "string"
},
"Main-Class": {
"type": "string"
},
"Manifest-Version": {
"type": "string"
},
"Minimum-Java-Version": {
"type": "string"
},
"Plugin-Dependencies": {
"type": "string"
},
"Plugin-Developers": {
"type": "string"
},
"Plugin-License-Name": {
"type": "string"
},
"Plugin-License-Url": {
"type": "string"
},
"Plugin-ScmUrl": {
"type": "string"
},
"Plugin-Version": {
"type": "string"
},
"Short-Name": {
"type": "string"
},
"Specification-Title": {
"type": "string"
}
},
"required": [
"Archiver-Version",
"Build-Jdk",
"Built-By",
"Created-By",
"Manifest-Version"
],
"type": "object"
}
},
"required": [
"main"
],
"type": "object"
},
"name": {
"type": "string"
},
"originPackage": {
"type": "string"
},
"package": {
"type": "string"
},
"platform": {
"type": "string"
},
"pomProperties": {
"properties": {
"artifactId": {
"type": "string"
},
"extraFields": {
"type": "null"
},
"groupId": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"artifactId",
"extraFields",
"groupId",
"name",
"path",
"version"
],
"type": "object"
},
"pullChecksum": {
"type": "string"
},
"pullDependencies": {
"type": "string"
},
"release": {
"type": "string"
},
"sitePackagesRootPath": {
"type": "string"
},
"size": {
"type": "integer"
},
"source": {
"type": "string"
},
"sourceRpm": {
"type": "string"
},
"topLevelPackages": {
"items": {
"type": "string"
},
"type": "array"
},
"url": {
"type": "string"
},
"vendor": {
"type": "string"
},
"version": {
"type": "string"
},
"virtualPath": {
"type": "string"
}
},
"type": "object"
},
"metadataType": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"foundBy",
"language",
"licenses",
"locations",
"metadataType",
"name",
"type",
"version"
],
"type": "object"
},
"type": "array"
},
"descriptor": {
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"distro": {
"properties": {
"idLike": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"idLike",
"name",
"version"
],
"type": "object"
},
"source": {
"properties": {
"target": {
"anyOf": [
{
"type": "string"
},
{
"properties": {
"digest": {
"type": "string"
},
"layers": {
"items": {
"properties": {
"digest": {
"type": "string"
},
"mediaType": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"required": [
"digest",
"mediaType",
"size"
],
"type": "object"
},
"type": "array"
},
"mediaType": {
"type": "string"
},
"scope": {
"type": "string"
},
"size": {
"type": "integer"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
},
"userInput": {
"type": "string"
}
},
"required": [
"digest",
"layers",
"mediaType",
"scope",
"size",
"tags",
"userInput"
],
"type": "object"
}
]
},
"type": {
"type": "string"
}
},
"required": [
"target",
"type"
],
"type": "object"
}
},
"required": [
"artifacts",
"descriptor",
"distro",
"source"
],
"type": "object"
}
+10 -1
View File
@@ -3,6 +3,7 @@ package cataloger
import (
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
@@ -36,13 +37,14 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
// request.
func Catalog(resolver source.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) {
func Catalog(resolver source.Resolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, error) {
catalog := pkg.NewCatalog()
filesProcessed, packagesDiscovered := newMonitor()
// perform analysis, accumulating errors for each failed analysis
var errs error
for _, theCataloger := range catalogers {
// find packages from the underlying raw data
packages, err := theCataloger.Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
@@ -55,6 +57,13 @@ func Catalog(resolver source.Resolver, catalogers ...Cataloger) (*pkg.Catalog, e
packagesDiscovered.N += int64(catalogedPackages)
for _, p := range packages {
// generate CPEs
p.CPEs = generatePackageCPEs(p)
// generate PURL
p.PURL = generatePackageURL(p, theDistro)
// add to catalog
catalog.Add(p)
}
}
+110
View File
@@ -0,0 +1,110 @@
package cataloger
import (
"fmt"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
)
// this is functionally equivalent to "*" and consistent with no input given (thus easier to test)
const any = ""
// generatePackageCPEs Create a list of CPEs, trying to guess the vendor, product tuple and setting TargetSoftware if possible
func generatePackageCPEs(p pkg.Package) []pkg.CPE {
targetSws := candidateTargetSoftwareAttrs(p)
vendors := candidateVendors(p)
products := candidateProducts(p)
keys := internal.NewStringSet()
cpes := make([]pkg.CPE, 0)
for _, product := range products {
for _, vendor := range append([]string{any}, vendors...) {
for _, targetSw := range append([]string{any}, targetSws...) {
// prevent duplicate entries...
key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw)
if keys.Contains(key) {
continue
}
keys.Add(key)
// add a new entry...
candidateCpe := wfn.NewAttributesWithAny()
candidateCpe.Part = "a"
candidateCpe.Product = product
candidateCpe.Vendor = vendor
candidateCpe.Version = p.Version
candidateCpe.TargetSW = targetSw
cpes = append(cpes, *candidateCpe)
}
}
}
return cpes
}
func candidateTargetSoftwareAttrs(p pkg.Package) []string {
// TODO: would be great to allow these to be overridden by user data/config
var targetSw []string
switch p.Language {
case pkg.Java:
targetSw = append(targetSw, "java", "maven")
case pkg.JavaScript:
targetSw = append(targetSw, "node.js", "nodejs")
case pkg.Ruby:
targetSw = append(targetSw, "ruby", "rails")
case pkg.Python:
targetSw = append(targetSw, "python")
}
if p.Type == pkg.JenkinsPluginPkg {
targetSw = append(targetSw, "jenkins", "cloudbees_jenkins")
}
return targetSw
}
func candidateVendors(p pkg.Package) []string {
vendors := candidateProducts(p)
switch p.Language {
case pkg.Python:
vendors = append(vendors, fmt.Sprintf("python-%s", p.Name))
case pkg.Java:
if p.MetadataType == pkg.JavaMetadataType {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
// derive the vendor from the groupID (e.g. org.sonatype.nexus --> sonatype)
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
fields := strings.Split(metadata.PomProperties.GroupID, ".")
if len(fields) >= 3 {
vendors = append(vendors, fields[1])
}
}
}
}
}
return vendors
}
func candidateProducts(p pkg.Package) []string {
var products = []string{p.Name}
switch p.Language {
case pkg.Java:
if p.MetadataType == pkg.JavaMetadataType {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
// derive the product from the groupID (e.g. org.sonatype.nexus --> nexus)
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
fields := strings.Split(metadata.PomProperties.GroupID, ".")
if len(fields) >= 3 {
products = append(products, fields[2])
}
}
}
}
default:
return products
}
return products
}
+182
View File
@@ -0,0 +1,182 @@
package cataloger
import (
"sort"
"testing"
"github.com/scylladb/go-set/strset"
"github.com/scylladb/go-set"
"github.com/anchore/syft/syft/pkg"
)
func TestGenerate(t *testing.T) {
tests := []struct {
name string
p pkg.Package
expected []string
}{
{
name: "python language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Python,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:python:*:*",
},
},
{
name: "javascript language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.JavaScript,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:nodejs:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:nodejs:*:*",
},
},
{
name: "ruby language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Ruby,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:rails:*:*",
},
},
{
name: "java language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
},
},
{
name: "java language with groupID",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
GroupID: "org.sonatype.nexus",
},
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:maven:*:*",
},
},
{
name: "jenkins package",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.JenkinsPluginPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := generatePackageCPEs(test.p)
expectedCpeSet := set.NewStringSet(test.expected...)
actualCpeSet := set.NewStringSet()
for _, a := range actual {
actualCpeSet.Add(a.BindToFmtString())
}
extra := strset.Difference(expectedCpeSet, actualCpeSet).List()
sort.Strings(extra)
for _, d := range extra {
t.Errorf("extra CPE: %+v", d)
}
missing := strset.Difference(actualCpeSet, expectedCpeSet).List()
sort.Strings(missing)
for _, d := range missing {
t.Errorf("missing CPE: %+v", d)
}
})
}
}
+9 -2
View File
@@ -33,6 +33,7 @@ func (c *Cataloger) Name() string {
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
// nolint:funlen
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
dbFileMatches, err := resolver.FilesByGlob(dpkgStatusGlob)
if err != nil {
@@ -66,18 +67,24 @@ func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
p.FoundBy = c.Name()
p.Locations = []source.Location{dbLocation}
metadata := p.Metadata.(pkg.DpkgMetadata)
if md5Reader, ok := md5ContentsByName[md5Key(*p)]; ok {
// attach the file list
metadata := p.Metadata.(pkg.DpkgMetadata)
metadata.Files = parseDpkgMD5Info(md5Reader)
p.Metadata = metadata
// keep a record of the file where this was discovered
if ref, ok := md5RefsByName[md5Key(*p)]; ok {
p.Locations = append(p.Locations, ref)
}
} else {
// ensure the file list is an empty collection (not nil)
metadata.Files = make([]pkg.DpkgFileRecord, 0)
}
// persist alterations
p.Metadata = metadata
copyrightReader, ok := copyrightContentsByName[p.Name]
if ok {
// attach the licenses
+2 -1
View File
@@ -9,7 +9,8 @@ import (
)
func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord {
var findings []pkg.DpkgFileRecord
// we must preallocate to ensure the resulting struct does not have null
var findings = make([]pkg.DpkgFileRecord, 0)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
@@ -130,6 +130,7 @@ func TestParseJar(t *testing.T) {
GroupID: "io.jenkins.plugins",
ArtifactID: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT",
Extra: map[string]string{},
},
},
},
@@ -185,6 +186,7 @@ func TestParseJar(t *testing.T) {
GroupID: "org.anchore",
ArtifactID: "example-java-app-maven",
Version: "0.1.0",
Extra: map[string]string{},
},
},
},
@@ -203,6 +205,7 @@ func TestParseJar(t *testing.T) {
GroupID: "joda-time",
ArtifactID: "joda-time",
Version: "2.9.2",
Extra: map[string]string{},
},
},
},
+5
View File
@@ -42,6 +42,11 @@ func parsePomProperties(path string, reader io.Reader) (*pkg.PomProperties, erro
return nil, fmt.Errorf("unable to parse pom.properties: %w", err)
}
// don't allow for a nil collection, ensure it is empty
if props.Extra == nil {
props.Extra = make(map[string]string)
}
props.Path = path
return &props, nil
+4 -2
View File
@@ -2,10 +2,11 @@ package java
import (
"encoding/json"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseJavaPomProperties(t *testing.T) {
@@ -20,6 +21,7 @@ func TestParseJavaPomProperties(t *testing.T) {
GroupID: "org.anchore",
ArtifactID: "example-java-app-maven",
Version: "0.1.0",
Extra: map[string]string{},
},
},
{
@@ -24,7 +24,7 @@ type PackageJSON struct {
Latest []string `json:"latest"`
Author Author `json:"author"`
License json.RawMessage `json:"license"`
Licenses []license `json:"licenses,omitempty"`
Licenses []license `json:"licenses"`
Name string `json:"name"`
Homepage string `json:"homepage"`
Description string `json:"description"`
+49
View File
@@ -0,0 +1,49 @@
package cataloger
import (
"regexp"
"strings"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/package-url/packageurl-go"
)
// generatePackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec)
func generatePackageURL(p pkg.Package, d *distro.Distro) string {
// default to pURLs on the metadata
if p.Metadata != nil {
if i, ok := p.Metadata.(interface{ PackageURL() string }); ok {
return i.PackageURL()
} else if i, ok := p.Metadata.(interface{ PackageURL(*distro.Distro) string }); ok {
return i.PackageURL(d)
}
}
var purlType = p.Type.PackageURLType()
var name = p.Name
var namespace = ""
switch {
case purlType == "":
// there is no purl type, don't attempt to craft a purl
// TODO: should this be a "generic" purl type instead?
return ""
case p.Type == pkg.GoModulePkg:
re := regexp.MustCompile(`(/)[^/]*$`)
fields := re.Split(p.Name, -1)
namespace = fields[0]
name = strings.TrimPrefix(p.Name, namespace+"/")
}
// generate a purl from the package data
pURL := packageurl.NewPackageURL(
purlType,
namespace,
name,
p.Version,
nil,
"")
return pURL.ToString()
}
@@ -1,83 +1,84 @@
package pkg
package cataloger
import (
"testing"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
)
func TestPackage_pURL(t *testing.T) {
func TestPackageURL(t *testing.T) {
tests := []struct {
pkg Package
distro distro.Distro
pkg pkg.Package
distro *distro.Distro
expected string
}{
{
pkg: Package{
pkg: pkg.Package{
Name: "github.com/anchore/syft",
Version: "v0.1.0",
Type: GoModulePkg,
Type: pkg.GoModulePkg,
},
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: GemPkg,
Type: pkg.GemPkg,
},
expected: "pkg:gem/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: NpmPkg,
Type: pkg.NpmPkg,
},
expected: "pkg:npm/name@v0.1.0",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.Ubuntu,
},
pkg: Package{
pkg: pkg.Package{
Name: "bad-name",
Version: "bad-v0.1.0",
Type: DebPkg,
Metadata: DpkgMetadata{
Type: pkg.DebPkg,
Metadata: pkg.DpkgMetadata{
Package: "name",
Version: "v0.1.0",
Architecture: "amd64",
@@ -86,14 +87,14 @@ func TestPackage_pURL(t *testing.T) {
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.CentOS,
},
pkg: Package{
pkg: pkg.Package{
Name: "bad-name",
Version: "bad-v0.1.0",
Type: RpmPkg,
Metadata: RpmdbMetadata{
Type: pkg.RpmPkg,
Metadata: pkg.RpmdbMetadata{
Name: "name",
Version: "v0.1.0",
Epoch: 2,
@@ -104,13 +105,13 @@ func TestPackage_pURL(t *testing.T) {
expected: "pkg:rpm/centos/name@2:v0.1.0-3?arch=amd64",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.UnknownDistroType,
},
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: DebPkg,
Type: pkg.DebPkg,
},
expected: "pkg:deb/name@v0.1.0",
},
@@ -118,7 +119,7 @@ func TestPackage_pURL(t *testing.T) {
for _, test := range tests {
t.Run(string(test.pkg.Type)+"|"+test.expected, func(t *testing.T) {
actual := test.pkg.PackageURL(test.distro)
actual := generatePackageURL(test.pkg, test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
@@ -23,29 +23,28 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
line := scanner.Text()
line = strings.TrimRight(line, "\n")
// empty line indicates end of entry
// An empty line means we are done parsing (either because there's no more data,
// or because a description follows as specified in
// https://packaging.python.org/specifications/core-metadata/#description;
// and at this time, we're not interested in the description).
if len(line) == 0 {
// if the entry has not started, keep parsing lines
if len(fields) == 0 {
continue
if len(fields) > 0 {
break
}
break
// however, if the field parsing has not started yet, keep scanning lines
continue
}
switch {
case strings.HasPrefix(line, " "):
// a field-body continuation
if len(key) == 0 {
return pkg.PythonPackageMetadata{}, fmt.Errorf("no match for continuation: line: '%s'", line)
updatedValue, err := handleFieldBodyContinuation(key, line, fields)
if err != nil {
return pkg.PythonPackageMetadata{}, err
}
val, ok := fields[key]
if !ok {
return pkg.PythonPackageMetadata{}, fmt.Errorf("no previous key exists, expecting: %s", key)
}
// concatenate onto previous value
val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
fields[key] = val
fields[key] = updatedValue
default:
// parse a new key (note, duplicate keys are overridden)
if i := strings.Index(line, ":"); i > 0 {
@@ -71,8 +70,23 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
// add additional metadata not stored in the egg/wheel metadata file
sitePackagesRoot := filepath.Clean(filepath.Join(filepath.Dir(path), ".."))
metadata.SitePackagesRootPath = sitePackagesRoot
metadata.SitePackagesRootPath = filepath.Clean(filepath.Join(filepath.Dir(path), ".."))
return metadata, nil
}
// handleFieldBodyContinuation returns the updated value for the specified field after processing the specified line.
// If the continuation cannot be processed, it returns an error.
func handleFieldBodyContinuation(key, line string, fields map[string]string) (string, error) {
if len(key) == 0 {
return "", fmt.Errorf("no match for continuation: line: '%s'", line)
}
val, ok := fields[key]
if !ok {
return "", fmt.Errorf("no previous key exists, expecting: %s", key)
}
// concatenate onto previous value
return fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line)), nil
}
-7
View File
@@ -14,13 +14,6 @@ type Distro struct {
IDLike string
}
// NewUnknownDistro creates a standardized Distro object for unidentifiable distros
func NewUnknownDistro() Distro {
return Distro{
Type: UnknownDistroType,
}
}
// NewDistro creates a new Distro object populated with the given values.
func NewDistro(t Type, ver, like string) (Distro, error) {
if ver == "" {
+8 -4
View File
@@ -17,8 +17,8 @@ type parseEntry struct {
}
// Identify parses distro-specific files to determine distro metadata like version and release.
func Identify(resolver source.Resolver) Distro {
distro := NewUnknownDistro()
func Identify(resolver source.Resolver) *Distro {
var distro *Distro
identityFiles := []parseEntry{
{
@@ -65,12 +65,16 @@ identifyLoop:
}
if candidateDistro := entry.fn(content); candidateDistro != nil {
distro = *candidateDistro
distro = candidateDistro
break identifyLoop
}
}
}
if distro != nil && distro.Type == UnknownDistroType {
return nil
}
return distro
}
@@ -113,7 +117,7 @@ func parseOsRelease(contents string) *Distro {
return assemble(id, vers, like)
}
var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d\.]+`)
var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d.]+`)
func parseBusyBox(contents string) *Distro {
matches := busyboxVersionMatcher.FindAllString(contents, -1)
+20 -3
View File
@@ -57,6 +57,11 @@ func TestIdentifyDistro(t *testing.T) {
Type: Ubuntu,
Version: "20.4.0",
},
{
fixture: "test-fixtures/os/oraclelinux",
Type: OracleLinux,
Version: "8.3.0",
},
{
fixture: "test-fixtures/os/empty",
Type: UnknownDistroType,
@@ -90,6 +95,12 @@ func TestIdentifyDistro(t *testing.T) {
}
d := Identify(s.Resolver)
if d == nil {
if test.Type == UnknownDistroType {
return
}
t.Fatalf("expected a distro but got none")
}
observedDistros.Add(d.String())
if d.Type != test.Type {
@@ -103,8 +114,8 @@ func TestIdentifyDistro(t *testing.T) {
return
}
if d.Version == nil {
t.Log("Distro doesn't have a Version")
if d.Version == nil && test.Version == "" {
// this distro does not have a version
return
}
@@ -116,7 +127,13 @@ func TestIdentifyDistro(t *testing.T) {
// ensure that test cases stay in sync with the distros that can be identified
if len(observedDistros) < len(definedDistros) {
t.Errorf("distro coverage incomplete (distro=%d, coverage=%d)", len(definedDistros), len(observedDistros))
for _, d := range definedDistros.ToSlice() {
t.Logf(" defined: %s", d)
}
for _, d := range observedDistros.ToSlice() {
t.Logf(" observed: %s", d)
}
t.Errorf("distro coverage incomplete (defined=%d, coverage=%d)", len(definedDistros), len(observedDistros))
}
}
@@ -0,0 +1,18 @@
NAME="Oracle Linux Server"
VERSION="8.3"
ID="ol"
ID_LIKE="fedora"
VARIANT="Server"
VARIANT_ID="server"
VERSION_ID="8.3"
PLATFORM_ID="platform:el8"
PRETTY_NAME="Oracle Linux Server 8.3"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:oracle:linux:8:3:server"
HOME_URL="https://linux.oracle.com/"
BUG_REPORT_URL="https://bugzilla.oracle.com/"
ORACLE_BUGZILLA_PRODUCT="Oracle Linux 8"
ORACLE_BUGZILLA_PRODUCT_VERSION=8.3
ORACLE_SUPPORT_PRODUCT="Oracle Linux"
ORACLE_SUPPORT_PRODUCT_VERSION=8.3
+3
View File
@@ -15,4 +15,7 @@ const (
// CatalogerFinished is a partybus event that occurs when the package cataloging has completed
CatalogerFinished partybus.EventType = "syft-cataloger-finished-event"
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
ImportStarted partybus.EventType = "syft-import-started-event"
)
+20
View File
@@ -6,6 +6,8 @@ package parsers
import (
"fmt"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/cataloger"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/presenter"
@@ -75,3 +77,21 @@ func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
return newVersion, nil
}
func ParseImportStarted(e partybus.Event) (string, progress.StagedProgressable, error) {
if err := checkEventType(e.Type, event.ImportStarted); err != nil {
return "", nil, err
}
imgName, ok := e.Source.(string)
if !ok {
return "", nil, newPayloadErr(e.Type, "Source", e.Source)
}
prog, ok := e.Value.(progress.StagedProgressable)
if !ok {
return "", nil, newPayloadErr(e.Type, "Value", e.Value)
}
return imgName, prog, nil
}
+32 -43
View File
@@ -34,79 +34,68 @@ import (
// Catalog the given image from a particular perspective (e.g. squashed source, all-layers source). Returns the discovered
// set of packages, the identified Linux distribution, and the source object used to wrap the data source.
func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, distro.Distro, error) {
log.Info("cataloging image")
s, cleanup, err := source.New(userInput, scope)
func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, *distro.Distro, error) {
theSource, cleanup, err := source.New(userInput, scope)
defer cleanup()
if err != nil {
return source.Source{}, nil, distro.Distro{}, err
return source.Source{}, nil, nil, err
}
d := IdentifyDistro(s)
catalog, err := CatalogFromScope(s)
if err != nil {
return source.Source{}, nil, distro.Distro{}, err
}
return s, catalog, d, nil
}
// IdentifyDistro attempts to discover what the underlying Linux distribution may be from the available flat files
// provided by the given source object. If results are inconclusive a "UnknownDistro" Type is returned.
func IdentifyDistro(s source.Source) distro.Distro {
d := distro.Identify(s.Resolver)
if d.Type != distro.UnknownDistroType {
log.Infof("identified distro: %s", d.String())
// find the distro
theDistro := distro.Identify(theSource.Resolver)
if theDistro != nil {
log.Infof("identified distro: %s", theDistro.String())
} else {
log.Info("could not identify distro")
}
return d
}
// Catalog the given source, which may represent a container image or filesystem. Returns the discovered set of packages.
func CatalogFromScope(s source.Source) (*pkg.Catalog, error) {
log.Info("building the catalog")
// conditionally have two sets of catalogers
// conditionally use the correct set of loggers based on the input type (container image or directory)
var catalogers []cataloger.Cataloger
switch s.Metadata.Scheme {
switch theSource.Metadata.Scheme {
case source.ImageScheme:
log.Info("cataloging image")
catalogers = cataloger.ImageCatalogers()
case source.DirectoryScheme:
log.Info("cataloging directory")
catalogers = cataloger.DirectoryCatalogers()
default:
return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.Metadata.Scheme)
return source.Source{}, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", theSource.Metadata.Scheme)
}
return cataloger.Catalog(s.Resolver, catalogers...)
catalog, err := cataloger.Catalog(theSource.Resolver, theDistro, catalogers...)
if err != nil {
return source.Source{}, nil, nil, err
}
return theSource, catalog, theDistro, nil
}
// CatalogFromJSON takes an existing syft report and generates native syft objects.
func CatalogFromJSON(reader io.Reader) (source.Metadata, *pkg.Catalog, distro.Distro, error) {
func CatalogFromJSON(reader io.Reader) (source.Metadata, *pkg.Catalog, *distro.Distro, error) {
var doc jsonPresenter.Document
var err error
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&doc); err != nil {
return source.Metadata{}, nil, distro.Distro{}, err
return source.Metadata{}, nil, nil, err
}
var pkgs = make([]pkg.Package, len(doc.Artifacts))
for i, a := range doc.Artifacts {
pkgs[i] = a.ToPackage()
pkgs[i], err = a.ToPackage()
if err != nil {
return source.Metadata{}, nil, nil, err
}
}
catalog := pkg.NewCatalog(pkgs...)
var distroType distro.Type
if doc.Distro.Name == "" {
distroType = distro.UnknownDistroType
} else {
distroType = distro.Type(doc.Distro.Name)
}
theDistro, err := distro.NewDistro(distroType, doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return source.Metadata{}, nil, distro.Distro{}, err
var theDistro *distro.Distro
if doc.Distro.Name != "" {
d, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return source.Metadata{}, nil, nil, err
}
theDistro = &d
}
return doc.Source.ToSourceMetadata(), catalog, theDistro, nil
+41
View File
@@ -0,0 +1,41 @@
package pkg
import (
"fmt"
"strings"
"github.com/facebookincubator/nvdtools/wfn"
)
type CPE = wfn.Attributes
func NewCPE(cpeStr string) (CPE, error) {
value, err := wfn.Parse(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
}
if value == nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr)
}
// we need to compare the raw data since we are constructing CPEs in other locations
value.Vendor = normalizeCpeField(value.Vendor)
value.Product = normalizeCpeField(value.Product)
value.Language = normalizeCpeField(value.Language)
value.Version = normalizeCpeField(value.Version)
value.TargetSW = normalizeCpeField(value.TargetSW)
value.Part = normalizeCpeField(value.Part)
value.Edition = normalizeCpeField(value.Edition)
value.Other = normalizeCpeField(value.Other)
value.SWEdition = normalizeCpeField(value.SWEdition)
value.TargetHW = normalizeCpeField(value.TargetHW)
value.Update = normalizeCpeField(value.Update)
return *value, nil
}
func normalizeCpeField(field string) string {
// keep dashes and forward slashes unescaped
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")
}
+48
View File
@@ -0,0 +1,48 @@
package pkg
import "testing"
func must(c CPE, e error) CPE {
if e != nil {
panic(e)
}
return c
}
func TestNewCPE(t *testing.T) {
tests := []struct {
name string
input string
expected CPE
}{
{
name: "gocase",
input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`,
expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)),
},
{
name: "dashes",
input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`,
expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)),
},
{
name: "URL escape characters",
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
expected: must(NewCPE(`cpe:2.3:a:$0.99_kindle_books_project:$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewCPE(test.input)
if err != nil {
t.Fatalf("got an error while creating CPE: %+v", err)
}
if actual.BindToFmtString() != test.expected.BindToFmtString() {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", test.expected.BindToFmtString(), actual.BindToFmtString())
}
})
}
}
+4 -1
View File
@@ -24,7 +24,10 @@ type DpkgFileRecord struct {
}
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
func (m DpkgMetadata) PackageURL(d distro.Distro) string {
func (m DpkgMetadata) PackageURL(d *distro.Distro) string {
if d == nil {
return ""
}
pURL := packageurl.NewPackageURL(
// TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21
"deb",
+3 -2
View File
@@ -1,9 +1,10 @@
package pkg
import (
"testing"
"github.com/anchore/syft/syft/distro"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestDpkgMetadata_pURL(t *testing.T) {
@@ -40,7 +41,7 @@ func TestDpkgMetadata_pURL(t *testing.T) {
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL(test.distro)
actual := test.metadata.PackageURL(&test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
+2 -44
View File
@@ -5,13 +5,8 @@ package pkg
import (
"fmt"
"regexp"
"strings"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/distro"
"github.com/package-url/packageurl-go"
)
// ID represents a unique value for each package added to a package catalog.
@@ -28,6 +23,8 @@ type Package struct {
Licenses []string // licenses discovered with the package metadata
Language Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
Type Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
CPEs []CPE // all possible Common Platform Enumerators
PURL string // the Package URL (see https://github.com/package-url/purl-spec)
MetadataType MetadataType // the shape of the additional data in the "metadata" field
Metadata interface{} // additional data found while parsing the package source
}
@@ -41,42 +38,3 @@ func (p Package) ID() ID {
func (p Package) String() string {
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
}
// PackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec)
func (p Package) PackageURL(d distro.Distro) string {
// default to pURLs on the metadata
if p.Metadata != nil {
if i, ok := p.Metadata.(interface{ PackageURL() string }); ok {
return i.PackageURL()
} else if i, ok := p.Metadata.(interface{ PackageURL(distro.Distro) string }); ok {
return i.PackageURL(d)
}
}
var purlType = p.Type.PackageURLType()
var name = p.Name
var namespace = ""
switch {
case purlType == "":
// there is no purl type, don't attempt to craft a purl
// TODO: should this be a "generic" purl type instead?
return ""
case p.Type == GoModulePkg:
re := regexp.MustCompile(`(\/)[^\/]*$`)
fields := re.Split(p.Name, -1)
namespace = fields[0]
name = strings.TrimPrefix(p.Name, namespace+"/")
}
// generate a purl from the package data
pURL := packageurl.NewPackageURL(
purlType,
namespace,
name,
p.Version,
nil,
"")
return pURL.ToString()
}
+5 -1
View File
@@ -33,7 +33,11 @@ type RpmdbFileRecord struct {
type RpmdbFileMode uint16
// PackageURL returns the PURL for the specific RHEL package (see https://github.com/package-url/purl-spec)
func (m RpmdbMetadata) PackageURL(d distro.Distro) string {
func (m RpmdbMetadata) PackageURL(d *distro.Distro) string {
if d == nil {
return ""
}
pURL := packageurl.NewPackageURL(
packageurl.TypeRPM,
d.Type.String(),
+1 -1
View File
@@ -43,7 +43,7 @@ func TestRpmMetadata_pURL(t *testing.T) {
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL(test.distro)
actual := test.metadata.PackageURL(&test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
@@ -11,25 +11,25 @@ import (
// 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:"bd:metadata"`
Timestamp string `xml:"bd:timestamp,omitempty"` // The date and time (timestamp) when the document was created
Tool *BdTool `xml:"bd:tool"` // The tool used to create the BOM.
Component *BdComponent `xml:"bd:component"` // The component that the BOM describes.
XMLName xml.Name `xml:"metadata"`
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
Tools []BdTool `xml:"tools>tool"` // The tool used to create the BOM.
Component *BdComponent `xml:"component"` // The component that the BOM describes.
}
// BdTool represents the tool that created the BOM report.
type BdTool struct {
XMLName xml.Name `xml:"bd:tool"`
Vendor string `xml:"bd:vendor,omitempty"` // The vendor of the tool used to create the BOM.
Name string `xml:"bd:name,omitempty"` // The name of the tool used to create the BOM.
Version string `xml:"bd:version,omitempty"` // The version of the tool used to create the BOM.
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
}
// BdComponent represents the software/package being cataloged.
type BdComponent struct {
XMLName xml.Name `xml:"bd:component"`
XMLName xml.Name `xml:"component"`
Component
}
@@ -38,10 +38,12 @@ func NewBomDescriptor(name, version string, srcMetadata source.Metadata) *BomDes
descriptor := BomDescriptor{
XMLName: xml.Name{},
Timestamp: time.Now().Format(time.RFC3339),
Tool: &BdTool{
Vendor: "anchore",
Name: name,
Version: version,
Tools: []BdTool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
}
@@ -51,7 +53,7 @@ func NewBomDescriptor(name, version string, srcMetadata source.Metadata) *BomDes
Component: Component{
Type: "container",
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.Digest,
Version: srcMetadata.ImageMetadata.ManifestDigest,
},
}
case source.DirectoryScheme:
+3 -6
View File
@@ -5,7 +5,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
@@ -17,20 +16,18 @@ import (
type Document struct {
XMLName xml.Name `xml:"bom"`
XMLNs string `xml:"xmlns,attr"`
XMLNsBd string `xml:"xmlns:bd,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
BomDescriptor *BomDescriptor `xml:"bd:metadata"` // The BOM descriptor extension
}
// NewDocumentFromCatalog returns a CycloneDX Document object populated with the catalog contents.
func NewDocument(catalog *pkg.Catalog, d distro.Distro, srcMetadata source.Metadata) Document {
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) Document {
versionInfo := version.FromBuild()
doc := Document{
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
XMLNsBd: "http://cyclonedx.org/schema/ext/bom-descriptor/1.0",
Version: 1,
SerialNumber: uuid.New().URN(),
BomDescriptor: NewBomDescriptor(internal.ApplicationName, versionInfo.Version, srcMetadata),
@@ -42,7 +39,7 @@ func NewDocument(catalog *pkg.Catalog, d distro.Distro, srcMetadata source.Metad
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
PackageURL: p.PackageURL(d),
PackageURL: p.PURL,
}
var licenses []License
for _, licenseName := range p.Licenses {
+2 -6
View File
@@ -7,8 +7,6 @@ import (
"encoding/xml"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -17,21 +15,19 @@ import (
type Presenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
distro distro.Distro
}
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Distro) *Presenter {
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
return &Presenter{
catalog: catalog,
srcMetadata: srcMetadata,
distro: d,
}
}
// Present writes the CycloneDX report to the given io.Writer.
func (pres *Presenter) Present(output io.Writer) error {
bom := NewDocument(pres.catalog, pres.distro, pres.srcMetadata)
bom := NewDocument(pres.catalog, pres.srcMetadata)
encoder := xml.NewEncoder(output)
encoder.Indent("", " ")
+5 -36
View File
@@ -7,7 +7,6 @@ import (
"testing"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/go-testutils"
"github.com/anchore/syft/syft/pkg"
@@ -61,12 +60,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
t.Fatal(err)
}
d, err := distro.NewDistro(distro.Ubuntu, "20.04", "debian")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s.Metadata, d)
pres := NewPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
@@ -109,17 +103,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
},
Type: pkg.RpmPkg,
FoundBy: "the-cataloger-1",
Metadata: pkg.RpmdbMetadata{
Name: "package1",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "1.0.1",
SourceRpm: "package1-1.0.1-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
},
PURL: "the-purl-1",
})
catalog.Add(pkg.Package{
Name: "package2",
@@ -133,17 +117,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
"MIT",
"Apache-v2",
},
Metadata: pkg.RpmdbMetadata{
Name: "package2",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "1.0.2",
SourceRpm: "package2-1.0.2-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
},
PURL: "the-purl-2",
})
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
@@ -151,20 +125,15 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
t.Fatal(err)
}
d, err := distro.NewDistro(distro.RedHat, "8", "")
if err != nil {
t.Fatal(err)
}
// This accounts for the non-deterministic digest value that we end up with when
// we build a container image dynamically during testing. Ultimately, we should
// use a golden image as a test fixture in place of building this image during
// testing. At that time, this line will no longer be necessary.
//
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
s.Metadata.ImageMetadata.Digest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
pres := NewPresenter(catalog, s.Metadata, d)
pres := NewPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
@@ -1,10 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:815fdd6b-917e-423d-8c91-1fe648141505">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:553b2601-a7b0-4461-bce9-fd56e2e2a4be">
<metadata>
<timestamp>2020-12-01T22:19:00-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="file">
<name>/some/path</name>
<version></version>
</component>
</metadata>
<components>
<component type="library">
<name>package1</name>
<version>1.0.1</version>
<purl>pkg:deb/ubuntu/package1@1.0.1?arch=amd64</purl>
</component>
<component type="library">
<name>package2</name>
@@ -17,19 +30,6 @@
<name>Apache-v2</name>
</license>
</licenses>
<purl>pkg:deb/ubuntu/package2@1.0.2?arch=amd64</purl>
</component>
</components>
<bd:metadata>
<bd:timestamp>2020-11-16T08:45:54-05:00</bd:timestamp>
<bd:tool>
<bd:vendor>anchore</bd:vendor>
<bd:name>syft</bd:name>
<bd:version>[not provided]</bd:version>
</bd:tool>
<bd:component type="file">
<name>/some/path</name>
<version></version>
</bd:component>
</bd:metadata>
</bom>
@@ -1,10 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:3cb10332-1645-44f6-be4a-4f8be5a60cf8">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:4a54c180-9469-4513-a1cb-af3fe1e83738">
<metadata>
<timestamp>2020-12-01T22:19:00-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component>
</metadata>
<components>
<component type="library">
<name>package1</name>
<version>1.0.1</version>
<purl>pkg:rpm/redhat/package1@0:1.0.1-1?arch=x86_64</purl>
<purl>the-purl-1</purl>
</component>
<component type="library">
<name>package2</name>
@@ -17,19 +31,7 @@
<name>Apache-v2</name>
</license>
</licenses>
<purl>pkg:rpm/redhat/package2@0:1.0.2-1?arch=x86_64</purl>
<purl>the-purl-2</purl>
</component>
</components>
<bd:metadata>
<bd:timestamp>2020-11-16T08:45:54-05:00</bd:timestamp>
<bd:tool>
<bd:vendor>anchore</bd:vendor>
<bd:name>syft</bd:name>
<bd:version>[not provided]</bd:version>
</bd:tool>
<bd:component type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</bd:component>
</bd:metadata>
</bom>
+4 -5
View File
@@ -10,14 +10,13 @@ type Distribution struct {
}
// NewDistribution creates a struct with the Linux distribution to be represented in JSON.
func NewDistribution(d distro.Distro) Distribution {
distroName := d.Name()
if distroName == "UnknownDistroType" {
distroName = ""
func NewDistribution(d *distro.Distro) Distribution {
if d == nil {
return Distribution{}
}
return Distribution{
Name: distroName,
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,
}
+8 -1
View File
@@ -1,6 +1,8 @@
package json
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
@@ -14,10 +16,11 @@ type Document struct {
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro Distribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// NewDocument creates and populates a new JSON document struct from the given cataloging results.
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Distro) (Document, error) {
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro) (Document, error) {
src, err := NewSource(srcMetadata)
if err != nil {
return Document{}, nil
@@ -31,6 +34,10 @@ func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Dis
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
},
Schema: Schema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}
for _, p := range catalog.Sorted() {
@@ -18,17 +18,19 @@ type Package struct {
type packageBasicMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
Type string `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
Language string `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
// packageCustomMetadata contains ambiguous values (type-wise) from pkg.Package.
type packageCustomMetadata struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata interface{} `json:"metadata,omitempty"`
MetadataType string `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
@@ -39,37 +41,66 @@ type packageMetadataUnpacker struct {
// NewPackage crates a new Package from the given pkg.Package.
func NewPackage(p *pkg.Package) (Package, error) {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return Package{
packageBasicMetadata: packageBasicMetadata{
Name: p.Name,
Version: p.Version,
Type: p.Type,
Type: string(p.Type),
FoundBy: p.FoundBy,
Locations: p.Locations,
Licenses: p.Licenses,
Language: p.Language,
Locations: locations,
Licenses: licenses,
Language: string(p.Language),
CPEs: cpes,
PURL: p.PURL,
},
packageCustomMetadata: packageCustomMetadata{
MetadataType: p.MetadataType,
MetadataType: string(p.MetadataType),
Metadata: p.Metadata,
},
}, nil
}
// ToPackage generates a pkg.Package from the current Package.
func (a Package) ToPackage() pkg.Package {
func (a Package) ToPackage() (pkg.Package, error) {
var cpes = make([]pkg.CPE, len(a.CPEs))
var err error
for i, c := range a.CPEs {
cpes[i], err = pkg.NewCPE(c)
if err != nil {
return pkg.Package{}, fmt.Errorf("unable to parse CPE from JSON package: %w", err)
}
}
return pkg.Package{
// does not include found-by and locations
Name: a.Name,
Version: a.Version,
FoundBy: a.FoundBy,
Licenses: a.Licenses,
Language: a.Language,
Language: pkg.Language(a.Language),
Locations: a.Locations,
Type: a.Type,
MetadataType: a.MetadataType,
CPEs: cpes,
PURL: a.PURL,
Type: pkg.Type(a.Type),
MetadataType: pkg.MetadataType(a.MetadataType),
Metadata: a.Metadata,
}
}, nil
}
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
@@ -86,9 +117,9 @@ func (a *Package) UnmarshalJSON(b []byte) error {
return err
}
a.MetadataType = pkg.MetadataType(unpacker.MetadataType)
a.MetadataType = unpacker.MetadataType
switch a.MetadataType {
switch pkg.MetadataType(a.MetadataType) {
case pkg.RpmdbMetadataType:
var payload pkg.RpmdbMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
+2 -2
View File
@@ -13,11 +13,11 @@ import (
type Presenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
distro distro.Distro
distro *distro.Distro
}
// NewPresenter creates a new JSON presenter object for the given cataloging results.
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d distro.Distro) *Presenter {
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro) *Presenter {
return &Presenter{
catalog: catalog,
srcMetadata: s,
+28 -2
View File
@@ -15,6 +15,13 @@ import (
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
func TestJsonDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
@@ -36,6 +43,10 @@ func TestJsonDirsPresenter(t *testing.T) {
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
Name: "package-2",
@@ -50,8 +61,12 @@ func TestJsonDirsPresenter(t *testing.T) {
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
d := distro.NewUnknownDistro()
var d *distro.Distro
s, err := source.NewFromDirectory("/some/path")
if err != nil {
t.Fatal(err)
@@ -107,6 +122,10 @@ func TestJsonImgsPresenter(t *testing.T) {
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
Name: "package-2",
@@ -121,10 +140,17 @@ func TestJsonImgsPresenter(t *testing.T) {
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
d := distro.NewUnknownDistro()
var d *distro.Distro
pres := NewPresenter(catalog, s.Metadata, d)
// run presenter
+6
View File
@@ -0,0 +1,6 @@
package json
type Schema struct {
Version string `json:"version"`
URL string `json:"url"`
}
@@ -14,6 +14,10 @@
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
@@ -35,8 +39,12 @@
"path": "/some/path/pkg1"
}
],
"licenses": null,
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
@@ -61,5 +69,9 @@
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.0.json"
}
}
@@ -15,6 +15,10 @@
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
@@ -37,8 +41,12 @@
"layerID": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf"
}
],
"licenses": null,
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
@@ -55,6 +63,13 @@
"type": "image",
"target": {
"userInput": "user-image-input",
"imageID": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
],
"imageSize": 65,
"scope": "AllLayers",
"layers": [
{
@@ -73,12 +88,8 @@
"size": 27
}
],
"size": 65,
"digest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
]
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxODAxLCJkaWdlc3QiOiJzaGEyNTY6MjczMTI1MWRjMzQ5NTFjMGU1MGZjYzY0M2I0YzVmNzQ5MjJkYWQxYTVkOThmMzAyYjUwNGNmNDZjZDVkOTM2OCJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMDQ4LCJkaWdlc3QiOiJzaGEyNTY6ZTE1OGI1N2Q2ZjVhOTZlZjVmZDIyZjJmZTc2YzcwYjViYTZmZjViMjYxOWY5ZDgzMTI1YjJhYWQwNDkyYWM3YiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpkYTIxMDU2ZTdiZjQzMDhlY2VhMGMwODM2ODQ4YTdmZTkyZjM4ZmRjZjM1YmMwOWVlNmQ5OGU3YWI3YmVlZWJmIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MzU4NCwiZGlnZXN0Ijoic2hhMjU2OmYwZTE4YWE2MDMyYzI0NjU5YTljNzQxZmMzNmNhNTZmNTg5NzgyZWExMzIwNjFjY2Y2ZjUyYjk1MjQwM2RhOTQifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpudWxsLCJJbWFnZSI6InNoYTI1NjpiMjQ5NTQwZjc4NDFlMmQxYTVjOTA2NzBkODhhOTgzYmI2Njc3NDNlNTc1N2FmZWI3MzBkMTQ1ZjQyOGEwN2M0IiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCIjKG5vcCkgQUREIGRpcjpjOTM3YzZhYTUwODkwN2UyODUwOWI2NDRhMTJmOGQ2YzY3ZDM0ZTk2OWY4M2IxNGRlZTkzZWExN2Q3NjkwMjhhIGluIC8gIl0sIkltYWdlIjoic2hhMjU2OmIyNDk1NDBmNzg0MWUyZDFhNWM5MDY3MGQ4OGE5ODNiYjY2Nzc0M2U1NzU3YWZlYjczMGQxNDVmNDI4YTA3YzQiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjAtMDktMjNUMTE6NTI6NDMuMTI0NjY2OFoiLCJkb2NrZXJfdmVyc2lvbiI6IjE5LjAzLjEyIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjAtMDktMjNUMTE6NTI6NDIuODczMDEyNloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWMzMmRhMjNkNTFlODAxZjAyZjkyNDEyM2VkMzA5OTBlYjNmMGZlYzFiOWVkNGYwYjA2YzI0ZTg4YjljMzY5NSBpbiAvc29tZWZpbGUtMS50eHQgIn0seyJjcmVhdGVkIjoiMjAyMC0wOS0yM1QxMTo1Mjo0Mi45ODY1OTg5WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTpkZjNiNzQ0ZjU0YTliMTZiOWI5YWVkNDBlM2U5OGQ5Y2EyYjQ5ZjVhNzdkOWZhOGE5NzY5MGQ3YmFmNTg4ODIwIGluIC9zb21lZmlsZS0yLnR4dCAifSx7ImNyZWF0ZWQiOiIyMDIwLTA5LTIzVDExOjUyOjQzLjEyNDY2NjhaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBkaXI6YzkzN2M2YWE1MDg5MDdlMjg1MDliNjQ0YTEyZjhkNmM2N2QzNGU5NjlmODNiMTRkZWU5M2VhMTdkNzY5MDI4YSBpbiAvICJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OmUxNThiNTdkNmY1YTk2ZWY1ZmQyMmYyZmU3NmM3MGI1YmE2ZmY1YjI2MTlmOWQ4MzEyNWIyYWFkMDQ5MmFjN2IiLCJzaGEyNTY6ZGEyMTA1NmU3YmY0MzA4ZWNlYTBjMDgzNjg0OGE3ZmU5MmYzOGZkY2YzNWJjMDllZTZkOThlN2FiN2JlZWViZiIsInNoYTI1NjpmMGUxOGFhNjAzMmMyNDY1OWE5Yzc0MWZjMzZjYTU2ZjU4OTc4MmVhMTMyMDYxY2NmNmY1MmI5NTI0MDNkYTk0Il19fQ=="
}
},
"distro": {
@@ -89,5 +100,9 @@
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.0.json"
}
}
+2 -2
View File
@@ -25,7 +25,7 @@ type Presenter interface {
}
// GetPresenter returns a presenter for images or directories
func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d distro.Distro) Presenter {
func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) Presenter {
switch option {
case JSONPresenter:
return json.NewPresenter(catalog, srcMetadata, d)
@@ -34,7 +34,7 @@ func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catal
case TablePresenter:
return table.NewPresenter(catalog)
case CycloneDxPresenter:
return cyclonedx.NewPresenter(catalog, srcMetadata, d)
return cyclonedx.NewPresenter(catalog, srcMetadata)
default:
return nil
}
+20 -14
View File
@@ -5,13 +5,16 @@ import "github.com/anchore/stereoscope/pkg/image"
// ImageMetadata represents all static metadata that defines what a container image is. This is useful to later describe
// "what" was cataloged without needing the more complicated stereoscope Image objects or Resolver objects.
type ImageMetadata struct {
UserInput string `json:"userInput"`
Scope Scope `json:"scope"` // specific perspective to catalog
Layers []LayerMetadata `json:"layers"`
Size int64 `json:"size"`
Digest string `json:"digest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
UserInput string `json:"userInput"`
ID string `json:"imageID"`
ManifestDigest string `json:"manifestDigest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
Size int64 `json:"imageSize"`
Scope Scope `json:"scope"` // specific perspective to catalog
Layers []LayerMetadata `json:"layers"`
RawManifest []byte `json:"manifest"`
RawConfig []byte `json:"config"`
}
// LayerMetadata represents all static metadata that defines what a container image layer is.
@@ -29,13 +32,16 @@ func NewImageMetadata(img *image.Image, userInput string, scope Scope) ImageMeta
tags[idx] = tag.String()
}
theImg := ImageMetadata{
UserInput: userInput,
Scope: scope,
Digest: img.Metadata.Digest,
Size: img.Metadata.Size,
MediaType: string(img.Metadata.MediaType),
Tags: tags,
Layers: make([]LayerMetadata, len(img.Layers)),
ID: img.Metadata.ID,
UserInput: userInput,
Scope: scope,
ManifestDigest: img.Metadata.ManifestDigest,
Size: img.Metadata.Size,
MediaType: string(img.Metadata.MediaType),
Tags: tags,
Layers: make([]LayerMetadata, len(img.Layers)),
RawConfig: img.Metadata.RawConfig,
RawManifest: img.Metadata.RawManifest,
}
// populate image metadata
+1 -1
View File
@@ -26,7 +26,7 @@ func TestDistroImage(t *testing.T) {
t.Fatalf("could not create distro: %+v", err)
}
for _, d := range deep.Equal(actualDistro, expected) {
for _, d := range deep.Equal(actualDistro, &expected) {
t.Errorf("found distro difference: %+v", d)
}
+5
View File
@@ -82,6 +82,11 @@ func TestCatalogFromJSON(t *testing.T) {
}
for _, d := range deep.Equal(a, e) {
// ignore errors for empty collections vs nil for select fields
// TODO: this is brittle, but not dangerously so. We should still find a better way to do this.
if d == "Licenses: [] != <nil slice>" {
continue
}
t.Errorf(" package %d (name=%s) diff: %+v", i, e.Name, d)
}
}
+40 -62
View File
@@ -3,24 +3,24 @@ package integration
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/xeipuuv/gojsonschema"
)
const jsonSchemaPath = "schema/json"
const jsonSchemaExamplesPath = jsonSchemaPath + "/examples"
func repoRoot(t *testing.T) string {
t.Helper()
@@ -36,7 +36,7 @@ func repoRoot(t *testing.T) string {
}
func validateAgainstV1Schema(t *testing.T, json string) {
fullSchemaPath := path.Join(repoRoot(t), jsonSchemaPath, "schema.json")
fullSchemaPath := path.Join(repoRoot(t), jsonSchemaPath, fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion))
schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", fullSchemaPath))
documentLoader := gojsonschema.NewStringLoader(json)
@@ -47,54 +47,13 @@ func validateAgainstV1Schema(t *testing.T, json string) {
if !result.Valid() {
t.Errorf("failed json schema validation:")
t.Errorf("JSON:\n%s\n", json)
for _, desc := range result.Errors() {
t.Errorf(" - %s\n", desc)
}
}
}
func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope source.Source, prefix string) {
// make the json output example dir if it does not exist
absJsonSchemaExamplesPath := path.Join(repoRoot(t), jsonSchemaExamplesPath)
if _, err := os.Stat(absJsonSchemaExamplesPath); os.IsNotExist(err) {
os.Mkdir(absJsonSchemaExamplesPath, 0755)
}
output := bytes.NewBufferString("")
d, err := distro.NewDistro(distro.CentOS, "5", "rhel fedora")
if err != nil {
t.Fatalf("bad distro: %+v", err)
}
p := presenter.GetPresenter(presenter.JSONPresenter, theScope.Metadata, catalog, d)
if p == nil {
t.Fatal("unable to get presenter")
}
err = p.Present(output)
if err != nil {
t.Fatalf("unable to present: %+v", err)
}
// we use the examples dir as a way to use integration tests to drive what valid examples are in case we
// want to update the json schema. We do not want to validate the output of the presentation format as the
// contents may change regularly, making the integration tests brittle.
testFileName := prefix + "_" + path.Base(t.Name()) + ".json"
testFilePath := path.Join(absJsonSchemaExamplesPath, testFileName)
fh, err := os.OpenFile(testFilePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("unable to open json example path: %+v", err)
}
_, err = fh.WriteString(output.String())
if err != nil {
t.Fatalf("unable to write json example: %+v", err)
}
validateAgainstV1Schema(t, output.String())
}
func TestJsonSchemaImg(t *testing.T) {
fixtureImageName := "image-pkg-coverage"
_, cleanup := imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
@@ -106,15 +65,25 @@ func TestJsonSchemaImg(t *testing.T) {
t.Fatalf("failed to catalog image: %+v", err)
}
var cases []testCase
cases = append(cases, commonTestCases...)
cases = append(cases, imageOnlyTestCases...)
output := bytes.NewBufferString("")
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testJsonSchema(t, catalog, src, "img")
})
d, err := distro.NewDistro(distro.CentOS, "5", "rhel fedora")
if err != nil {
t.Fatalf("bad distro: %+v", err)
}
p := presenter.GetPresenter(presenter.JSONPresenter, src.Metadata, catalog, &d)
if p == nil {
t.Fatal("unable to get presenter")
}
err = p.Present(output)
if err != nil {
t.Fatalf("unable to present: %+v", err)
}
validateAgainstV1Schema(t, output.String())
}
func TestJsonSchemaDirs(t *testing.T) {
@@ -123,13 +92,22 @@ func TestJsonSchemaDirs(t *testing.T) {
t.Errorf("unable to create source from dir: %+v", err)
}
var cases []testCase
cases = append(cases, commonTestCases...)
cases = append(cases, dirOnlyTestCases...)
output := bytes.NewBufferString("")
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testJsonSchema(t, catalog, src, "dir")
})
d, err := distro.NewDistro(distro.CentOS, "5", "rhel fedora")
if err != nil {
t.Fatalf("bad distro: %+v", err)
}
p := presenter.GetPresenter(presenter.JSONPresenter, src.Metadata, catalog, &d)
if p == nil {
t.Fatal("unable to get presenter")
}
err = p.Present(output)
if err != nil {
t.Fatalf("unable to present: %+v", err)
}
validateAgainstV1Schema(t, output.String())
}
+1 -3
View File
@@ -1,5 +1,3 @@
// +build integration
package integration
import (
@@ -21,7 +19,7 @@ func TestRegression212ApkBufferSize(t *testing.T) {
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
defer cleanup()
catalog, _, _, err := syft.Catalog("docker-archive:"+tarPath, source.SquashedScope)
_, catalog, _, err := syft.Catalog("docker-archive:"+tarPath, source.SquashedScope)
if err != nil {
t.Fatalf("failed to catalog image: %+v", err)
}
+45
View File
@@ -181,6 +181,7 @@ func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus
}
// FetchImageHandler periodically writes a the image save and write-to-disk process in the form of a progress bar.
// nolint:dupl
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := stereoEventParsers.ParseFetchImage(event)
if err != nil {
@@ -307,3 +308,47 @@ func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybu
return nil
}
// ImportStartedHandler shows the intermittent upload progress to Anchore Enterprise.
// nolint:dupl
func ImportStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := syftEventParsers.ParseImportStarted(event)
if err != nil {
return fmt.Errorf("bad %s event: %w", event.Type, err)
}
line, err := fr.Append()
if err != nil {
return err
}
wg.Add(1)
formatter, spinner := startProcess()
stream := progress.Stream(ctx, prog, interval)
title := tileFormat.Sprint("Uploading image")
formatFn := func(p progress.Progress) {
progStr, err := formatter.Format(p)
spin := color.Magenta.Sprint(spinner.Next())
if err != nil {
_, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err))
} else {
auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage())
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s %s", spin, title, progStr, auxInfo))
}
}
go func() {
defer wg.Done()
formatFn(progress.Progress{})
for p := range stream {
formatFn(p)
}
spin := color.Green.Sprint(completedStatus)
title = tileFormat.Sprint("Uploaded image")
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
}()
return err
}
+4 -1
View File
@@ -27,7 +27,7 @@ func NewHandler() *Handler {
// RespondsTo indicates if the handler is capable of handling the given event.
func (r *Handler) RespondsTo(event partybus.Event) bool {
switch event.Type {
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted, syftEvent.ImportStarted:
return true
default:
return false
@@ -48,6 +48,9 @@ func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Ev
case syftEvent.CatalogerStarted:
return CatalogerStartedHandler(ctx, fr, event, wg)
case syftEvent.ImportStarted:
return ImportStartedHandler(ctx, fr, event, wg)
}
return nil
}