mirror of
https://github.com/anchore/syft.git
synced 2026-10-25 03:22:22 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52bac6e2fd | ||
|
|
2d0c127419 | ||
|
|
1a124bd77b | ||
|
|
a5fd83b21d | ||
|
|
f87c59b4eb | ||
|
|
3ce7eabc98 | ||
|
|
6f9ded60ed | ||
|
|
65cbacd135 | ||
|
|
2989d3d975 | ||
|
|
ae71b8832d | ||
|
|
c42b036f46 | ||
|
|
8a17bfb69f | ||
|
|
0ed30138c4 | ||
|
|
3aaa0e5566 |
@@ -3,6 +3,7 @@ CHANGELOG.md
|
||||
/snapshot
|
||||
.server/
|
||||
.vscode/
|
||||
.history/
|
||||
*.fingerprint
|
||||
*.tar
|
||||
*.jar
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
@@ -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 +0,0 @@
|
||||
examples/
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
genson
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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), `\/`, "/")
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
+16
-14
@@ -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:
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("", " ")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user