Compare commits

..
Author SHA1 Message Date
Alex Goodman ecf11f0e3d [WIP] migrate helper functions for spdx
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 14:32:44 -05:00
Alex Goodman 628c2e44a5 add test reminders to put new relationships under test
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 13:34:41 -05:00
Alex Goodman aba0de6e65 update ownership-by-file-overlap relationship description
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 11:15:18 -05:00
Alex Goodman cb0278f3bc hoist up package relationship discovery to analysis stage
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 11:05:01 -05:00
Alex Goodman b519340b86 enable source.Location to be identifiable
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 09:05:33 -05:00
Alex Goodman 2356539ebe use zero value for nils in ID generation
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-09 17:26:38 -05:00
Alex Goodman 1537e73083 rename Identity() to ID()
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-09 17:23:48 -05:00
Alex Goodman 52adfcbd44 stabilize package ID relative to encode-decode format cycles
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-09 16:53:16 -05:00
Alex Goodman a3f0d659da adjust spdx helpers to use copy of packages
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-09 10:53:55 -05:00
Alex Goodman 8a0fa5d3ad remove catalog ID assignment
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-09 10:49:37 -05:00
Alex Goodman 253faf5652 fix linting
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:50:35 -05:00
Alex Goodman 69d2b1ba3c add artifact.Identifiable by Identity() method
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:46:14 -05:00
Alex Goodman b08a11e46d fix more tests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:46:12 -05:00
Alex Goodman fef951c29b return relationships from tasks
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:44:34 -05:00
Alex Goodman a906b9a03a migrate pkg.ID and pkg.Relationship to artifact package
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:43:20 -05:00
123 changed files with 906 additions and 1632 deletions
+20 -17
View File
@@ -37,6 +37,10 @@ ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
# used to generate the changelog from the second to last tag to the current tag (used in the release pipeline when the release tag is in place)
LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --max-count=1))
SECOND_TO_LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --skip=1 --max-count=1))
## Variable assertions
ifndef TEMPDIR
@@ -99,7 +103,7 @@ bootstrap-tools: $(TEMPDIR)
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0-beta
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
.PHONY: bootstrap-go
@@ -190,11 +194,6 @@ fixtures:
generate-json-schema: ## Generate a new json schema
cd schema/json && go run generate.go
.PHONY: generate-license-list
generate-license-list: ## Generate an updated spdx license list
go generate ./internal/spdxlicense/...
gofmt -s -w ./internal/spdxlicense
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
@@ -253,19 +252,27 @@ cli: $(SNAPSHOTDIR) ## Run CLI tests
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
go test -count=1 -v ./test/cli
.PHONY: changelog
changelog: clean-changelog CHANGELOG.md
.PHONY: changelog-release
changelog-release:
$(TEMPDIR)/chronicle --since-tag $(SECOND_TO_LAST_TAG) --until-tag $(LAST_TAG) -vv > CHANGELOG.md
@printf '\n$(BOLD)$(CYAN)Release $(VERSION) Changelog$(RESET)\n\n'
@cat CHANGELOG.md
.PHONY: changelog-unreleased
changelog-unreleased: ## show the current changelog that will be produced on the next release (note: requires GITHUB_TOKEN set)
$(TEMPDIR)/chronicle --since-tag $(LAST_TAG) -vv > CHANGELOG.md
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
@docker run -it --rm \
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
rawkode/mdv \
-t 748.5989 \
/CHANGELOG.md
CHANGELOG.md:
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
.PHONY: release
release: clean-dist CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS.
release: clean-dist changelog-release ## Build and publish final binaries and packages. Intended to be run only on macOS.
$(call title,Publishing release artifacts)
# Prepare for macOS-specific signing process
@@ -306,13 +313,9 @@ clean-snapshot:
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist: clean-changelog
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-changelog
clean-changelog:
rm -f CHANGELOG.md
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache
.PHONY: clear-test-image-tar-cache
+16 -11
View File
@@ -14,8 +14,10 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
@@ -253,25 +255,26 @@ func packagesExecWorker(userInput string) <-chan error {
}
defer cleanup()
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
catalog, relationships, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %w", err)
return
}
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, src.Metadata, catalog, d); err != nil {
errs <- err
return
}
}
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: d,
},
Source: src.Metadata,
}
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, sbomResult); err != nil {
errs <- err
return
}
Relationships: relationships,
Source: src.Metadata,
}
bus.Publish(partybus.Event{
@@ -282,7 +285,7 @@ func packagesExecWorker(userInput string) <-chan error {
return errs
}
func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
@@ -317,7 +320,9 @@ func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SBOM: s,
SourceMetadata: s,
Catalog: catalog,
Distro: d,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
Timeout: appConfig.Anchore.ImportTimeout,
+47 -13
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"sync"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/stereoscope"
@@ -88,7 +90,6 @@ func powerUserExec(_ *cobra.Command, args []string) error {
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
)
}
func powerUserExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
@@ -109,28 +110,61 @@ func powerUserExecWorker(userInput string) <-chan error {
}
defer cleanup()
analysisResults := sbom.SBOM{
s := sbom.SBOM{
Source: src.Metadata,
}
wg := &sync.WaitGroup{}
var results []<-chan artifact.Relationship
for _, task := range tasks {
wg.Add(1)
go func(task powerUserTask) {
defer wg.Done()
if err = task(&analysisResults.Artifacts, src); err != nil {
errs <- err
return
}
}(task)
c := make(chan artifact.Relationship)
results = append(results, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
wg.Wait()
for relationship := range mergeResults(results...) {
s.Relationships = append(s.Relationships, relationship)
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: poweruser.NewJSONPresenter(analysisResults, *appConfig),
Value: poweruser.NewJSONPresenter(s, *appConfig),
})
}()
return errs
}
func runTask(t powerUserTask, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}
func mergeResults(cs ...<-chan artifact.Relationship) <-chan artifact.Relationship {
var wg sync.WaitGroup
var results = make(chan artifact.Relationship)
wg.Add(len(cs))
for _, c := range cs {
go func(c <-chan artifact.Relationship) {
for n := range c {
results <- n
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(results)
}()
return results
}
+27 -25
View File
@@ -4,6 +4,8 @@ import (
"crypto"
"fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft"
@@ -11,7 +13,7 @@ import (
"github.com/anchore/syft/syft/source"
)
type powerUserTask func(*sbom.Artifacts, *source.Source) error
type powerUserTask func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func powerUserTasks() ([]powerUserTask, error) {
var tasks []powerUserTask
@@ -43,16 +45,16 @@ func catalogPackagesTask() (powerUserTask, error) {
return nil, nil
}
task := func(results *sbom.Artifacts, src *source.Source) error {
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
results.PackageCatalog = packageCatalog
results.Distro = theDistro
return nil
return relationships, nil
}
return task, nil
@@ -65,18 +67,18 @@ func catalogFileMetadataTask() (powerUserTask, error) {
metadataCataloger := file.NewMetadataCataloger()
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileMetadata = result
return nil
return nil, nil
}
return task, nil
@@ -111,18 +113,18 @@ func catalogFileDigestsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileDigests = result
return nil
return nil, nil
}
return task, nil
@@ -143,18 +145,18 @@ func catalogSecretsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := secretsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.Secrets = result
return nil
return nil, nil
}
return task, nil
@@ -171,18 +173,18 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := classifierCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileClassifications = result
return nil
return nil, nil
}
return task, nil
@@ -198,18 +200,18 @@ func catalogContentsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := contentsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileContents = result
return nil
return nil, nil
}
return task, nil
+2 -1
View File
@@ -24,8 +24,9 @@ require (
github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.0
github.com/jinzhu/copier v0.3.2
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/pelletier/go-toml v1.8.1
+4 -2
View File
@@ -454,6 +454,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -544,8 +546,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+11 -6
View File
@@ -6,19 +6,24 @@ import (
"fmt"
"time"
"github.com/antihax/optional"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/antihax/optional"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
type ImportConfig struct {
ImageMetadata image.Metadata
SBOM sbom.SBOM
SourceMetadata source.Metadata
Catalog *pkg.Catalog
Distro *distro.Distro
Dockerfile []byte
OverwriteExistingUpload bool
Timeout uint
@@ -68,19 +73,19 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
prog.N++
sessionID := startOperation.Uuid
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM, stage)
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, stage)
if err != nil {
return fmt.Errorf("failed to import Package SBOM: %w", err)
}
prog.N++
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawManifest, stage)
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawManifest, stage)
if err != nil {
return fmt.Errorf("failed to import Manifest: %w", err)
}
prog.N++
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage)
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawConfig, stage)
if err != nil {
return fmt.Errorf("failed to import Config: %w", err)
}
+16 -3
View File
@@ -14,17 +14,30 @@ import (
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/source"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
type packageSBOMImportAPI interface {
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
}
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
func packageSbomModel(srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
// TODO: once the top-level API is refactored and SBOMs are the unit of work, then this function will be passed an SBOM and there would be no more need to create an SBOM object here.
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: d,
},
Source: srcMetadata,
}
err := syftjson.Format().Presenter(s).Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
@@ -39,11 +52,11 @@ func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
return &model, nil
}
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s sbom.SBOM, stage *progress.Stage) (string, error) {
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
log.Debug("importing package SBOM")
stage.Current = "package SBOM"
model, err := packageSbomModel(s)
model, err := packageSbomModel(s, catalog, d)
if err != nil {
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
}
+3 -19
View File
@@ -74,15 +74,7 @@ func TestPackageSbomToModel(t *testing.T) {
c := pkg.NewCatalog(p)
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: c,
Distro: &d,
},
Source: m,
}
model, err := packageSbomModel(sbomResult)
model, err := packageSbomModel(m, c, &d)
if err != nil {
t.Fatalf("unable to generate model from source material: %+v", err)
}
@@ -205,15 +197,7 @@ func TestPackageSbomImport(t *testing.T) {
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &d,
},
Source: m,
}
theModel, err := packageSbomModel(sbomResult)
theModel, err := packageSbomModel(m, catalog, &d)
if err != nil {
t.Fatalf("could not get sbom model: %+v", err)
}
@@ -252,7 +236,7 @@ func TestPackageSbomImport(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, sbomResult, &progress.Stage{})
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Description(p *pkg.Package) string {
func Description(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@@ -14,10 +14,6 @@ func Description(p *pkg.Package) string {
return ""
}
func packageExists(p *pkg.Package) bool {
return p != nil
}
func hasMetadata(p *pkg.Package) bool {
return packageExists(p) && p.Metadata != nil
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
@@ -50,7 +50,7 @@ func Test_Description(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Description(&test.input))
assert.Equal(t, test.expected, Description(test.input))
})
}
}
@@ -0,0 +1,32 @@
package spdxhelpers
import (
"path"
"strings"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
func DocumentName(srcMetadata source.Metadata) string {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
case source.DirectoryScheme:
return cleanSPDXName(srcMetadata.Path)
}
// TODO: is this alright?
return uuid.Must(uuid.NewRandom()).String()
}
func cleanSPDXName(name string) string {
// remove # according to specification
name = strings.ReplaceAll(name, "#", "-")
// remove : for url construction
name = strings.ReplaceAll(name, ":", "-")
// clean relative pathing
return path.Clean(name)
}
@@ -0,0 +1,29 @@
package spdxhelpers
import (
"fmt"
"path"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
const SyftDocumentNamespace = "https://anchore.com/syft"
func DocumentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
input = "image"
case source.DirectoryScheme:
input = "dir"
}
uniqueID := uuid.Must(uuid.NewRandom())
identifier := path.Join(input, uniqueID.String())
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
}
return path.Join(SyftDocumentNamespace, identifier)
}
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func DownloadLocation(p *pkg.Package) string {
func DownloadLocation(p pkg.Package) string {
// 3.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
@@ -48,7 +48,7 @@ func Test_DownloadLocation(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, DownloadLocation(&test.input))
assert.Equal(t, test.expected, DownloadLocation(test.input))
})
}
}
@@ -6,13 +6,9 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func ExternalRefs(p *pkg.Package) (externalRefs []model.ExternalRef) {
func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
externalRefs = make([]model.ExternalRef, 0)
if !packageExists(p) {
return externalRefs
}
for _, c := range p.CPEs {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.SecurityReferenceCategory,
@@ -39,7 +39,7 @@ func Test_ExternalRefs(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, ExternalRefs(&test.input))
assert.ElementsMatch(t, test.expected, ExternalRefs(test.input))
})
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Files(packageSpdxID string, p *pkg.Package) (files []model.File, fileIDs []string, relationships []model.Relationship) {
func Files(packageSpdxID string, p pkg.Package) (files []model.File, fileIDs []string, relationships []model.Relationship) {
files = make([]model.File, 0)
fileIDs = make([]string, 0)
relationships = make([]model.Relationship, 0)
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Homepage(p *pkg.Package) string {
func Homepage(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.GemMetadata:
@@ -50,7 +50,7 @@ func Test_Homepage(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Homepage(&test.input))
assert.Equal(t, test.expected, Homepage(test.input))
})
}
}
@@ -7,7 +7,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func License(p *pkg.Package) string {
func License(p pkg.Package) string {
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Appendix IV;
@@ -17,7 +17,7 @@ func License(p *pkg.Package) string {
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if !packageExists(p) || len(p.Licenses) == 0 {
if len(p.Licenses) == 0 {
return "NONE"
}
@@ -67,7 +67,7 @@ func Test_License(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, License(&test.input))
assert.Equal(t, test.expected, License(test.input))
})
}
}
@@ -108,7 +108,7 @@ func Test_Originator(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Originator(&test.input))
assert.Equal(t, test.expected, Originator(test.input))
})
}
}
@@ -6,7 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Originator(p *pkg.Package) string {
func Originator(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@@ -6,11 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func SourceInfo(p *pkg.Package) string {
if !packageExists(p) {
return ""
}
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
case pkg.RpmPkg:
@@ -31,8 +27,6 @@ func SourceInfo(p *pkg.Package) string {
answer = "acquired package info from go module information"
case pkg.RustPkg:
answer = "acquired package info from rust cargo manifest"
case pkg.PhpComposerPkg:
answer = "acquired package info from PHP composer manifest"
default:
answer = "acquired package info from the following paths"
}
@@ -124,14 +124,6 @@ func Test_SourceInfo(t *testing.T) {
"from rust cargo manifest",
},
},
{
input: pkg.Package{
Type: pkg.PhpComposerPkg,
},
expected: []string{
"from PHP composer manifest",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
@@ -139,7 +131,7 @@ func Test_SourceInfo(t *testing.T) {
if test.input.Type != "" {
pkgTypes = append(pkgTypes, test.input.Type)
}
actual := SourceInfo(&test.input)
actual := SourceInfo(test.input)
for _, expected := range test.expected {
assert.Contains(t, actual, expected)
}
@@ -133,7 +133,6 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
@@ -154,7 +153,6 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
@@ -197,7 +195,6 @@ func newDirectoryCatalog() *pkg.Catalog {
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Type: pkg.PythonPkg,
@@ -223,7 +220,6 @@ func newDirectoryCatalog() *pkg.Catalog {
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
@@ -33,7 +33,7 @@ func toFormatModel(s sbom.SBOM) model.Document {
return doc
}
func toComponent(p *pkg.Package) model.Component {
func toComponent(p pkg.Package) model.Component {
return model.Component{
Type: "library", // TODO: this is not accurate
Name: p.Name,
+22
View File
@@ -0,0 +1,22 @@
package spdx22json
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/sbom"
)
func decoder(reader io.Reader) (*sbom.SBOM, error) {
dec := json.NewDecoder(reader)
var doc model.Document
err := dec.Decode(&doc)
if err != nil {
return nil, fmt.Errorf("unable to decode syft-json: %w", err)
}
return toSyftModel(doc)
}
@@ -0,0 +1,46 @@
package spdx22json
import (
"bytes"
"strings"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodeCycle(t *testing.T) {
testImage := "image-simple"
originalSBOM := testutils.ImageInput(t, testImage)
var buf bytes.Buffer
assert.NoError(t, encoder(&buf, originalSBOM))
actualSBOM, err := decoder(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
t.Errorf("metadata difference: %+v", d)
}
actualPackages := actualSBOM.Artifacts.PackageCatalog.Sorted()
for idx, p := range originalSBOM.Artifacts.PackageCatalog.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
for _, d := range deep.Equal(p, actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("package difference (%s): %+v", p.Name, d)
}
}
}
-2
View File
@@ -7,8 +7,6 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const anchoreNamespace = "https://anchore.com/syft"
func encoder(output io.Writer, s sbom.SBOM) error {
doc := toFormatModel(s)
+2 -47
View File
@@ -2,8 +2,6 @@ package spdx22json
import (
"fmt"
"path"
"strings"
"time"
"github.com/anchore/syft/syft/sbom"
@@ -14,13 +12,11 @@ import (
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
func toFormatModel(s sbom.SBOM) model.Document {
name := documentName(s.Source)
name := spdxhelpers.DocumentName(s.Source)
packages, files, relationships := extractFromCatalog(s.Artifacts.PackageCatalog)
return model.Document{
@@ -39,43 +35,13 @@ func toFormatModel(s sbom.SBOM) model.Document {
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: documentNamespace(name, s.Source),
DocumentNamespace: spdxhelpers.DocumentNamespace(name, s.Source),
Packages: packages,
Files: files,
Relationships: relationships,
}
}
func documentName(srcMetadata source.Metadata) string {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
case source.DirectoryScheme:
return cleanSPDXName(srcMetadata.Path)
}
// TODO: is this alright?
return uuid.Must(uuid.NewRandom()).String()
}
func documentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
input = "image"
case source.DirectoryScheme:
input = "dir"
}
uniqueID := uuid.Must(uuid.NewRandom())
identifier := path.Join(input, uniqueID.String())
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
}
return path.Join(anchoreNamespace, identifier)
}
func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []model.Relationship) {
packages := make([]model.Package, 0)
relationships := make([]model.Relationship, 0)
@@ -115,14 +81,3 @@ func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []
return packages, files, relationships
}
func cleanSPDXName(name string) string {
// remove # according to specification
name = strings.ReplaceAll(name, "#", "-")
// remove : for url construction
name = strings.ReplaceAll(name, ":", "-")
// clean relative pathing
return path.Clean(name)
}
@@ -0,0 +1,10 @@
package spdx22json
import (
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/sbom"
)
func toSyftModel(doc model.Document) (*sbom.SBOM, error) {
}
@@ -256,7 +256,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2
return results
}
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
for _, ref := range spdxhelpers.ExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference2_2{
Category: string(ref.ReferenceCategory),
+1 -5
View File
@@ -31,11 +31,7 @@ func TestEncodeDecodeCycle(t *testing.T) {
continue
}
// ids will never be equal
p.ID = ""
actualPackages[idx].ID = ""
for _, d := range deep.Equal(*p, *actualPackages[idx]) {
for _, d := range deep.Equal(p, actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "package-1-id",
"id": "cbf4f3077fc7deee",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -36,7 +36,7 @@
}
},
{
"id": "package-2-id",
"id": "1a39aadd9705c2b9",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "package-1-id",
"id": "d1d433485a31ed07",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -32,7 +32,7 @@
}
},
{
"id": "package-2-id",
"id": "2db629ca48fa6786",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
+9 -7
View File
@@ -3,6 +3,8 @@ package syftjson
import (
"fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal"
@@ -23,7 +25,7 @@ func ToFormatModel(s sbom.SBOM, applicationConfig interface{}) model.Document {
return model.Document{
Artifacts: toPackageModels(s.Artifacts.PackageCatalog),
ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(s.Artifacts.PackageCatalog)),
ArtifactRelationships: toRelationshipModel(s.Relationships),
Source: src,
Distro: toDistroModel(s.Artifacts.Distro),
Descriptor: model.Descriptor{
@@ -50,7 +52,7 @@ func toPackageModels(catalog *pkg.Catalog) []model.Package {
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p *pkg.Package) model.Package {
func toPackageModel(p pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
@@ -69,7 +71,7 @@ func toPackageModel(p *pkg.Package) model.Package {
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID),
ID: string(p.ID()),
Name: p.Name,
Version: p.Version,
Type: p.Type,
@@ -87,14 +89,14 @@ func toPackageModel(p *pkg.Package) model.Package {
}
}
func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship {
func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship {
result := make([]model.Relationship, len(relationships))
for i, r := range relationships {
result[i] = model.Relationship{
Parent: string(r.Parent),
Child: string(r.Child),
Parent: string(r.From.ID()),
Child: string(r.To.ID()),
Type: string(r.Type),
Metadata: r.Metadata,
Metadata: r.Data,
}
}
return result
@@ -61,7 +61,6 @@ func toSyftPackage(p model.Package) pkg.Package {
}
return pkg.Package{
ID: pkg.ID(p.ID),
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
@@ -33,7 +33,6 @@ func TestJSONPresenter(t *testing.T) {
catalog := pkg.NewCatalog()
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
@@ -57,7 +56,6 @@ func TestJSONPresenter(t *testing.T) {
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
@@ -72,7 +72,7 @@
],
"artifacts": [
{
"id": "package-1-id",
"id": "b84dfe0eb2c5670f",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -102,7 +102,7 @@
}
},
{
"id": "package-2-id",
"id": "6619226d6979963f",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
+2 -10
View File
@@ -1,9 +1,9 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at 2021-11-15 14:32:22.081911 -0500 EST m=+0.159302107
// This file was generated by robots at 2021-09-29 23:42:29.616023 -0400 EDT m=+0.184689484
// using data from https://spdx.org/licenses/licenses.json
package spdxlicense
const Version = "3.15"
const Version = "3.14"
var licenseIDs = map[string]string{
"0bsd": "0BSD",
@@ -367,12 +367,6 @@ var licenseIDs = map[string]string{
"cnri-jython": "CNRI-Jython",
"cnri-python": "CNRI-Python",
"cnri-python-gpl-compatible": "CNRI-Python-GPL-Compatible",
"coil-1": "COIL-1.0",
"coil-1.0": "COIL-1.0",
"coil-1.0.0": "COIL-1.0",
"community-spec-1": "Community-Spec-1.0",
"community-spec-1.0": "Community-Spec-1.0",
"community-spec-1.0.0": "Community-Spec-1.0",
"condor-1": "Condor-1.1",
"condor-1.1": "Condor-1.1",
"condor-1.1.0": "Condor-1.1",
@@ -446,7 +440,6 @@ var licenseIDs = map[string]string{
"eupl-1.2.0": "EUPL-1.2",
"eurosym": "Eurosym",
"fair": "Fair",
"fdk-aac": "FDK-AAC",
"frameworx-1": "Frameworx-1.0",
"frameworx-1.0": "Frameworx-1.0",
"frameworx-1.0.0": "Frameworx-1.0",
@@ -658,7 +651,6 @@ var licenseIDs = map[string]string{
"liliq-rplus-1": "LiLiQ-Rplus-1.1",
"liliq-rplus-1.1": "LiLiQ-Rplus-1.1",
"liliq-rplus-1.1.0": "LiLiQ-Rplus-1.1",
"linux-man-pages-copyleft": "Linux-man-pages-copyleft",
"linux-openib": "Linux-OpenIB",
"lpl-1": "LPL-1.0",
"lpl-1.0": "LPL-1.0",
-70
View File
@@ -1,70 +0,0 @@
(registered 2021-11-05, last updated 2021-11-05)
Media type name: application
Media subtype name: vnd.syft+json
Required parameters: N/A
Optional parameters:
version
The version parameter refers to the Syft specification version in use.
version = 1*DIGIT "." 1*DIGIT "." 1*DIGIT
Encoding considerations: binary
This media type has all of the same encoding considerations of
application/json as described in [RFC8259]
Security considerations:
This media type has all of the same security
considerations of application/json as described in [RFC8259].
Depending on the operational context of the device or software being described by the SBOM there may be additional security requirements. These may include but are not limited to, encryption at rest, encryption in transit, and restrictions on the transmission of the SBOM to 3rd parties. These additional requirements are considered out of scope for the specification. They will typically be enforced by contract or copyright terms.
Interoperability considerations:
This media type has the same interoperability considerations of application/json as described in [RFC8259].
Published specification:
The specification can be found on the main Syft GitHub repository under the schema directory https://github.com/anchore/syft/blob/main/schema/json
Applications which use this media:
This media type is used to specify a software bill of materials.
It will be used by tools that produce SBOMs either during the software build process or as a result of software composition analysis.
It will also be used by tools that consume SBOMs for software
supply chain, component, supplier, license, and vulnerability
analysis.
Fragment identifier considerations:
N/A
Restrictions on usage:
N/A
Provisional registration? (standards tree only):
N/A
Additional information:
1. Deprecated alias names for this type: N/A
2. Magic number(s): N/A
3. File extension(s): .syft.json
4. Macintosh file type code: N/A
5. Object Identifiers: N/A
General Comments:
Person to contact for further information:
1. Name: Dan Luhring
2. Email: dan.luhring@anchore.com
Intended usage: Common
The Syft SBOM format is an open-source software bill of materials specification. It is intended to be exchanged between different parties of the software supply chain.
Author/Change controller: Dan Luhring, on behalf of Anchore
+26
View File
@@ -0,0 +1,26 @@
package artifact
import (
"fmt"
"github.com/mitchellh/hashstructure/v2"
)
// ID represents a unique value for each package added to a package catalog.
type ID string
type Identifiable interface {
ID() ID
}
func IDFromHash(obj interface{}) (ID, error) {
f, err := hashstructure.Hash(obj, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
SlicesAsSets: true,
})
if err != nil {
return "", fmt.Errorf("could not build ID for object=%+v: %+v", obj, err)
}
return ID(fmt.Sprintf("%x", f)), nil
}
+17
View File
@@ -0,0 +1,17 @@
package artifact
const (
// OwnershipByFileOverlapRelationship indicates that the parent package claims ownership of a child package since
// the parent metadata indicates overlap with a location that a cataloger found the child package by. This is
// by definition a package-to-package relationship and is created only after all package cataloging has been completed.
OwnershipByFileOverlapRelationship RelationshipType = "ownership-by-file-overlap"
)
type RelationshipType string
type Relationship struct {
From Identifiable `json:"from"`
To Identifiable `json:"to"`
Type RelationshipType `json:"type"`
Data interface{} `json:"data,omitempty"`
}
-5
View File
@@ -123,11 +123,6 @@ func TestIdentifyDistro(t *testing.T) {
Type: Mariner,
Version: "1.0.0",
},
{
fixture: "test-fixtures/os/rockylinux",
Type: RockyLinux,
Version: "8.4.0",
},
}
observedDistros := internal.NewStringSet()
@@ -1,13 +0,0 @@
NAME="Rocky Linux"
VERSION="8.4 (Green Obsidian)"
ID="rocky"
ID_LIKE="rhel fedora"
VERSION_ID="8.4"
PLATFORM_ID="platform:el8"
PRETTY_NAME="Rocky Linux 8.4 (Green Obsidian)"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:rocky:rocky:8.4:GA"
HOME_URL="https://rockylinux.org/"
BUG_REPORT_URL="https://bugs.rockylinux.org/"
ROCKY_SUPPORT_PRODUCT="Rocky Linux"
ROCKY_SUPPORT_PRODUCT_VERSION="8"
-3
View File
@@ -21,7 +21,6 @@ const (
Photon Type = "photon"
Windows Type = "windows"
Mariner Type = "mariner"
RockyLinux Type = "rockylinux"
)
// All contains all Linux distribution options
@@ -41,7 +40,6 @@ var All = []Type{
Photon,
Windows,
Mariner,
RockyLinux,
}
// IDMapping connects a distro ID like "ubuntu" to a Distro type
@@ -61,7 +59,6 @@ var IDMapping = map[string]Type{
"photon": Photon,
"windows": Windows,
"mariner": Mariner,
"rocky": RockyLinux,
}
// String returns the string representation of the given Linux distribution.
+8 -6
View File
@@ -19,6 +19,8 @@ package syft
import (
"fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/distro"
@@ -32,10 +34,10 @@ import (
// CatalogPackages takes an inventory of packages from 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 CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, *distro.Distro, error) {
func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, []artifact.Relationship, *distro.Distro, error) {
resolver, err := src.FileResolver(scope)
if err != nil {
return nil, nil, fmt.Errorf("unable to determine resolver while cataloging packages: %w", err)
return nil, nil, nil, fmt.Errorf("unable to determine resolver while cataloging packages: %w", err)
}
// find the distro
@@ -59,15 +61,15 @@ func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, *dis
log.Info("cataloging directory")
catalogers = cataloger.DirectoryCatalogers()
default:
return nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
}
catalog, err := cataloger.Catalog(resolver, theDistro, catalogers...)
catalog, relationships, err := cataloger.Catalog(resolver, theDistro, catalogers...)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
return catalog, theDistro, nil
return catalog, relationships, theDistro, nil
}
// SetLogger sets the logger object used for all syft logging calls.
+31 -66
View File
@@ -4,6 +4,9 @@ import (
"sort"
"sync"
"github.com/anchore/syft/syft/artifact"
"github.com/jinzhu/copier"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
@@ -11,18 +14,18 @@ import (
// Catalog represents a collection of Packages.
type Catalog struct {
byID map[ID]*Package
idsByType map[Type][]ID
idsByPath map[string][]ID // note: this is real path or virtual path
byID map[artifact.ID]Package
idsByType map[Type][]artifact.ID
idsByPath map[string][]artifact.ID // note: this is real path or virtual path
lock sync.RWMutex
}
// NewCatalog returns a new empty Catalog
func NewCatalog(pkgs ...Package) *Catalog {
catalog := Catalog{
byID: make(map[ID]*Package),
idsByType: make(map[Type][]ID),
idsByPath: make(map[string][]ID),
byID: make(map[artifact.ID]Package),
idsByType: make(map[Type][]artifact.ID),
idsByPath: make(map[string][]artifact.ID),
}
for _, p := range pkgs {
@@ -38,21 +41,26 @@ func (c *Catalog) PackageCount() int {
}
// Package returns the package with the given ID.
func (c *Catalog) Package(id ID) *Package {
func (c *Catalog) Package(id artifact.ID) *Package {
v, exists := c.byID[id]
if !exists {
return nil
}
return v
var p Package
if err := copier.Copy(&p, &v); err != nil {
log.Warnf("unable to copy package id=%q name=%q: %+v", id, v.Name, err)
return nil
}
return &p
}
// PackagesByPath returns all packages that were discovered from the given path.
func (c *Catalog) PackagesByPath(path string) []*Package {
func (c *Catalog) PackagesByPath(path string) []Package {
return c.Packages(c.idsByPath[path])
}
// Packages returns all packages for the given ID.
func (c *Catalog) Packages(ids []ID) (result []*Package) {
func (c *Catalog) Packages(ids []artifact.ID) (result []Package) {
for _, i := range ids {
p, exists := c.byID[i]
if exists {
@@ -67,68 +75,32 @@ func (c *Catalog) Add(p Package) {
c.lock.Lock()
defer c.lock.Unlock()
if p.ID == "" {
fingerprint, err := p.Fingerprint()
if err != nil {
log.Warnf("failed to add package to catalog: %w", err)
return
}
p.ID = ID(fingerprint)
}
// note: since we are capturing the ID, we cannot modify the package being added from this point forward
id := p.ID()
// store by package ID
c.byID[p.ID] = &p
c.byID[id] = p
// store by package type
c.idsByType[p.Type] = append(c.idsByType[p.Type], p.ID)
c.idsByType[p.Type] = append(c.idsByType[p.Type], id)
// store by file location paths
observedPaths := internal.NewStringSet()
for _, l := range p.Locations {
if l.RealPath != "" && !observedPaths.Contains(l.RealPath) {
c.idsByPath[l.RealPath] = append(c.idsByPath[l.RealPath], p.ID)
c.idsByPath[l.RealPath] = append(c.idsByPath[l.RealPath], id)
observedPaths.Add(l.RealPath)
}
if l.VirtualPath != "" && l.RealPath != l.VirtualPath && !observedPaths.Contains(l.VirtualPath) {
c.idsByPath[l.VirtualPath] = append(c.idsByPath[l.VirtualPath], p.ID)
c.idsByPath[l.VirtualPath] = append(c.idsByPath[l.VirtualPath], id)
observedPaths.Add(l.VirtualPath)
}
}
}
func (c *Catalog) Remove(id ID) {
c.lock.Lock()
defer c.lock.Unlock()
_, exists := c.byID[id]
if !exists {
log.Errorf("package ID does not exist in the catalog : id=%+v", id)
return
}
// Remove all index references to this package ID
for t, ids := range c.idsByType {
c.idsByType[t] = removeID(id, ids)
if len(c.idsByType[t]) == 0 {
delete(c.idsByType, t)
}
}
for p, ids := range c.idsByPath {
c.idsByPath[p] = removeID(id, ids)
if len(c.idsByPath[p]) == 0 {
delete(c.idsByPath, p)
}
}
// Remove package
delete(c.byID, id)
}
// Enumerate all packages for the given type(s), enumerating all packages if no type is specified.
func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
channel := make(chan *Package)
func (c *Catalog) Enumerate(types ...Type) <-chan Package {
channel := make(chan Package)
go func() {
defer close(channel)
for ty, ids := range c.idsByType {
@@ -146,7 +118,10 @@ func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
}
}
for _, id := range ids {
channel <- c.Package(id)
p := c.Package(id)
if p != nil {
channel <- *p
}
}
}
}()
@@ -155,8 +130,7 @@ func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
// Sorted enumerates all packages for the given types sorted by package name. Enumerates all packages if no type
// is specified.
func (c *Catalog) Sorted(types ...Type) []*Package {
pkgs := make([]*Package, 0)
func (c *Catalog) Sorted(types ...Type) (pkgs []Package) {
for p := range c.Enumerate(types...) {
pkgs = append(pkgs, p)
}
@@ -176,12 +150,3 @@ func (c *Catalog) Sorted(types ...Type) []*Package {
return pkgs
}
func removeID(id ID, target []ID) (result []ID) {
for _, value := range target {
if value != id {
result = append(result, value)
}
}
return result
}
+42 -80
View File
@@ -10,7 +10,6 @@ import (
var catalogAddAndRemoveTestPkgs = []Package{
{
ID: "my-id",
Locations: []source.Location{
{
RealPath: "/a/path",
@@ -24,7 +23,6 @@ var catalogAddAndRemoveTestPkgs = []Package{
Type: RpmPkg,
},
{
ID: "my-other-id",
Locations: []source.Location{
{
RealPath: "/c/path",
@@ -45,6 +43,11 @@ type expectedIndexes struct {
}
func TestCatalogAddPopulatesIndex(t *testing.T) {
fixtureID := func(i int) string {
return string(catalogAddAndRemoveTestPkgs[i].ID())
}
tests := []struct {
name string
pkgs []Package
@@ -55,16 +58,16 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
pkgs: catalogAddAndRemoveTestPkgs,
expectedIndexes: expectedIndexes{
byType: map[Type]*strset.Set{
RpmPkg: strset.New("my-id"),
NpmPkg: strset.New("my-other-id"),
RpmPkg: strset.New(fixtureID(0)),
NpmPkg: strset.New(fixtureID(1)),
},
byPath: map[string]*strset.Set{
"/another/path": strset.New("my-id", "my-other-id"),
"/a/path": strset.New("my-id"),
"/b/path": strset.New("my-id"),
"/bee/path": strset.New("my-id"),
"/c/path": strset.New("my-other-id"),
"/d/path": strset.New("my-other-id"),
"/another/path": strset.New(fixtureID(0), fixtureID(1)),
"/a/path": strset.New(fixtureID(0)),
"/b/path": strset.New(fixtureID(0)),
"/bee/path": strset.New(fixtureID(0)),
"/c/path": strset.New(fixtureID(1)),
"/d/path": strset.New(fixtureID(1)),
},
},
},
@@ -80,50 +83,6 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
}
}
func TestCatalogRemove(t *testing.T) {
tests := []struct {
name string
pkgs []Package
removeId ID
expectedIndexes expectedIndexes
}{
{
name: "vanilla-add",
removeId: "my-other-id",
pkgs: catalogAddAndRemoveTestPkgs,
expectedIndexes: expectedIndexes{
byType: map[Type]*strset.Set{
RpmPkg: strset.New("my-id"),
},
byPath: map[string]*strset.Set{
"/another/path": strset.New("my-id"),
"/a/path": strset.New("my-id"),
"/b/path": strset.New("my-id"),
"/bee/path": strset.New("my-id"),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := NewCatalog(test.pkgs...)
c.Remove(test.removeId)
assertIndexes(t, c, test.expectedIndexes)
if c.Package(test.removeId) != nil {
t.Errorf("expected package to be removed, but was found!")
}
if c.PackageCount() != len(test.pkgs)-1 {
t.Errorf("expected count to be affected but was not")
}
})
}
}
func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
// assert path index
if len(c.idsByPath) != len(expectedIndexes.byPath) {
@@ -132,7 +91,7 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
for path, expectedIds := range expectedIndexes.byPath {
actualIds := strset.New()
for _, p := range c.PackagesByPath(path) {
actualIds.Add(string(p.ID))
actualIds.Add(string(p.ID()))
}
if !expectedIds.IsEqual(actualIds) {
@@ -147,7 +106,7 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
for ty, expectedIds := range expectedIndexes.byType {
actualIds := strset.New()
for p := range c.Enumerate(ty) {
actualIds.Add(string(p.ID))
actualIds.Add(string(p.ID()))
}
if !expectedIds.IsEqual(actualIds) {
@@ -157,39 +116,42 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
}
func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
p1 := Package{
Locations: []source.Location{
{
RealPath: "/b/path",
VirtualPath: "/another/path",
},
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
Name: "Package-1",
}
p2 := Package{
Locations: []source.Location{
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
Name: "Package-2",
}
tests := []struct {
name string
pkg Package
}{
{
name: "multiple locations with shared path",
pkg: Package{
ID: "my-id",
Locations: []source.Location{
{
RealPath: "/b/path",
VirtualPath: "/another/path",
},
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
},
pkg: p1,
},
{
name: "one location with shared path",
pkg: Package{
ID: "my-id",
Locations: []source.Location{
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
},
pkg: p2,
},
}
+6 -4
View File
@@ -8,6 +8,8 @@ import (
"strconv"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/internal/log"
@@ -21,7 +23,7 @@ var _ common.ParserFn = parseApkDB
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
// larger capacity for the scanner.
const maxScannerCapacity = 1024 * 1024
// a new larger buffer for the scanner
@@ -47,7 +49,7 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
for scanner.Scan() {
metadata, err := parseApkDBEntry(strings.NewReader(scanner.Text()))
if err != nil {
return nil, err
return nil, nil, err
}
if metadata != nil {
packages = append(packages, pkg.Package{
@@ -62,10 +64,10 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse APK DB file: %w", err)
return nil, nil, fmt.Errorf("failed to parse APK DB file: %w", err)
}
return packages, nil
return packages, nil, nil
}
// nolint:funlen
@@ -775,7 +775,8 @@ func TestMultiplePackages(t *testing.T) {
}
}()
pkgs, err := parseApkDB(file.Name(), file)
// TODO: no relationships are under test yet
pkgs, _, err := parseApkDB(file.Name(), file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
+10 -4
View File
@@ -3,6 +3,7 @@ package cataloger
import (
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
@@ -38,8 +39,9 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
// request.
func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, error) {
func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, []artifact.Relationship, error) {
catalog := pkg.NewCatalog()
var allRelationships []artifact.Relationship
filesProcessed, packagesDiscovered := newMonitor()
@@ -47,7 +49,7 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
var errs error
for _, theCataloger := range catalogers {
// find packages from the underlying raw data
packages, err := theCataloger.Catalog(resolver)
packages, relationships, err := theCataloger.Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
continue
@@ -68,14 +70,18 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
// add to catalog
catalog.Add(p)
}
allRelationships = append(allRelationships, relationships...)
}
allRelationships = append(allRelationships, pkg.NewRelationships(catalog)...)
if errs != nil {
return nil, errs
return nil, nil, errs
}
filesProcessed.SetCompleted()
packagesDiscovered.SetCompleted()
return catalog, nil
return catalog, allRelationships, nil
}
+2 -3
View File
@@ -6,13 +6,13 @@ catalogers defined in child packages as well as the interface definition to impl
package cataloger
import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/apkdb"
"github.com/anchore/syft/syft/pkg/cataloger/deb"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
"github.com/anchore/syft/syft/pkg/cataloger/php"
"github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/pkg/cataloger/rpmdb"
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
@@ -27,7 +27,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 source.FileResolver) ([]pkg.Package, error)
Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error)
}
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
@@ -50,7 +50,6 @@ func DirectoryCatalogers() []Cataloger {
ruby.NewGemFileLockCataloger(),
python.NewPythonIndexCataloger(),
python.NewPythonPackageCataloger(),
php.NewPHPIndexCataloger(),
javascript.NewJavascriptLockCataloger(),
deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(),
+1 -1
View File
@@ -83,7 +83,7 @@ func candidateVendors(p pkg.Package) []string {
// allow * as a candidate. Note: do NOT allow Java packages to have * vendors.
switch p.Language {
case pkg.Ruby, pkg.JavaScript:
vendors.addValue("*")
vendors.addValue(wfn.Any)
}
switch p.MetadataType {
@@ -637,7 +637,7 @@ func TestCandidateProducts(t *testing.T) {
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v %+v", test.p, test.expected), func(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, candidateProducts(test.p))
})
}
+14 -10
View File
@@ -6,6 +6,8 @@ package common
import (
"fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
@@ -35,18 +37,18 @@ func (c *GenericCataloger) Name() string {
}
// 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 source.FileResolver) ([]pkg.Package, error) {
func (c *GenericCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var packages []pkg.Package
parserByLocation := c.selectFiles(resolver)
var relationships []artifact.Relationship
for location, parser := range parserByLocation {
for location, parser := range c.selectFiles(resolver) {
contentReader, err := resolver.FileContentsByLocation(location)
if err != nil {
// TODO: fail or log?
return nil, fmt.Errorf("unable to fetch contents for location=%v : %w", location, err)
return nil, nil, fmt.Errorf("unable to fetch contents for location=%v : %w", location, err)
}
entries, err := parser(location.RealPath, contentReader)
discoveredPackages, discoveredRelationships, err := parser(location.RealPath, contentReader)
internal.CloseAndLogError(contentReader, location.VirtualPath)
if err != nil {
// TODO: should we fail? or only log?
@@ -54,14 +56,16 @@ func (c *GenericCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package,
continue
}
for _, entry := range entries {
entry.FoundBy = c.upstreamCataloger
entry.Locations = []source.Location{location}
for _, p := range discoveredPackages {
p.FoundBy = c.upstreamCataloger
p.Locations = append(p.Locations, location)
packages = append(packages, entry)
packages = append(packages, p)
}
relationships = append(relationships, discoveredRelationships...)
}
return packages, nil
return packages, relationships, nil
}
// SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging
@@ -8,11 +8,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
func parser(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
contents, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
@@ -21,7 +22,7 @@ func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
{
Name: string(contents),
},
}, nil
}, nil, nil
}
func TestGenericCataloger(t *testing.T) {
@@ -47,7 +48,7 @@ func TestGenericCataloger(t *testing.T) {
}
}
actualPkgs, err := cataloger.Catalog(resolver)
actualPkgs, _, err := cataloger.Catalog(resolver)
assert.NoError(t, err)
assert.Len(t, actualPkgs, len(expectedPkgs))
+2 -1
View File
@@ -3,8 +3,9 @@ package common
import (
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
)
// ParserFn standardizes a function signature for parser functions that accept the virtual file path (not usable for file reads) and contents and return any discovered packages from that file
type ParserFn func(string, io.Reader) ([]pkg.Package, error)
type ParserFn func(string, io.Reader) ([]pkg.Package, []artifact.Relationship, error)
+9 -9
View File
@@ -13,6 +13,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -36,24 +37,23 @@ 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 source.FileResolver) ([]pkg.Package, error) {
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDBGlob)
if err != nil {
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
return nil, nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
}
var results []pkg.Package
var pkgs []pkg.Package
var allPackages []pkg.Package
for _, dbLocation := range dbFileMatches {
dbContents, err := resolver.FileContentsByLocation(dbLocation)
if err != nil {
return nil, err
return nil, nil, err
}
pkgs, err = parseDpkgStatus(dbContents)
pkgs, err := parseDpkgStatus(dbContents)
internal.CloseAndLogError(dbContents, dbLocation.VirtualPath)
if err != nil {
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.RealPath, err)
return nil, nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.RealPath, err)
}
for i := range pkgs {
@@ -70,9 +70,9 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error)
addLicenses(resolver, dbLocation, p)
}
results = append(results, pkgs...)
allPackages = append(allPackages, pkgs...)
}
return results, nil
return allPackages, nil, nil
}
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
+1 -1
View File
@@ -100,7 +100,7 @@ func TestDpkgCataloger(t *testing.T) {
t.Errorf("could not get resolver error: %+v", err)
}
actual, err := c.Catalog(resolver)
actual, _, err := c.Catalog(resolver)
if err != nil {
t.Fatalf("failed to catalog: %+v", err)
}
+1 -1
View File
@@ -23,7 +23,7 @@ var (
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0)
var packages []pkg.Package
continueProcessing := true
for continueProcessing {
@@ -6,7 +6,10 @@ package golang
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -35,17 +38,18 @@ func (c *Cataloger) Name() string {
}
// 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 source.FileResolver) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0)
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
fileMatches, err := resolver.FilesByMIMEType(mimeTypes...)
if err != nil {
return pkgs, fmt.Errorf("failed to find bin by mime types: %w", err)
return pkgs, nil, fmt.Errorf("failed to find bin by mime types: %w", err)
}
for _, location := range fileMatches {
r, err := resolver.FileContentsByLocation(location)
if err != nil {
return pkgs, fmt.Errorf("failed to resolve file contents by location: %w", err)
return pkgs, nil, fmt.Errorf("failed to resolve file contents by location: %w", err)
}
goPkgs, err := parseGoBin(location, r)
@@ -53,9 +57,9 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error)
log.Warnf("could not parse possible go binary: %+v", err)
}
r.Close()
internal.CloseAndLogError(r, location.RealPath)
pkgs = append(pkgs, goPkgs...)
}
return pkgs, nil
return pkgs, nil, nil
}
+1 -3
View File
@@ -23,9 +23,7 @@ func parseGoBin(location source.Location, reader io.ReadCloser) ([]pkg.Package,
goVersion, mod := findVers(x)
pkgs := buildGoPkgInfo(location, mod, goVersion)
return pkgs, nil
return buildGoPkgInfo(location, mod, goVersion), nil
}
func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Package {
+5 -4
View File
@@ -6,22 +6,23 @@ import (
"io/ioutil"
"sort"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"golang.org/x/mod/modfile"
)
// parseGoMod takes a go.mod and lists all packages discovered.
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) {
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
packages := make(map[string]pkg.Package)
contents, err := ioutil.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read go module: %w", err)
return nil, nil, fmt.Errorf("failed to read go module: %w", err)
}
file, err := modfile.Parse(path, contents, nil)
if err != nil {
return nil, fmt.Errorf("failed to parse go module: %w", err)
return nil, nil, fmt.Errorf("failed to parse go module: %w", err)
}
for _, m := range file.Require {
@@ -59,5 +60,5 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) {
return pkgsSlice[i].Name < pkgsSlice[j].Name
})
return pkgsSlice, nil
return pkgsSlice, nil, nil
}
@@ -70,7 +70,8 @@ func TestParseGoMod(t *testing.T) {
t.Fatalf(err.Error())
}
actual, err := parseGoMod(test.fixture, f)
// TODO: no relationships are under test yet
actual, _, err := parseGoMod(test.fixture, f)
if err != nil {
t.Fatalf(err.Error())
}
+23 -17
View File
@@ -9,6 +9,7 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -34,12 +35,12 @@ type archiveParser struct {
}
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
return nil, err
return nil, nil, err
}
return parser.parse()
}
@@ -80,29 +81,31 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
}
// parse the loaded archive and return all packages found.
func (j *archiveParser) parse() ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
// find the parent package from the java manifest
parentPkg, err := j.discoverMainPackage()
if err != nil {
return nil, fmt.Errorf("could not generate package from %s: %w", j.virtualPath, err)
return nil, nil, fmt.Errorf("could not generate package from %s: %w", j.virtualPath, err)
}
// find aux packages from pom.properties/pom.xml and potentially modify the existing parentPkg
auxPkgs, err := j.discoverPkgsFromAllMavenFiles(parentPkg)
if err != nil {
return nil, err
return nil, nil, err
}
pkgs = append(pkgs, auxPkgs...)
if j.detectNested {
// find nested java archive packages
nestedPkgs, err := j.discoverPkgsFromNestedArchives(parentPkg)
nestedPkgs, nestedRelationships, err := j.discoverPkgsFromNestedArchives(parentPkg)
if err != nil {
return nil, err
return nil, nil, err
}
pkgs = append(pkgs, nestedPkgs...)
relationships = append(relationships, nestedRelationships...)
}
// lastly, add the parent package to the list (assuming the parent exists)
@@ -110,7 +113,7 @@ func (j *archiveParser) parse() ([]pkg.Package, error) {
pkgs = append([]pkg.Package{*parentPkg}, pkgs...)
}
return pkgs, nil
return pkgs, relationships, nil
}
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
@@ -189,31 +192,32 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
// discoverPkgsFromNestedArchives finds Java archives within Java archives, returning all listed Java packages found and
// associating each discovered package to the given parent package.
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
// search and parse pom.properties files & fetch the contents
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
if err != nil {
return nil, fmt.Errorf("unable to extract files from zip: %w", err)
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
}
// discover nested artifacts
for archivePath, archiveOpener := range openers {
archiveReadCloser, err := archiveOpener.Open()
if err != nil {
return nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
}
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
nestedPkgs, err := parseJavaArchive(nestedPath, archiveReadCloser)
nestedPkgs, nestedRelationships, err := parseJavaArchive(nestedPath, archiveReadCloser)
if err != nil {
if closeErr := archiveReadCloser.Close(); closeErr != nil {
log.Warnf("unable to close archived file from tempdir: %+v", closeErr)
}
return nil, fmt.Errorf("unable to process nested java archive (%s): %w", archivePath, err)
return nil, nil, fmt.Errorf("unable to process nested java archive (%s): %w", archivePath, err)
}
if err = archiveReadCloser.Close(); err != nil {
return nil, fmt.Errorf("unable to close archived file from tempdir: %w", err)
return nil, nil, fmt.Errorf("unable to close archived file from tempdir: %w", err)
}
// attach the parent package to all discovered packages that are not already associated with a java archive
@@ -226,9 +230,11 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) (
}
pkgs = append(pkgs, p)
}
relationships = append(relationships, nestedRelationships...)
}
return pkgs, nil
return pkgs, relationships, nil
}
func pomPropertiesByParentPath(archivePath string, extractPaths []string, virtualPath string) (map[string]pkg.PomProperties, error) {
@@ -242,7 +242,7 @@ func TestParseJar(t *testing.T) {
t.Fatalf("should not have filed... %+v", err)
}
actual, err := parser.parse()
actual, _, err := parser.parse()
if err != nil {
t.Fatalf("failed to parse java archive: %+v", err)
}
@@ -507,7 +507,7 @@ func TestParseNestedJar(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseJavaArchive(fixture.Name(), fixture)
actual, _, err := parseJavaArchive(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse java archive: %+v", err)
}
@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -162,8 +163,8 @@ func licensesFromJSON(p PackageJSON) ([]string, error) {
}
// parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0)
func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
var packages []pkg.Package
dec := json.NewDecoder(reader)
for {
@@ -171,17 +172,17 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
if err := dec.Decode(&p); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
return nil, nil, fmt.Errorf("failed to parse package.json file: %w", err)
}
if !p.hasNameAndVersionValues() {
log.Debug("encountered package.json file without a name and/or version field, ignoring this file")
return nil, nil
return nil, nil, nil
}
licenses, err := licensesFromJSON(p)
if err != nil {
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
return nil, nil, fmt.Errorf("failed to parse package.json file: %w", err)
}
packages = append(packages, pkg.Package{
@@ -200,7 +201,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
})
}
return packages, nil
return packages, nil, nil
}
func (p PackageJSON) hasNameAndVersionValues() bool {
@@ -124,7 +124,7 @@ func TestParsePackageJSON(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePackageJSON("", fixture)
actual, _, err := parsePackageJSON("", fixture)
if err != nil {
t.Fatalf("failed to parse package-lock.json: %+v", err)
}
@@ -150,7 +150,8 @@ func TestParsePackageJSON_Partial(t *testing.T) { // see https://github.com/anch
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePackageJSON("", fixture)
// TODO: no relationships are under test yet
actual, _, err := parsePackageJSON("", fixture)
if err != nil {
t.Fatalf("failed to parse package-lock.json: %+v", err)
}
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -28,11 +29,11 @@ type Dependency struct {
}
// parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages.
func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, error) {
func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
// in the case we find package-lock.json files in the node_modules directories, skip those
// as the whole purpose of the lock file is for the specific dependencies of the root project
if pathContainsNodeModulesDirectory(path) {
return nil, nil
return nil, nil, nil
}
var packages []pkg.Package
@@ -43,7 +44,7 @@ func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, error) {
if err := dec.Decode(&lock); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse package-lock.json file: %w", err)
return nil, nil, fmt.Errorf("failed to parse package-lock.json file: %w", err)
}
for name, pkgMeta := range lock.Dependencies {
packages = append(packages, pkg.Package{
@@ -55,5 +56,5 @@ func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, error) {
}
}
return packages, nil
return packages, nil, nil
}
@@ -109,7 +109,8 @@ func TestParsePackageLock(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePackageLock(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parsePackageLock(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse package-lock.json: %+v", err)
}
@@ -7,6 +7,7 @@ import (
"regexp"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -34,11 +35,11 @@ const (
noVersion = ""
)
func parseYarnLock(path string, reader io.Reader) ([]pkg.Package, error) {
func parseYarnLock(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
// in the case we find yarn.lock files in the node_modules directories, skip those
// as the whole purpose of the lock file is for the specific dependencies of the project
if pathContainsNodeModulesDirectory(path) {
return nil, nil
return nil, nil, nil
}
var packages []pkg.Package
@@ -79,10 +80,10 @@ func parseYarnLock(path string, reader io.Reader) ([]pkg.Package, error) {
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse yarn.lock file: %w", err)
return nil, nil, fmt.Errorf("failed to parse yarn.lock file: %w", err)
}
return packages, nil
return packages, nil, nil
}
func findPackageName(line string) string {
@@ -70,7 +70,8 @@ func TestParseYarnLock(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseYarnLock(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parseYarnLock(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse yarn.lock: %+v", err)
}
-17
View File
@@ -1,17 +0,0 @@
/*
Package python provides a concrete Cataloger implementation for Python ecosystem files (egg, wheel, requirements.txt).
*/
package php
import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// NewPythonIndexCataloger returns a new cataloger for python packages referenced from poetry lock files, requirements.txt files, and setup.py files.
func NewPHPIndexCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/composer.lock": parseComposerLock,
}
return common.NewGenericCataloger(nil, globParsers, "php-index-cataloger")
}
@@ -1,50 +0,0 @@
package php
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
type ComposerLock struct {
Packages []Dependency `json:"packages"`
PackageDev []Dependency `json:"packages-dev"`
}
type Dependency struct {
Name string `json:"name"`
Version string `json:"version"`
}
// integrity check
var _ common.ParserFn = parseComposerLock
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
func parseComposerLock(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0)
dec := json.NewDecoder(reader)
for {
var lock ComposerLock
if err := dec.Decode(&lock); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse composer.lock file: %w", err)
}
for _, pkgMeta := range lock.Packages {
version := pkgMeta.Version
name := pkgMeta.Name
packages = append(packages, pkg.Package{
Name: name,
Version: version,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
})
}
}
return packages, nil
}
@@ -1,40 +0,0 @@
package php
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseComposerFileLock(t *testing.T) {
expected := []pkg.Package{
{
Name: "adoy/fastcgi-client",
Version: "1.0.2",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
{
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
}
fixture, err := os.Open("test-fixtures/composer.lock")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseComposerLock(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
differences := deep.Equal(expected, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}
}
-298
View File
@@ -1,298 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "746ba78c06aef0cf954135ea909f9eb9",
"packages": [
{
"name": "adoy/fastcgi-client",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/adoy/PHP-FastCGI-Client.git",
"reference": "6d9a552f0206a1db7feb442824540aa6c55e5b27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/adoy/PHP-FastCGI-Client/zipball/6d9a552f0206a1db7feb442824540aa6c55e5b27",
"reference": "6d9a552f0206a1db7feb442824540aa6c55e5b27",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-0": {
"Adoy\\FastCGI\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Pierrick Charron",
"email": "pierrick@adoy.net"
}
],
"description": "Lightweight, single file FastCGI client for PHP.",
"keywords": [
"fastcgi",
"fcgi"
],
"time": "2019-12-11T13:49:21+00:00"
},
{
"name": "alcaeus/mongo-php-adapter",
"version": "1.1.11",
"source": {
"type": "git",
"url": "https://github.com/alcaeus/mongo-php-adapter.git",
"reference": "43b6add94c8b4cb9890d662cba4c0defde733dcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alcaeus/mongo-php-adapter/zipball/43b6add94c8b4cb9890d662cba4c0defde733dcf",
"reference": "43b6add94c8b4cb9890d662cba4c0defde733dcf",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-hash": "*",
"ext-mongodb": "^1.2.0",
"mongodb/mongodb": "^1.0.1",
"php": "^5.6 || ^7.0"
},
"provide": {
"ext-mongo": "1.6.14"
},
"require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6.0 || ^7.0",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-0": {
"Mongo": "lib/Mongo"
},
"psr-4": {
"Alcaeus\\MongoDbAdapter\\": "lib/Alcaeus/MongoDbAdapter"
},
"files": [
"lib/Mongo/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "alcaeus",
"email": "alcaeus@alcaeus.org"
},
{
"name": "Olivier Lechevalier",
"email": "olivier.lechevalier@gmail.com"
}
],
"description": "Adapter to provide ext-mongo interface on top of mongo-php-libary",
"keywords": [
"database",
"mongodb"
],
"time": "2019-11-11T20:47:32+00:00"
}
],
"packages-dev": [
{
"name": "behat/gherkin",
"version": "v4.6.2",
"source": {
"type": "git",
"url": "https://github.com/Behat/Gherkin.git",
"reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31",
"reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31",
"shasum": ""
},
"require": {
"php": ">=5.3.1"
},
"require-dev": {
"phpunit/phpunit": "~4.5|~5",
"symfony/phpunit-bridge": "~2.7|~3|~4",
"symfony/yaml": "~2.3|~3|~4"
},
"suggest": {
"symfony/yaml": "If you want to parse features, represented in YAML files"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-0": {
"Behat\\Gherkin": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
}
],
"description": "Gherkin DSL parser for PHP 5.3",
"homepage": "http://behat.org/",
"keywords": [
"BDD",
"Behat",
"Cucumber",
"DSL",
"gherkin",
"parser"
],
"time": "2020-03-17T14:03:26+00:00"
},
{
"name": "codeception/codeception",
"version": "4.1.6",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Codeception.git",
"reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9",
"reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9",
"shasum": ""
},
"require": {
"behat/gherkin": "^4.4.0",
"codeception/lib-asserts": "^1.0",
"codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0",
"codeception/stub": "^2.0 | ^3.0",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"guzzlehttp/psr7": "~1.4",
"php": ">=5.6.0 <8.0",
"symfony/console": ">=2.7 <6.0",
"symfony/css-selector": ">=2.7 <6.0",
"symfony/event-dispatcher": ">=2.7 <6.0",
"symfony/finder": ">=2.7 <6.0",
"symfony/yaml": ">=2.7 <6.0"
},
"require-dev": {
"codeception/module-asserts": "*@dev",
"codeception/module-cli": "*@dev",
"codeception/module-db": "*@dev",
"codeception/module-filesystem": "*@dev",
"codeception/module-phpbrowser": "*@dev",
"codeception/specify": "~0.3",
"codeception/util-universalframework": "*@dev",
"monolog/monolog": "~1.8",
"squizlabs/php_codesniffer": "~2.0",
"symfony/process": ">=2.7 <6.0",
"vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0"
},
"suggest": {
"codeception/specify": "BDD-style code blocks",
"codeception/verify": "BDD-style assertions",
"hoa/console": "For interactive console functionality",
"stecman/symfony-console-completion": "For BASH autocompletion",
"symfony/phpunit-bridge": "For phpunit-bridge support"
},
"bin": [
"codecept"
],
"type": "library",
"extra": {
"branch-alias": []
},
"autoload": {
"psr-4": {
"Codeception\\": "src/Codeception",
"Codeception\\Extension\\": "ext"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Bodnarchuk",
"email": "davert@mail.ua",
"homepage": "http://codegyre.com"
}
],
"description": "BDD-style testing framework",
"homepage": "http://codeception.com/",
"keywords": [
"BDD",
"TDD",
"acceptance testing",
"functional testing",
"unit testing"
],
"time": "2020-06-07T16:31:51+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"intelligence/bt-base": 20,
"intelligence/bt-common": 20,
"intelligence/bt-lib-tools": 20,
"intelligence/ci-lib-cache": 20,
"intelligence/ci-lib-client": 20,
"intelligence/ci-lib-contributed": 20,
"intelligence/ci-lib-eav": 20,
"intelligence/ci-lib-report": 20,
"intelligence/git-leaks-elastic-client": 20,
"intelligence/phishing-elastic-client": 20,
"libs/gib-sso-client": 20,
"mozhin/phplib": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.4.0",
"ext-amqp": "^1.9",
"ext-ctype": "*",
"ext-curl": "^7.4",
"ext-date": "^7.4",
"ext-fileinfo": "*",
"ext-geoip": "^1.1",
"ext-gettext": "*",
"ext-iconv": "*",
"ext-imagick": "^3.4",
"ext-imap": "^7.4",
"ext-intl": "^7.4",
"ext-json": "*",
"ext-mbstring": "^7.4",
"ext-mongodb": "^1.4",
"ext-mysqli": "^7.4",
"ext-pdo_mysql": "^7.4",
"ext-redis": ">=3.1"
},
"platform-dev": []
}
@@ -7,6 +7,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
@@ -31,13 +32,13 @@ 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 source.FileResolver) ([]pkg.Package, error) {
func (c *PackageCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var fileMatches []source.Location
for _, glob := range []string{eggMetadataGlob, wheelMetadataGlob, eggFileMetadataGlob} {
matches, err := resolver.FilesByGlob(glob)
if err != nil {
return nil, fmt.Errorf("failed to find files by glob: %s", glob)
return nil, nil, fmt.Errorf("failed to find files by glob: %s", glob)
}
fileMatches = append(fileMatches, matches...)
}
@@ -46,13 +47,13 @@ func (c *PackageCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package,
for _, location := range fileMatches {
p, err := c.catalogEggOrWheel(resolver, location)
if err != nil {
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", location.RealPath, err)
return nil, nil, fmt.Errorf("unable to catalog python package=%+v: %w", location.RealPath, err)
}
if p != nil {
pkgs = append(pkgs, *p)
}
}
return pkgs, nil
return pkgs, nil, nil
}
// catalogEggOrWheel takes the primary metadata file reference and returns the python package it represents.
@@ -144,7 +144,7 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
test.expectedPackage.Locations = locations
actual, err := NewPythonPackageCataloger().Catalog(resolver)
actual, _, err := NewPythonPackageCataloger().Catalog(resolver)
if err != nil {
t.Fatalf("failed to catalog python package: %+v", err)
}
@@ -173,7 +173,7 @@ func TestIgnorePackage(t *testing.T) {
t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := source.NewMockResolverForPaths(test.MetadataFixture)
actual, err := NewPythonPackageCataloger().Catalog(resolver)
actual, _, err := NewPythonPackageCataloger().Catalog(resolver)
if err != nil {
t.Fatalf("failed to catalog python package: %+v", err)
}
@@ -6,6 +6,7 @@ import (
"io"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -37,7 +38,7 @@ type Dependency struct {
var _ common.ParserFn = parsePipfileLock
// parsePipfileLock is a parser function for Pipfile.lock contents, returning "Default" python packages discovered.
func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, error) {
func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
packages := make([]pkg.Package, 0)
dec := json.NewDecoder(reader)
@@ -46,7 +47,7 @@ func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, error) {
if err := dec.Decode(&lock); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse Pipfile.lock file: %w", err)
return nil, nil, fmt.Errorf("failed to parse Pipfile.lock file: %w", err)
}
for name, pkgMeta := range lock.Default {
version := strings.TrimPrefix(pkgMeta.Version, "==")
@@ -59,5 +60,5 @@ func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, error) {
}
}
return packages, nil
return packages, nil, nil
}
@@ -39,7 +39,8 @@ func TestParsePipFileLock(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePipfileLock(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parsePipfileLock(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/pelletier/go-toml"
@@ -13,17 +14,17 @@ import (
var _ common.ParserFn = parsePoetryLock
// parsePoetryLock is a parser function for poetry.lock contents, returning all python packages discovered.
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) {
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
tree, err := toml.LoadReader(reader)
if err != nil {
return nil, fmt.Errorf("unable to load poetry.lock for parsing: %v", err)
return nil, nil, fmt.Errorf("unable to load poetry.lock for parsing: %v", err)
}
metadata := PoetryMetadata{}
err = tree.Unmarshal(&metadata)
if err != nil {
return nil, fmt.Errorf("unable to parse poetry.lock: %v", err)
return nil, nil, fmt.Errorf("unable to parse poetry.lock: %v", err)
}
return metadata.Pkgs(), nil
return metadata.Pkgs(), nil, nil
}
@@ -45,7 +45,8 @@ func TestParsePoetryLock(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePoetryLock(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parsePoetryLock(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
@@ -6,6 +6,7 @@ import (
"io"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -15,7 +16,7 @@ var _ common.ParserFn = parseRequirementsTxt
// parseRequirementsTxt takes a Python requirements.txt file, returning all Python packages that are locked to a
// specific version.
func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
packages := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
@@ -55,10 +56,10 @@ func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse python requirements file: %w", err)
return nil, nil, fmt.Errorf("failed to parse python requirements file: %w", err)
}
return packages, nil
return packages, nil, nil
}
// removeTrailingComment takes a requirements.txt line and strips off comment strings.
@@ -50,7 +50,8 @@ func TestParseRequirementsTxt(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseRequirementsTxt(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parseRequirementsTxt(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"regexp"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -19,7 +20,7 @@ var _ common.ParserFn = parseSetup
// " mypy2 == v0.770", ' mypy3== v0.770', --> match(name=mypy2 version=v0.770), match(name=mypy3, version=v0.770)
var pinnedDependency = regexp.MustCompile(`['"]\W?(\w+\W?==\W?[\w\.]*)`)
func parseSetup(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseSetup(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
packages := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
@@ -46,5 +47,5 @@ func parseSetup(_ string, reader io.Reader) ([]pkg.Package, error) {
}
}
return packages, nil
return packages, nil, nil
}
@@ -45,7 +45,7 @@ func TestParseSetup(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseSetup(fixture.Name(), fixture)
actual, _, err := parseSetup(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
+9 -6
View File
@@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -27,24 +28,26 @@ func (c *Cataloger) Name() string {
}
// 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 source.FileResolver) ([]pkg.Package, error) {
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
fileMatches, err := resolver.FilesByGlob(pkg.RpmDBGlob)
if err != nil {
return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
return nil, nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
}
var pkgs []pkg.Package
for _, location := range fileMatches {
dbContentReader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, err
return nil, nil, err
}
pkgs, err = parseRpmDB(resolver, location, dbContentReader)
discoveredPkgs, err := parseRpmDB(resolver, location, dbContentReader)
internal.CloseAndLogError(dbContentReader, location.VirtualPath)
if err != nil {
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", location.RealPath, err)
return nil, nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", location.RealPath, err)
}
pkgs = append(pkgs, discoveredPkgs...)
}
return pkgs, nil
return pkgs, nil, nil
}
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -16,7 +17,7 @@ var _ common.ParserFn = parseGemFileLockEntries
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
// parseGemFileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered.
func parseGemFileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseGemFileLockEntries(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
@@ -49,9 +50,9 @@ func parseGemFileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error)
}
}
if err := scanner.Err(); err != nil {
return nil, err
return nil, nil, err
}
return pkgs, nil
return pkgs, nil, nil
}
func isDependencyLine(line string) bool {
@@ -68,7 +68,8 @@ func TestParseGemfileLockEntries(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseGemFileLockEntries(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parseGemFileLockEntries(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse gemfile lock: %+v", err)
}
+4 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -60,7 +61,7 @@ func processList(s string) []string {
return results
}
func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var fields = make(map[string]interface{})
scanner := bufio.NewScanner(reader)
@@ -93,7 +94,7 @@ func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
if fields["name"] != "" && fields["version"] != "" {
var metadata pkg.GemMetadata
if err := mapstructure.Decode(fields, &metadata); err != nil {
return nil, fmt.Errorf("unable to decode gem metadata: %w", err)
return nil, nil, fmt.Errorf("unable to decode gem metadata: %w", err)
}
pkgs = append(pkgs, pkg.Package{
@@ -107,7 +108,7 @@ func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
})
}
return pkgs, nil
return pkgs, nil, nil
}
// renderUtf8 takes any string escaped string sub-sections from the ruby string and replaces those sections with the UTF8 runes.
@@ -31,7 +31,8 @@ func TestParseGemspec(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseGemSpecEntries(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parseGemSpecEntries(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse gemspec: %+v", err)
}
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/pelletier/go-toml"
@@ -13,17 +14,17 @@ import (
var _ common.ParserFn = parseCargoLock
// parseCargoLock is a parser function for Cargo.lock contents, returning all rust cargo crates discovered.
func parseCargoLock(_ string, reader io.Reader) ([]pkg.Package, error) {
func parseCargoLock(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
tree, err := toml.LoadReader(reader)
if err != nil {
return nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
}
metadata := CargoMetadata{}
err = tree.Unmarshal(&metadata)
if err != nil {
return nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)
return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)
}
return metadata.Pkgs(), nil
return metadata.Pkgs(), nil, nil
}
@@ -177,7 +177,8 @@ func TestParseCargoLock(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseCargoLock(fixture.Name(), fixture)
// TODO: no relationships are under test yet
actual, _, err := parseCargoLock(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
+3
View File
@@ -45,5 +45,8 @@ func MustCPE(cpeStr string) CPE {
func normalizeCpeField(field string) string {
// keep dashes and forward slashes unescaped
if field == "*" {
return wfn.Any
}
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")
}
+35 -1
View File
@@ -1,6 +1,10 @@
package pkg
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
)
func must(c CPE, e error) CPE {
if e != nil {
@@ -46,3 +50,33 @@ func TestNewCPE(t *testing.T) {
})
}
}
func Test_normalizeCpeField(t *testing.T) {
tests := []struct {
field string
expected string
}{
{
field: "something",
expected: "something",
},
{
field: "some\\thing",
expected: `some\thing`,
},
{
field: "*",
expected: "",
},
{
field: "",
expected: "",
},
}
for _, test := range tests {
t.Run(test.field, func(t *testing.T) {
assert.Equal(t, test.expected, normalizeCpeField(test.field))
})
}
}
-4
View File
@@ -1,4 +0,0 @@
package pkg
// ID represents a unique value for each package added to a package catalog.
type ID string
+1 -1
View File
@@ -21,7 +21,7 @@ type JavaMetadata struct {
Manifest *JavaManifest `mapstructure:"Manifest" json:"manifest,omitempty"`
PomProperties *PomProperties `mapstructure:"PomProperties" json:"pomProperties,omitempty"`
PomProject *PomProject `mapstructure:"PomProject" json:"pomProject,omitempty"`
Parent *Package `json:"-"`
Parent *Package `hash:"ignore" json:"-"` // note: the parent cannot be included in the minimal definition of uniqueness since this field is not reproducible in an encode-decode cycle (is lossy).
}
// PomProperties represents the fields of interest extracted from a Java archive's pom.properties file.

Some files were not shown because too many files have changed in this diff Show More