Compare commits

...
37 Commits
Author SHA1 Message Date
Alex Goodman f87c59b4eb Merge pull request #286 from anchore/rm-tree-catalog-sync-check
Bump stereoscope to remove tree-catalog syft check
2020-12-03 16:32:24 -05:00
Alex Goodman 3ce7eabc98 bump stereoscope to remove tree-catalog syft check
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-12-03 16:20:17 -05:00
Alex Goodman 6f9ded60ed Merge pull request #279 from anchore/enhance-java-cpe-by-group-id
Include CPEs with elements from POM GroupId fields
2020-12-02 07:50:31 -05:00
Dan Luhring 65cbacd135 Clarify python wheel parsing process (#281)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-12-01 16:30:30 -05:00
Alex Goodman 2989d3d975 include CPEs with elementds from POM GroupId fields
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-12-01 08:00:49 -05:00
Dan Luhring ae71b8832d Update stereoscope version to fix opaque directory merge issue (#278)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-30 11:37:44 -05:00
Alex Goodman c42b036f46 Merge pull request #276 from anchore/generate-json-schema
Generate json schema from struct definitions
2020-11-23 11:22:33 -05:00
Alex Goodman 8a17bfb69f generate json schema from struct definitions
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-20 15:47:10 -05:00
Alex Goodman 0ed30138c4 Merge pull request #271 from anchore/cpe-generation
Move package PURL and CPEs to Package definition
2020-11-19 12:38:20 -05:00
Alex Goodman 3aaa0e5566 move package purl and cpes (identities) to pkg.Package
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-19 11:25:51 -05:00
Alex Goodman 71939557e6 Merge pull request #266 from anchore/document-import
Add JSON document import
2020-11-17 13:21:57 -05:00
Alex Goodman 030427bb33 rename json artifact to package + update resolver integrity checks
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:13 -05:00
Alex Goodman 569a598df7 minimize pointer usage & order return types consistently
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:13 -05:00
Dan Luhring 0ad8c53ec2 Fix cyclonedx test fixture usage of dynamic digest value
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-17 12:37:13 -05:00
Alex Goodman c892c3609e improve doc comments
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:13 -05:00
Alex Goodman 62b03f3a91 dont export structs used for unmarshaling
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman 4b45c42f5a make cyclonedx presenter generally reusable (for grype)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman f46de19c6b migrate scope option to image metadata (from source)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman 91baabe5a1 add image metadata as catalogFromJSON return
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman 6f7a4fd3e4 move source metadata upstream and fix tests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman aa0d444fd4 fix tests to use location instead of file.Reference
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:12 -05:00
Alex Goodman b694dacb21 add source.Location + reorient Resolvers to use it
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:11 -05:00
Alex Goodman 9668341a14 rename scope to source
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:37:11 -05:00
Alex Goodman 495fb0a45f add sbom document import lib helper function
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-11-17 12:36:51 -05:00
Dan Luhring a640a2c4cd Merge pull request #267 from anchore/improve-java-version-recognition
Improve Java version recognition
2020-11-17 10:59:54 -05:00
Dan Luhring 3e8bca6911 Rework Java archive name and version detection and clean up tests
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-17 08:35:10 -05:00
Dan Luhring 5afdd574a8 Update existing archive test cases to correct names and versions
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-17 08:35:10 -05:00
Dan Luhring 0ebe791acd Add archive filename test case for failing example from #255
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-17 08:35:10 -05:00
Dan Luhring a5b72405dd Merge pull request #265 from anchore/package-json-license-objects
Improve package.json license parsing
2020-11-14 13:06:24 -05:00
Dan Luhring 4861f69d7c Fix spelling in logger setup
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:35:55 -05:00
Dan Luhring 6bde075cd3 Remove unused argument value
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:20:06 -05:00
Dan Luhring f7be73dbcc Fix package.json parsing for no licenses
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:12:13 -05:00
Dan Luhring fc99538d78 Fix package.json parsing for multiple licenses objects
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:12:13 -05:00
Dan Luhring 4d4e242251 Fix package.json parsing for license object
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:12:12 -05:00
Dan Luhring 416afa015e Add test for package.json with no licenses
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:11:46 -05:00
Dan Luhring bd4c784a64 Add test for package.json multiple license objects
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 11:01:59 -05:00
Dan Luhring 624a090631 Add test for package.json license object
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-11-14 09:50:17 -05:00
133 changed files with 2841 additions and 2011 deletions
+5 -17
View File
@@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 72
COVERAGE_THRESHOLD := 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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+14 -8
View File
@@ -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=
+5 -5
View File
@@ -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 -1
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
/*
Package internal contains miscellaneous functions and objects useful within syft but should not be used externally.
*/
package internal
+1
View File
@@ -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
+2
View File
@@ -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)
}
+6
View File
@@ -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
+5
View File
@@ -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
-21
View File
@@ -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)
}
+12
View File
@@ -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...)
}
+4
View File
@@ -0,0 +1,4 @@
/*
Package logger contains implementations for the syft.logger.Logger interface.
*/
package logger
+20
View File
@@ -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...)
}
+10
View File
@@ -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
}
+3
View File
@@ -1,3 +1,6 @@
/*
Package version contains all build time metadata (version, build time, git commit, etc).
*/
package version
import (
+1
View File
@@ -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)
-9
View File
@@ -1,9 +0,0 @@
## Updating the JSON schema
Today the JSON schema is generated from integration test data. Specifically, when integration tests are run, the `/schema/json/examples` directory is populated with syft JSON output data. This examples directory is used to drive automatically generating the JSON schema.
The caveats with this approach is:
1) the JSON schema is only as good as the examples provided
2) there is an integration test that ensures that the JSON schema is valid relative to what the code currently generates.
This means to update the JSON schema you need to
1) Open up `test/integration/json_schema_test.go` and comment out invocations of the `validateAgainstV1Schema` function.
2) From the root of the repo run `generate-json-schema`. Now there should be a new schema generated at `/schema/json/schema.json`
3) Uncomment the `validateAgainstV1Schema` function.
-1
View File
@@ -1 +0,0 @@
examples/
+35
View File
@@ -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)
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/env/bin python3
import os
import glob
import json
from genson import SchemaBuilder
EXAMPLES_DIR = "examples/"
OUTPUT = "schema.json"
def main():
builder = SchemaBuilder()
print("Generating new Syft json schema...")
for filepath in glob.glob(os.path.join(EXAMPLES_DIR, '*.json')):
with open(filepath, 'r') as f:
print(f" adding {filepath}")
builder.add_object(json.loads(f.read()))
print("Building schema...")
new_schema = builder.to_schema()
with open(OUTPUT, 'w') as f:
f.write(json.dumps(new_schema, sort_keys=True, indent=4))
print(f"New schema written to '{OUTPUT}'")
if __name__ == "__main__":
main()
-1
View File
@@ -1 +0,0 @@
genson
+159 -408
View File
@@ -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"
}
}
}
+12 -3
View File
@@ -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)
}
}
+2 -2
View File
@@ -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.
+19 -20
View File
@@ -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)
}
+19 -17
View File
@@ -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)
}
}
}
+110
View File
@@ -0,0 +1,110 @@
package cataloger
import (
"fmt"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
)
// this is functionally equivalent to "*" and consistent with no input given (thus easier to test)
const any = ""
// generatePackageCPEs Create a list of CPEs, trying to guess the vendor, product tuple and setting TargetSoftware if possible
func generatePackageCPEs(p pkg.Package) []pkg.CPE {
targetSws := candidateTargetSoftwareAttrs(p)
vendors := candidateVendors(p)
products := candidateProducts(p)
keys := internal.NewStringSet()
cpes := make([]pkg.CPE, 0)
for _, product := range products {
for _, vendor := range append([]string{any}, vendors...) {
for _, targetSw := range append([]string{any}, targetSws...) {
// prevent duplicate entries...
key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw)
if keys.Contains(key) {
continue
}
keys.Add(key)
// add a new entry...
candidateCpe := wfn.NewAttributesWithAny()
candidateCpe.Part = "a"
candidateCpe.Product = product
candidateCpe.Vendor = vendor
candidateCpe.Version = p.Version
candidateCpe.TargetSW = targetSw
cpes = append(cpes, *candidateCpe)
}
}
}
return cpes
}
func candidateTargetSoftwareAttrs(p pkg.Package) []string {
// TODO: would be great to allow these to be overridden by user data/config
var targetSw []string
switch p.Language {
case pkg.Java:
targetSw = append(targetSw, "java", "maven")
case pkg.JavaScript:
targetSw = append(targetSw, "node.js", "nodejs")
case pkg.Ruby:
targetSw = append(targetSw, "ruby", "rails")
case pkg.Python:
targetSw = append(targetSw, "python")
}
if p.Type == pkg.JenkinsPluginPkg {
targetSw = append(targetSw, "jenkins", "cloudbees_jenkins")
}
return targetSw
}
func candidateVendors(p pkg.Package) []string {
vendors := candidateProducts(p)
switch p.Language {
case pkg.Python:
vendors = append(vendors, fmt.Sprintf("python-%s", p.Name))
case pkg.Java:
if p.MetadataType == pkg.JavaMetadataType {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
// derive the vendor from the groupID (e.g. org.sonatype.nexus --> sonatype)
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
fields := strings.Split(metadata.PomProperties.GroupID, ".")
if len(fields) >= 3 {
vendors = append(vendors, fields[1])
}
}
}
}
}
return vendors
}
func candidateProducts(p pkg.Package) []string {
var products = []string{p.Name}
switch p.Language {
case pkg.Java:
if p.MetadataType == pkg.JavaMetadataType {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
// derive the product from the groupID (e.g. org.sonatype.nexus --> nexus)
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
fields := strings.Split(metadata.PomProperties.GroupID, ".")
if len(fields) >= 3 {
products = append(products, fields[2])
}
}
}
}
default:
return products
}
return products
}
+182
View File
@@ -0,0 +1,182 @@
package cataloger
import (
"sort"
"testing"
"github.com/scylladb/go-set/strset"
"github.com/scylladb/go-set"
"github.com/anchore/syft/syft/pkg"
)
func TestGenerate(t *testing.T) {
tests := []struct {
name string
p pkg.Package
expected []string
}{
{
name: "python language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Python,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:python:*:*",
},
},
{
name: "javascript language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.JavaScript,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:nodejs:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:nodejs:*:*",
},
},
{
name: "ruby language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Ruby,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:rails:*:*",
},
},
{
name: "java language",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
},
},
{
name: "java language with groupID",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
GroupID: "org.sonatype.nexus",
},
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:maven:*:*",
},
},
{
name: "jenkins package",
p: pkg.Package{
Name: "name",
Version: "3.2",
FoundBy: "some-analyzer",
Language: pkg.Java,
Type: pkg.JenkinsPluginPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := generatePackageCPEs(test.p)
expectedCpeSet := set.NewStringSet(test.expected...)
actualCpeSet := set.NewStringSet()
for _, a := range actual {
actualCpeSet.Add(a.BindToFmtString())
}
extra := strset.Difference(expectedCpeSet, actualCpeSet).List()
sort.Strings(extra)
for _, d := range extra {
t.Errorf("extra CPE: %+v", d)
}
missing := strset.Difference(actualCpeSet, expectedCpeSet).List()
sort.Strings(missing)
for _, d := range missing {
t.Errorf("missing CPE: %+v", d)
}
})
}
}
+41 -52
View File
@@ -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
+6 -6
View File
@@ -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)
+85 -48
View File
@@ -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, "")
}
+18 -16
View File
@@ -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))
}
})
+34 -38
View File
@@ -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 {
+4 -4
View File
@@ -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"
}
}
+49
View File
@@ -0,0 +1,49 @@
package cataloger
import (
"regexp"
"strings"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/package-url/packageurl-go"
)
// generatePackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec)
func generatePackageURL(p pkg.Package, d *distro.Distro) string {
// default to pURLs on the metadata
if p.Metadata != nil {
if i, ok := p.Metadata.(interface{ PackageURL() string }); ok {
return i.PackageURL()
} else if i, ok := p.Metadata.(interface{ PackageURL(*distro.Distro) string }); ok {
return i.PackageURL(d)
}
}
var purlType = p.Type.PackageURLType()
var name = p.Name
var namespace = ""
switch {
case purlType == "":
// there is no purl type, don't attempt to craft a purl
// TODO: should this be a "generic" purl type instead?
return ""
case p.Type == pkg.GoModulePkg:
re := regexp.MustCompile(`(/)[^/]*$`)
fields := re.Split(p.Name, -1)
namespace = fields[0]
name = strings.TrimPrefix(p.Name, namespace+"/")
}
// generate a purl from the package data
pURL := packageurl.NewPackageURL(
purlType,
namespace,
name,
p.Version,
nil,
"")
return pURL.ToString()
}
@@ -1,83 +1,84 @@
package pkg
package cataloger
import (
"testing"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
)
func TestPackage_pURL(t *testing.T) {
func TestPackageURL(t *testing.T) {
tests := []struct {
pkg Package
distro distro.Distro
pkg pkg.Package
distro *distro.Distro
expected string
}{
{
pkg: Package{
pkg: pkg.Package{
Name: "github.com/anchore/syft",
Version: "v0.1.0",
Type: GoModulePkg,
Type: pkg.GoModulePkg,
},
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: PythonPkg,
Type: pkg.PythonPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: GemPkg,
Type: pkg.GemPkg,
},
expected: "pkg:gem/name@v0.1.0",
},
{
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: NpmPkg,
Type: pkg.NpmPkg,
},
expected: "pkg:npm/name@v0.1.0",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.Ubuntu,
},
pkg: Package{
pkg: pkg.Package{
Name: "bad-name",
Version: "bad-v0.1.0",
Type: DebPkg,
Metadata: DpkgMetadata{
Type: pkg.DebPkg,
Metadata: pkg.DpkgMetadata{
Package: "name",
Version: "v0.1.0",
Architecture: "amd64",
@@ -86,14 +87,14 @@ func TestPackage_pURL(t *testing.T) {
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.CentOS,
},
pkg: Package{
pkg: pkg.Package{
Name: "bad-name",
Version: "bad-v0.1.0",
Type: RpmPkg,
Metadata: RpmdbMetadata{
Type: pkg.RpmPkg,
Metadata: pkg.RpmdbMetadata{
Name: "name",
Version: "v0.1.0",
Epoch: 2,
@@ -104,13 +105,13 @@ func TestPackage_pURL(t *testing.T) {
expected: "pkg:rpm/centos/name@2:v0.1.0-3?arch=amd64",
},
{
distro: distro.Distro{
distro: &distro.Distro{
Type: distro.UnknownDistroType,
},
pkg: Package{
pkg: pkg.Package{
Name: "name",
Version: "v0.1.0",
Type: DebPkg,
Type: pkg.DebPkg,
},
expected: "pkg:deb/name@v0.1.0",
},
@@ -118,7 +119,7 @@ func TestPackage_pURL(t *testing.T) {
for _, test := range tests {
t.Run(string(test.pkg.Type)+"|"+test.expected, func(t *testing.T) {
actual := test.pkg.PackageURL(test.distro)
actual := generatePackageURL(test.pkg, test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
+25 -32
View File
@@ -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
}
+29 -29
View File
@@ -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"},
},
},
}
+9 -8
View File
@@ -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
+8 -11
View File
@@ -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)
}
+16 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+25 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+2 -1
View File
@@ -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{})
+1
View File
@@ -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
View File
@@ -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
+41
View File
@@ -0,0 +1,41 @@
package pkg
import (
"fmt"
"strings"
"github.com/facebookincubator/nvdtools/wfn"
)
type CPE = wfn.Attributes
func NewCPE(cpeStr string) (CPE, error) {
value, err := wfn.Parse(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
}
if value == nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr)
}
// we need to compare the raw data since we are constructing CPEs in other locations
value.Vendor = normalizeCpeField(value.Vendor)
value.Product = normalizeCpeField(value.Product)
value.Language = normalizeCpeField(value.Language)
value.Version = normalizeCpeField(value.Version)
value.TargetSW = normalizeCpeField(value.TargetSW)
value.Part = normalizeCpeField(value.Part)
value.Edition = normalizeCpeField(value.Edition)
value.Other = normalizeCpeField(value.Other)
value.SWEdition = normalizeCpeField(value.SWEdition)
value.TargetHW = normalizeCpeField(value.TargetHW)
value.Update = normalizeCpeField(value.Update)
return *value, nil
}
func normalizeCpeField(field string) string {
// keep dashes and forward slashes unescaped
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")
}
+48
View File
@@ -0,0 +1,48 @@
package pkg
import "testing"
func must(c CPE, e error) CPE {
if e != nil {
panic(e)
}
return c
}
func TestNewCPE(t *testing.T) {
tests := []struct {
name string
input string
expected CPE
}{
{
name: "gocase",
input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`,
expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)),
},
{
name: "dashes",
input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`,
expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)),
},
{
name: "URL escape characters",
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
expected: must(NewCPE(`cpe:2.3:a:$0.99_kindle_books_project:$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewCPE(test.input)
if err != nil {
t.Fatalf("got an error while creating CPE: %+v", err)
}
if actual.BindToFmtString() != test.expected.BindToFmtString() {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", test.expected.BindToFmtString(), actual.BindToFmtString())
}
})
}
}
+6 -1
View File
@@ -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",
+3 -2
View File
@@ -1,9 +1,10 @@
package pkg
import (
"testing"
"github.com/anchore/syft/syft/distro"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestDpkgMetadata_pURL(t *testing.T) {
@@ -40,7 +41,7 @@ func TestDpkgMetadata_pURL(t *testing.T) {
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL(test.distro)
actual := test.metadata.PackageURL(&test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
+1
View File
@@ -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"`
+1
View File
@@ -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
View File
@@ -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)
}
+9 -7
View File
@@ -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
View File
@@ -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()
}
+5 -4
View File
@@ -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.
+8 -1
View File
@@ -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(),
+1 -1
View File
@@ -43,7 +43,7 @@ func TestRpmMetadata_pURL(t *testing.T) {
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL(test.distro)
actual := test.metadata.PackageURL(&test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
+3
View File
@@ -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:
+25 -7
View File
@@ -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
}
+1 -1
View File
@@ -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.)
}
+18 -19
View File
@@ -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
}
+9 -43
View File
@@ -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("", " ")
+21 -45
View File
@@ -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>
-33
View File
@@ -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
}
+7
View File
@@ -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"`
}
+23
View File
@@ -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,
}
}
+21 -27
View File
@@ -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
}
-44
View File
@@ -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
}
-45
View File
@@ -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)
}
}
+172
View File
@@ -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
}
+12 -9
View File
@@ -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
}
+67 -21
View File
@@ -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))
}
}
+52 -6
View File
@@ -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]"
}
}
+6 -6
View File
@@ -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
}
+1 -4
View File
@@ -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,
}
}
+9 -10
View File
@@ -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