mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 06:07:20 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f87c59b4eb | ||
|
|
3ce7eabc98 | ||
|
|
6f9ded60ed | ||
|
|
65cbacd135 | ||
|
|
2989d3d975 | ||
|
|
ae71b8832d | ||
|
|
c42b036f46 | ||
|
|
8a17bfb69f | ||
|
|
0ed30138c4 | ||
|
|
3aaa0e5566 | ||
|
|
71939557e6 | ||
|
|
030427bb33 | ||
|
|
569a598df7 | ||
|
|
0ad8c53ec2 | ||
|
|
c892c3609e | ||
|
|
62b03f3a91 | ||
|
|
4b45c42f5a | ||
|
|
f46de19c6b | ||
|
|
91baabe5a1 | ||
|
|
6f7a4fd3e4 | ||
|
|
aa0d444fd4 | ||
|
|
b694dacb21 | ||
|
|
9668341a14 | ||
|
|
495fb0a45f | ||
|
|
a640a2c4cd | ||
|
|
3e8bca6911 | ||
|
|
5afdd574a8 | ||
|
|
0ebe791acd | ||
|
|
a5b72405dd | ||
|
|
4861f69d7c | ||
|
|
6bde075cd3 | ||
|
|
f7be73dbcc | ||
|
|
fc99538d78 | ||
|
|
4d4e242251 | ||
|
|
416afa015e | ||
|
|
bd4c784a64 | ||
|
|
624a090631 |
@@ -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 := 70
|
||||
|
||||
## 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/*
|
||||
|
||||
+7
-6
@@ -4,14 +4,15 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gookit/color"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/format"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/syft"
|
||||
@@ -49,8 +50,8 @@ func setGlobalCliOptions() {
|
||||
// scan options
|
||||
flag := "scope"
|
||||
rootCmd.Flags().StringP(
|
||||
"scope", "s", scope.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", scope.Options))
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
@@ -101,7 +102,7 @@ func initLogging() {
|
||||
logWrapper := logger.NewLogrusLogger(cfg)
|
||||
syft.SetLogger(logWrapper)
|
||||
stereoscope.SetLogger(&logger.LogrusNestedLogger{
|
||||
Logger: logWrapper.Logger.WithField("from-lib", "steroscope"),
|
||||
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ func logAppConfig() {
|
||||
if err != nil {
|
||||
log.Debugf("Could not display application config: %+v", err)
|
||||
} else {
|
||||
log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr)))
|
||||
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(string(appCfgStr)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -92,7 +92,7 @@ func startWorker(userInput string) <-chan error {
|
||||
}
|
||||
}
|
||||
|
||||
catalog, scope, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
||||
src, catalog, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog input: %+v", err)
|
||||
return
|
||||
@@ -100,7 +100,7 @@ func startWorker(userInput string) <-chan error {
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.CatalogerFinished,
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, src.Metadata, catalog, distro),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
|
||||
@@ -4,13 +4,15 @@ go 1.14
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
|
||||
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-20201203153145-3f9a05a624d7
|
||||
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 +23,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,23 +119,25 @@ 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/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e h1:kty6r0R2JeaNPeWKSYDC+HW3hkqwFh4PP5TQ8pUPYFw=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e/go.mod h1:iYuIG0Nai8dR0ri3LhZQKUyO1loxUWAGvoWhXDmjy1A=
|
||||
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=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/stereoscope v0.0.0-20200925184903-c82da54e98fe h1:m4NSyTo2fVUoUHAV/ZVqE/PFMr/y8oz9HRrhWLk9It0=
|
||||
github.com/anchore/stereoscope v0.0.0-20200925184903-c82da54e98fe/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
|
||||
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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
|
||||
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
|
||||
@@ -164,8 +166,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar v1.3.3 h1:pVP1d49CcQQaNOl+PI6sPybIrIOD/6sux31PFdmhTH0=
|
||||
github.com/bmatcuk/doublestar v1.3.3/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
|
||||
@@ -260,9 +260,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=
|
||||
@@ -301,8 +304,6 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
@@ -507,6 +508,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=
|
||||
@@ -726,6 +729,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=
|
||||
@@ -793,6 +798,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=
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
@@ -23,7 +23,7 @@ type Application struct {
|
||||
ConfigPath string
|
||||
PresenterOpt presenter.Option
|
||||
Output string `mapstructure:"output"`
|
||||
ScopeOpt scope.Option
|
||||
ScopeOpt source.Scope
|
||||
Scope string `mapstructure:"scope"`
|
||||
Quiet bool `mapstructure:"quiet"`
|
||||
Log Logging `mapstructure:"log"`
|
||||
@@ -79,9 +79,9 @@ func (cfg *Application) Build() error {
|
||||
}
|
||||
cfg.PresenterOpt = presenterOption
|
||||
|
||||
// set the scope
|
||||
scopeOption := scope.ParseOption(cfg.Scope)
|
||||
if scopeOption == scope.UnknownScope {
|
||||
// set the source
|
||||
scopeOption := source.ParseScope(cfg.Scope)
|
||||
if scopeOption == source.UnknownScope {
|
||||
return fmt.Errorf("bad --scope value '%s'", cfg.Scope)
|
||||
}
|
||||
cfg.ScopeOpt = scopeOption
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
|
||||
// note: do not change this
|
||||
// ApplicationName is the non-capitalized name of the application (do not change this)
|
||||
const ApplicationName = "syft"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package internal contains miscellaneous functions and objects useful within syft but should not be used externally.
|
||||
*/
|
||||
package internal
|
||||
@@ -1,5 +1,6 @@
|
||||
package file
|
||||
|
||||
// GlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
|
||||
// Source: https://research.swtch.com/glob.go
|
||||
func GlobMatch(pattern, name string) bool {
|
||||
px := 0
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Opener is an object that stores a path to later be opened as a file.
|
||||
type Opener struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// Open the stored path as a io.ReadCloser.
|
||||
func (o Opener) Open() (io.ReadCloser, error) {
|
||||
return os.Open(o.path)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,20 @@ import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// ZipFileManifest is a collection of paths and their file metadata.
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// newZipManifest creates an empty ZipFileManifest.
|
||||
func newZipManifest() ZipFileManifest {
|
||||
return make(ZipFileManifest)
|
||||
}
|
||||
|
||||
// Add a new path and it's file metadata to the collection.
|
||||
func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
|
||||
z[entry] = info
|
||||
}
|
||||
|
||||
// GlobMatch returns the path keys that match the given value(s).
|
||||
func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
uniqueMatches := internal.NewStringSet()
|
||||
|
||||
@@ -43,6 +47,7 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
return results
|
||||
}
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
@@ -62,6 +67,7 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||
func normalizeZipEntryName(entry string) string {
|
||||
if !strings.HasPrefix(entry, "/") {
|
||||
return "/" + entry
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// represents the order of bytes
|
||||
_ = iota
|
||||
KB = 1 << (10 * iota)
|
||||
MB
|
||||
@@ -33,6 +34,7 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
return results
|
||||
}
|
||||
|
||||
// TraverseFilesInZip enumerates all paths stored within a zip archive using the visitor pattern.
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
@@ -63,6 +65,7 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractFromZipToUniqueTempFile extracts select paths for the given archive to a temporary directory, returning file openers for each file extracted.
|
||||
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
|
||||
results := make(map[string]Opener)
|
||||
|
||||
@@ -121,6 +124,7 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// ContentsFromZip extracts select paths for the given archive and returns a set of string contents for each path.
|
||||
func ContentsFromZip(archivePath string, paths ...string) (map[string]string, error) {
|
||||
results := make(map[string]string)
|
||||
|
||||
@@ -162,6 +166,7 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// UnzipToDir extracts a zip archive to a target directory.
|
||||
func UnzipToDir(archivePath, targetDir string) error {
|
||||
visitor := func(file *zip.File) error {
|
||||
// the zip-slip attack protection is still being erroneously detected
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package format
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
DefaultColor Color = iota + 30
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
White
|
||||
)
|
||||
|
||||
type Color uint8
|
||||
|
||||
// TODO: not cross platform (windows...)
|
||||
func (c Color) Format(s string) string {
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
|
||||
}
|
||||
@@ -1,37 +1,49 @@
|
||||
/*
|
||||
Package log contains the singleton object and helper functions for facilitating logging within the syft library.
|
||||
*/
|
||||
package log
|
||||
|
||||
import "github.com/anchore/syft/syft/logger"
|
||||
|
||||
// Log is the singleton used to facilitate logging internally within syft
|
||||
var Log logger.Logger = &nopLogger{}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
Log.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func Error(args ...interface{}) {
|
||||
Log.Error(args...)
|
||||
}
|
||||
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
Log.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func Warn(args ...interface{}) {
|
||||
Log.Warn(args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func Infof(format string, args ...interface{}) {
|
||||
Log.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func Info(args ...interface{}) {
|
||||
Log.Info(args...)
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
Log.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func Debug(args ...interface{}) {
|
||||
Log.Debug(args...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package logger contains implementations for the syft.logger.Logger interface.
|
||||
*/
|
||||
package logger
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
// LogrusConfig contains all configurable values for the Logrus logger
|
||||
type LogrusConfig struct {
|
||||
EnableConsole bool
|
||||
EnableFile bool
|
||||
@@ -18,16 +19,19 @@ type LogrusConfig struct {
|
||||
FileLocation string
|
||||
}
|
||||
|
||||
// LogrusLogger contains all runtime values for using Logrus with the configured output target and input configuration values.
|
||||
type LogrusLogger struct {
|
||||
Config LogrusConfig
|
||||
Logger *logrus.Logger
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
// LogrusNestedLogger is a wrapper for Logrus to enable nested logging configuration (loggers that always attach key-value pairs to all log entries)
|
||||
type LogrusNestedLogger struct {
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewLogrusLogger creates a new LogrusLogger with the given configuration
|
||||
func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
appLogger := logrus.New()
|
||||
|
||||
@@ -76,66 +80,82 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
|
||||
l.Logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func (l *LogrusLogger) Infof(format string, args ...interface{}) {
|
||||
l.Logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func (l *LogrusLogger) Warnf(format string, args ...interface{}) {
|
||||
l.Logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Logger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func (l *LogrusLogger) Debug(args ...interface{}) {
|
||||
l.Logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func (l *LogrusLogger) Info(args ...interface{}) {
|
||||
l.Logger.Info(args...)
|
||||
}
|
||||
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func (l *LogrusLogger) Warn(args ...interface{}) {
|
||||
l.Logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func (l *LogrusLogger) Error(args ...interface{}) {
|
||||
l.Logger.Error(args...)
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) {
|
||||
l.Logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) {
|
||||
l.Logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func (l *LogrusNestedLogger) Warnf(format string, args ...interface{}) {
|
||||
l.Logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Logger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func (l *LogrusNestedLogger) Debug(args ...interface{}) {
|
||||
l.Logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func (l *LogrusNestedLogger) Info(args ...interface{}) {
|
||||
l.Logger.Info(args...)
|
||||
}
|
||||
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func (l *LogrusNestedLogger) Warn(args ...interface{}) {
|
||||
l.Logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func (l *LogrusNestedLogger) Error(args ...interface{}) {
|
||||
l.Logger.Error(args...)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package internal
|
||||
|
||||
import "sort"
|
||||
|
||||
// StringSet represents a set of string types.
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
// NewStringSet creates a new empty StringSet.
|
||||
func NewStringSet() StringSet {
|
||||
return make(StringSet)
|
||||
}
|
||||
|
||||
// NewStringSetFromSlice creates a StringSet populated with values from the given slice.
|
||||
func NewStringSetFromSlice(start []string) StringSet {
|
||||
ret := make(StringSet)
|
||||
for _, s := range start {
|
||||
@@ -14,19 +19,23 @@ func NewStringSetFromSlice(start []string) StringSet {
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add a string to the set.
|
||||
func (s StringSet) Add(i string) {
|
||||
s[i] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove a string from the set.
|
||||
func (s StringSet) Remove(i string) {
|
||||
delete(s, i)
|
||||
}
|
||||
|
||||
// Contains indicates if the given string is contained within the set.
|
||||
func (s StringSet) Contains(i string) bool {
|
||||
_, ok := s[i]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ToSlice returns a sorted slice of strings that are contained within the set.
|
||||
func (s StringSet) ToSlice() []string {
|
||||
ret := make([]string, len(s))
|
||||
idx := 0
|
||||
@@ -34,5 +43,6 @@ func (s StringSet) ToSlice() []string {
|
||||
ret[idx] = v
|
||||
idx++
|
||||
}
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/*
|
||||
Package version contains all build time metadata (version, build time, git commit, etc).
|
||||
*/
|
||||
package version
|
||||
|
||||
import (
|
||||
|
||||
@@ -18,6 +18,7 @@ var latestAppVersionURL = struct {
|
||||
path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
|
||||
}
|
||||
|
||||
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
|
||||
func IsUpdateAvailable() (bool, string, error) {
|
||||
currentVersionStr := FromBuild().Version
|
||||
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
examples/
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/jsonschema"
|
||||
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.
|
||||
*/
|
||||
|
||||
func main() {
|
||||
j := jsonschema.Reflect(&jsonPresenter.Document{})
|
||||
filename := "schema.json"
|
||||
fh, err := os.OpenFile("schema.json", os.O_RDWR|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
enc := json.NewEncoder(fh)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(&j)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
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
|
||||
+159
-408
@@ -1,414 +1,165 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"properties": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Document",
|
||||
"definitions": {
|
||||
"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"
|
||||
],
|
||||
"properties": {
|
||||
"artifacts": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"foundBy": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"licenses": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"locations": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"layerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"layerIndex",
|
||||
"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"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foundBy",
|
||||
"licenses",
|
||||
"locations",
|
||||
"name",
|
||||
"type",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Package"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"digest",
|
||||
"layers",
|
||||
"mediaType",
|
||||
"size",
|
||||
"tags"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
"$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"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"required": [
|
||||
"artifacts",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
"Location": {
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"layerID": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Package": {
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"type",
|
||||
"foundBy",
|
||||
"locations",
|
||||
"licenses",
|
||||
"language",
|
||||
"cpes",
|
||||
"purl",
|
||||
"metadataType"
|
||||
],
|
||||
"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": {
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Source": {
|
||||
"required": [
|
||||
"type",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ 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/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
@@ -32,17 +33,18 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
|
||||
return &filesProcessed, &packagesDiscovered
|
||||
}
|
||||
|
||||
// Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages.
|
||||
// Catalog a given source (container image or filesystem) with the given catalogers, returning all discovered packages.
|
||||
// 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 scope.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 scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, er
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/anchore/syft/syft/cataloger/rpmdb"
|
||||
"github.com/anchore/syft/syft/cataloger/ruby"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Cataloger describes behavior for an object to participate in parsing container image or file system
|
||||
@@ -25,7 +25,7 @@ type Cataloger interface {
|
||||
// Name returns a string that uniquely describes a cataloger
|
||||
Name() string
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
||||
Catalog(resolver scope.Resolver) ([]pkg.Package, error)
|
||||
Catalog(resolver source.Resolver) ([]pkg.Package, error)
|
||||
}
|
||||
|
||||
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
||||
|
||||
@@ -6,10 +6,9 @@ package common
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// GenericCataloger implements the Catalog interface and is responsible for dispatching the proper parser function for
|
||||
@@ -17,8 +16,8 @@ import (
|
||||
type GenericCataloger struct {
|
||||
globParsers map[string]ParserFn
|
||||
pathParsers map[string]ParserFn
|
||||
selectedFiles []file.Reference
|
||||
parsers map[file.Reference]ParserFn
|
||||
selectedFiles []source.Location
|
||||
parsers map[source.Location]ParserFn
|
||||
upstreamCataloger string
|
||||
}
|
||||
|
||||
@@ -27,8 +26,8 @@ func NewGenericCataloger(pathParsers map[string]ParserFn, globParsers map[string
|
||||
return &GenericCataloger{
|
||||
globParsers: globParsers,
|
||||
pathParsers: pathParsers,
|
||||
selectedFiles: make([]file.Reference, 0),
|
||||
parsers: make(map[file.Reference]ParserFn),
|
||||
selectedFiles: make([]source.Location, 0),
|
||||
parsers: make(map[source.Location]ParserFn),
|
||||
upstreamCataloger: upstreamCataloger,
|
||||
}
|
||||
}
|
||||
@@ -39,7 +38,7 @@ func (c *GenericCataloger) Name() string {
|
||||
}
|
||||
|
||||
// register pairs a set of file references with a parser function for future cataloging (when the file contents are resolved)
|
||||
func (c *GenericCataloger) register(files []file.Reference, parser ParserFn) {
|
||||
func (c *GenericCataloger) register(files []source.Location, parser ParserFn) {
|
||||
c.selectedFiles = append(c.selectedFiles, files...)
|
||||
for _, f := range files {
|
||||
c.parsers[f] = parser
|
||||
@@ -48,14 +47,14 @@ func (c *GenericCataloger) register(files []file.Reference, parser ParserFn) {
|
||||
|
||||
// clear deletes all registered file-reference-to-parser-function pairings from former SelectFiles() and register() calls
|
||||
func (c *GenericCataloger) clear() {
|
||||
c.selectedFiles = make([]file.Reference, 0)
|
||||
c.parsers = make(map[file.Reference]ParserFn)
|
||||
c.selectedFiles = make([]source.Location, 0)
|
||||
c.parsers = make(map[source.Location]ParserFn)
|
||||
}
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
||||
func (c *GenericCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
func (c *GenericCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
fileSelection := c.selectFiles(resolver)
|
||||
contents, err := resolver.MultipleFileContentsByRef(fileSelection...)
|
||||
contents, err := resolver.MultipleFileContentsByLocation(fileSelection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -63,10 +62,10 @@ func (c *GenericCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, erro
|
||||
}
|
||||
|
||||
// SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging
|
||||
func (c *GenericCataloger) selectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
func (c *GenericCataloger) selectFiles(resolver source.FileResolver) []source.Location {
|
||||
// select by exact path
|
||||
for path, parser := range c.pathParsers {
|
||||
files, err := resolver.FilesByPath(file.Path(path))
|
||||
files, err := resolver.FilesByPath(path)
|
||||
if err != nil {
|
||||
log.Warnf("cataloger failed to select files by path: %+v", err)
|
||||
}
|
||||
@@ -90,28 +89,28 @@ func (c *GenericCataloger) selectFiles(resolver scope.FileResolver) []file.Refer
|
||||
}
|
||||
|
||||
// catalog takes a set of file contents and uses any configured parser functions to resolve and return discovered packages
|
||||
func (c *GenericCataloger) catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
func (c *GenericCataloger) catalog(contents map[source.Location]string) ([]pkg.Package, error) {
|
||||
defer c.clear()
|
||||
|
||||
packages := make([]pkg.Package, 0)
|
||||
|
||||
for reference, parser := range c.parsers {
|
||||
content, ok := contents[reference]
|
||||
for location, parser := range c.parsers {
|
||||
content, ok := contents[location]
|
||||
if !ok {
|
||||
log.Warnf("cataloger '%s' missing file content: %+v", c.upstreamCataloger, reference)
|
||||
log.Warnf("cataloger '%s' missing file content: %+v", c.upstreamCataloger, location)
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := parser(string(reference.Path), strings.NewReader(content))
|
||||
entries, err := parser(location.Path, strings.NewReader(content))
|
||||
if err != nil {
|
||||
// TODO: should we fail? or only log?
|
||||
log.Warnf("cataloger '%s' failed to parse entries (reference=%+v): %+v", c.upstreamCataloger, reference, err)
|
||||
log.Warnf("cataloger '%s' failed to parse entries (location=%+v): %+v", c.upstreamCataloger, location, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
entry.FoundBy = c.upstreamCataloger
|
||||
entry.Source = []file.Reference{reference}
|
||||
entry.Locations = []source.Location{location}
|
||||
|
||||
packages = append(packages, entry)
|
||||
}
|
||||
|
||||
@@ -6,48 +6,50 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
type testResolverMock struct {
|
||||
contents map[file.Reference]string
|
||||
contents map[source.Location]string
|
||||
}
|
||||
|
||||
func newTestResolver() *testResolverMock {
|
||||
return &testResolverMock{
|
||||
contents: make(map[file.Reference]string),
|
||||
contents: make(map[source.Location]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testResolverMock) FileContentsByRef(_ file.Reference) (string, error) {
|
||||
func (r *testResolverMock) FileContentsByLocation(_ source.Location) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (r *testResolverMock) MultipleFileContentsByRef(_ ...file.Reference) (map[file.Reference]string, error) {
|
||||
func (r *testResolverMock) MultipleFileContentsByLocation([]source.Location) (map[source.Location]string, error) {
|
||||
return r.contents, nil
|
||||
}
|
||||
|
||||
func (r *testResolverMock) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
|
||||
results := make([]file.Reference, len(paths))
|
||||
func (r *testResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
|
||||
results := make([]source.Location, len(paths))
|
||||
|
||||
for idx, p := range paths {
|
||||
results[idx] = file.NewFileReference(p)
|
||||
results[idx] = source.NewLocation(p)
|
||||
r.contents[results[idx]] = fmt.Sprintf("%s file contents!", p)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *testResolverMock) FilesByGlob(_ ...string) ([]file.Reference, error) {
|
||||
func (r *testResolverMock) FilesByGlob(_ ...string) ([]source.Location, error) {
|
||||
path := "/a-path.txt"
|
||||
ref := file.NewFileReference(file.Path(path))
|
||||
r.contents[ref] = fmt.Sprintf("%s file contents!", path)
|
||||
return []file.Reference{ref}, nil
|
||||
location := source.NewLocation(path)
|
||||
r.contents[location] = fmt.Sprintf("%s file contents!", path)
|
||||
return []source.Location{location}, nil
|
||||
}
|
||||
|
||||
func (r *testResolverMock) RelativeFileByPath(_ file.Reference, _ string) (*file.Reference, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
func (r *testResolverMock) RelativeFileByPath(_ source.Location, _ string) *source.Location {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
@@ -94,8 +96,8 @@ func TestGenericCataloger(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, p := range actualPkgs {
|
||||
ref := p.Source[0]
|
||||
exP, ok := expectedPkgs[string(ref.Path)]
|
||||
ref := p.Locations[0]
|
||||
exP, ok := expectedPkgs[ref.Path]
|
||||
if !ok {
|
||||
t.Errorf("missing expected pkg: ref=%+v", ref)
|
||||
continue
|
||||
@@ -106,7 +108,7 @@ func TestGenericCataloger(t *testing.T) {
|
||||
}
|
||||
|
||||
if exP.Name != p.Name {
|
||||
t.Errorf("bad contents mapping: %+v", p.Source)
|
||||
t.Errorf("bad contents mapping: %+v", p.Locations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,30 +33,30 @@ 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.
|
||||
func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
dbFileMatches, err := resolver.FilesByGlob(dpkgStatusGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
for _, dbRef := range dbFileMatches {
|
||||
dbContents, err := resolver.FileContentsByRef(dbRef)
|
||||
for _, dbLocation := range dbFileMatches {
|
||||
dbContents, err := resolver.FileContentsByLocation(dbLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkgs, err = parseDpkgStatus(strings.NewReader(dbContents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbRef.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.Path, err)
|
||||
}
|
||||
|
||||
md5ContentsByName, md5RefsByName, err := fetchMd5Contents(resolver, dbRef, pkgs)
|
||||
md5ContentsByName, md5RefsByName, err := fetchMd5Contents(resolver, dbLocation, pkgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err)
|
||||
}
|
||||
|
||||
copyrightContentsByName, copyrightRefsByName, err := fetchCopyrightContents(resolver, dbRef, pkgs)
|
||||
copyrightContentsByName, copyrightRefsByName, err := fetchCopyrightContents(resolver, dbLocation, pkgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
p.FoundBy = c.Name()
|
||||
p.Source = []file.Reference{dbRef}
|
||||
p.Locations = []source.Location{dbLocation}
|
||||
|
||||
if md5Reader, ok := md5ContentsByName[md5Key(*p)]; ok {
|
||||
// attach the file list
|
||||
@@ -74,7 +74,7 @@ func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if ref, ok := md5RefsByName[md5Key(*p)]; ok {
|
||||
p.Source = append(p.Source, ref)
|
||||
p.Locations = append(p.Locations, ref)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if ref, ok := copyrightRefsByName[p.Name]; ok {
|
||||
p.Source = append(p.Source, ref)
|
||||
p.Locations = append(p.Locations, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,93 +93,82 @@ func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func fetchMd5Contents(resolver scope.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) {
|
||||
func fetchMd5Contents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
|
||||
// fetch all MD5 file contents. This approach is more efficient than fetching each MD5 file one at a time
|
||||
|
||||
var md5FileMatches []file.Reference
|
||||
var nameByRef = make(map[file.Reference]string)
|
||||
parentPath, err := dbRef.Path.ParentPath()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to find parent of path=%+v: %w", dbRef.Path, err)
|
||||
}
|
||||
var md5FileMatches []source.Location
|
||||
var nameByRef = make(map[source.Location]string)
|
||||
parentPath := filepath.Dir(dbLocation.Path)
|
||||
|
||||
for _, p := range pkgs {
|
||||
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
|
||||
name := md5Key(p)
|
||||
md5sumPath := path.Join(string(parentPath), "info", name+md5sumsExt)
|
||||
md5SumRef, err := resolver.RelativeFileByPath(dbRef, md5sumPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to find relative md5sum from path=%+v: %w", dbRef.Path, err)
|
||||
}
|
||||
md5sumPath := path.Join(parentPath, "info", name+md5sumsExt)
|
||||
md5SumLocation := resolver.RelativeFileByPath(dbLocation, md5sumPath)
|
||||
|
||||
if md5SumRef == nil {
|
||||
if md5SumLocation == nil {
|
||||
// the most specific key did not work, fallback to just the name
|
||||
// look for /var/lib/dpkg/info/NAME.md5sums
|
||||
name := p.Name
|
||||
md5sumPath := path.Join(string(parentPath), "info", name+md5sumsExt)
|
||||
md5SumRef, err = resolver.RelativeFileByPath(dbRef, md5sumPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to find relative md5sum from path=%+v: %w", dbRef.Path, err)
|
||||
}
|
||||
md5sumPath := path.Join(parentPath, "info", name+md5sumsExt)
|
||||
md5SumLocation = resolver.RelativeFileByPath(dbLocation, md5sumPath)
|
||||
}
|
||||
// we should have at least one reference
|
||||
if md5SumRef != nil {
|
||||
md5FileMatches = append(md5FileMatches, *md5SumRef)
|
||||
nameByRef[*md5SumRef] = name
|
||||
if md5SumLocation != nil {
|
||||
md5FileMatches = append(md5FileMatches, *md5SumLocation)
|
||||
nameByRef[*md5SumLocation] = name
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the md5 contents
|
||||
md5ContentsByRef, err := resolver.MultipleFileContentsByRef(md5FileMatches...)
|
||||
md5ContentsByLocation, err := resolver.MultipleFileContentsByLocation(md5FileMatches)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// organize content results and refs by a combination of name and architecture
|
||||
var contentsByName = make(map[string]io.Reader)
|
||||
var refsByName = make(map[string]file.Reference)
|
||||
for ref, contents := range md5ContentsByRef {
|
||||
name := nameByRef[ref]
|
||||
var refsByName = make(map[string]source.Location)
|
||||
for location, contents := range md5ContentsByLocation {
|
||||
name := nameByRef[location]
|
||||
contentsByName[name] = strings.NewReader(contents)
|
||||
refsByName[name] = ref
|
||||
refsByName[name] = location
|
||||
}
|
||||
|
||||
return contentsByName, refsByName, nil
|
||||
}
|
||||
|
||||
func fetchCopyrightContents(resolver scope.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) {
|
||||
func fetchCopyrightContents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
|
||||
// fetch all copyright file contents. This approach is more efficient than fetching each copyright file one at a time
|
||||
|
||||
var copyrightFileMatches []file.Reference
|
||||
var nameByRef = make(map[file.Reference]string)
|
||||
var copyrightFileMatches []source.Location
|
||||
var nameByLocation = make(map[source.Location]string)
|
||||
for _, p := range pkgs {
|
||||
// look for /usr/share/docs/NAME/copyright files
|
||||
name := p.Name
|
||||
copyrightPath := path.Join(docsPath, name, "copyright")
|
||||
copyrightRef, err := resolver.RelativeFileByPath(dbRef, copyrightPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to find relative copyright from path=%+v: %w", dbRef.Path, err)
|
||||
}
|
||||
copyrightLocation := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
||||
|
||||
// we may not have a copyright file for each package, ignore missing files
|
||||
if copyrightRef != nil {
|
||||
copyrightFileMatches = append(copyrightFileMatches, *copyrightRef)
|
||||
nameByRef[*copyrightRef] = name
|
||||
if copyrightLocation != nil {
|
||||
copyrightFileMatches = append(copyrightFileMatches, *copyrightLocation)
|
||||
nameByLocation[*copyrightLocation] = name
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the copyright contents
|
||||
copyrightContentsByRef, err := resolver.MultipleFileContentsByRef(copyrightFileMatches...)
|
||||
copyrightContentsByLocation, err := resolver.MultipleFileContentsByLocation(copyrightFileMatches)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// organize content results and refs by package name
|
||||
var contentsByName = make(map[string]io.Reader)
|
||||
var refsByName = make(map[string]file.Reference)
|
||||
for ref, contents := range copyrightContentsByRef {
|
||||
name := nameByRef[ref]
|
||||
var refsByName = make(map[string]source.Location)
|
||||
for location, contents := range copyrightContentsByLocation {
|
||||
name := nameByLocation[location]
|
||||
contentsByName[name] = strings.NewReader(contents)
|
||||
refsByName[name] = ref
|
||||
refsByName[name] = location
|
||||
}
|
||||
|
||||
return contentsByName, refsByName, nil
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestDpkgCataloger(t *testing.T) {
|
||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-dpkg")
|
||||
defer cleanup()
|
||||
|
||||
s, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
|
||||
s, err := source.NewFromImage(img, source.AllLayersScope, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -77,11 +77,11 @@ func TestDpkgCataloger(t *testing.T) {
|
||||
for idx := range actual {
|
||||
a := &actual[idx]
|
||||
// we will test the sources separately
|
||||
var sourcesList = make([]string, len(a.Source))
|
||||
for i, s := range a.Source {
|
||||
sourcesList[i] = string(s.Path)
|
||||
var sourcesList = make([]string, len(a.Locations))
|
||||
for i, s := range a.Locations {
|
||||
sourcesList[i] = s.Path
|
||||
}
|
||||
a.Source = nil
|
||||
a.Locations = nil
|
||||
|
||||
for _, d := range deep.Equal(sourcesList, test.sources[a.Name]) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
|
||||
@@ -10,45 +10,100 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// match on versions and anything after the version. This is used to isolate the name from the version.
|
||||
// match examples:
|
||||
// wagon-webdav-1.0.2-rc1-hudson.jar ---> -1.0.2-rc1-hudson.jar
|
||||
// windows-remote-command-1.0.jar ---> -1.0.jar
|
||||
// wstx-asl-1-2.jar ---> -1-2.jar
|
||||
// guava-rc0.jar ---> -rc0.jar
|
||||
var versionAreaPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)(?P<remaining>.*)$`)
|
||||
|
||||
// match on explicit versions. This is used for extracting version information.
|
||||
// match examples:
|
||||
// pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1)
|
||||
// pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1)
|
||||
// pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3)
|
||||
var versionPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)`)
|
||||
// nameAndVersionPattern finds the package name and version (as named capture
|
||||
// groups) in a string. The pattern's strategy is to start at the beginning of
|
||||
// the string, and for every next dash-delimited group, consider the group to be
|
||||
// a continuation of the package name, unless the group begins with a number or
|
||||
// matches any of a specified set of "version-indicating" patterns. When a given
|
||||
// group meets this criterion, consider the group and the remainder of the
|
||||
// string to be the package version.
|
||||
//
|
||||
// Regex components of note:
|
||||
//
|
||||
// (?Ui) ... Sets the "U" and the "i" options for this Regex —— (ungreedy,
|
||||
// and case-insensitive, respectively). "Ungreedy" is important so that the '*' that trails the package name
|
||||
// component doesn't consume the rest of the string.
|
||||
//
|
||||
// [[:alpha:]][[:word:]]* ... Matches any word, and the word can include "word" characters (
|
||||
// which includes numbers and underscores), but the first character of the word MUST be a letter.
|
||||
//
|
||||
// (?:\.[[:alpha:]][[:word:]]*)* ... This looks redundant, but it's not. It
|
||||
// extends the previous pattern such that the net effect of both components is
|
||||
// that words can also include a period and more words (thus, when combined, not
|
||||
// only is "something" matched, but so is "com.prefix.thing"
|
||||
//
|
||||
// (?:\d.*|(?:build\d*.*)|(?:rc?\d+(?:^[[:alpha:]].*)?)) ...
|
||||
// This match group covers the "version-indicating" patterns mentioned in the above description. Given the pipes (
|
||||
// '|'), this functions as a series of 'OR'-joined conditions:
|
||||
//
|
||||
// \d.* ... "If it starts with a numeric digit, this is a version, no matter what follows."
|
||||
// build\d*.* ... "If it starts with "build" and then a numeric digit immediately after, this is a version."
|
||||
// rc?\d+(?:^[[:alpha:]].*)? ... "If it starts with "r" or "rc" and then one or more numeric digits immediately
|
||||
// after, but no alpha characters right after that (in the same word), this is a version."
|
||||
//
|
||||
// Match examples:
|
||||
// some-package-4.0.1 --> name="some-package", version="4.0.1"
|
||||
// prefix.thing-4 --> name="prefix.thing", version="4"
|
||||
// my-http2-server-5 --> name="my-http2-server", version="5"
|
||||
// jetpack-build235-rc5 --> name="jetpack", version="build2.0-rc5"
|
||||
// ironman-r4-2009 --> name="ironman", version="r4-2009"
|
||||
var nameAndVersionPattern = regexp.MustCompile(`(?Ui)^(?P<name>(?:[[:alpha:]][[:word:]]*(?:\.[[:alpha:]][[:word:]]*)*-?)+)(?:-(?P<version>(?:\d.*|(?:build\d*.*)|(?:rc?\d+(?:^[[:alpha:]].*)?))))?$`)
|
||||
|
||||
type archiveFilename struct {
|
||||
raw string
|
||||
fields []map[string]string
|
||||
raw string
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
// TODO: Remove this method once we're using Go 1.15+.
|
||||
//
|
||||
// Go 1.15 introduces a `SubexpIndex` method for the Regexp type that would let
|
||||
// this code be made more elegant. Once we've reached 1.15, we should eliminate
|
||||
// this function in favor of that method.
|
||||
func subexpIndex(re *regexp.Regexp, name string) int {
|
||||
for i, subexpName := range re.SubexpNames() {
|
||||
if subexpName == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func getSubexp(matches []string, subexpName string, re *regexp.Regexp, raw string) string {
|
||||
if len(matches) < 1 {
|
||||
log.Warnf("unexpectedly empty matches for archive '%s'", raw)
|
||||
return ""
|
||||
}
|
||||
|
||||
index := subexpIndex(re, subexpName)
|
||||
if index < 1 {
|
||||
log.Warnf("unexpected index of '%s' capture group for Java archive '%s'", subexpName, raw)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prevent out-of-range panic
|
||||
if len(matches) < index+1 {
|
||||
log.Warnf("no match found for '%s' in '%s'", subexpName, matches[0])
|
||||
return ""
|
||||
}
|
||||
|
||||
return matches[index]
|
||||
}
|
||||
|
||||
func newJavaArchiveFilename(raw string) archiveFilename {
|
||||
// trim the file extension and remove any path prefixes
|
||||
name := strings.TrimSuffix(filepath.Base(raw), filepath.Ext(raw))
|
||||
cleanedFileName := strings.TrimSuffix(filepath.Base(raw), filepath.Ext(raw))
|
||||
|
||||
matches := versionPattern.FindAllStringSubmatch(name, -1)
|
||||
fields := make([]map[string]string, 0)
|
||||
for _, match := range matches {
|
||||
item := make(map[string]string)
|
||||
for i, name := range versionPattern.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
item[name] = match[i]
|
||||
}
|
||||
}
|
||||
fields = append(fields, item)
|
||||
}
|
||||
matches := nameAndVersionPattern.FindStringSubmatch(cleanedFileName)
|
||||
|
||||
name := getSubexp(matches, "name", nameAndVersionPattern, raw)
|
||||
version := getSubexp(matches, "version", nameAndVersionPattern, raw)
|
||||
|
||||
return archiveFilename{
|
||||
raw: raw,
|
||||
fields: fields,
|
||||
raw: raw,
|
||||
name: name,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,21 +121,3 @@ func (a archiveFilename) pkgType() pkg.Type {
|
||||
return pkg.UnknownPkg
|
||||
}
|
||||
}
|
||||
|
||||
func (a archiveFilename) version() string {
|
||||
if len(a.fields) > 1 {
|
||||
log.Warnf("discovered multiple name-version pairings from %q: %+v", a.raw, a.fields)
|
||||
return ""
|
||||
} else if len(a.fields) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.fields[0]["version"]
|
||||
}
|
||||
|
||||
func (a archiveFilename) name() string {
|
||||
// derive the name from the archive name (no path or extension) and remove any versions found
|
||||
basename := filepath.Base(a.raw)
|
||||
cleaned := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
return versionAreaPattern.ReplaceAllString(cleaned, "")
|
||||
}
|
||||
|
||||
@@ -93,23 +93,18 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/jtidy-4aug2000r7-dev-hudson-1.jar",
|
||||
// I don't see how we can reliably account for this case
|
||||
//version: "4aug2000r7-dev-hudson-1",
|
||||
version: "",
|
||||
filename: "/hudson.war:WEB-INF/lib/jtidy-4aug2000r7-dev-hudson-1.jar",
|
||||
version: "4aug2000r7-dev-hudson-1",
|
||||
extension: "jar",
|
||||
name: "jtidy",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/trilead-ssh2-build212-hudson-5.jar",
|
||||
// I don't see how we can reliably account for this case
|
||||
//version: "build212-hudson-5",
|
||||
version: "5",
|
||||
filename: "/hudson.war:WEB-INF/lib/trilead-ssh2-build212-hudson-5.jar",
|
||||
version: "build212-hudson-5",
|
||||
extension: "jar",
|
||||
// name: "trilead-ssh2",
|
||||
name: "trilead-ssh2-build212-hudson",
|
||||
ty: pkg.JavaPkg,
|
||||
name: "trilead-ssh2",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/guava-r06.jar",
|
||||
@@ -118,6 +113,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
name: "guava",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "BOOT-INF/lib/spring-data-r2dbc-1.1.0.RELEASE.jar", // Regression: https://github.com/anchore/syft/issues/255
|
||||
version: "1.1.0.RELEASE",
|
||||
extension: "jar",
|
||||
name: "spring-data-r2dbc",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -129,24 +131,24 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
t.Errorf("mismatched type: %+v != %v", ty, test.ty)
|
||||
}
|
||||
|
||||
version := obj.version()
|
||||
version := obj.version
|
||||
if version != test.version {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(version, test.version, true)
|
||||
diffs := dmp.DiffMain(test.version, version, true)
|
||||
t.Errorf("mismatched version:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
extension := obj.extension()
|
||||
if extension != test.extension {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(extension, test.extension, true)
|
||||
diffs := dmp.DiffMain(test.extension, extension, true)
|
||||
t.Errorf("mismatched extension:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
name := obj.name()
|
||||
name := obj.name
|
||||
if name != test.name {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(name, test.name, true)
|
||||
diffs := dmp.DiffMain(test.name, name, true)
|
||||
t.Errorf("mismatched name:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -303,7 +303,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "jul-to-slf4j",
|
||||
@@ -315,7 +315,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-validation",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "hibernate-validator",
|
||||
@@ -327,7 +327,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-expression",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "jakarta.validation-api",
|
||||
@@ -335,11 +335,11 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-web",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-actuator",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "log4j-api",
|
||||
@@ -359,23 +359,23 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-aop",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-actuator-autoconfigure",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-jcl",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-boot",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-logging",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "jakarta.annotation-api",
|
||||
@@ -383,7 +383,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-webmvc",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "HdrHistogram",
|
||||
@@ -391,7 +391,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-web",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "logback-classic",
|
||||
@@ -403,7 +403,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-json",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "jackson-databind",
|
||||
@@ -419,7 +419,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-autoconfigure",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "jackson-datatype-jdk8",
|
||||
@@ -435,11 +435,11 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-beans",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-actuator",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "slf4j-api",
|
||||
@@ -447,7 +447,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-core",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "logback-core",
|
||||
@@ -467,7 +467,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-boot-starter-tomcat",
|
||||
Version: "2.2.2",
|
||||
Version: "2.2.2.RELEASE",
|
||||
},
|
||||
{
|
||||
Name: "classmate",
|
||||
@@ -475,7 +475,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "spring-context",
|
||||
Version: "5.2.2",
|
||||
Version: "5.2.2.RELEASE",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -509,32 +509,28 @@ func TestParseNestedJar(t *testing.T) {
|
||||
expectedNameVersionPairSet.Add(makeKey(&e))
|
||||
}
|
||||
|
||||
actualNameVersionPairSet := internal.NewStringSet()
|
||||
for _, a := range actual {
|
||||
key := makeKey(&a)
|
||||
actualNameVersionPairSet.Add(key)
|
||||
if !expectedNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("extra package: %s", a)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range expectedNameVersionPairSet.ToSlice() {
|
||||
if !actualNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("missing package: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(actual) != len(expectedNameVersionPairSet) {
|
||||
actualNameVersionPairSet := internal.NewStringSet()
|
||||
for _, a := range actual {
|
||||
key := makeKey(&a)
|
||||
actualNameVersionPairSet.Add(key)
|
||||
if !expectedNameVersionPairSet.Contains(key) {
|
||||
t.Logf("extra package: %s", a)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range expectedNameVersionPairSet.ToSlice() {
|
||||
if !actualNameVersionPairSet.Contains(key) {
|
||||
t.Logf("missing package: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedNameVersionPairSet))
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
actualKey := makeKey(&a)
|
||||
|
||||
if !expectedNameVersionPairSet.Contains(actualKey) {
|
||||
t.Errorf("unexpected pkg: %q", actualKey)
|
||||
}
|
||||
|
||||
metadata := a.Metadata.(pkg.JavaMetadata)
|
||||
if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
|
||||
if metadata.Parent != nil {
|
||||
|
||||
@@ -93,8 +93,8 @@ func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error)
|
||||
func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
var name string
|
||||
switch {
|
||||
case filenameObj.name() != "":
|
||||
name = filenameObj.name()
|
||||
case filenameObj.name != "":
|
||||
name = filenameObj.name
|
||||
case manifest.Main["Name"] != "":
|
||||
// Manifest original spec...
|
||||
name = manifest.Main["Name"]
|
||||
@@ -117,8 +117,8 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
|
||||
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
var version string
|
||||
switch {
|
||||
case filenameObj.version() != "":
|
||||
version = filenameObj.version()
|
||||
case filenameObj.version != "":
|
||||
version = filenameObj.version
|
||||
case manifest.Main["Implementation-Version"] != "":
|
||||
version = manifest.Main["Implementation-Version"]
|
||||
case manifest.Main["Specification-Version"] != "":
|
||||
|
||||
@@ -2,6 +2,7 @@ package javascript
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
@@ -22,7 +23,8 @@ type PackageJSON struct {
|
||||
Version string `json:"version"`
|
||||
Latest []string `json:"latest"`
|
||||
Author Author `json:"author"`
|
||||
License string `json:"license"`
|
||||
License json.RawMessage `json:"license"`
|
||||
Licenses []license `json:"licenses"`
|
||||
Name string `json:"name"`
|
||||
Homepage string `json:"homepage"`
|
||||
Description string `json:"description"`
|
||||
@@ -107,6 +109,56 @@ func (r *Repository) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type license struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func licenseFromJSON(b []byte) (string, error) {
|
||||
// first try as string
|
||||
var licenseString string
|
||||
err := json.Unmarshal(b, &licenseString)
|
||||
if err == nil {
|
||||
return licenseString, nil
|
||||
}
|
||||
|
||||
// then try as object (this format is deprecated)
|
||||
var licenseObject license
|
||||
err = json.Unmarshal(b, &licenseObject)
|
||||
if err == nil {
|
||||
return licenseObject.Type, nil
|
||||
}
|
||||
|
||||
return "", errors.New("unable to unmarshal license field as either string or object")
|
||||
}
|
||||
|
||||
func licensesFromJSON(p PackageJSON) ([]string, error) {
|
||||
if p.License == nil && p.Licenses == nil {
|
||||
// This package.json doesn't specify any licenses whatsoever
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
singleLicense, err := licenseFromJSON(p.License)
|
||||
if err == nil {
|
||||
return []string{singleLicense}, nil
|
||||
}
|
||||
|
||||
// The "licenses" field is deprecated. It should be inspected as a last resort.
|
||||
if p.Licenses != nil {
|
||||
mapLicenses := func(licenses []license) []string {
|
||||
mappedLicenses := make([]string, len(licenses))
|
||||
for i, l := range licenses {
|
||||
mappedLicenses[i] = l.Type
|
||||
}
|
||||
return mappedLicenses
|
||||
}
|
||||
|
||||
return mapLicenses(p.Licenses), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to parse license field: %w", err)
|
||||
}
|
||||
|
||||
// parsePackageJson parses a package.json and returns the discovered JavaScript packages.
|
||||
func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
packages := make([]pkg.Package, 0)
|
||||
@@ -120,10 +172,15 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
|
||||
}
|
||||
|
||||
licenses, err := licensesFromJSON(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
|
||||
}
|
||||
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Licenses: []string{p.License},
|
||||
Licenses: licenses,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
@@ -131,7 +188,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
Author: p.Author.AuthorString(),
|
||||
Homepage: p.Homepage,
|
||||
URL: p.Repository.URL,
|
||||
Licenses: []string{p.License},
|
||||
Licenses: licenses,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,57 @@ func TestParsePackageJSON(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/pkg-json/package-license-object.json",
|
||||
ExpectedPkg: pkg.Package{
|
||||
Name: "npm",
|
||||
Version: "6.14.6",
|
||||
Type: pkg.NpmPkg,
|
||||
Licenses: []string{"ISC"},
|
||||
Language: pkg.JavaScript,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
Homepage: "https://docs.npmjs.com/",
|
||||
URL: "https://github.com/npm/cli",
|
||||
Licenses: []string{"ISC"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/pkg-json/package-license-objects.json",
|
||||
ExpectedPkg: pkg.Package{
|
||||
Name: "npm",
|
||||
Version: "6.14.6",
|
||||
Type: pkg.NpmPkg,
|
||||
Licenses: []string{"MIT", "Apache-2.0"},
|
||||
Language: pkg.JavaScript,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
Homepage: "https://docs.npmjs.com/",
|
||||
URL: "https://github.com/npm/cli",
|
||||
Licenses: []string{"MIT", "Apache-2.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/pkg-json/package-no-license.json",
|
||||
ExpectedPkg: pkg.Package{
|
||||
Name: "npm",
|
||||
Version: "6.14.6",
|
||||
Type: pkg.NpmPkg,
|
||||
Licenses: []string{},
|
||||
Language: pkg.JavaScript,
|
||||
MetadataType: pkg.NpmPackageJSONMetadataType,
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
Homepage: "https://docs.npmjs.com/",
|
||||
URL: "https://github.com/npm/cli",
|
||||
Licenses: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/pkg-json/package-nested-author.json",
|
||||
ExpectedPkg: pkg.Package{
|
||||
@@ -73,7 +124,7 @@ func TestParsePackageJSON(t *testing.T) {
|
||||
t.Fatalf("failed to open fixture: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := parsePackageJSON(fixture.Name(), fixture)
|
||||
actual, err := parsePackageJSON("", fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse package-lock.json: %+v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "6.14.6",
|
||||
"name": "npm",
|
||||
"description": "a package manager for JavaScript",
|
||||
"homepage": "https://docs.npmjs.com/",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/npm/cli"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://npm.community/c/bugs"
|
||||
},
|
||||
"main": "./lib/npm.js",
|
||||
"license": {
|
||||
"type" : "ISC",
|
||||
"url" : "https://opensource.org/licenses/ISC"
|
||||
},
|
||||
"engines": {
|
||||
"node": "6 >=6.2.0 || 8 || >=9.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "6.14.6",
|
||||
"name": "npm",
|
||||
"description": "a package manager for JavaScript",
|
||||
"homepage": "https://docs.npmjs.com/",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/npm/cli"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://npm.community/c/bugs"
|
||||
},
|
||||
"main": "./lib/npm.js",
|
||||
"licenses": [
|
||||
{ "type": "MIT",
|
||||
"url": "https://www.opensource.org/licenses/mit-license.php"
|
||||
},
|
||||
{ "type": "Apache-2.0",
|
||||
"url": "https://opensource.org/licenses/apache2.0.php"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "6 >=6.2.0 || 8 || >=9.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "6.14.6",
|
||||
"name": "npm",
|
||||
"description": "a package manager for JavaScript",
|
||||
"homepage": "https://docs.npmjs.com/",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/npm/cli"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://npm.community/c/bugs"
|
||||
},
|
||||
"main": "./lib/npm.js",
|
||||
"engines": {
|
||||
"node": "6 >=6.2.0 || 8 || >=9.3.0"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -8,11 +8,9 @@ import (
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,9 +31,9 @@ func (c *PackageCataloger) Name() string {
|
||||
}
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing python egg and wheel installations.
|
||||
func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
func (c *PackageCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
// nolint:prealloc
|
||||
var fileMatches []file.Reference
|
||||
var fileMatches []source.Location
|
||||
|
||||
for _, glob := range []string{eggMetadataGlob, wheelMetadataGlob} {
|
||||
matches, err := resolver.FilesByGlob(glob)
|
||||
@@ -46,10 +44,10 @@ func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, erro
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
for _, ref := range fileMatches {
|
||||
p, err := c.catalogEggOrWheel(resolver, ref)
|
||||
for _, location := range fileMatches {
|
||||
p, err := c.catalogEggOrWheel(resolver, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", ref.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", location.Path, err)
|
||||
}
|
||||
if p != nil {
|
||||
pkgs = append(pkgs, *p)
|
||||
@@ -59,8 +57,8 @@ func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, erro
|
||||
}
|
||||
|
||||
// catalogEggOrWheel takes the primary metadata file reference and returns the python package it represents.
|
||||
func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRef file.Reference) (*pkg.Package, error) {
|
||||
metadata, sources, err := c.assembleEggOrWheelMetadata(resolver, metadataRef)
|
||||
func (c *PackageCataloger) catalogEggOrWheel(resolver source.Resolver, metadataLocation source.Location) (*pkg.Package, error) {
|
||||
metadata, sources, err := c.assembleEggOrWheelMetadata(resolver, metadataLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +72,7 @@ func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRe
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
FoundBy: c.Name(),
|
||||
Source: sources,
|
||||
Locations: sources,
|
||||
Licenses: licenses,
|
||||
Language: pkg.Python,
|
||||
Type: pkg.PythonPkg,
|
||||
@@ -84,22 +82,19 @@ func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRe
|
||||
}
|
||||
|
||||
// fetchRecordFiles finds a corresponding RECORD file for the given python package metadata file and returns the set of file records contained.
|
||||
func (c *PackageCataloger) fetchRecordFiles(resolver scope.Resolver, metadataRef file.Reference) (files []pkg.PythonFileRecord, sources []file.Reference, err error) {
|
||||
func (c *PackageCataloger) fetchRecordFiles(resolver source.Resolver, metadataLocation source.Location) (files []pkg.PythonFileRecord, sources []source.Location, err error) {
|
||||
// we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory
|
||||
// or for an image... for an image the METADATA file may be present within multiple layers, so it is important
|
||||
// to reconcile the RECORD path to the same layer (or the next adjacent lower layer).
|
||||
|
||||
// lets find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
|
||||
recordPath := filepath.Join(filepath.Dir(string(metadataRef.Path)), "RECORD")
|
||||
recordRef, err := resolver.RelativeFileByPath(metadataRef, recordPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
recordPath := filepath.Join(filepath.Dir(metadataLocation.Path), "RECORD")
|
||||
recordRef := resolver.RelativeFileByPath(metadataLocation, recordPath)
|
||||
|
||||
if recordRef != nil {
|
||||
sources = append(sources, *recordRef)
|
||||
|
||||
recordContents, err := resolver.FileContentsByRef(*recordRef)
|
||||
recordContents, err := resolver.FileContentsByLocation(*recordRef)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -116,22 +111,20 @@ func (c *PackageCataloger) fetchRecordFiles(resolver scope.Resolver, metadataRef
|
||||
}
|
||||
|
||||
// fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained.
|
||||
func (c *PackageCataloger) fetchTopLevelPackages(resolver scope.Resolver, metadataRef file.Reference) (pkgs []string, sources []file.Reference, err error) {
|
||||
func (c *PackageCataloger) fetchTopLevelPackages(resolver source.Resolver, metadataLocation source.Location) (pkgs []string, sources []source.Location, err error) {
|
||||
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
|
||||
parentDir := filepath.Dir(string(metadataRef.Path))
|
||||
parentDir := filepath.Dir(metadataLocation.Path)
|
||||
topLevelPath := filepath.Join(parentDir, "top_level.txt")
|
||||
topLevelRef, err := resolver.RelativeFileByPath(metadataRef, topLevelPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
topLevelRef := resolver.RelativeFileByPath(metadataLocation, topLevelPath)
|
||||
|
||||
if topLevelRef == nil {
|
||||
log.Warnf("missing python package top_level.txt (package=%q)", string(metadataRef.Path))
|
||||
log.Warnf("missing python package top_level.txt (package=%q)", metadataLocation.Path)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
sources = append(sources, *topLevelRef)
|
||||
|
||||
topLevelContents, err := resolver.FileContentsByRef(*topLevelRef)
|
||||
topLevelContents, err := resolver.FileContentsByLocation(*topLevelRef)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -149,21 +142,21 @@ func (c *PackageCataloger) fetchTopLevelPackages(resolver scope.Resolver, metada
|
||||
}
|
||||
|
||||
// assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from.
|
||||
func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver scope.Resolver, metadataRef file.Reference) (*pkg.PythonPackageMetadata, []file.Reference, error) {
|
||||
var sources = []file.Reference{metadataRef}
|
||||
func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver source.Resolver, metadataLocation source.Location) (*pkg.PythonPackageMetadata, []source.Location, error) {
|
||||
var sources = []source.Location{metadataLocation}
|
||||
|
||||
metadataContents, err := resolver.FileContentsByRef(metadataRef)
|
||||
metadataContents, err := resolver.FileContentsByLocation(metadataLocation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
metadata, err := parseWheelOrEggMetadata(metadataRef.Path, strings.NewReader(metadataContents))
|
||||
metadata, err := parseWheelOrEggMetadata(metadataLocation.Path, strings.NewReader(metadataContents))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// attach any python files found for the given wheel/egg installation
|
||||
r, s, err := c.fetchRecordFiles(resolver, metadataRef)
|
||||
r, s, err := c.fetchRecordFiles(resolver, metadataLocation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -171,7 +164,7 @@ func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver scope.Resolver, m
|
||||
metadata.Files = r
|
||||
|
||||
// attach any top-level package names found for the given wheel/egg installation
|
||||
p, s, err := c.fetchTopLevelPackages(resolver, metadataRef)
|
||||
p, s, err := c.fetchTopLevelPackages(resolver, metadataLocation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
@@ -18,10 +18,10 @@ type pythonTestResolverMock struct {
|
||||
metadataReader io.Reader
|
||||
recordReader io.Reader
|
||||
topLevelReader io.Reader
|
||||
metadataRef *file.Reference
|
||||
recordRef *file.Reference
|
||||
topLevelRef *file.Reference
|
||||
contents map[file.Reference]string
|
||||
metadataRef *source.Location
|
||||
recordRef *source.Location
|
||||
topLevelRef *source.Location
|
||||
contents map[source.Location]string
|
||||
}
|
||||
|
||||
func newTestResolver(metaPath, recordPath, topPath string) *pythonTestResolverMock {
|
||||
@@ -46,17 +46,17 @@ func newTestResolver(metaPath, recordPath, topPath string) *pythonTestResolverMo
|
||||
}
|
||||
}
|
||||
|
||||
var recordRef *file.Reference
|
||||
var recordRef *source.Location
|
||||
if recordReader != nil {
|
||||
ref := file.NewFileReference("test-fixtures/dist-info/RECORD")
|
||||
ref := source.NewLocation("test-fixtures/dist-info/RECORD")
|
||||
recordRef = &ref
|
||||
}
|
||||
var topLevelRef *file.Reference
|
||||
var topLevelRef *source.Location
|
||||
if topLevelReader != nil {
|
||||
ref := file.NewFileReference("test-fixtures/dist-info/top_level.txt")
|
||||
ref := source.NewLocation("test-fixtures/dist-info/top_level.txt")
|
||||
topLevelRef = &ref
|
||||
}
|
||||
metadataRef := file.NewFileReference("test-fixtures/dist-info/METADATA")
|
||||
metadataRef := source.NewLocation("test-fixtures/dist-info/METADATA")
|
||||
return &pythonTestResolverMock{
|
||||
recordReader: recordReader,
|
||||
metadataReader: metadataReader,
|
||||
@@ -64,11 +64,11 @@ func newTestResolver(metaPath, recordPath, topPath string) *pythonTestResolverMo
|
||||
metadataRef: &metadataRef,
|
||||
recordRef: recordRef,
|
||||
topLevelRef: topLevelRef,
|
||||
contents: make(map[file.Reference]string),
|
||||
contents: make(map[source.Location]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *pythonTestResolverMock) FileContentsByRef(ref file.Reference) (string, error) {
|
||||
func (r *pythonTestResolverMock) FileContentsByLocation(ref source.Location) (string, error) {
|
||||
switch {
|
||||
case r.topLevelRef != nil && ref.Path == r.topLevelRef.Path:
|
||||
b, err := ioutil.ReadAll(r.topLevelReader)
|
||||
@@ -92,25 +92,25 @@ func (r *pythonTestResolverMock) FileContentsByRef(ref file.Reference) (string,
|
||||
return "", fmt.Errorf("invalid value given")
|
||||
}
|
||||
|
||||
func (r *pythonTestResolverMock) MultipleFileContentsByRef(_ ...file.Reference) (map[file.Reference]string, error) {
|
||||
func (r *pythonTestResolverMock) MultipleFileContentsByLocation(_ []source.Location) (map[source.Location]string, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (r *pythonTestResolverMock) FilesByPath(_ ...file.Path) ([]file.Reference, error) {
|
||||
func (r *pythonTestResolverMock) FilesByPath(_ ...string) ([]source.Location, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (r *pythonTestResolverMock) FilesByGlob(_ ...string) ([]file.Reference, error) {
|
||||
func (r *pythonTestResolverMock) FilesByGlob(_ ...string) ([]source.Location, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (r *pythonTestResolverMock) RelativeFileByPath(_ file.Reference, path string) (*file.Reference, error) {
|
||||
func (r *pythonTestResolverMock) RelativeFileByPath(_ source.Location, path string) *source.Location {
|
||||
switch {
|
||||
case strings.Contains(path, "RECORD"):
|
||||
return r.recordRef, nil
|
||||
return r.recordRef
|
||||
case strings.Contains(path, "top_level.txt"):
|
||||
return r.topLevelRef, nil
|
||||
return r.topLevelRef
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid RelativeFileByPath value given: %q", path)
|
||||
panic(fmt.Errorf("invalid RelativeFileByPath value given: %q", path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
|
||||
AuthorEmail: "me@kennethreitz.org",
|
||||
SitePackagesRootPath: "test-fixtures",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.Digest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
|
||||
{Path: "requests/__init__.py", Digest: &pkg.Digest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
|
||||
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
|
||||
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
|
||||
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
|
||||
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
|
||||
{Path: "requests/__version__.py", Digest: &pkg.Digest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
|
||||
{Path: "requests/utils.py", Digest: &pkg.Digest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
|
||||
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
|
||||
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
|
||||
},
|
||||
TopLevelPackages: []string{"requests"},
|
||||
},
|
||||
@@ -174,11 +174,11 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
|
||||
AuthorEmail: "georg@python.org",
|
||||
SitePackagesRootPath: "test-fixtures",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{Path: "../../../bin/pygmentize", Digest: &pkg.Digest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
|
||||
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.Digest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
|
||||
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
|
||||
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
|
||||
{Path: "Pygments-2.6.1.dist-info/RECORD"},
|
||||
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
|
||||
{Path: "pygments/util.py", Digest: &pkg.Digest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
|
||||
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
|
||||
},
|
||||
TopLevelPackages: []string{"pygments", "something_else"},
|
||||
},
|
||||
@@ -214,13 +214,13 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
|
||||
resolver := newTestResolver(test.MetadataFixture, test.RecordFixture, test.TopLevelFixture)
|
||||
|
||||
// note that the source is the record ref created by the resolver mock... attach the expected values
|
||||
test.ExpectedPackage.Source = []file.Reference{*resolver.metadataRef}
|
||||
test.ExpectedPackage.Locations = []source.Location{*resolver.metadataRef}
|
||||
if resolver.recordRef != nil {
|
||||
test.ExpectedPackage.Source = append(test.ExpectedPackage.Source, *resolver.recordRef)
|
||||
test.ExpectedPackage.Locations = append(test.ExpectedPackage.Locations, *resolver.recordRef)
|
||||
}
|
||||
|
||||
if resolver.topLevelRef != nil {
|
||||
test.ExpectedPackage.Source = append(test.ExpectedPackage.Source, *resolver.topLevelRef)
|
||||
test.ExpectedPackage.Locations = append(test.ExpectedPackage.Locations, *resolver.topLevelRef)
|
||||
}
|
||||
// end patching expected values with runtime data...
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -16,7 +14,7 @@ import (
|
||||
|
||||
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
|
||||
// returning all Python packages listed.
|
||||
func parseWheelOrEggMetadata(path file.Path, reader io.Reader) (pkg.PythonPackageMetadata, error) {
|
||||
func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMetadata, error) {
|
||||
fields := make(map[string]string)
|
||||
var key string
|
||||
|
||||
@@ -25,29 +23,28 @@ func parseWheelOrEggMetadata(path file.Path, reader io.Reader) (pkg.PythonPackag
|
||||
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 {
|
||||
@@ -73,8 +70,23 @@ func parseWheelOrEggMetadata(path file.Path, reader io.Reader) (pkg.PythonPackag
|
||||
|
||||
// add additional metadata not stored in the egg/wheel metadata file
|
||||
|
||||
sitePackagesRoot := filepath.Clean(filepath.Join(filepath.Dir(string(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
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
@@ -48,7 +46,7 @@ func TestParseWheelEggMetadata(t *testing.T) {
|
||||
t.Fatalf("failed to open fixture: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := parseWheelOrEggMetadata(file.Path(test.Fixture), fixture)
|
||||
actual, err := parseWheelOrEggMetadata(test.Fixture, fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse: %+v", err)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func parseWheelOrEggRecord(reader io.Reader) ([]pkg.PythonFileRecord, error) {
|
||||
return nil, fmt.Errorf("unexpected python record digest: %q", item)
|
||||
}
|
||||
|
||||
record.Digest = &pkg.Digest{
|
||||
record.Digest = &pkg.PythonFileDigest{
|
||||
Algorithm: fields[0],
|
||||
Value: fields[1],
|
||||
}
|
||||
|
||||
@@ -16,22 +16,22 @@ func TestParseWheelEggRecord(t *testing.T) {
|
||||
{
|
||||
Fixture: "test-fixtures/egg-info/RECORD",
|
||||
ExpectedMetadata: []pkg.PythonFileRecord{
|
||||
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.Digest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
|
||||
{Path: "requests/__init__.py", Digest: &pkg.Digest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
|
||||
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
|
||||
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
|
||||
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
|
||||
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
|
||||
{Path: "requests/__version__.py", Digest: &pkg.Digest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
|
||||
{Path: "requests/utils.py", Digest: &pkg.Digest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
|
||||
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
|
||||
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/dist-info/RECORD",
|
||||
ExpectedMetadata: []pkg.PythonFileRecord{
|
||||
{Path: "../../../bin/pygmentize", Digest: &pkg.Digest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
|
||||
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.Digest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
|
||||
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
|
||||
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
|
||||
{Path: "Pygments-2.6.1.dist-info/RECORD"},
|
||||
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
|
||||
{Path: "pygments/util.py", Digest: &pkg.Digest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
|
||||
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
packagesGlob = "**/var/lib/rpm/Packages"
|
||||
packagesGlob = "**/var/lib/rpm/Packages"
|
||||
catalogerName = "rpmdb-cataloger"
|
||||
)
|
||||
|
||||
type Cataloger struct{}
|
||||
@@ -24,26 +25,26 @@ func NewRpmdbCataloger() *Cataloger {
|
||||
|
||||
// Name returns a string that uniquely describes a cataloger
|
||||
func (c *Cataloger) Name() string {
|
||||
return "rpmdb-cataloger"
|
||||
return catalogerName
|
||||
}
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.
|
||||
func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
|
||||
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
fileMatches, err := resolver.FilesByGlob(packagesGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
for _, dbRef := range fileMatches {
|
||||
dbContents, err := resolver.FileContentsByRef(dbRef)
|
||||
for _, location := range fileMatches {
|
||||
dbContents, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkgs, err = parseRpmDB(resolver, dbRef, strings.NewReader(dbContents))
|
||||
pkgs, err = parseRpmDB(resolver, location, strings.NewReader(dbContents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", dbRef.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", location.Path, err)
|
||||
}
|
||||
}
|
||||
return pkgs, nil
|
||||
|
||||
@@ -6,18 +6,15 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
|
||||
rpmdb "github.com/anchore/go-rpmdb/pkg"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it.
|
||||
func parseRpmDB(resolver scope.FileResolver, dbRef file.Reference, reader io.Reader) ([]pkg.Package, error) {
|
||||
func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader io.Reader) ([]pkg.Package, error) {
|
||||
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
|
||||
@@ -54,10 +51,10 @@ func parseRpmDB(resolver scope.FileResolver, dbRef file.Reference, reader io.Rea
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: entry.Name,
|
||||
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does
|
||||
//Version: fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch),
|
||||
Source: []file.Reference{dbRef},
|
||||
Name: entry.Name,
|
||||
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does, instead of fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch)
|
||||
Locations: []source.Location{dbLocation},
|
||||
FoundBy: catalogerName,
|
||||
Type: pkg.RpmPkg,
|
||||
MetadataType: pkg.RpmdbMetadataType,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
@@ -80,11 +77,11 @@ func parseRpmDB(resolver scope.FileResolver, dbRef file.Reference, reader io.Rea
|
||||
return allPkgs, nil
|
||||
}
|
||||
|
||||
func extractRpmdbFileRecords(resolver scope.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) {
|
||||
func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) {
|
||||
var records = make([]pkg.RpmdbFileRecord, 0)
|
||||
|
||||
for _, record := range entry.Files {
|
||||
refs, err := resolver.FilesByPath(file.Path(record.Path))
|
||||
refs, err := resolver.FilesByPath(record.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve path=%+v: %w", record.Path, err)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
@@ -20,28 +21,29 @@ func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
|
||||
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
|
||||
if r.ignorePaths {
|
||||
// act as if no paths exist
|
||||
return nil, nil
|
||||
}
|
||||
// act as if all files exist
|
||||
var refs = make([]file.Reference, len(paths))
|
||||
var locations = make([]source.Location, len(paths))
|
||||
for i, p := range paths {
|
||||
refs[i] = file.NewFileReference(p)
|
||||
locations[i] = source.NewLocation(p)
|
||||
}
|
||||
return refs, nil
|
||||
return locations, nil
|
||||
}
|
||||
|
||||
func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]file.Reference, error) {
|
||||
func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]source.Location, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(file.Reference, string) (*file.Reference, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(source.Location, string) *source.Location {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestParseRpmDB(t *testing.T) {
|
||||
dbRef := file.NewFileReference("test-path")
|
||||
dbLocation := source.NewLocation("test-path")
|
||||
|
||||
tests := []struct {
|
||||
fixture string
|
||||
@@ -56,7 +58,8 @@ func TestParseRpmDB(t *testing.T) {
|
||||
"dive": {
|
||||
Name: "dive",
|
||||
Version: "0.9.2-1",
|
||||
Source: []file.Reference{dbRef},
|
||||
Locations: []source.Location{dbLocation},
|
||||
FoundBy: catalogerName,
|
||||
Type: pkg.RpmPkg,
|
||||
MetadataType: pkg.RpmdbMetadataType,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
@@ -82,7 +85,8 @@ func TestParseRpmDB(t *testing.T) {
|
||||
"dive": {
|
||||
Name: "dive",
|
||||
Version: "0.9.2-1",
|
||||
Source: []file.Reference{dbRef},
|
||||
Locations: []source.Location{dbLocation},
|
||||
FoundBy: catalogerName,
|
||||
Type: pkg.RpmPkg,
|
||||
MetadataType: pkg.RpmdbMetadataType,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
@@ -118,7 +122,7 @@ func TestParseRpmDB(t *testing.T) {
|
||||
|
||||
fileResolver := newTestFileResolver(test.ignorePaths)
|
||||
|
||||
actual, err := parseRpmDB(fileResolver, dbRef, fixture)
|
||||
actual, err := parseRpmDB(fileResolver, dbLocation, fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse rpmdb: %+v", err)
|
||||
}
|
||||
|
||||
+10
-12
@@ -6,6 +6,7 @@ import (
|
||||
hashiVer "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// Distro represents a Linux Distribution.
|
||||
type Distro struct {
|
||||
Type Type
|
||||
Version *hashiVer.Version
|
||||
@@ -13,13 +14,7 @@ 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 == "" {
|
||||
return Distro{Type: t}, nil
|
||||
@@ -36,6 +31,12 @@ func NewDistro(t Type, ver, like string) (Distro, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name provides a string repr of the distro
|
||||
func (d Distro) Name() string {
|
||||
return string(d.Type)
|
||||
}
|
||||
|
||||
// MajorVersion returns the major version value from the pseudo-semantically versioned distro version value.
|
||||
func (d Distro) MajorVersion() string {
|
||||
if d.Version == nil {
|
||||
return fmt.Sprint("(version unknown)")
|
||||
@@ -43,10 +44,12 @@ func (d Distro) MajorVersion() string {
|
||||
return fmt.Sprintf("%d", d.Version.Segments()[0])
|
||||
}
|
||||
|
||||
// FullVersion returns the original user version value.
|
||||
func (d Distro) FullVersion() string {
|
||||
return d.RawVersion
|
||||
}
|
||||
|
||||
// String returns a human-friendly representation of the Linux distribution.
|
||||
func (d Distro) String() string {
|
||||
versionStr := "(version unknown)"
|
||||
if d.RawVersion != "" {
|
||||
@@ -54,8 +57,3 @@ func (d Distro) String() string {
|
||||
}
|
||||
return fmt.Sprintf("%s %s", d.Type, versionStr)
|
||||
}
|
||||
|
||||
// Name provides a string repr of the distro
|
||||
func (d Distro) Name() string {
|
||||
return d.Type.String()
|
||||
}
|
||||
|
||||
+15
-18
@@ -4,22 +4,21 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// returns a distro or nil
|
||||
type parseFunc func(string) *Distro
|
||||
|
||||
type parseEntry struct {
|
||||
path file.Path
|
||||
path string
|
||||
fn parseFunc
|
||||
}
|
||||
|
||||
// Identify parses distro-specific files to determine distro metadata like version and release.
|
||||
func Identify(resolver scope.Resolver) Distro {
|
||||
distro := NewUnknownDistro()
|
||||
func Identify(resolver source.Resolver) *Distro {
|
||||
var distro *Distro
|
||||
|
||||
identityFiles := []parseEntry{
|
||||
{
|
||||
@@ -41,25 +40,19 @@ func Identify(resolver scope.Resolver) Distro {
|
||||
|
||||
identifyLoop:
|
||||
for _, entry := range identityFiles {
|
||||
refs, err := resolver.FilesByPath(entry.path)
|
||||
locations, err := resolver.FilesByPath(entry.path)
|
||||
if err != nil {
|
||||
log.Errorf("unable to get path refs from %s: %s", entry.path, err)
|
||||
log.Errorf("unable to get path locations from %s: %s", entry.path, err)
|
||||
break
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
if len(locations) == 0 {
|
||||
log.Debugf("No Refs found from path: %s", entry.path)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
contents, err := resolver.MultipleFileContentsByRef(ref)
|
||||
content, ok := contents[ref]
|
||||
|
||||
if !ok {
|
||||
log.Infof("no content present for ref: %s", ref)
|
||||
continue
|
||||
}
|
||||
for _, location := range locations {
|
||||
content, err := resolver.FileContentsByLocation(location)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("unable to get contents from %s: %s", entry.path, err)
|
||||
@@ -72,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
|
||||
}
|
||||
|
||||
@@ -120,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)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestIdentifyDistro(t *testing.T) {
|
||||
@@ -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,
|
||||
@@ -78,18 +83,24 @@ func TestIdentifyDistro(t *testing.T) {
|
||||
|
||||
observedDistros := internal.NewStringSet()
|
||||
definedDistros := internal.NewStringSet()
|
||||
for _, d := range All {
|
||||
definedDistros.Add(d.String())
|
||||
for _, distroType := range All {
|
||||
definedDistros.Add(string(distroType))
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
s, err := scope.NewScopeFromDir(test.fixture)
|
||||
s, err := source.NewFromDirectory(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to produce a new scope for testing: %s", test.fixture)
|
||||
t.Fatalf("unable to produce a new source for testing: %s", test.fixture)
|
||||
}
|
||||
|
||||
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
|
||||
+22
-37
@@ -1,37 +1,25 @@
|
||||
package distro
|
||||
|
||||
// Type represents the different Linux distribution options
|
||||
type Type string
|
||||
|
||||
const (
|
||||
UnknownDistroType Type = iota
|
||||
Debian
|
||||
Ubuntu
|
||||
RedHat
|
||||
CentOS
|
||||
Fedora
|
||||
Alpine
|
||||
Busybox
|
||||
AmazonLinux
|
||||
OracleLinux
|
||||
ArchLinux
|
||||
OpenSuseLeap
|
||||
// represents the set of valid/supported Linux Distributions
|
||||
UnknownDistroType Type = "UnknownDistroType"
|
||||
Debian Type = "debian"
|
||||
Ubuntu Type = "ubuntu"
|
||||
RedHat Type = "redhat"
|
||||
CentOS Type = "centos"
|
||||
Fedora Type = "fedora"
|
||||
Alpine Type = "alpine"
|
||||
Busybox Type = "busybox"
|
||||
AmazonLinux Type = "amazonlinux"
|
||||
OracleLinux Type = "oraclelinux"
|
||||
ArchLinux Type = "archlinux"
|
||||
OpenSuseLeap Type = "opensuseleap"
|
||||
)
|
||||
|
||||
type Type int
|
||||
|
||||
var distroStr = []string{
|
||||
"UnknownDistroType",
|
||||
"debian",
|
||||
"ubuntu",
|
||||
"redhat",
|
||||
"centos",
|
||||
"fedora",
|
||||
"alpine",
|
||||
"busybox",
|
||||
"amazn",
|
||||
"oraclelinux",
|
||||
"archlinux",
|
||||
"opensuse-leap",
|
||||
}
|
||||
|
||||
// All contains all Linux distribution options
|
||||
var All = []Type{
|
||||
Debian,
|
||||
Ubuntu,
|
||||
@@ -46,14 +34,6 @@ var All = []Type{
|
||||
OpenSuseLeap,
|
||||
}
|
||||
|
||||
func (t Type) String() string {
|
||||
if int(t) >= len(distroStr) || t < 0 {
|
||||
return distroStr[0]
|
||||
}
|
||||
|
||||
return distroStr[t]
|
||||
}
|
||||
|
||||
// IDMapping connects a distro ID like "ubuntu" to a Distro type
|
||||
var IDMapping = map[string]Type{
|
||||
"debian": Debian,
|
||||
@@ -68,3 +48,8 @@ var IDMapping = map[string]Type{
|
||||
"arch": ArchLinux,
|
||||
"opensuse-leap": OpenSuseLeap,
|
||||
}
|
||||
|
||||
// String returns the string representation of the given Linux distribution.
|
||||
func (t Type) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
+7
-2
@@ -7,7 +7,12 @@ package event
|
||||
import "github.com/wagoodman/go-partybus"
|
||||
|
||||
const (
|
||||
// AppUpdateAvailable is a partybus event that occurs when an application update is available
|
||||
AppUpdateAvailable partybus.EventType = "syft-app-update-available"
|
||||
CatalogerStarted partybus.EventType = "syft-cataloger-started-event"
|
||||
CatalogerFinished partybus.EventType = "syft-cataloger-finished-event"
|
||||
|
||||
// CatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
||||
CatalogerStarted partybus.EventType = "syft-cataloger-started-event"
|
||||
|
||||
// CatalogerFinished is a partybus event that occurs when the package cataloging has completed
|
||||
CatalogerFinished partybus.EventType = "syft-cataloger-finished-event"
|
||||
)
|
||||
|
||||
+59
-37
@@ -7,8 +7,8 @@ Here is what the main execution path for syft does:
|
||||
2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object
|
||||
3. Invoke a single presenter to show the contents of the catalog
|
||||
|
||||
A Scope object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
||||
providing a way to inspect paths and file content within the image. The Scope object, not the image object, is used
|
||||
A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
||||
providing a way to inspect paths and file content within the image. The Source object, not the image object, is used
|
||||
throughout the main execution path. This abstraction allows for decoupling of what is cataloged (a docker image, an OCI
|
||||
image, a filesystem, etc) and how it is cataloged (the individual catalogers).
|
||||
|
||||
@@ -17,7 +17,9 @@ Similar to the cataloging process, Linux distribution identification is also per
|
||||
package syft
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -25,58 +27,78 @@ import (
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/logger"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// Catalog the given image from a particular perspective (e.g. squashed scope, all-layers scope). Returns the discovered
|
||||
// set of packages, the identified Linux distribution, and the scope object used to wrap the data source.
|
||||
func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scope, *distro.Distro, error) {
|
||||
log.Info("cataloging image")
|
||||
s, cleanup, err := scope.NewScope(userInput, scoptOpt)
|
||||
// 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) {
|
||||
theSource, cleanup, err := source.New(userInput, scope)
|
||||
defer cleanup()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return source.Source{}, nil, nil, err
|
||||
}
|
||||
|
||||
d := IdentifyDistro(s)
|
||||
|
||||
catalog, err := CatalogFromScope(s)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return catalog, &s, &d, nil
|
||||
}
|
||||
|
||||
// IdentifyDistro attempts to discover what the underlying Linux distribution may be from the available flat files
|
||||
// provided by the given scope object. If results are inconclusive a "UnknownDistro" Type is returned.
|
||||
func IdentifyDistro(s scope.Scope) 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 scope, which may represent a container image or filesystem. Returns the discovered set of packages.
|
||||
func CatalogFromScope(s scope.Scope) (*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.Scheme {
|
||||
case scope.ImageScheme:
|
||||
switch theSource.Metadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
log.Info("cataloging image")
|
||||
catalogers = cataloger.ImageCatalogers()
|
||||
case scope.DirectoryScheme:
|
||||
case source.DirectoryScheme:
|
||||
log.Info("cataloging directory")
|
||||
catalogers = cataloger.DirectoryCatalogers()
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.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) {
|
||||
var doc jsonPresenter.Document
|
||||
var err error
|
||||
decoder := json.NewDecoder(reader)
|
||||
if err := decoder.Decode(&doc); err != nil {
|
||||
return source.Metadata{}, nil, nil, err
|
||||
}
|
||||
|
||||
var pkgs = make([]pkg.Package, len(doc.Artifacts))
|
||||
for i, a := range doc.Artifacts {
|
||||
pkgs[i], err = a.ToPackage()
|
||||
if err != nil {
|
||||
return source.Metadata{}, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
catalog := pkg.NewCatalog(pkgs...)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SetLogger sets the logger object used for all syft logging calls.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
Defines the logging interface which is used throughout the syft library.
|
||||
Package logger defines the logging interface which is used throughout the syft library.
|
||||
*/
|
||||
package logger
|
||||
|
||||
// Logger represents the behavior for logging within the syft library.
|
||||
type Logger interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
Error(args ...interface{})
|
||||
|
||||
@@ -35,6 +35,7 @@ type ApkFileRecord struct {
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
|
||||
func (m ApkMetadata) PackageURL() string {
|
||||
pURL := packageurl.NewPackageURL(
|
||||
// note: this is currently a candidate and not technically within spec
|
||||
|
||||
+18
-8
@@ -4,7 +4,8 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
@@ -14,17 +15,23 @@ var nextPackageID int64
|
||||
type Catalog struct {
|
||||
byID map[ID]*Package
|
||||
byType map[Type][]*Package
|
||||
byFile map[file.Reference][]*Package
|
||||
byFile map[source.Location][]*Package
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCatalog returns a new empty Catalog
|
||||
func NewCatalog() *Catalog {
|
||||
return &Catalog{
|
||||
func NewCatalog(pkgs ...Package) *Catalog {
|
||||
catalog := Catalog{
|
||||
byID: make(map[ID]*Package),
|
||||
byType: make(map[Type][]*Package),
|
||||
byFile: make(map[file.Reference][]*Package),
|
||||
byFile: make(map[source.Location][]*Package),
|
||||
}
|
||||
|
||||
for _, p := range pkgs {
|
||||
catalog.Add(p)
|
||||
}
|
||||
|
||||
return &catalog
|
||||
}
|
||||
|
||||
// PackageCount returns the total number of packages that have been added.
|
||||
@@ -38,8 +45,8 @@ func (c *Catalog) Package(id ID) *Package {
|
||||
}
|
||||
|
||||
// PackagesByFile returns all packages that were discovered from the given source file reference.
|
||||
func (c *Catalog) PackagesByFile(ref file.Reference) []*Package {
|
||||
return c.byFile[ref]
|
||||
func (c *Catalog) PackagesByFile(location source.Location) []*Package {
|
||||
return c.byFile[location]
|
||||
}
|
||||
|
||||
// Add a package to the Catalog.
|
||||
@@ -65,7 +72,7 @@ func (c *Catalog) Add(p Package) {
|
||||
c.byType[p.Type] = append(c.byType[p.Type], &p)
|
||||
|
||||
// store by file references
|
||||
for _, s := range p.Source {
|
||||
for _, s := range p.Locations {
|
||||
_, ok := c.byFile[s]
|
||||
if !ok {
|
||||
c.byFile[s] = make([]*Package, 0)
|
||||
@@ -111,6 +118,9 @@ func (c *Catalog) Sorted(types ...Type) []*Package {
|
||||
|
||||
sort.SliceStable(pkgs, func(i, j int) bool {
|
||||
if pkgs[i].Name == pkgs[j].Name {
|
||||
if pkgs[i].Version == pkgs[j].Version {
|
||||
return pkgs[i].Type < pkgs[j].Type
|
||||
}
|
||||
return pkgs[i].Version < pkgs[j].Version
|
||||
}
|
||||
return pkgs[i].Name < pkgs[j].Name
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,17 @@ type DpkgMetadata struct {
|
||||
Files []DpkgFileRecord `json:"files"`
|
||||
}
|
||||
|
||||
// DpkgFileRecord represents a single file attributed to a debian package.
|
||||
type DpkgFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
MD5 string `json:"md5"`
|
||||
}
|
||||
|
||||
func (m DpkgMetadata) PackageURL(d distro.Distro) string {
|
||||
// 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 {
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package pkg
|
||||
|
||||
// GemMetadata represents all metadata parsed from the gemspec file
|
||||
type GemMetadata struct {
|
||||
Name string `mapstructure:"name" json:"name"`
|
||||
Version string `mapstructure:"version" json:"version"`
|
||||
|
||||
@@ -26,6 +26,7 @@ type JavaManifest struct {
|
||||
NamedSections map[string]map[string]string `json:"namedSections,omitempty"`
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
|
||||
func (m JavaMetadata) PackageURL() string {
|
||||
if m.PomProperties != nil {
|
||||
pURL := packageurl.NewPackageURL(
|
||||
|
||||
+14
-22
@@ -1,25 +1,19 @@
|
||||
package pkg
|
||||
|
||||
// Language represents a single programming language.
|
||||
type Language string
|
||||
|
||||
const (
|
||||
UnknownLanguage Language = iota
|
||||
Java
|
||||
JavaScript
|
||||
Python
|
||||
Ruby
|
||||
Go
|
||||
// the full set of supported programming languages
|
||||
UnknownLanguage Language = "UnknownLanguage"
|
||||
Java Language = "java"
|
||||
JavaScript Language = "javascript"
|
||||
Python Language = "python"
|
||||
Ruby Language = "ruby"
|
||||
Go Language = "go"
|
||||
)
|
||||
|
||||
type Language uint
|
||||
|
||||
var languageStr = []string{
|
||||
"UnknownLanguage",
|
||||
"java",
|
||||
"javascript",
|
||||
"python",
|
||||
"ruby",
|
||||
"go",
|
||||
}
|
||||
|
||||
// AllLanguages is a set of all programming languages detected by syft.
|
||||
var AllLanguages = []Language{
|
||||
Java,
|
||||
JavaScript,
|
||||
@@ -28,9 +22,7 @@ var AllLanguages = []Language{
|
||||
Go,
|
||||
}
|
||||
|
||||
func (t Language) String() string {
|
||||
if int(t) >= len(languageStr) {
|
||||
return languageStr[0]
|
||||
}
|
||||
return languageStr[t]
|
||||
// String returns the string representation of the language.
|
||||
func (l Language) String() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package pkg
|
||||
|
||||
// MetadataType represents the data shape stored within pkg.Package.Metadata.
|
||||
type MetadataType string
|
||||
|
||||
const (
|
||||
// this is the full set of data shapes that can be represented within the pkg.Package.Metadata field
|
||||
UnknownMetadataType MetadataType = "UnknownMetadata"
|
||||
ApkMetadataType MetadataType = "apk-metadata"
|
||||
DpkgMetadataType MetadataType = "dpkg-metadata"
|
||||
GemMetadataType MetadataType = "gem-metadata"
|
||||
JavaMetadataType MetadataType = "java-metadata"
|
||||
NpmPackageJSONMetadataType MetadataType = "npm-package-json-metadata"
|
||||
RpmdbMetadataType MetadataType = "rpmdb-metadata"
|
||||
PythonPackageMetadataType MetadataType = "python-package-metadata"
|
||||
ApkMetadataType MetadataType = "ApkMetadata"
|
||||
DpkgMetadataType MetadataType = "DpkgMetadata"
|
||||
GemMetadataType MetadataType = "GemMetadata"
|
||||
JavaMetadataType MetadataType = "JavaMetadata"
|
||||
NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata"
|
||||
RpmdbMetadataType MetadataType = "RpmdbMetadata"
|
||||
PythonPackageMetadataType MetadataType = "PythonPackageMetadata"
|
||||
)
|
||||
|
||||
+14
-54
@@ -5,29 +5,28 @@ package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// ID represents a unique value for each package added to a package catalog.
|
||||
type ID int64
|
||||
|
||||
// Package represents an application or library that has been bundled into a distributable format.
|
||||
type Package struct {
|
||||
id ID // uniquely identifies a package, set by the cataloger
|
||||
Name string `json:"manifest"` // the package name
|
||||
Version string `json:"version"` // the version of the package
|
||||
FoundBy string `json:"foundBy"` // the specific cataloger that discovered this package
|
||||
Source []file.Reference `json:"sources"` // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
|
||||
id ID // uniquely identifies a package, set by the cataloger
|
||||
Name string // the package name
|
||||
Version string // the version of the package
|
||||
FoundBy string // the specific cataloger that discovered this package
|
||||
Locations []source.Location // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
|
||||
// TODO: should we move licenses into metadata?
|
||||
Licenses []string `json:"licenses"` // licenses discovered with the package metadata
|
||||
Language Language `json:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
|
||||
Type Type `json:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
|
||||
MetadataType MetadataType `json:"metadataType"` // the shape of the additional data in the "metadata" field
|
||||
Metadata interface{} `json:"metadata,omitempty"` // additional data found while parsing the package source
|
||||
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
|
||||
}
|
||||
|
||||
// ID returns the package ID, which is unique relative to a package catalog.
|
||||
@@ -39,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()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package pkg
|
||||
|
||||
type Digest struct {
|
||||
// PythonFileDigest represents the file metadata for a single file attributed to a python package.
|
||||
type PythonFileDigest struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package
|
||||
type PythonFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
Digest *Digest `json:"digest,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Digest *PythonFileDigest `json:"digest,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
// PythonPackageMetadata represents all captured data for a python egg or wheel package.
|
||||
|
||||
@@ -21,6 +21,7 @@ type RpmdbMetadata struct {
|
||||
Files []RpmdbFileRecord `json:"files"`
|
||||
}
|
||||
|
||||
// RpmdbFileRecord represents the file metadata for a single file attributed to a RPM package.
|
||||
type RpmdbFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
Mode RpmdbFileMode `json:"mode"`
|
||||
@@ -28,9 +29,15 @@ type RpmdbFileRecord struct {
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
// RpmdbFileMode is the raw file mode for a single file. This can be interpreted as the linux stat.h mode (see https://pubs.opengroup.org/onlinepubs/007908799/xsh/sysstat.h.html)
|
||||
type RpmdbFileMode uint16
|
||||
|
||||
func (m RpmdbMetadata) PackageURL(d distro.Distro) string {
|
||||
// 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 {
|
||||
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)
|
||||
|
||||
@@ -6,6 +6,7 @@ import "github.com/package-url/packageurl-go"
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// the full set of supported packages
|
||||
UnknownPkg Type = "UnknownPackage"
|
||||
ApkPkg Type = "apk"
|
||||
GemPkg Type = "gem"
|
||||
@@ -18,6 +19,7 @@ const (
|
||||
GoModulePkg Type = "go-module"
|
||||
)
|
||||
|
||||
// AllPkgs represents all supported package types
|
||||
var AllPkgs = []Type{
|
||||
ApkPkg,
|
||||
GemPkg,
|
||||
@@ -30,6 +32,7 @@ var AllPkgs = []Type{
|
||||
GoModulePkg,
|
||||
}
|
||||
|
||||
// PackageURLType returns the PURL package type for the current package.
|
||||
func (t Type) PackageURLType() string {
|
||||
switch t {
|
||||
case ApkPkg:
|
||||
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Source: https://cyclonedx.org/ext/bom-descriptor/
|
||||
@@ -35,15 +34,34 @@ type BdComponent struct {
|
||||
}
|
||||
|
||||
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
||||
func NewBomDescriptor() *BomDescriptor {
|
||||
versionInfo := version.FromBuild()
|
||||
return &BomDescriptor{
|
||||
func NewBomDescriptor(name, version string, srcMetadata source.Metadata) *BomDescriptor {
|
||||
descriptor := BomDescriptor{
|
||||
XMLName: xml.Name{},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tool: &BdTool{
|
||||
Vendor: "anchore",
|
||||
Name: internal.ApplicationName,
|
||||
Version: versionInfo.Version,
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
descriptor.Component = &BdComponent{
|
||||
Component: Component{
|
||||
Type: "container",
|
||||
Name: srcMetadata.ImageMetadata.UserInput,
|
||||
Version: srcMetadata.ImageMetadata.Digest,
|
||||
},
|
||||
}
|
||||
case source.DirectoryScheme:
|
||||
descriptor.Component = &BdComponent{
|
||||
Component: Component{
|
||||
Type: "file",
|
||||
Name: srcMetadata.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &descriptor
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type Component struct {
|
||||
Description string `xml:"description,omitempty"` // A description of the component
|
||||
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
|
||||
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
|
||||
// TODO: scope, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
||||
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
||||
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package cyclonedx
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -22,25 +23,25 @@ type Document struct {
|
||||
BomDescriptor *BomDescriptor `xml:"bd:metadata"` // The BOM descriptor extension
|
||||
}
|
||||
|
||||
// NewDocument returns an empty CycloneDX Document object.
|
||||
func NewDocument() Document {
|
||||
return 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(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocumentFromCatalog returns a CycloneDX Document object populated with the catalog contents.
|
||||
func NewDocumentFromCatalog(catalog *pkg.Catalog, d distro.Distro) Document {
|
||||
bom := NewDocument()
|
||||
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),
|
||||
}
|
||||
|
||||
// attach components
|
||||
for p := range catalog.Enumerate() {
|
||||
component := Component{
|
||||
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 {
|
||||
@@ -51,10 +52,8 @@ func NewDocumentFromCatalog(catalog *pkg.Catalog, d distro.Distro) Document {
|
||||
if len(licenses) > 0 {
|
||||
component.Licenses = &licenses
|
||||
}
|
||||
bom.Components = append(bom.Components, component)
|
||||
doc.Components = append(doc.Components, component)
|
||||
}
|
||||
|
||||
bom.BomDescriptor = NewBomDescriptor()
|
||||
|
||||
return bom
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -5,63 +5,29 @@ package cyclonedx
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Presenter writes a CycloneDX report from the given Catalog and Scope contents
|
||||
// Presenter writes a CycloneDX report from the given Catalog and Locations contents
|
||||
type Presenter struct {
|
||||
catalog *pkg.Catalog
|
||||
scope scope.Scope
|
||||
distro distro.Distro
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewPresenter creates a CycloneDX presenter from the given Catalog and Scope objects.
|
||||
func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Presenter {
|
||||
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
|
||||
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
|
||||
return &Presenter{
|
||||
catalog: catalog,
|
||||
scope: s,
|
||||
distro: d,
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present writes the CycloneDX report to the given io.Writer.
|
||||
func (pres *Presenter) Present(output io.Writer) error {
|
||||
bom := NewDocumentFromCatalog(pres.catalog, pres.distro)
|
||||
|
||||
switch src := pres.scope.Source.(type) {
|
||||
case scope.DirSource:
|
||||
bom.BomDescriptor.Component = &BdComponent{
|
||||
Component: Component{
|
||||
Type: "file",
|
||||
Name: src.Path,
|
||||
Version: "",
|
||||
},
|
||||
}
|
||||
case scope.ImageSource:
|
||||
var imageID string
|
||||
var versionStr string
|
||||
if len(src.Img.Metadata.Tags) > 0 {
|
||||
imageID = src.Img.Metadata.Tags[0].Context().Name()
|
||||
versionStr = src.Img.Metadata.Tags[0].TagStr()
|
||||
} else {
|
||||
imageID = src.Img.Metadata.Digest
|
||||
}
|
||||
bom.BomDescriptor.Component = &BdComponent{
|
||||
Component: Component{
|
||||
Type: "container",
|
||||
Name: imageID,
|
||||
Version: versionStr,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported source: %T", src)
|
||||
}
|
||||
bom := NewDocument(pres.catalog, pres.srcMetadata)
|
||||
|
||||
encoder := xml.NewEncoder(output)
|
||||
encoder.Indent("", " ")
|
||||
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
@@ -29,7 +27,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Source: []file.Reference{
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
@@ -43,7 +41,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Source: []file.Reference{
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
},
|
||||
Licenses: []string{
|
||||
@@ -57,17 +55,12 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
s, err := scope.NewScopeFromDir("/some/path")
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d, err := distro.NewDistro(distro.Ubuntu, "20.04", "debian")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pres := NewPresenter(catalog, s, d)
|
||||
pres := NewPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
@@ -105,28 +98,18 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-1.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-1.txt"), img),
|
||||
},
|
||||
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",
|
||||
Version: "2.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-2.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-2.txt"), img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
@@ -134,30 +117,23 @@ 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 := scope.NewScopeFromImage(img, scope.AllLayersScope)
|
||||
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
|
||||
if err != nil {
|
||||
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"
|
||||
|
||||
pres := NewPresenter(catalog, s, d)
|
||||
pres := NewPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
@@ -178,7 +154,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?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:2bbada20-3e87-44ea-9a56-1aa0e4dd01a0">
|
||||
<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:aa1738d1-1c6e-45e4-a530-5a04d2c2b1a7">
|
||||
<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,11 +16,10 @@
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>pkg:deb/ubuntu/package2@1.0.2?arch=amd64</purl>
|
||||
</component>
|
||||
</components>
|
||||
<bd:metadata>
|
||||
<bd:timestamp>2020-09-23T18:26:58-04:00</bd:timestamp>
|
||||
<bd:timestamp>2020-11-19T10:11:26-05:00</bd:timestamp>
|
||||
<bd:tool>
|
||||
<bd:vendor>anchore</bd:vendor>
|
||||
<bd:name>syft</bd:name>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?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:94dae829-4d5d-482f-afab-27f43f919e2c">
|
||||
<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:8a4c332c-365c-454d-a43c-d9344577d105">
|
||||
<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 +17,19 @@
|
||||
<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-09-23T18:26:58-04:00</bd:timestamp>
|
||||
<bd:timestamp>2020-11-19T10:11:26-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>index.docker.io/library/stereoscope-fixture-image-simple</name>
|
||||
<version>04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7</version>
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</bd:component>
|
||||
</bd:metadata>
|
||||
</bom>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
FoundBy []string `json:"foundBy"`
|
||||
Locations Locations `json:"locations,omitempty"`
|
||||
Licenses []string `json:"licenses"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func NewArtifact(p *pkg.Package, s scope.Scope) (Artifact, error) {
|
||||
locations, err := NewLocations(p, s)
|
||||
if err != nil {
|
||||
return Artifact{}, err
|
||||
}
|
||||
|
||||
return Artifact{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Type: string(p.Type),
|
||||
FoundBy: []string{p.FoundBy},
|
||||
Locations: locations,
|
||||
Licenses: p.Licenses,
|
||||
Metadata: p.Metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package json
|
||||
|
||||
// Descriptor describes what created the document as well as surrounding metadata
|
||||
type Descriptor struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package json
|
||||
|
||||
import "github.com/anchore/syft/syft/distro"
|
||||
|
||||
// Distribution provides information about a detected Linux Distribution.
|
||||
type Distribution struct {
|
||||
Name string `json:"name"` // Name of the Linux distribution
|
||||
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
||||
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
||||
}
|
||||
|
||||
// NewDistribution creates a struct with the Linux distribution to be represented in JSON.
|
||||
func NewDistribution(d *distro.Distro) Distribution {
|
||||
if d == nil {
|
||||
return Distribution{}
|
||||
}
|
||||
|
||||
return Distribution{
|
||||
Name: d.Name(),
|
||||
Version: d.FullVersion(),
|
||||
IDLike: d.IDLike,
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,40 @@
|
||||
package json
|
||||
|
||||
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/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Document represents the syft cataloging findings as a JSON document
|
||||
type Document struct {
|
||||
Artifacts []Artifact `json:"artifacts"`
|
||||
Source Source `json:"source"`
|
||||
Distro Distribution `json:"distro"`
|
||||
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
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
|
||||
}
|
||||
|
||||
// Distritbution provides information about a detected Linux Distribution
|
||||
type Distribution struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
IDLike string `json:"idLike"`
|
||||
}
|
||||
|
||||
func NewDocument(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) (Document, error) {
|
||||
doc := Document{
|
||||
Artifacts: make([]Artifact, 0),
|
||||
}
|
||||
|
||||
src, err := NewSource(s)
|
||||
// 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) {
|
||||
src, err := NewSource(srcMetadata)
|
||||
if err != nil {
|
||||
return Document{}, nil
|
||||
}
|
||||
doc.Source = src
|
||||
distroName := d.Name()
|
||||
if distroName == "UnknownDistroType" {
|
||||
distroName = ""
|
||||
}
|
||||
doc.Distro = Distribution{
|
||||
Name: distroName,
|
||||
Version: d.FullVersion(),
|
||||
IDLike: d.IDLike,
|
||||
|
||||
doc := Document{
|
||||
Artifacts: make([]Package, 0),
|
||||
Source: src,
|
||||
Distro: NewDistribution(d),
|
||||
Descriptor: Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range catalog.Sorted() {
|
||||
art, err := NewArtifact(p, s)
|
||||
art, err := NewPackage(p)
|
||||
if err != nil {
|
||||
return Document{}, err
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Layers []Layer `json:"layers"`
|
||||
Size int64 `json:"size"`
|
||||
Digest string `json:"digest"`
|
||||
MediaType string `json:"mediaType"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type Layer struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func NewImage(src scope.ImageSource) *Image {
|
||||
// populate artifacts...
|
||||
tags := make([]string, len(src.Img.Metadata.Tags))
|
||||
for idx, tag := range src.Img.Metadata.Tags {
|
||||
tags[idx] = tag.String()
|
||||
}
|
||||
img := Image{
|
||||
Digest: src.Img.Metadata.Digest,
|
||||
Size: src.Img.Metadata.Size,
|
||||
MediaType: string(src.Img.Metadata.MediaType),
|
||||
Tags: tags,
|
||||
Layers: make([]Layer, len(src.Img.Layers)),
|
||||
}
|
||||
|
||||
// populate image metadata
|
||||
for idx, l := range src.Img.Layers {
|
||||
img.Layers[idx] = Layer{
|
||||
MediaType: string(l.Metadata.MediaType),
|
||||
Digest: l.Metadata.Digest,
|
||||
Size: l.Metadata.Size,
|
||||
}
|
||||
}
|
||||
return &img
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
type Locations interface{}
|
||||
|
||||
type ImageLocation struct {
|
||||
Path string `json:"path"`
|
||||
LayerIndex uint `json:"layerIndex"`
|
||||
}
|
||||
|
||||
func NewLocations(p *pkg.Package, s scope.Scope) (Locations, error) {
|
||||
switch src := s.Source.(type) {
|
||||
case scope.ImageSource:
|
||||
locations := make([]ImageLocation, len(p.Source))
|
||||
for idx := range p.Source {
|
||||
entry, err := src.Img.FileCatalog.Get(p.Source[idx])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find layer index for source-idx=%d package=%s", idx, p.Name)
|
||||
}
|
||||
|
||||
artifactSource := ImageLocation{
|
||||
LayerIndex: entry.Source.Metadata.Index,
|
||||
Path: string(p.Source[idx].Path),
|
||||
}
|
||||
|
||||
locations[idx] = artifactSource
|
||||
}
|
||||
return locations, nil
|
||||
|
||||
case scope.DirSource:
|
||||
locations := make([]string, len(p.Source))
|
||||
for idx := range p.Source {
|
||||
locations[idx] = string(p.Source[idx].Path)
|
||||
}
|
||||
return locations, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to determine source: %T", src)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
|
||||
type Package struct {
|
||||
packageBasicMetadata
|
||||
packageCustomMetadata
|
||||
}
|
||||
|
||||
// packageBasicMetadata contains non-ambiguous values (type-wise) from pkg.Package.
|
||||
type packageBasicMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type pkg.Type `json:"type"`
|
||||
FoundBy string `json:"foundBy"`
|
||||
Locations []source.Location `json:"locations"`
|
||||
Licenses []string `json:"licenses"`
|
||||
Language pkg.Language `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"`
|
||||
}
|
||||
|
||||
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
|
||||
type packageMetadataUnpacker struct {
|
||||
MetadataType string `json:"metadataType"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
|
||||
// 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,
|
||||
FoundBy: p.FoundBy,
|
||||
Locations: locations,
|
||||
Licenses: licenses,
|
||||
Language: p.Language,
|
||||
CPEs: cpes,
|
||||
PURL: p.PURL,
|
||||
},
|
||||
packageCustomMetadata: packageCustomMetadata{
|
||||
MetadataType: p.MetadataType,
|
||||
Metadata: p.Metadata,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToPackage generates a pkg.Package from the current 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,
|
||||
Locations: a.Locations,
|
||||
CPEs: cpes,
|
||||
PURL: a.PURL,
|
||||
Type: a.Type,
|
||||
MetadataType: a.MetadataType,
|
||||
Metadata: a.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
|
||||
// nolint:funlen
|
||||
func (a *Package) UnmarshalJSON(b []byte) error {
|
||||
var basic packageBasicMetadata
|
||||
if err := json.Unmarshal(b, &basic); err != nil {
|
||||
return err
|
||||
}
|
||||
a.packageBasicMetadata = basic
|
||||
|
||||
var unpacker packageMetadataUnpacker
|
||||
if err := json.Unmarshal(b, &unpacker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.MetadataType = pkg.MetadataType(unpacker.MetadataType)
|
||||
|
||||
switch a.MetadataType {
|
||||
case pkg.RpmdbMetadataType:
|
||||
var payload pkg.RpmdbMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.PythonPackageMetadataType:
|
||||
var payload pkg.PythonPackageMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.DpkgMetadataType:
|
||||
var payload pkg.DpkgMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.ApkMetadataType:
|
||||
var payload pkg.ApkMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.JavaMetadataType:
|
||||
var payload pkg.JavaMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.NpmPackageJSONMetadataType:
|
||||
var payload pkg.NpmPackageJSONMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case pkg.GemMetadataType:
|
||||
var payload pkg.GemMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Metadata = payload
|
||||
case "":
|
||||
// there may be packages with no metadata, which is OK
|
||||
default:
|
||||
return fmt.Errorf("unsupported package metadata type: %+v", a.MetadataType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,25 +6,28 @@ import (
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Presenter is a JSON presentation object for the syft results
|
||||
type Presenter struct {
|
||||
catalog *pkg.Catalog
|
||||
scope scope.Scope
|
||||
distro distro.Distro
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
distro *distro.Distro
|
||||
}
|
||||
|
||||
func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Presenter {
|
||||
// NewPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro) *Presenter {
|
||||
return &Presenter{
|
||||
catalog: catalog,
|
||||
scope: s,
|
||||
distro: d,
|
||||
catalog: catalog,
|
||||
srcMetadata: s,
|
||||
distro: d,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
func (pres *Presenter) Present(output io.Writer) error {
|
||||
doc, err := NewDocument(pres.catalog, pres.scope, pres.distro)
|
||||
doc, err := NewDocument(pres.catalog, pres.srcMetadata, pres.distro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,16 +6,22 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -25,27 +31,47 @@ func TestJsonDirsPresenter(t *testing.T) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Source: []file.Reference{
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
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",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Source: []file.Reference{
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
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()
|
||||
s, err := scope.NewScopeFromDir("/some/path")
|
||||
var d *distro.Distro
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewPresenter(catalog, s, d)
|
||||
pres := NewPresenter(catalog, s.Metadata, d)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
@@ -62,7 +88,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
@@ -84,25 +110,45 @@ func TestJsonImgsPresenter(t *testing.T) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-1.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-1.txt"), img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-2.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-2.txt"), img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
})
|
||||
|
||||
s, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
|
||||
d := distro.NewUnknownDistro()
|
||||
pres := NewPresenter(catalog, s, d)
|
||||
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
|
||||
var d *distro.Distro
|
||||
pres := NewPresenter(catalog, s.Metadata, d)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
@@ -119,7 +165,7 @@ func TestJsonImgsPresenter(t *testing.T) {
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Source object represents the thing that was cataloged
|
||||
type Source struct {
|
||||
Type string `json:"type"`
|
||||
Target interface{} `json:"target"`
|
||||
}
|
||||
|
||||
func NewSource(s scope.Scope) (Source, error) {
|
||||
switch src := s.Source.(type) {
|
||||
case scope.ImageSource:
|
||||
// sourceUnpacker is used to unmarshal Source objects
|
||||
type sourceUnpacker struct {
|
||||
Type string `json:"type"`
|
||||
Target json.RawMessage `json:"target"`
|
||||
}
|
||||
|
||||
// NewSource creates a new source object to be represented into JSON.
|
||||
func NewSource(src source.Metadata) (Source, error) {
|
||||
switch src.Scheme {
|
||||
case source.ImageScheme:
|
||||
return Source{
|
||||
Type: "image",
|
||||
Target: NewImage(src),
|
||||
Target: src.ImageMetadata,
|
||||
}, nil
|
||||
case scope.DirSource:
|
||||
case source.DirectoryScheme:
|
||||
return Source{
|
||||
Type: "directory",
|
||||
Target: src.Path,
|
||||
@@ -27,3 +36,40 @@ func NewSource(s scope.Scope) (Source, error) {
|
||||
return Source{}, fmt.Errorf("unsupported source: %T", src)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates a source object from JSON bytes.
|
||||
func (s *Source) UnmarshalJSON(b []byte) error {
|
||||
var unpacker sourceUnpacker
|
||||
if err := json.Unmarshal(b, &unpacker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Type = unpacker.Type
|
||||
|
||||
switch s.Type {
|
||||
case "image":
|
||||
var payload source.ImageMetadata
|
||||
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Target = payload
|
||||
default:
|
||||
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToSourceMetadata takes a source object represented from JSON and creates a source.Metadata object.
|
||||
func (s *Source) ToSourceMetadata() source.Metadata {
|
||||
var metadata source.Metadata
|
||||
switch s.Type {
|
||||
case "directory":
|
||||
metadata.Scheme = source.DirectoryScheme
|
||||
metadata.Path = s.Target.(string)
|
||||
case "image":
|
||||
metadata.Scheme = source.ImageScheme
|
||||
metadata.ImageMetadata = s.Target.(source.ImageMetadata)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
@@ -3,26 +3,58 @@
|
||||
{
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "deb",
|
||||
"foundBy": [
|
||||
"the-cataloger-1"
|
||||
],
|
||||
"type": "python",
|
||||
"foundBy": "the-cataloger-1",
|
||||
"locations": [
|
||||
"/some/path/pkg1"
|
||||
{
|
||||
"path": "/some/path/pkg1"
|
||||
}
|
||||
],
|
||||
"licenses": null
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"language": "python",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
],
|
||||
"purl": "a-purl-2",
|
||||
"metadataType": "PythonPackageMetadata",
|
||||
"metadata": {
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"license": "",
|
||||
"author": "",
|
||||
"authorEmail": "",
|
||||
"platform": "",
|
||||
"sitePackagesRootPath": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
"foundBy": [
|
||||
"the-cataloger-2"
|
||||
],
|
||||
"foundBy": "the-cataloger-2",
|
||||
"locations": [
|
||||
"/some/path/pkg1"
|
||||
{
|
||||
"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",
|
||||
"source": "",
|
||||
"version": "2.0.1",
|
||||
"architecture": "",
|
||||
"maintainer": "",
|
||||
"installedSize": 0,
|
||||
"files": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
@@ -33,5 +65,9 @@
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,56 +3,86 @@
|
||||
{
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "deb",
|
||||
"foundBy": [
|
||||
"the-cataloger-1"
|
||||
],
|
||||
"type": "python",
|
||||
"foundBy": "the-cataloger-1",
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-1.txt",
|
||||
"layerIndex": 0
|
||||
"layerID": "sha256:e158b57d6f5a96ef5fd22f2fe76c70b5ba6ff5b2619f9d83125b2aad0492ac7b"
|
||||
}
|
||||
],
|
||||
"licenses": null
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"language": "python",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
|
||||
],
|
||||
"purl": "a-purl-1",
|
||||
"metadataType": "PythonPackageMetadata",
|
||||
"metadata": {
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"license": "",
|
||||
"author": "",
|
||||
"authorEmail": "",
|
||||
"platform": "",
|
||||
"sitePackagesRootPath": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
"foundBy": [
|
||||
"the-cataloger-2"
|
||||
],
|
||||
"foundBy": "the-cataloger-2",
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-2.txt",
|
||||
"layerIndex": 1
|
||||
"layerID": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf"
|
||||
}
|
||||
],
|
||||
"licenses": null
|
||||
"licenses": [],
|
||||
"language": "",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
],
|
||||
"purl": "a-purl-2",
|
||||
"metadataType": "DpkgMetadata",
|
||||
"metadata": {
|
||||
"package": "package-2",
|
||||
"source": "",
|
||||
"version": "2.0.1",
|
||||
"architecture": "",
|
||||
"maintainer": "",
|
||||
"installedSize": 0,
|
||||
"files": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "image",
|
||||
"target": {
|
||||
"userInput": "user-image-input",
|
||||
"scope": "AllLayers",
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:78783bfc74fef84f899b4977561ad1172f87753f82cc2157b06bf097e56dfbce",
|
||||
"digest": "sha256:e158b57d6f5a96ef5fd22f2fe76c70b5ba6ff5b2619f9d83125b2aad0492ac7b",
|
||||
"size": 22
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:54ec7f643dafbf9f27032a5e60afe06248c0e99b50aed54bb0fe28ea4825ccaf",
|
||||
"digest": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf",
|
||||
"size": 16
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:ec4775a139c45b1ddf9ea8e1cb43385e92e5c0bf6ec2e3f4192372785b18c106",
|
||||
"digest": "sha256:f0e18aa6032c24659a9c741fc36ca56f589782ea132061ccf6f52b952403da94",
|
||||
"size": 27
|
||||
}
|
||||
],
|
||||
"size": 65,
|
||||
"digest": "sha256:fedd7bcc0b90f071501b662d8e7c9ac7548b88daba6b3deedfdf33f22ed8d95b",
|
||||
"digest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"tags": [
|
||||
"stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
|
||||
@@ -63,5 +93,9 @@
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/anchore/syft/syft/presenter/json"
|
||||
"github.com/anchore/syft/syft/presenter/table"
|
||||
"github.com/anchore/syft/syft/presenter/text"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Presenter defines the expected behavior for an object responsible for displaying arbitrary input and processed data
|
||||
@@ -25,16 +25,16 @@ type Presenter interface {
|
||||
}
|
||||
|
||||
// GetPresenter returns a presenter for images or directories
|
||||
func GetPresenter(option Option, s scope.Scope, 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, s, *d)
|
||||
return json.NewPresenter(catalog, srcMetadata, d)
|
||||
case TextPresenter:
|
||||
return text.NewPresenter(catalog, s)
|
||||
return text.NewPresenter(catalog, srcMetadata)
|
||||
case TablePresenter:
|
||||
return table.NewPresenter(catalog, s)
|
||||
return table.NewPresenter(catalog)
|
||||
case CycloneDxPresenter:
|
||||
return cyclonedx.NewPresenter(catalog, s, *d)
|
||||
return cyclonedx.NewPresenter(catalog, srcMetadata)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,18 +9,15 @@ import (
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
type Presenter struct {
|
||||
catalog *pkg.Catalog
|
||||
scope scope.Scope
|
||||
}
|
||||
|
||||
func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter {
|
||||
func NewPresenter(catalog *pkg.Catalog) *Presenter {
|
||||
return &Presenter{
|
||||
catalog: catalog,
|
||||
scope: s,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package table
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"github.com/go-test/deep"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
@@ -29,25 +29,24 @@ func TestTablePresenter(t *testing.T) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-1.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-1.txt"), img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Source: []file.Reference{
|
||||
*img.SquashedTree().File("/somefile-2.txt"),
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*img.SquashedTree().File("/somefile-2.txt"), img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
s, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
|
||||
pres := NewPresenter(catalog, s)
|
||||
pres := NewPresenter(catalog)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
err := pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user