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
107 changed files with 1776 additions and 5226 deletions
-5
View File
@@ -15,8 +15,3 @@ ignore-packages:
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
# (we choose Apache-2.0)
- github.com/spdx/tools-golang
# from: https://github.com/xi2/xz/blob/master/LICENSE
# All these files have been put into the public domain.
# You can do whatever you want with these files.
- github.com/xi2/xz
+21 -18
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
@@ -135,7 +139,7 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
go mod tidy
.PHONY: check-licenses
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
check-licenses:
$(TEMPDIR)/bouncer check
check-go-mod-tidy:
@@ -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
-12
View File
@@ -11,16 +11,6 @@
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
### Join us at the virtual OSS meetup!
When: December 1, 2021 @ 11am-noon PT (2-3pm ET)
We’ve got security experts from VMware and Anchore who will share their tips, tricks, and lessons learned on securing containers using [Syft](https://github.com/anchore/syft), [Grype](https://github.com/anchore/grype), and [Cartographer](https://github.com/vmware-tanzu/cartographer).
Register now: https://get.anchore.com/anchore-oss-meetup-dec-1-2021/
---
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
## Features
@@ -201,8 +191,6 @@ Configuration search paths:
- `.syft.yaml`
- `.syft/config.yaml`
- `<git repo root>/.syft.yaml`
- `<git repo root>/.syft/config.yaml`
- `~/.syft.yaml`
- `<XDG_CONFIG_HOME>/syft/config.yaml`
+2 -8
View File
@@ -48,13 +48,7 @@ func initCmdAliasBindings() {
panic(err)
}
// enable all cataloger by default if power-user command is run
if activeCmd == powerUserCmd {
config.PowerUserCatalogerEnabledDefault()
}
switch activeCmd {
case packagesCmd, rootCmd:
if activeCmd == packagesCmd || activeCmd == rootCmd {
// note: we need to lazily bind config options since they are shared between both the root command
// and the packages command. Otherwise there will be global viper state that is in contention.
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
@@ -64,7 +58,7 @@ func initCmdAliasBindings() {
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err)
}
default:
} else {
// even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes.
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
+4 -2
View File
@@ -14,7 +14,7 @@ import (
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:funlen
// nolint:gocognit,funlen
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
defer cleanupFn()
events := subscription.Events()
@@ -41,7 +41,9 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
if err != nil {
// capture the error from the worker and unsubscribe to complete a graceful shutdown
retErr = multierror.Append(retErr, err)
_ = subscription.Unsubscribe()
if err := subscription.Unsubscribe(); err != nil {
retErr = multierror.Append(retErr, err)
}
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
forceTeardown = true
+23 -42
View File
@@ -13,10 +13,11 @@ import (
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"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"
@@ -239,12 +240,6 @@ func packagesExecWorker(userInput string) <-chan error {
go func() {
defer close(errs)
tasks, err := tasks()
if err != nil {
errs <- err
return
}
f := formats.ByOption(packagesPresenterOpt)
if f == nil {
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
@@ -258,54 +253,39 @@ func packagesExecWorker(userInput string) <-chan error {
errs <- fmt.Errorf("failed to determine image source: %w", err)
return
}
if cleanup != nil {
defer cleanup()
}
defer cleanup()
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: appConfig,
},
catalog, relationships, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %w", err)
return
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, s); err != nil {
if err := runPackageSbomUpload(src, src.Metadata, catalog, d); err != nil {
errs <- err
return
}
}
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: d,
},
Relationships: relationships,
Source: src.Metadata,
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: f.Presenter(s),
Value: f.Presenter(sbomResult),
})
}()
return errs
}
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
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 {
@@ -340,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,
@@ -349,6 +331,5 @@ func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
if err := c.Import(context.Background(), importCfg); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
}
return nil
}
+49 -30
View File
@@ -2,20 +2,20 @@ package cmd
import (
"fmt"
"os"
"sync"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
@@ -23,8 +23,6 @@ import (
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
@@ -78,10 +76,6 @@ func powerUserExec(_ *cobra.Command, args []string) error {
if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
if err != nil {
@@ -101,11 +95,7 @@ func powerUserExecWorker(userInput string) <-chan error {
go func() {
defer close(errs)
appConfig.Secrets.Cataloger.Enabled = true
appConfig.FileMetadata.Cataloger.Enabled = true
appConfig.FileContents.Cataloger.Enabled = true
appConfig.FileClassification.Cataloger.Enabled = true
tasks, err := tasks()
tasks, err := powerUserTasks()
if err != nil {
errs <- err
return
@@ -118,34 +108,63 @@ func powerUserExecWorker(userInput string) <-chan error {
errs <- err
return
}
if cleanup != nil {
defer cleanup()
}
defer cleanup()
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: appConfig,
},
}
var relationships []<-chan artifact.Relationship
var results []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
results = append(results, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
for relationship := range mergeResults(results...) {
s.Relationships = append(s.Relationships, relationship)
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: syftjson.Format().Presenter(s),
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
}
+20 -33
View File
@@ -4,25 +4,27 @@ import (
"crypto"
"fmt"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
type powerUserTask func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func tasks() ([]task, error) {
var tasks []task
func powerUserTasks() ([]powerUserTask, error) {
var tasks []powerUserTask
generators := []func() (task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
generators := []func() (powerUserTask, error){
catalogPackagesTask,
catalogFileMetadataTask,
catalogFileDigestsTask,
catalogSecretsTask,
catalogFileClassificationsTask,
catalogContentsTask,
}
for _, generator := range generators {
@@ -30,7 +32,6 @@ func tasks() ([]task, error) {
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
@@ -39,7 +40,7 @@ func tasks() ([]task, error) {
return tasks, nil
}
func generateCatalogPackagesTask() (task, error) {
func catalogPackagesTask() (powerUserTask, error) {
if !appConfig.Package.Cataloger.Enabled {
return nil, nil
}
@@ -59,7 +60,7 @@ func generateCatalogPackagesTask() (task, error) {
return task, nil
}
func generateCatalogFileMetadataTask() (task, error) {
func catalogFileMetadataTask() (powerUserTask, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
@@ -83,7 +84,7 @@ func generateCatalogFileMetadataTask() (task, error) {
return task, nil
}
func generateCatalogFileDigestsTask() (task, error) {
func catalogFileDigestsTask() (powerUserTask, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
@@ -129,7 +130,7 @@ func generateCatalogFileDigestsTask() (task, error) {
return task, nil
}
func generateCatalogSecretsTask() (task, error) {
func catalogSecretsTask() (powerUserTask, error) {
if !appConfig.Secrets.Cataloger.Enabled {
return nil, nil
}
@@ -161,7 +162,7 @@ func generateCatalogSecretsTask() (task, error) {
return task, nil
}
func generateCatalogFileClassificationsTask() (task, error) {
func catalogFileClassificationsTask() (powerUserTask, error) {
if !appConfig.FileClassification.Cataloger.Enabled {
return nil, nil
}
@@ -189,7 +190,7 @@ func generateCatalogFileClassificationsTask() (task, error) {
return task, nil
}
func generateCatalogContentsTask() (task, error) {
func catalogContentsTask() (powerUserTask, error) {
if !appConfig.FileContents.Cataloger.Enabled {
return nil, nil
}
@@ -215,17 +216,3 @@ func generateCatalogContentsTask() (task, error) {
return task, nil
}
func runTask(t task, 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
}
}
+12 -17
View File
@@ -12,47 +12,42 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/anchore/stereoscope v0.0.0-20211024152658-003132a67c10
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v20.10.10+incompatible
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.7
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.4.1
github.com/google/uuid v1.2.0
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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.4.1
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/pelletier/go-toml v1.9.3
github.com/pelletier/go-toml v1.8.1
github.com/pkg/profile v1.5.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.8.1
github.com/spdx/tools-golang v0.1.0
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.7.0
github.com/vifraa/gopom v0.1.0
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/mod v0.4.2
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
gopkg.in/yaml.v2 v2.4.0
)
+447 -657
View File
File diff suppressed because it is too large Load Diff
+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)
}
+7 -27
View File
@@ -59,10 +59,8 @@ func TestPackageSbomToModel(t *testing.T) {
FoundBy: "foundBy",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
RealPath: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
@@ -76,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)
}
@@ -159,10 +149,8 @@ func TestPackageSbomImport(t *testing.T) {
FoundBy: "foundBy",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
RealPath: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
@@ -209,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)
}
@@ -256,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 {
+18 -68
View File
@@ -3,10 +3,7 @@ package config
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strings"
@@ -20,8 +17,6 @@ import (
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
var catalogerEnabledDefault = false
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
@@ -49,11 +44,6 @@ type Application struct {
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
}
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
func PowerUserCatalogerEnabledDefault() {
catalogerEnabledDefault = true
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
config := &Application{
CliOptions: cliOpts,
@@ -166,10 +156,6 @@ func (cfg *Application) parseLogLevelOption() error {
}
}
if cfg.Log.Level == "" {
cfg.Log.Level = cfg.Log.LevelOpt.String()
}
return nil
}
@@ -209,80 +195,44 @@ func readConfig(v *viper.Viper, configPath string) error {
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName("." + internal.ApplicationName)
if exists, err := readViperConfig(v); exists || err != nil {
return err
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if exists, err := readViperConfig(v); exists || err != nil {
return err
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
if root := gitRepoRoot(); root != "" {
// 3. look for <git repo root>/.<appname>.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
v.AddConfigPath(root)
v.SetConfigName("." + internal.ApplicationName)
if exists, err := readViperConfig(v); exists {
if err != nil {
return err
}
return os.Chdir(root)
}
// 4. look for <git repo root>/.<appname>/config.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
v.AddConfigPath(filepath.Join(root, "."+internal.ApplicationName))
v.SetConfigName("config")
if exists, err := readViperConfig(v); exists {
if err != nil {
return err
}
return os.Chdir(root)
}
}
// 5. look for ~/.<appname>.yaml
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if exists, err := readViperConfig(v); exists || err != nil {
return err
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 6. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
v.SetConfigName("config")
if exists, err := readViperConfig(v); exists || err != nil {
return err
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
return ErrApplicationConfigNotFound
}
func readViperConfig(v *viper.Viper) (bool, error) {
err := v.ReadInConfig()
if err == nil {
return true, nil
} else if errors.As(err, &viper.ConfigFileNotFoundError{}) {
return false, nil
}
return true, fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
func gitRepoRoot() string {
root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
return ""
}
absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
if err != nil {
return ""
}
return absRepoRoot
}
+1 -1
View File
@@ -10,7 +10,7 @@ type fileClassification struct {
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.enabled", true)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
+1 -1
View File
@@ -13,7 +13,7 @@ type fileContents struct {
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.enabled", true)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
+1 -1
View File
@@ -11,7 +11,7 @@ type FileMetadata struct {
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.enabled", true)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
+1 -1
View File
@@ -15,7 +15,7 @@ type secrets struct {
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.enabled", true)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
+1 -1
View File
@@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON presenter
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "2.0.0"
JSONSchemaVersion = "1.1.0"
)
@@ -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)
}
@@ -0,0 +1,51 @@
package spdxhelpers
import (
"crypto/sha256"
"fmt"
"path/filepath"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/pkg"
)
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)
if !hasMetadata(p) {
return files, fileIDs, relationships
}
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
if !ok {
return files, fileIDs, relationships
}
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
baseFileName := filepath.Base(ownedFilePath)
pathHash := sha256.Sum256([]byte(ownedFilePath))
fileSpdxID := model.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
fileIDs = append(fileIDs, fileSpdxID)
files = append(files, model.File{
FileName: ownedFilePath,
Item: model.Item{
Element: model.Element{
SPDXID: fileSpdxID,
Name: baseFileName,
},
},
})
relationships = append(relationships, model.Relationship{
SpdxElementID: packageSpdxID,
RelationshipType: model.ContainsRelationship,
RelatedSpdxElement: fileSpdxID,
})
}
return files, fileIDs, relationships
}
@@ -27,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"
}
@@ -19,8 +19,14 @@ func Test_SourceInfo(t *testing.T) {
input: pkg.Package{
// note: no type given
Locations: []source.Location{
source.NewVirtualLocation("/a-place", "/b-place"),
source.NewVirtualLocation("/c-place", "/d-place"),
{
RealPath: "/a-place",
VirtualPath: "/b-place",
},
{
RealPath: "/c-place",
VirtualPath: "/d-place",
},
},
},
expected: []string{
@@ -118,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 {
+2 -20
View File
@@ -124,15 +124,6 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
Distro: &dist,
},
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
}
@@ -196,15 +187,6 @@ func DirectoryInput(t testing.TB) sbom.SBOM {
Distro: &dist,
},
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
}
@@ -218,7 +200,7 @@ func newDirectoryCatalog() *pkg.Catalog {
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
source.NewLocation("/some/path/pkg1"),
{RealPath: "/some/path/pkg1"},
},
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
@@ -243,7 +225,7 @@ func newDirectoryCatalog() *pkg.Catalog {
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
source.NewLocation("/some/path/pkg1"),
{RealPath: "/some/path/pkg1"},
},
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
+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)
+12 -12
View File
@@ -3,17 +3,17 @@ package model
type FileType string
const (
DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation
ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif)
VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*)
ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.)
SpdxFileType FileType = "SPDX" // if the file is an SPDX document
ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*)
SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.)
BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.)
TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*)
AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3)
OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.)
DocumentationFileType FileType = "DOCUMENTATION"
ImageFileType FileType = "IMAGE"
VideoFileType FileType = "VIDEO"
ArchiveFileType FileType = "ARCHIVE"
SpdxFileType FileType = "SPDX"
ApplicationFileType FileType = "APPLICATION"
SourceFileType FileType = "SOURCE"
BinaryFileType FileType = "BINARY"
TextFileType FileType = "TEXT"
AudioFileType FileType = "AUDIO"
OtherFileType FileType = "OTHER"
)
type File struct {
@@ -36,6 +36,6 @@ type File struct {
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
// from some SPDX formats (deprecated).
// from some SPDX formats(deprecated).
ArtifactOf []string `json:"artifactOf,omitempty"`
}
@@ -3,18 +3,18 @@
"name": "/some/path",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-11-17T19:35:54.834877Z",
"created": "2021-10-29T16:26:08.995826Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.15"
"licenseListVersion": "3.14"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https:/anchore.com/syft/dir/some/path-65e2226e-a61e-4ed1-81bb-56022e1ff1eb",
"documentNamespace": "https:/anchore.com/syft/dir/some/path-5362d380-914a-458f-b059-d8d27899574c",
"packages": [
{
"SPDXID": "SPDXRef-2a115ac97d018a0e",
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
@@ -31,12 +31,15 @@
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
],
"licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-5e920b2bece2c3ae",
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
@@ -57,5 +60,20 @@
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
"versionInfo": "2.0.1"
}
],
"files": [
{
"SPDXID": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a",
"name": "foo",
"licenseConcluded": "",
"fileName": "/some/path/pkg1/dependencies/foo"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
"relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
}
]
}
@@ -3,18 +3,18 @@
"name": "user-image-input",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-11-17T19:35:57.761372Z",
"created": "2021-10-29T16:26:09.001799Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.15"
"licenseListVersion": "3.14"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-5383918f-ec96-4aa9-b756-ad16e1ada31e",
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-3ad8571c-513f-4fce-944e-5125353c3186",
"packages": [
{
"SPDXID": "SPDXRef-888661d4f0362f02",
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
@@ -36,7 +36,7 @@
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-4068ff5e8926b305",
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
+24 -207
View File
@@ -2,29 +2,22 @@ package spdx22json
import (
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"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{
Element: model.Element{
@@ -42,60 +35,26 @@ func toFormatModel(s sbom.SBOM) model.Document {
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: documentNamespace(name, s.Source),
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
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 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)
}
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 toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []model.Package {
func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []model.Relationship) {
packages := make([]model.Package, 0)
relationships := make([]model.Relationship, 0)
files := make([]model.File, 0)
for _, p := range catalog.Sorted() {
license := spdxhelpers.License(p)
packageSpdxID := model.ElementID(p.ID()).String()
packageSpdxID := model.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
packageFiles, fileIDs, packageFileRelationships := spdxhelpers.Files(packageSpdxID, p)
files = append(files, packageFiles...)
relationships = append(relationships, packageFileRelationships...)
// note: the license concluded and declared should be the same since we are collecting license information
// from the project data itself (the installed package files).
@@ -104,16 +63,14 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m
DownloadLocation: spdxhelpers.DownloadLocation(p),
ExternalRefs: spdxhelpers.ExternalRefs(p),
FilesAnalyzed: false,
HasFiles: fileIDsForPackage(packageSpdxID, relationships),
HasFiles: fileIDs,
Homepage: spdxhelpers.Homepage(p),
// The Declared License is what the authors of a project believe govern the package
LicenseDeclared: license,
Originator: spdxhelpers.Originator(p),
SourceInfo: spdxhelpers.SourceInfo(p),
VersionInfo: p.Version,
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
Originator: spdxhelpers.Originator(p),
SourceInfo: spdxhelpers.SourceInfo(p),
VersionInfo: p.Version,
Item: model.Item{
// The Concluded License field is the license the SPDX file creator believes governs the package
LicenseConcluded: license,
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
Element: model.Element{
SPDXID: packageSpdxID,
Name: p.Name,
@@ -122,145 +79,5 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m
})
}
return packages
}
func fileIDsForPackage(packageSpdxID string, relationships []artifact.Relationship) (fileIDs []string) {
for _, relationship := range relationships {
if relationship.Type != artifact.ContainsRelationship {
continue
}
if _, ok := relationship.From.(pkg.Package); !ok {
continue
}
if _, ok := relationship.To.(source.Coordinates); !ok {
continue
}
if string(relationship.From.ID()) == packageSpdxID {
fileIDs = append(fileIDs, string(relationship.To.ID()))
}
}
return fileIDs
}
func toFiles(s sbom.SBOM) []model.File {
results := make([]model.File, 0)
artifacts := s.Artifacts
for _, coordinates := range sbom.AllCoordinates(s) {
var metadata *source.FileMetadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
// TODO: add file classifications (?) and content as a snippet
var comment string
if coordinates.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
}
results = append(results, model.File{
Item: model.Item{
Element: model.Element{
SPDXID: string(coordinates.ID()),
Name: filepath.Base(coordinates.RealPath),
Comment: comment,
},
// required, no attempt made to determine license information
LicenseConcluded: "NOASSERTION",
},
Checksums: toFileChecksums(digests),
FileName: coordinates.RealPath,
FileTypes: toFileTypes(metadata),
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].FileName < results[j].FileName
})
return results
}
func toFileChecksums(digests []file.Digest) (checksums []model.Checksum) {
for _, digest := range digests {
checksums = append(checksums, model.Checksum{
Algorithm: digest.Algorithm,
ChecksumValue: digest.Value,
})
}
return checksums
}
func toFileTypes(metadata *source.FileMetadata) (ty []string) {
if metadata == nil {
return nil
}
mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0]
switch mimeTypePrefix {
case "image":
ty = append(ty, string(model.ImageFileType))
case "video":
ty = append(ty, string(model.VideoFileType))
case "application":
ty = append(ty, string(model.ApplicationFileType))
case "text":
ty = append(ty, string(model.TextFileType))
case "audio":
ty = append(ty, string(model.AudioFileType))
}
if internal.IsExecutable(metadata.MIMEType) {
ty = append(ty, string(model.BinaryFileType))
}
if internal.IsArchive(metadata.MIMEType) {
ty = append(ty, string(model.ArchiveFileType))
}
// TODO: add support for source, spdx, and documentation file types
if len(ty) == 0 {
ty = append(ty, string(model.OtherFileType))
}
return ty
}
func toRelationships(relationships []artifact.Relationship) (result []model.Relationship) {
for _, r := range relationships {
exists, relationshipType, comment := lookupRelationship(r.Type)
if !exists {
log.Warnf("unable to convert relationship from SPDX 2.2 JSON, dropping: %+v", r)
continue
}
result = append(result, model.Relationship{
SpdxElementID: string(r.From.ID()),
RelationshipType: relationshipType,
RelatedSpdxElement: string(r.To.ID()),
Comment: comment,
})
}
return result
}
func lookupRelationship(ty artifact.RelationshipType) (bool, model.RelationshipType, string) {
switch ty {
case artifact.ContainsRelationship:
return true, model.ContainsRelationship, ""
case artifact.OwnershipByFileOverlapRelationship:
return true, model.OtherRelationship, fmt.Sprintf("%s: 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", ty)
}
return false, "", ""
return packages, files, relationships
}
@@ -1,256 +0,0 @@
package spdx22json
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
func Test_toFileTypes(t *testing.T) {
tests := []struct {
name string
metadata source.FileMetadata
expected []string
}{
{
name: "application",
metadata: source.FileMetadata{
MIMEType: "application/vnd.unknown",
},
expected: []string{
string(model.ApplicationFileType),
},
},
{
name: "archive",
metadata: source.FileMetadata{
MIMEType: "application/zip",
},
expected: []string{
string(model.ApplicationFileType),
string(model.ArchiveFileType),
},
},
{
name: "audio",
metadata: source.FileMetadata{
MIMEType: "audio/ogg",
},
expected: []string{
string(model.AudioFileType),
},
},
{
name: "video",
metadata: source.FileMetadata{
MIMEType: "video/3gpp",
},
expected: []string{
string(model.VideoFileType),
},
},
{
name: "text",
metadata: source.FileMetadata{
MIMEType: "text/html",
},
expected: []string{
string(model.TextFileType),
},
},
{
name: "image",
metadata: source.FileMetadata{
MIMEType: "image/png",
},
expected: []string{
string(model.ImageFileType),
},
},
{
name: "binary",
metadata: source.FileMetadata{
MIMEType: "application/x-sharedlib",
},
expected: []string{
string(model.ApplicationFileType),
string(model.BinaryFileType),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
})
}
}
func Test_lookupRelationship(t *testing.T) {
tests := []struct {
input artifact.RelationshipType
exists bool
ty model.RelationshipType
comment string
}{
{
input: artifact.ContainsRelationship,
exists: true,
ty: model.ContainsRelationship,
},
{
input: artifact.OwnershipByFileOverlapRelationship,
exists: true,
ty: model.OtherRelationship,
comment: "ownership-by-file-overlap: 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",
},
{
input: "made-up",
exists: false,
},
}
for _, test := range tests {
t.Run(string(test.input), func(t *testing.T) {
exists, ty, comment := lookupRelationship(test.input)
assert.Equal(t, exists, test.exists)
assert.Equal(t, ty, test.ty)
assert.Equal(t, comment, test.comment)
})
}
}
func Test_toFileChecksums(t *testing.T) {
tests := []struct {
name string
digests []file.Digest
expected []model.Checksum
}{
{
name: "empty",
},
{
name: "has digests",
digests: []file.Digest{
{
Algorithm: "sha256",
Value: "deadbeefcafe",
},
{
Algorithm: "md5",
Value: "meh",
},
},
expected: []model.Checksum{
{
Algorithm: "sha256",
ChecksumValue: "deadbeefcafe",
},
{
Algorithm: "md5",
ChecksumValue: "meh",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
})
}
}
func Test_fileIDsForPackage(t *testing.T) {
p := pkg.Package{
Name: "bogus",
}
c := source.Coordinates{
RealPath: "/path",
FileSystemID: "nowhere",
}
tests := []struct {
name string
id string
relationships []artifact.Relationship
expected []string
}{
{
name: "find file IDs for packages with package-file relationships",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: []string{
string(c.ID()),
},
},
{
name: "ignore package-to-package",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "ignore file-to-file",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: c,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "ignore file-to-package",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: c,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "filter by relationship type",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.OwnershipByFileOverlapRelationship,
},
},
expected: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, fileIDsForPackage(test.id, test.relationships))
})
}
}
@@ -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) {
}
+2 -1
View File
@@ -8,7 +8,8 @@ import (
)
func encoder(output io.Writer, s sbom.SBOM) error {
doc := ToFormatModel(s)
// TODO: application config not available yet
doc := ToFormatModel(s, nil)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
-173
View File
@@ -4,15 +4,6 @@ import (
"flag"
"testing"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal/formats/common/testutils"
)
@@ -33,167 +24,3 @@ func TestImagePresenter(t *testing.T) {
*updateJson,
)
}
func TestEncodeFullJSONDocument(t *testing.T) {
catalog := pkg.NewCatalog()
p1 := pkg.Package{
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a/place/a",
},
},
},
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
Files: []pkg.PythonFileRecord{},
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
}
p2 := pkg.Package{
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/b/place/b",
},
},
},
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
Files: []pkg.DpkgFileRecord{},
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
}
catalog.Add(p1)
catalog.Add(p2)
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
FileMetadata: map[source.Coordinates]source.FileMetadata{
source.NewLocation("/a/place").Coordinates: {
Mode: 0775,
Type: "directory",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/a/place/a").Coordinates: {
Mode: 0775,
Type: "regularFile",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/b").Coordinates: {
Mode: 0775,
Type: "symbolicLink",
LinkDestination: "/c",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/b/place/b").Coordinates: {
Mode: 0644,
Type: "regularFile",
UserID: 1,
GroupID: 2,
},
},
FileDigests: map[source.Coordinates][]file.Digest{
source.NewLocation("/a/place/a").Coordinates: {
{
Algorithm: "sha256",
Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
},
},
source.NewLocation("/b/place/b").Coordinates: {
{
Algorithm: "sha256",
Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c",
},
},
},
FileContents: map[source.Coordinates]string{
source.NewLocation("/a/place/a").Coordinates: "the-contents",
},
Distro: &distro.Distro{
Type: distro.RedHat,
RawVersion: "7",
IDLike: "rhel",
},
},
Relationships: []artifact.Relationship{
{
From: p1,
To: p2,
Type: artifact.OwnershipByFileOverlapRelationship,
Data: map[string]string{
"file": "path",
},
},
},
Source: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-image-input",
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
},
Size: 38,
Layers: []source.LayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
Size: 22,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
Size: 16,
},
},
RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."),
RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."),
RepoDigests: []string{},
},
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(s),
*updateJson,
)
}
+4 -6
View File
@@ -4,12 +4,10 @@ package model
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []Relationship `json:"artifactRelationships"`
Files []File `json:"files,omitempty"` // note: must have omitempty
Secrets []Secrets `json:"secrets,omitempty"` // note: must have omitempty
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// Descriptor describes what created the document as well as surrounding metadata
-25
View File
@@ -1,25 +0,0 @@
package model
import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type File struct {
ID string `json:"id"`
Location source.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
Classifications []file.Classification `json:"classifications,omitempty"`
}
type FileMetadataEntry struct {
Mode int `json:"mode"`
Type source.FileType `json:"type"`
LinkDestination string `json:"linkDestination,omitempty"`
UserID int `json:"userID"`
GroupID int `json:"groupID"`
MIMEType string `json:"mimeType"`
}
+11 -11
View File
@@ -9,7 +9,7 @@ import (
"github.com/anchore/syft/syft/source"
)
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling.
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type Package struct {
PackageBasicData
PackageCustomData
@@ -17,16 +17,16 @@ type Package struct {
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
type PackageBasicData struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Coordinates `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
@@ -4,5 +4,5 @@ type Relationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata,omitempty"`
Metadata interface{} `json:"metadata"`
}
@@ -1,11 +0,0 @@
package model
import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type Secrets struct {
Location source.Coordinates `json:"location"`
Secrets []file.SearchResult `json:"secrets"`
}
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "2a115ac97d018a0e",
"id": "cbf4f3077fc7deee",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -36,7 +36,7 @@
}
},
{
"id": "5e920b2bece2c3ae",
"id": "1a39aadd9705c2b9",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -77,13 +77,10 @@
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"configuration": {
"config-key": "config-value"
}
"version": "[not provided]"
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "888661d4f0362f02",
"id": "d1d433485a31ed07",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -32,7 +32,7 @@
}
},
{
"id": "4068ff5e8926b305",
"id": "2db629ca48fa6786",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -98,13 +98,10 @@
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"configuration": {
"config-key": "config-value"
}
"version": "[not provided]"
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
+15 -102
View File
@@ -2,10 +2,6 @@ package syftjson
import (
"fmt"
"sort"
"strconv"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/artifact"
@@ -14,13 +10,14 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// TODO: this is exported for the use of the power-user command (temp)
func ToFormatModel(s sbom.SBOM) model.Document {
// TODO: this is export4ed for the use of the power-user command (temp)
func ToFormatModel(s sbom.SBOM, applicationConfig interface{}) model.Document {
src, err := toSourceModel(s.Source)
if err != nil {
log.Warnf("unable to create syft-json source object: %+v", err)
@@ -29,11 +26,13 @@ func ToFormatModel(s sbom.SBOM) model.Document {
return model.Document{
Artifacts: toPackageModels(s.Artifacts.PackageCatalog),
ArtifactRelationships: toRelationshipModel(s.Relationships),
Files: toFile(s),
Secrets: toSecrets(s.Artifacts.Secrets),
Source: src,
Distro: toDistroModel(s.Artifacts.Distro),
Descriptor: toDescriptor(s.Descriptor),
Descriptor: model.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: applicationConfig,
},
Schema: model.Schema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
@@ -41,93 +40,6 @@ func ToFormatModel(s sbom.SBOM) model.Document {
}
}
func toDescriptor(d sbom.Descriptor) model.Descriptor {
return model.Descriptor{
Name: d.Name,
Version: d.Version,
Configuration: d.Configuration,
}
}
func toSecrets(data map[source.Coordinates][]file.SearchResult) []model.Secrets {
results := make([]model.Secrets, 0)
for coordinates, secrets := range data {
results = append(results, model.Secrets{
Location: coordinates,
Secrets: secrets,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
func toFile(s sbom.SBOM) []model.File {
results := make([]model.File, 0)
artifacts := s.Artifacts
for _, coordinates := range sbom.AllCoordinates(s) {
var metadata *source.FileMetadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
var classifications []file.Classification
if classificationsForLocation, exists := artifacts.FileClassifications[coordinates]; exists {
classifications = classificationsForLocation
}
var contents string
if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists {
contents = contentsForLocation
}
results = append(results, model.File{
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Classifications: classifications,
Contents: contents,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMetadata) *model.FileMetadataEntry {
if metadata == nil {
return nil
}
mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode))
if err != nil {
log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err)
mode = 0
}
return &model.FileMetadataEntry{
Mode: mode,
Type: metadata.Type,
LinkDestination: metadata.LinkDestination,
UserID: metadata.UserID,
GroupID: metadata.GroupID,
MIMEType: metadata.MIMEType,
}
}
func toPackageModels(catalog *pkg.Catalog) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
@@ -146,16 +58,17 @@ func toPackageModel(p pkg.Package) model.Package {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
var coordinates = make([]source.Coordinates, len(p.Locations))
for i, l := range p.Locations {
coordinates[i] = l.Coordinates
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID()),
@@ -163,7 +76,7 @@ func toPackageModel(p pkg.Package) model.Package {
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: coordinates,
Locations: locations,
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
+2 -16
View File
@@ -20,19 +20,10 @@ func toSyftModel(doc model.Document) (*sbom.SBOM, error) {
PackageCatalog: toSyftCatalog(doc.Artifacts),
Distro: &dist,
},
Source: *toSyftSourceData(doc.Source),
Descriptor: toSyftDescriptor(doc.Descriptor),
Source: *toSyftSourceData(doc.Source),
}, nil
}
func toSyftDescriptor(d model.Descriptor) sbom.Descriptor {
return sbom.Descriptor{
Name: d.Name,
Version: d.Version,
Configuration: d.Configuration,
}
}
func toSyftSourceData(s model.Source) *source.Metadata {
switch s.Type {
case "directory":
@@ -69,16 +60,11 @@ func toSyftPackage(p model.Package) pkg.Package {
cpes = append(cpes, value)
}
var locations = make([]source.Location, len(p.Locations))
for i, c := range p.Locations {
locations[i] = source.NewLocationFromCoordinates(c)
}
return pkg.Package{
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: locations,
Locations: p.Locations,
Licenses: p.Licenses,
Language: p.Language,
Type: p.Type,
-72
View File
@@ -1,72 +0,0 @@
package internal
import "github.com/scylladb/go-set/strset"
var (
ArchiveMIMETypeSet = strset.New(
// derived from https://en.wikipedia.org/wiki/List_of_archive_formats
[]string{
// archive only
"application/x-archive",
"application/x-cpio",
"application/x-shar",
"application/x-iso9660-image",
"application/x-sbx",
"application/x-tar",
// compression only
"application/x-bzip2",
"application/gzip",
"application/x-lzip",
"application/x-lzma",
"application/x-lzop",
"application/x-snappy-framed",
"application/x-xz",
"application/x-compress",
"application/zstd",
// archiving and compression
"application/x-7z-compressed",
"application/x-ace-compressed",
"application/x-astrotite-afa",
"application/x-alz-compressed",
"application/vnd.android.package-archive",
"application/x-freearc",
"application/x-arj",
"application/x-b1",
"application/vnd.ms-cab-compressed",
"application/x-cfs-compressed",
"application/x-dar",
"application/x-dgc-compressed",
"application/x-apple-diskimage",
"application/x-gca-compressed",
"application/java-archive",
"application/x-lzh",
"application/x-lzx",
"application/x-rar-compressed",
"application/x-stuffit",
"application/x-stuffitx",
"application/x-gtar",
"application/x-ms-wim",
"application/x-xar",
"application/zip",
"application/x-zoo",
}...,
)
ExecutableMIMETypeSet = strset.New(
[]string{
"application/x-executable",
"application/x-mach-binary",
"application/x-elf",
"application/x-sharedlib",
"application/vnd.microsoft.portable-executable",
}...,
)
)
func IsArchive(mimeType string) bool {
return ArchiveMIMETypeSet.Has(mimeType)
}
func IsExecutable(mimeType string) bool {
return ExecutableMIMETypeSet.Has(mimeType)
}
-57
View File
@@ -1,57 +0,0 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_IsArchive(t *testing.T) {
tests := []struct {
name string
mimeType string
expected bool
}{
{
name: "not an archive",
mimeType: "application/vnd.unknown",
expected: false,
},
{
name: "archive",
mimeType: "application/x-rar-compressed",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, IsArchive(test.mimeType))
})
}
}
func Test_IsExecutable(t *testing.T) {
tests := []struct {
name string
mimeType string
expected bool
}{
{
name: "not an executable",
mimeType: "application/vnd.unknown",
expected: false,
},
{
name: "executable",
mimeType: "application/x-mach-binary",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, IsExecutable(test.mimeType))
})
}
}
@@ -0,0 +1,35 @@
package poweruser
import (
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/sbom"
)
type JSONDocument struct {
// note: poweruser.JSONDocument is meant to always be a superset of packages.JSONDocument, any additional fields
// here should be optional by supplying "omitempty" on these fields hint to the jsonschema generator to not
// require these fields. As an accepted rule in this repo all collections should still be initialized in the
// context of being used in a JSON document.
FileClassifications []JSONFileClassifications `json:"fileClassifications,omitempty"` // note: must have omitempty
FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty
Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty
model.Document
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(s sbom.SBOM, appConfig interface{}) (JSONDocument, error) {
fileMetadata, err := NewJSONFileMetadata(s.Artifacts.FileMetadata, s.Artifacts.FileDigests)
if err != nil {
return JSONDocument{}, err
}
return JSONDocument{
FileClassifications: NewJSONFileClassifications(s.Artifacts.FileClassifications),
FileContents: NewJSONFileContents(s.Artifacts.FileContents),
FileMetadata: fileMetadata,
Secrets: NewJSONSecrets(s.Artifacts.Secrets),
Document: syftjson.ToFormatModel(s, appConfig),
}, nil
}
@@ -0,0 +1,34 @@
package poweruser
import (
"sort"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type JSONFileClassifications struct {
Location source.Location `json:"location"`
Classification file.Classification `json:"classification"`
}
func NewJSONFileClassifications(data map[source.Location][]file.Classification) []JSONFileClassifications {
results := make([]JSONFileClassifications, 0)
for location, classifications := range data {
for _, classification := range classifications {
results = append(results, JSONFileClassifications{
Location: location,
Classification: classification,
})
}
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].Location.RealPath == results[j].Location.RealPath {
return results[i].Location.VirtualPath < results[j].Location.VirtualPath
}
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
@@ -0,0 +1,31 @@
package poweruser
import (
"sort"
"github.com/anchore/syft/syft/source"
)
type JSONFileContents struct {
Location source.Location `json:"location"`
Contents string `json:"contents"`
}
func NewJSONFileContents(data map[source.Location]string) []JSONFileContents {
results := make([]JSONFileContents, 0)
for location, contents := range data {
results = append(results, JSONFileContents{
Location: location,
Contents: contents,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].Location.RealPath == results[j].Location.RealPath {
return results[i].Location.VirtualPath < results[j].Location.VirtualPath
}
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
@@ -0,0 +1,63 @@
package poweruser
import (
"fmt"
"sort"
"strconv"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type JSONFileMetadata struct {
Location source.Location `json:"location"`
Metadata JSONFileMetadataEntry `json:"metadata"`
}
type JSONFileMetadataEntry struct {
Mode int `json:"mode"`
Type source.FileType `json:"type"`
LinkDestination string `json:"linkDestination,omitempty"`
UserID int `json:"userID"`
GroupID int `json:"groupID"`
Digests []file.Digest `json:"digests,omitempty"`
MIMEType string `json:"mimeType"`
}
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
results := make([]JSONFileMetadata, 0)
for location, metadata := range data {
mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode))
if err != nil {
return nil, fmt.Errorf("invalid mode found in file catalog @ location=%+v mode=%q: %w", location, metadata.Mode, err)
}
var digestResults []file.Digest
if digestsForLocation, exists := digests[location]; exists {
digestResults = digestsForLocation
}
results = append(results, JSONFileMetadata{
Location: location,
Metadata: JSONFileMetadataEntry{
Mode: mode,
Type: metadata.Type,
LinkDestination: metadata.LinkDestination,
UserID: metadata.UserID,
GroupID: metadata.GroupID,
Digests: digestResults,
MIMEType: metadata.MIMEType,
},
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].Location.RealPath == results[j].Location.RealPath {
return results[i].Location.VirtualPath < results[j].Location.VirtualPath
}
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results, nil
}
@@ -0,0 +1,36 @@
package poweruser
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/sbom"
)
// JSONPresenter is a JSON presentation object for the syft results
type JSONPresenter struct {
sbom sbom.SBOM
config interface{}
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewJSONPresenter(s sbom.SBOM, appConfig interface{}) *JSONPresenter {
return &JSONPresenter{
sbom: s,
config: appConfig,
}
}
// Present the PackageCatalog results to the given writer.
func (p *JSONPresenter) Present(output io.Writer) error {
doc, err := NewJSONDocument(p.sbom, p.config)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
@@ -0,0 +1,185 @@
package poweruser
import (
"bytes"
"flag"
"testing"
"github.com/anchore/syft/syft/sbom"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/anchore/syft/syft/file"
"github.com/anchore/go-testutils"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
func TestJSONPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
{
RealPath: "/a/place/a",
},
},
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
Files: []pkg.PythonFileRecord{},
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
{
RealPath: "/b/place/b",
},
},
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
Files: []pkg.DpkgFileRecord{},
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
appConfig := config.Application{
FileMetadata: config.FileMetadata{
Digests: []string{"sha256"},
},
}
cfg := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
FileMetadata: map[source.Location]source.FileMetadata{
source.NewLocation("/a/place"): {
Mode: 0775,
Type: "directory",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/a/place/a"): {
Mode: 0775,
Type: "regularFile",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/b"): {
Mode: 0775,
Type: "symbolicLink",
LinkDestination: "/c",
UserID: 0,
GroupID: 0,
},
source.NewLocation("/b/place/b"): {
Mode: 0644,
Type: "regularFile",
UserID: 1,
GroupID: 2,
},
},
FileDigests: map[source.Location][]file.Digest{
source.NewLocation("/a/place/a"): {
{
Algorithm: "sha256",
Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
},
},
source.NewLocation("/b/place/b"): {
{
Algorithm: "sha256",
Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c",
},
},
},
FileContents: map[source.Location]string{
source.NewLocation("/a/place/a"): "the-contents",
},
Distro: &distro.Distro{
Type: distro.RedHat,
RawVersion: "7",
IDLike: "rhel",
},
},
Source: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-image-input",
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
},
Size: 38,
Layers: []source.LayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
Size: 22,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
Size: 16,
},
},
RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."),
RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."),
RepoDigests: []string{},
},
},
}
if err := NewJSONPresenter(cfg, appConfig).Present(&buffer); err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateJSONGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
expected := testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
@@ -0,0 +1,32 @@
package poweruser
import (
"sort"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type JSONSecrets struct {
Location source.Location `json:"location"`
Secrets []file.SearchResult `json:"secrets"`
}
func NewJSONSecrets(data map[source.Location][]file.SearchResult) []JSONSecrets {
results := make([]JSONSecrets, 0)
for location, secrets := range data {
results = append(results, JSONSecrets{
Location: location,
Secrets: secrets,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].Location.RealPath != results[j].Location.RealPath {
return results[i].Location.VirtualPath < results[j].Location.VirtualPath
}
return false
})
return results
}
@@ -1,7 +1,78 @@
{
"fileContents": [
{
"location": {
"path": "/a/place/a"
},
"contents": "the-contents"
}
],
"fileMetadata": [
{
"location": {
"path": "/a/place"
},
"metadata": {
"mode": 775,
"type": "directory",
"userID": 0,
"groupID": 0,
"mimeType": ""
}
},
{
"location": {
"path": "/a/place/a"
},
"metadata": {
"mode": 775,
"type": "regularFile",
"userID": 0,
"groupID": 0,
"digests": [
{
"algorithm": "sha256",
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
}
],
"mimeType": ""
}
},
{
"location": {
"path": "/b"
},
"metadata": {
"mode": 775,
"type": "symbolicLink",
"linkDestination": "/c",
"userID": 0,
"groupID": 0,
"mimeType": ""
}
},
{
"location": {
"path": "/b/place/b"
},
"metadata": {
"mode": 644,
"type": "regularFile",
"userID": 1,
"groupID": 2,
"digests": [
{
"algorithm": "sha256",
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
}
],
"mimeType": ""
}
}
],
"artifacts": [
{
"id": "962403cfb7be50d7",
"id": "b84dfe0eb2c5670f",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -31,7 +102,7 @@
}
},
{
"id": "b11f44847bba0ed1",
"id": "6619226d6979963f",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -60,84 +131,7 @@
}
}
],
"artifactRelationships": [
{
"parent": "962403cfb7be50d7",
"child": "b11f44847bba0ed1",
"type": "ownership-by-file-overlap",
"metadata": {
"file": "path"
}
}
],
"files": [
{
"id": "913b4592e2c2ebdf",
"location": {
"path": "/a/place"
},
"metadata": {
"mode": 775,
"type": "directory",
"userID": 0,
"groupID": 0,
"mimeType": ""
}
},
{
"id": "e7c88bd18e11b0b",
"location": {
"path": "/a/place/a"
},
"metadata": {
"mode": 775,
"type": "regularFile",
"userID": 0,
"groupID": 0,
"mimeType": ""
},
"contents": "the-contents",
"digests": [
{
"algorithm": "sha256",
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
}
]
},
{
"id": "5c3dc6885f48b5a1",
"location": {
"path": "/b"
},
"metadata": {
"mode": 775,
"type": "symbolicLink",
"linkDestination": "/c",
"userID": 0,
"groupID": 0,
"mimeType": ""
}
},
{
"id": "799d2f12da0bcec4",
"location": {
"path": "/b/place/b"
},
"metadata": {
"mode": 644,
"type": "regularFile",
"userID": 1,
"groupID": 2,
"mimeType": ""
},
"digests": [
{
"algorithm": "sha256",
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
}
]
}
],
"artifactRelationships": [],
"source": {
"type": "image",
"target": {
@@ -173,13 +167,77 @@
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"version": "[not provided]",
"configuration": {
"config-key": "config-value"
"configPath": "",
"output": "",
"file": "",
"quiet": false,
"check-for-app-update": false,
"anchore": {
"host": "",
"path": "",
"dockerfile": "",
"overwrite-existing-image": false,
"import-timeout": 0
},
"dev": {
"profile-cpu": false,
"profile-mem": false
},
"log": {
"structured": false,
"level": "",
"file-location": ""
},
"package": {
"cataloger": {
"enabled": false,
"scope": ""
}
},
"file-metadata": {
"cataloger": {
"enabled": false,
"scope": ""
},
"digests": [
"sha256"
]
},
"file-classification": {
"cataloger": {
"enabled": false,
"scope": ""
}
},
"file-contents": {
"cataloger": {
"enabled": false,
"scope": ""
},
"skip-files-above-size": 0,
"globs": null
},
"secrets": {
"cataloger": {
"enabled": false,
"scope": ""
},
"additional-patterns": null,
"exclude-pattern-names": null,
"reveal-values": false,
"skip-files-above-size": 0
},
"registry": {
"insecure-skip-tls-verify": false,
"insecure-use-http": false,
"auth": null
}
}
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
+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",
+2
View File
@@ -11,6 +11,8 @@ import (
"golang.org/x/term"
)
// TODO: build tags to exclude options from windows
// Select is responsible for determining the specific UI function given select user option, the current platform
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
+2 -2
View File
@@ -12,7 +12,7 @@ import (
"github.com/alecthomas/jsonschema"
"github.com/anchore/syft/internal"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/syft/pkg"
)
@@ -48,7 +48,7 @@ func build() *jsonschema.Schema {
return strings.TrimPrefix(r.Name(), "JSON")
},
}
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&syftjsonModel.Document{}))
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&poweruser.JSONDocument{}))
metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{}))
// TODO: inject source definitions
File diff suppressed because it is too large Load Diff
-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
+7 -11
View File
@@ -1,21 +1,17 @@
package artifact
const (
// OwnershipByFileOverlapRelationship (supports package-to-package linkages) 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 relationship must be created only after all package cataloging
// has been completed.
// 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"
// ContainsRelationship (supports any-to-any linkages) is a proxy for the SPDX 2.2 CONTAINS relationship.
ContainsRelationship RelationshipType = "contains"
)
type RelationshipType string
type Relationship struct {
From Identifiable
To Identifiable
Type RelationshipType
Data interface{}
From Identifiable `json:"from"`
To Identifiable `json:"to"`
Type RelationshipType `json:"type"`
Data interface{} `json:"data,omitempty"`
}
+1 -1
View File
@@ -60,7 +60,7 @@ identifyLoop:
}
if len(locations) == 0 {
log.Debugf("path not found: %s", entry.path)
log.Debugf("No Refs found from path: %s", entry.path)
continue
}
-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.
+3 -3
View File
@@ -15,8 +15,8 @@ func NewClassificationCataloger(classifiers []Classifier) (*ClassificationCatalo
}, nil
}
func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates][]Classification, error) {
results := make(map[source.Coordinates][]Classification)
func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[source.Location][]Classification, error) {
results := make(map[source.Location][]Classification)
numResults := 0
for location := range resolver.AllLocations() {
@@ -26,7 +26,7 @@ func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[sou
return nil, err
}
if result != nil {
results[location.Coordinates] = append(results[location.Coordinates], *result)
results[location] = append(results[location], *result)
numResults++
}
}
+3 -9
View File
@@ -19,9 +19,7 @@ func TestFilepathMatches(t *testing.T) {
{
name: "simple-filename-match",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "python2.7",
},
RealPath: "python2.7",
},
patterns: []string{
`python([0-9]+\.[0-9]+)$`,
@@ -31,9 +29,7 @@ func TestFilepathMatches(t *testing.T) {
{
name: "filepath-match",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "/usr/bin/python2.7",
},
RealPath: "/usr/bin/python2.7",
},
patterns: []string{
`python([0-9]+\.[0-9]+)$`,
@@ -63,9 +59,7 @@ func TestFilepathMatches(t *testing.T) {
{
name: "anchored-filename-match-FAILS",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "/usr/bin/python2.7",
},
RealPath: "/usr/bin/python2.7",
},
patterns: []string{
`^python([0-9]+\.[0-9]+)$`,
+3 -3
View File
@@ -23,8 +23,8 @@ func NewContentsCataloger(globs []string, skipFilesAboveSize int64) (*ContentsCa
}, nil
}
func (i *ContentsCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates]string, error) {
results := make(map[source.Coordinates]string)
func (i *ContentsCataloger) Catalog(resolver source.FileResolver) (map[source.Location]string, error) {
results := make(map[source.Location]string)
var locations []source.Location
locations, err := resolver.FilesByGlob(i.globs...)
@@ -49,7 +49,7 @@ func (i *ContentsCataloger) Catalog(resolver source.FileResolver) (map[source.Co
if err != nil {
return nil, err
}
results[location.Coordinates] = result
results[location] = result
}
log.Debugf("file contents cataloger processed %d files", len(results))
+16 -16
View File
@@ -15,41 +15,41 @@ func TestContentsCataloger(t *testing.T) {
globs []string
maxSize int64
files []string
expected map[source.Coordinates]string
expected map[source.Location]string
}{
{
name: "multi-pattern",
globs: []string{"test-fixtures/last/*.txt", "test-fixtures/*.txt"},
files: allFiles,
expected: map[source.Coordinates]string{
source.NewLocation("test-fixtures/last/path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/another-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
expected: map[source.Location]string{
source.NewLocation("test-fixtures/last/path.txt"): "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/another-path.txt"): "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt"): "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
},
},
{
name: "no-patterns",
globs: []string{},
files: []string{"test-fixtures/last/path.txt", "test-fixtures/another-path.txt", "test-fixtures/a-path.txt"},
expected: map[source.Coordinates]string{},
expected: map[source.Location]string{},
},
{
name: "all-txt",
globs: []string{"**/*.txt"},
files: allFiles,
expected: map[source.Coordinates]string{
source.NewLocation("test-fixtures/last/path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/another-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
expected: map[source.Location]string{
source.NewLocation("test-fixtures/last/path.txt"): "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/another-path.txt"): "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt"): "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
},
},
{
name: "subpath",
globs: []string{"test-fixtures/*.txt"},
files: allFiles,
expected: map[source.Coordinates]string{
source.NewLocation("test-fixtures/another-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
expected: map[source.Location]string{
source.NewLocation("test-fixtures/another-path.txt"): "dGVzdC1maXh0dXJlcy9hbm90aGVyLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt"): "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
},
},
{
@@ -57,9 +57,9 @@ func TestContentsCataloger(t *testing.T) {
maxSize: 42,
globs: []string{"**/*.txt"},
files: allFiles,
expected: map[source.Coordinates]string{
source.NewLocation("test-fixtures/last/path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt").Coordinates: "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
expected: map[source.Location]string{
source.NewLocation("test-fixtures/last/path.txt"): "dGVzdC1maXh0dXJlcy9sYXN0L3BhdGgudHh0IGZpbGUgY29udGVudHMh",
source.NewLocation("test-fixtures/a-path.txt"): "dGVzdC1maXh0dXJlcy9hLXBhdGgudHh0IGZpbGUgY29udGVudHMh",
},
},
}
+3 -3
View File
@@ -29,8 +29,8 @@ func NewDigestsCataloger(hashes []crypto.Hash) (*DigestsCataloger, error) {
}, nil
}
func (i *DigestsCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates][]Digest, error) {
results := make(map[source.Coordinates][]Digest)
func (i *DigestsCataloger) Catalog(resolver source.FileResolver) (map[source.Location][]Digest, error) {
results := make(map[source.Location][]Digest)
var locations []source.Location
for location := range resolver.AllLocations() {
locations = append(locations, location)
@@ -48,7 +48,7 @@ func (i *DigestsCataloger) Catalog(resolver source.FileResolver) (map[source.Coo
return nil, err
}
prog.N++
results[location.Coordinates] = result
results[location] = result
}
log.Debugf("file digests cataloger processed %d files", prog.N)
prog.SetCompleted()
+6 -6
View File
@@ -16,8 +16,8 @@ import (
"github.com/anchore/syft/syft/source"
)
func testDigests(t testing.TB, files []string, hashes ...crypto.Hash) map[source.Coordinates][]Digest {
digests := make(map[source.Coordinates][]Digest)
func testDigests(t testing.TB, files []string, hashes ...crypto.Hash) map[source.Location][]Digest {
digests := make(map[source.Location][]Digest)
for _, f := range files {
fh, err := os.Open(f)
@@ -32,7 +32,7 @@ func testDigests(t testing.TB, files []string, hashes ...crypto.Hash) map[source
for _, hash := range hashes {
h := hash.New()
h.Write(b)
digests[source.NewLocation(f).Coordinates] = append(digests[source.NewLocation(f).Coordinates], Digest{
digests[source.NewLocation(f)] = append(digests[source.NewLocation(f)], Digest{
Algorithm: CleanDigestAlgorithmName(hash.String()),
Value: fmt.Sprintf("%x", h.Sum(nil)),
})
@@ -49,7 +49,7 @@ func TestDigestsCataloger_SimpleContents(t *testing.T) {
name string
digests []crypto.Hash
files []string
expected map[source.Coordinates][]Digest
expected map[source.Location][]Digest
catalogErr bool
}{
{
@@ -160,13 +160,13 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
}
l := source.NewLocationFromImage(test.path, *ref, img)
if len(actual[l.Coordinates]) == 0 {
if len(actual[l]) == 0 {
if test.expected != "" {
t.Fatalf("no digest found, but expected one")
}
} else {
assert.Equal(t, actual[l.Coordinates][0].Value, test.expected, "mismatched digests")
assert.Equal(t, actual[l][0].Value, test.expected, "mismatched digests")
}
})
}
+3 -3
View File
@@ -16,8 +16,8 @@ func NewMetadataCataloger() *MetadataCataloger {
return &MetadataCataloger{}
}
func (i *MetadataCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates]source.FileMetadata, error) {
results := make(map[source.Coordinates]source.FileMetadata)
func (i *MetadataCataloger) Catalog(resolver source.FileResolver) (map[source.Location]source.FileMetadata, error) {
results := make(map[source.Location]source.FileMetadata)
var locations []source.Location
for location := range resolver.AllLocations() {
locations = append(locations, location)
@@ -30,7 +30,7 @@ func (i *MetadataCataloger) Catalog(resolver source.FileResolver) (map[source.Co
return nil, err
}
results[location.Coordinates] = metadata
results[location] = metadata
prog.N++
}
log.Debugf("file metadata cataloger processed %d files", prog.N)
+1 -1
View File
@@ -136,7 +136,7 @@ func TestFileMetadataCataloger(t *testing.T) {
l := source.NewLocationFromImage(test.path, *ref, img)
assert.Equal(t, test.expected, actual[l.Coordinates], "mismatched metadata")
assert.Equal(t, test.expected, actual[l], "mismatched metadata")
})
}
+3 -3
View File
@@ -40,8 +40,8 @@ func NewSecretsCataloger(patterns map[string]*regexp.Regexp, revealValues bool,
}, nil
}
func (i *SecretsCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates][]SearchResult, error) {
results := make(map[source.Coordinates][]SearchResult)
func (i *SecretsCataloger) Catalog(resolver source.FileResolver) (map[source.Location][]SearchResult, error) {
results := make(map[source.Location][]SearchResult)
var locations []source.Location
for location := range resolver.AllLocations() {
locations = append(locations, location)
@@ -60,7 +60,7 @@ func (i *SecretsCataloger) Catalog(resolver source.FileResolver) (map[source.Coo
}
if len(result) > 0 {
secretsDiscovered.N += int64(len(result))
results[location.Coordinates] = result
results[location] = result
}
prog.N++
}
+4 -4
View File
@@ -198,11 +198,11 @@ func TestSecretsCataloger(t *testing.T) {
}
loc := source.NewLocation(test.fixture)
if _, exists := actualResults[loc.Coordinates]; !exists {
if _, exists := actualResults[loc]; !exists {
t.Fatalf("could not find location=%q in results", loc)
}
assert.Equal(t, test.expected, actualResults[loc.Coordinates], "mismatched secrets")
assert.Equal(t, test.expected, actualResults[loc], "mismatched secrets")
})
}
}
@@ -432,13 +432,13 @@ j4f668YfhUbKdRF6S6734856
}
loc := source.NewLocation(test.fixture)
if _, exists := actualResults[loc.Coordinates]; !exists && test.expected != nil {
if _, exists := actualResults[loc]; !exists && test.expected != nil {
t.Fatalf("could not find location=%q in results", loc)
} else if !exists && test.expected == nil {
return
}
assert.Equal(t, test.expected, actualResults[loc.Coordinates], "mismatched secrets")
assert.Equal(t, test.expected, actualResults[loc], "mismatched secrets")
})
}
}
+28 -7
View File
@@ -11,15 +11,27 @@ import (
var catalogAddAndRemoveTestPkgs = []Package{
{
Locations: []source.Location{
source.NewVirtualLocation("/a/path", "/another/path"),
source.NewVirtualLocation("/b/path", "/bee/path"),
{
RealPath: "/a/path",
VirtualPath: "/another/path",
},
{
RealPath: "/b/path",
VirtualPath: "/bee/path",
},
},
Type: RpmPkg,
},
{
Locations: []source.Location{
source.NewVirtualLocation("/c/path", "/another/path"),
source.NewVirtualLocation("/d/path", "/another/path"),
{
RealPath: "/c/path",
VirtualPath: "/another/path",
},
{
RealPath: "/d/path",
VirtualPath: "/another/path",
},
},
Type: NpmPkg,
},
@@ -106,8 +118,14 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
p1 := Package{
Locations: []source.Location{
source.NewVirtualLocation("/b/path", "/another/path"),
source.NewVirtualLocation("/b/path", "/b/path"),
{
RealPath: "/b/path",
VirtualPath: "/another/path",
},
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
Name: "Package-1",
@@ -115,7 +133,10 @@ func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
p2 := Package{
Locations: []source.Location{
source.NewVirtualLocation("/b/path", "/b/path"),
{
RealPath: "/b/path",
VirtualPath: "/b/path",
},
},
Type: RpmPkg,
Name: "Package-2",
-42
View File
@@ -1,8 +1,6 @@
package cataloger
import (
"fmt"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
@@ -69,14 +67,6 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
// generate PURL
p.PURL = generatePackageURL(p, theDistro)
// create file-to-package relationships for files owned by the package
owningRelationships, err := packageFileOwnershipRelationships(p, resolver)
if err != nil {
log.Warnf("unable to create any package-file relationships for package name=%q: %w", p.Name, err)
} else {
allRelationships = append(allRelationships, owningRelationships...)
}
// add to catalog
catalog.Add(p)
}
@@ -95,35 +85,3 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
return catalog, allRelationships, nil
}
func packageFileOwnershipRelationships(p pkg.Package, resolver source.FilePathResolver) ([]artifact.Relationship, error) {
fileOwner, ok := p.Metadata.(pkg.FileOwner)
if !ok {
return nil, nil
}
var relationships []artifact.Relationship
for _, path := range fileOwner.OwnedFiles() {
locations, err := resolver.FilesByPath(path)
if err != nil {
return nil, fmt.Errorf("unable to find path for path=%q: %w", path, err)
}
if len(locations) == 0 {
// TODO: this is a known-unknown that could later be persisted in the SBOM (or as a validation failure)
log.Warnf("unable to find location which a package claims ownership of: %s", path)
continue
}
for _, l := range locations {
relationships = append(relationships, artifact.Relationship{
From: p,
To: l.Coordinates,
Type: artifact.ContainsRelationship,
})
}
}
return relationships, nil
}
-3
View File
@@ -13,7 +13,6 @@ import (
"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"
@@ -36,7 +35,6 @@ func ImageCatalogers() []Cataloger {
return []Cataloger{
ruby.NewGemSpecCataloger(),
python.NewPythonPackageCataloger(),
php.NewPHPComposerInstalledCataloger(),
javascript.NewJavascriptPackageCataloger(),
deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(),
@@ -52,7 +50,6 @@ func DirectoryCatalogers() []Cataloger {
ruby.NewGemFileLockCataloger(),
python.NewPythonIndexCataloger(),
python.NewPythonPackageCataloger(),
php.NewPHPComposerLockCataloger(),
javascript.NewJavascriptLockCataloger(),
deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(),
+10 -1
View File
@@ -16,6 +16,15 @@ import (
const catalogerName = "go-module-binary-cataloger"
// current mime types to search by to discover go binaries
var mimeTypes = []string{
"application/x-executable",
"application/x-mach-binary",
"application/x-elf",
"application/x-sharedlib",
"application/vnd.microsoft.portable-executable",
}
type Cataloger struct{}
// NewGoModuleBinaryCataloger returns a new Golang cataloger object.
@@ -32,7 +41,7 @@ func (c *Cataloger) Name() string {
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
fileMatches, err := resolver.FilesByMIMEType(internal.ExecutableMIMETypeSet.List()...)
fileMatches, err := resolver.FilesByMIMEType(mimeTypes...)
if err != nil {
return pkgs, nil, fmt.Errorf("failed to find bin by mime types: %w", err)
}
+11 -26
View File
@@ -34,10 +34,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
RealPath: "/a-path",
FileSystemID: "layer-id",
},
},
MetadataType: pkg.GolangBinMetadataType,
@@ -53,10 +51,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
RealPath: "/a-path",
FileSystemID: "layer-id",
},
},
MetadataType: pkg.GolangBinMetadataType,
@@ -83,10 +79,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
RealPath: "/a-path",
FileSystemID: "layer-id",
},
},
MetadataType: pkg.GolangBinMetadataType,
@@ -102,10 +96,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
RealPath: "/a-path",
FileSystemID: "layer-id",
},
},
MetadataType: pkg.GolangBinMetadataType,
@@ -121,10 +113,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
RealPath: "/a-path",
FileSystemID: "layer-id",
},
},
MetadataType: pkg.GolangBinMetadataType,
@@ -140,12 +130,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
location := source.Location{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
}
location := source.Location{RealPath: "/a-path", FileSystemID: "layer-id"}
pkgs := buildGoPkgInfo(location, tt.mod, goCompiledVersion)
assert.Equal(t, tt.expected, pkgs)
})
-26
View File
@@ -1,26 +0,0 @@
/*
Package php provides a concrete Cataloger implementation for PHP ecosystem files.
*/
package php
import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// NewPHPComposerInstalledCataloger returns a new cataloger for PHP installed.json files.
func NewPHPComposerInstalledCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/installed.json": parseInstalledJSON,
}
return common.NewGenericCataloger(nil, globParsers, "php-composer-installed-cataloger")
}
// NewPHPComposerLockCataloger returns a new cataloger for PHP composer.lock files.
func NewPHPComposerLockCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/composer.lock": parseComposerLock,
}
return common.NewGenericCataloger(nil, globParsers, "php-composer-lock-cataloger")
}
@@ -1,52 +0,0 @@
package php
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"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, []artifact.Relationship, 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, 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, nil
}
@@ -1,41 +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)
}
// TODO: no relationships are under test yet
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)
}
}
@@ -1,67 +0,0 @@
package php
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// Note: composer version 2 introduced a new structure for the installed.json file, so we support both
type installedJSONComposerV2 struct {
Packages []Dependency `json:"packages"`
}
func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
type compv2 struct {
Packages []Dependency `json:"packages"`
}
compv2er := new(compv2)
err := json.Unmarshal(data, &compv2er)
if err != nil {
// If we had an err or, we may be dealing with a composer v.1 installed.json
// which should be all arrays
var packages []Dependency
err := json.Unmarshal(data, &packages)
if err != nil {
return err
}
w.Packages = packages
return nil
}
w.Packages = compv2er.Packages
return nil
}
// integrity check
var _ common.ParserFn = parseComposerLock
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
func parseInstalledJSON(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
packages := make([]pkg.Package, 0)
dec := json.NewDecoder(reader)
for {
var lock installedJSONComposerV2
if err := dec.Decode(&lock); err == io.EOF {
break
} else if err != nil {
return nil, 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, nil
}
@@ -1,73 +0,0 @@
package php
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseInstalledJsonComposerV1(t *testing.T) {
expected := []pkg.Package{
{
Name: "asm89/stack-cors",
Version: "1.3.0",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
{
Name: "behat/mink",
Version: "v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
}
fixture, err := os.Open("test-fixtures/vendor/composer_1/installed.json")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
// TODO: no relationships are under test yet
actual, _, err := parseInstalledJSON(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)
}
}
func TestParseInstalledJsonComposerV2(t *testing.T) {
expected := []pkg.Package{
{
Name: "asm89/stack-cors",
Version: "1.3.0",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
{
Name: "behat/mink",
Version: "v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
}
fixture, err := os.Open("test-fixtures/vendor/composer_2/installed.json")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
// TODO: no relationships are under test yet
actual, _, err := parseInstalledJSON(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": []
}
@@ -1,127 +0,0 @@
[
{
"name": "asm89/stack-cors",
"version": "1.3.0",
"version_normalized": "1.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/asm89/stack-cors.git",
"reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08",
"reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08",
"shasum": ""
},
"require": {
"php": ">=5.5.9",
"symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0",
"symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0"
},
"require-dev": {
"phpunit/phpunit": "^5.0 || ^4.8.10",
"squizlabs/php_codesniffer": "^2.3"
},
"time": "2019-12-24T22:41:47+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Asm89\\Stack\\": "src/Asm89/Stack/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alexander",
"email": "iam.asm89@gmail.com"
}
],
"description": "Cross-origin resource sharing library and stack middleware",
"homepage": "https://github.com/asm89/stack-cors",
"keywords": [
"cors",
"stack"
],
"support": {
"issues": "https://github.com/asm89/stack-cors/issues",
"source": "https://github.com/asm89/stack-cors/tree/1.3.0"
}
},
{
"name": "behat/mink",
"version": "v1.8.1",
"version_normalized": "1.8.1.0",
"source": {
"type": "git",
"url": "https://github.com/minkphp/Mink.git",
"reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
"reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
"shasum": ""
},
"require": {
"php": ">=5.3.1",
"symfony/css-selector": "^2.7|^3.0|^4.0|^5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20",
"symfony/debug": "^2.7|^3.0|^4.0",
"symfony/phpunit-bridge": "^3.4.38 || ^5.0.5"
},
"suggest": {
"behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
"behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
"behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
"behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
"dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
},
"time": "2020-03-11T15:45:53+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Behat\\Mink\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
}
],
"description": "Browser controller/emulator abstraction for PHP",
"homepage": "http://mink.behat.org/",
"keywords": [
"browser",
"testing",
"web"
],
"support": {
"issues": "https://github.com/minkphp/Mink/issues",
"source": "https://github.com/minkphp/Mink/tree/v1.8.1"
}
}
]
@@ -1,135 +0,0 @@
{
"packages": [
{
"name": "asm89/stack-cors",
"version": "1.3.0",
"version_normalized": "1.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/asm89/stack-cors.git",
"reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08",
"reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08",
"shasum": ""
},
"require": {
"php": ">=5.5.9",
"symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0",
"symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0"
},
"require-dev": {
"phpunit/phpunit": "^5.0 || ^4.8.10",
"squizlabs/php_codesniffer": "^2.3"
},
"time": "2019-12-24T22:41:47+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Asm89\\Stack\\": "src/Asm89/Stack/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alexander",
"email": "iam.asm89@gmail.com"
}
],
"description": "Cross-origin resource sharing library and stack middleware",
"homepage": "https://github.com/asm89/stack-cors",
"keywords": [
"cors",
"stack"
],
"support": {
"issues": "https://github.com/asm89/stack-cors/issues",
"source": "https://github.com/asm89/stack-cors/tree/1.3.0"
},
"install-path": "../asm89/stack-cors"
},
{
"name": "behat/mink",
"version": "v1.8.1",
"version_normalized": "1.8.1.0",
"source": {
"type": "git",
"url": "https://github.com/minkphp/Mink.git",
"reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
"reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
"shasum": ""
},
"require": {
"php": ">=5.3.1",
"symfony/css-selector": "^2.7|^3.0|^4.0|^5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20",
"symfony/debug": "^2.7|^3.0|^4.0",
"symfony/phpunit-bridge": "^3.4.38 || ^5.0.5"
},
"suggest": {
"behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
"behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
"behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
"behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
"dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
},
"time": "2020-03-11T15:45:53+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Behat\\Mink\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
}
],
"description": "Browser controller/emulator abstraction for PHP",
"homepage": "http://mink.behat.org/",
"keywords": [
"browser",
"testing",
"web"
],
"support": {
"issues": "https://github.com/minkphp/Mink/issues",
"source": "https://github.com/minkphp/Mink/tree/v1.8.1"
},
"install-path": "../behat/mink"
}
],
"dev": true,
"dev-package-names": [
"behat/mink"
]
}
-2
View File
@@ -9,7 +9,6 @@ const (
Java Language = "java"
JavaScript Language = "javascript"
Python Language = "python"
PHP Language = "php"
Ruby Language = "ruby"
Go Language = "go"
Rust Language = "rust"
@@ -20,7 +19,6 @@ var AllLanguages = []Language{
Java,
JavaScript,
Python,
PHP,
Ruby,
Go,
Rust,
+3 -5
View File
@@ -14,11 +14,9 @@ func TestFingerprint(t *testing.T) {
FoundBy: "Archimedes",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "39.0742° N, 21.8243° E",
FileSystemID: "Earth",
},
VirtualPath: "/Ancient-Greece",
RealPath: "39.0742° N, 21.8243° E",
VirtualPath: "/Ancient-Greece",
FileSystemID: "Earth",
},
},
Licenses: []string{
@@ -19,8 +19,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
setup: func(t testing.TB) ([]Package, []artifact.Relationship) {
parent := Package{
Locations: []source.Location{
source.NewVirtualLocation("/a/path", "/another/path"),
source.NewVirtualLocation("/b/path", "/bee/path"),
{
RealPath: "/a/path",
VirtualPath: "/another/path",
},
{
RealPath: "/b/path",
VirtualPath: "/bee/path",
},
},
Type: RpmPkg,
MetadataType: RpmdbMetadataType,
@@ -35,8 +41,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
child := Package{
Locations: []source.Location{
source.NewVirtualLocation("/c/path", "/another/path"),
source.NewVirtualLocation("/d/path", "/another/path"),
{
RealPath: "/c/path",
VirtualPath: "/another/path",
},
{
RealPath: "/d/path",
VirtualPath: "/another/path",
},
},
Type: NpmPkg,
}
@@ -60,8 +72,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
setup: func(t testing.TB) ([]Package, []artifact.Relationship) {
parent := Package{
Locations: []source.Location{
source.NewVirtualLocation("/a/path", "/some/other/path"),
source.NewVirtualLocation("/b/path", "/bee/path"),
{
RealPath: "/a/path",
VirtualPath: "/some/other/path",
},
{
RealPath: "/b/path",
VirtualPath: "/bee/path",
},
},
Type: RpmPkg,
MetadataType: RpmdbMetadataType,
@@ -76,8 +94,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
child := Package{
Locations: []source.Location{
source.NewVirtualLocation("/c/path", "/another/path"),
source.NewLocation("/d/path"),
{
RealPath: "/c/path",
VirtualPath: "/another/path",
},
{
RealPath: "/d/path",
VirtualPath: "",
},
},
Type: NpmPkg,
}
@@ -100,8 +124,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
setup: func(t testing.TB) ([]Package, []artifact.Relationship) {
parent := Package{
Locations: []source.Location{
source.NewVirtualLocation("/a/path", "/some/other/path"),
source.NewVirtualLocation("/b/path", "/bee/path"),
{
RealPath: "/a/path",
VirtualPath: "/some/other/path",
},
{
RealPath: "/b/path",
VirtualPath: "/bee/path",
},
},
Type: RpmPkg,
MetadataType: RpmdbMetadataType,
@@ -116,8 +146,14 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
child := Package{
Locations: []source.Location{
source.NewVirtualLocation("/c/path", "/another/path"),
source.NewLocation("/d/path"),
{
RealPath: "/c/path",
VirtualPath: "/another/path",
},
{
RealPath: "/d/path",
VirtualPath: "",
},
},
Type: NpmPkg,
}
-4
View File
@@ -14,7 +14,6 @@ const (
RpmPkg Type = "rpm"
NpmPkg Type = "npm"
PythonPkg Type = "python"
PhpComposerPkg Type = "php-composer"
JavaPkg Type = "java-archive"
JenkinsPluginPkg Type = "jenkins-plugin"
GoModulePkg Type = "go-module"
@@ -30,7 +29,6 @@ var AllPkgs = []Type{
RpmPkg,
NpmPkg,
PythonPkg,
PhpComposerPkg,
JavaPkg,
JenkinsPluginPkg,
GoModulePkg,
@@ -49,8 +47,6 @@ func (t Type) PackageURLType() string {
return "deb"
case PythonPkg:
return packageurl.TypePyPi
case PhpComposerPkg:
return packageurl.TypeComposer
case NpmPkg:
return packageurl.TypeNPM
case JavaPkg, JenkinsPluginPkg:
+5 -46
View File
@@ -12,55 +12,14 @@ type SBOM struct {
Artifacts Artifacts
Relationships []artifact.Relationship
Source source.Metadata
Descriptor Descriptor
}
type Artifacts struct {
PackageCatalog *pkg.Catalog
FileMetadata map[source.Coordinates]source.FileMetadata
FileDigests map[source.Coordinates][]file.Digest
FileClassifications map[source.Coordinates][]file.Classification
FileContents map[source.Coordinates]string
Secrets map[source.Coordinates][]file.SearchResult
FileMetadata map[source.Location]source.FileMetadata
FileDigests map[source.Location][]file.Digest
FileClassifications map[source.Location][]file.Classification
FileContents map[source.Location]string
Secrets map[source.Location][]file.SearchResult
Distro *distro.Distro
}
type Descriptor struct {
Name string
Version string
Configuration interface{}
}
func AllCoordinates(sbom SBOM) []source.Coordinates {
set := source.NewCoordinateSet()
for coordinates := range sbom.Artifacts.FileMetadata {
set.Add(coordinates)
}
for coordinates := range sbom.Artifacts.FileContents {
set.Add(coordinates)
}
for coordinates := range sbom.Artifacts.FileClassifications {
set.Add(coordinates)
}
for coordinates := range sbom.Artifacts.FileDigests {
set.Add(coordinates)
}
for _, relationship := range sbom.Relationships {
for _, coordinates := range extractCoordinates(relationship) {
set.Add(coordinates)
}
}
return set.ToSlice()
}
func extractCoordinates(relationship artifact.Relationship) (results []source.Coordinates) {
if coordinates, exists := relationship.From.(source.Coordinates); exists {
results = append(results, coordinates)
}
if coordinates, exists := relationship.To.(source.Coordinates); exists {
results = append(results, coordinates)
}
return results
}
-81
View File
@@ -1,81 +0,0 @@
package source
import (
"fmt"
"sort"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
)
// Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g. image and directory sources)
type Coordinates struct {
RealPath string `json:"path"` // The path where all path ancestors have no hardlinks / symlinks
FileSystemID string `json:"layerID,omitempty"` // An ID representing the filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank.
}
// CoordinateSet represents a set of string types.
type CoordinateSet map[Coordinates]struct{}
// NewCoordinateSet creates a CoordinateSet populated with values from the given slice.
func NewCoordinateSet(start ...Coordinates) CoordinateSet {
ret := make(CoordinateSet)
for _, s := range start {
ret.Add(s)
}
return ret
}
func (c Coordinates) ID() artifact.ID {
f, err := artifact.IDFromHash(c)
if err != nil {
// TODO: what to do in this case?
log.Warnf("unable to get fingerprint of location coordinate=%+v: %+v", c, err)
return ""
}
return f
}
func (c Coordinates) String() string {
str := fmt.Sprintf("RealPath=%q", c.RealPath)
if c.FileSystemID != "" {
str += fmt.Sprintf(" Layer=%q", c.FileSystemID)
}
return fmt.Sprintf("Location<%s>", str)
}
// Add a string to the set.
func (s CoordinateSet) Add(i Coordinates) {
s[i] = struct{}{}
}
// Remove a string from the set.
func (s CoordinateSet) Remove(i Coordinates) {
delete(s, i)
}
// Contains indicates if the given string is contained within the set.
func (s CoordinateSet) Contains(i Coordinates) bool {
_, ok := s[i]
return ok
}
// ToSlice returns a sorted slice of Locations that are contained within the set.
func (s CoordinateSet) ToSlice() []Coordinates {
ret := make([]Coordinates, len(s))
idx := 0
for v := range s {
ret[idx] = v
idx++
}
sort.SliceStable(ret, func(i, j int) bool {
if ret[i].RealPath == ret[j].RealPath {
return ret[i].FileSystemID < ret[j].FileSystemID
}
return ret[i].RealPath < ret[j].RealPath
})
return ret
}
-51
View File
@@ -1,51 +0,0 @@
package source
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCoordinateSet(t *testing.T) {
binA := Coordinates{
RealPath: "/bin",
FileSystemID: "a",
}
binB := Coordinates{
RealPath: "/bin",
FileSystemID: "b",
}
tests := []struct {
name string
input []Coordinates
expected []Coordinates
}{
{
name: "de-dup same location",
input: []Coordinates{
binA, binA, binA,
},
expected: []Coordinates{
binA,
},
},
{
name: "dont de-dup different filesystem",
input: []Coordinates{
binB, binA,
},
expected: []Coordinates{
binA, binB,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, NewCoordinateSet(test.input...).ToSlice())
})
}
}
+12 -28
View File
@@ -33,11 +33,10 @@ type pathFilterFn func(string) bool
// directoryResolver implements path and content access for the directory data source.
type directoryResolver struct {
path string
cwdRelativeToRoot string
cwd string
fileTree *filetree.FileTree
metadata map[file.ID]FileMetadata
path string
cwd string
fileTree *filetree.FileTree
metadata map[file.ID]FileMetadata
// TODO: wire up to report these paths in the json report
pathFilterFns []pathFilterFn
refsByMIMEType map[string][]file.Reference
@@ -50,27 +49,18 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR
return nil, fmt.Errorf("could not create directory resolver: %w", err)
}
var cwdRelRoot string
if path.IsAbs(root) {
cwdRelRoot, err = filepath.Rel(cwd, root)
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
}
}
if pathFilters == nil {
pathFilters = []pathFilterFn{isUnixSystemRuntimePath}
}
resolver := directoryResolver{
path: root,
cwd: cwd,
cwdRelativeToRoot: cwdRelRoot,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
@@ -210,11 +200,7 @@ func (r directoryResolver) requestPath(userPath string) (string, error) {
if filepath.IsAbs(userPath) {
// don't allow input to potentially hop above root path
userPath = path.Join(r.path, userPath)
} else {
// ensure we take into account any relative difference between the root path and the CWD for relative requests
userPath = path.Join(r.cwdRelativeToRoot, userPath)
}
var err error
userPath, err = filepath.Abs(userPath)
if err != nil {
@@ -226,9 +212,7 @@ func (r directoryResolver) requestPath(userPath string) (string, error) {
func (r directoryResolver) responsePath(path string) string {
// always return references relative to the request path (not absolute path)
if filepath.IsAbs(path) {
// we need to account for the cwd relative to the running process and the given root for the directory resolver
prefix := filepath.Clean(filepath.Join(r.cwd, r.cwdRelativeToRoot))
return strings.TrimPrefix(path, prefix+string(filepath.Separator))
return strings.TrimPrefix(path, r.cwd+string(filepath.Separator))
}
return path
}
+11 -59
View File
@@ -10,8 +10,6 @@ import (
"syscall"
"testing"
"github.com/stretchr/testify/require"
"github.com/scylladb/go-set/strset"
"github.com/anchore/stereoscope/pkg/file"
@@ -19,60 +17,6 @@ import (
"github.com/wagoodman/go-progress"
)
func TestDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) {
cases := []struct {
name string
relativeRoot string
input string
expected []string
}{
{
name: "should find a file from an absolute input",
relativeRoot: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path",
relativeRoot: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path (root above cwd)",
relativeRoot: "../",
input: "sbom/sbom.go",
expected: []string{
"sbom/sbom.go",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// note: this test is all about asserting correct functionality when the given analysis path
// is an absolute path
absRoot, err := filepath.Abs(c.relativeRoot)
require.NoError(t, err)
resolver, err := newDirectoryResolver(absRoot)
assert.NoError(t, err)
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, len(c.expected))
s := strset.New()
for _, actual := range refs {
s.Add(actual.RealPath)
}
assert.ElementsMatch(t, c.expected, s.List())
})
}
}
func TestDirectoryResolver_FilesByPath(t *testing.T) {
cases := []struct {
name string
@@ -141,10 +85,18 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
}
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, c.refCount)
if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
}
if len(refs) != c.refCount {
t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount)
}
for _, actual := range refs {
assert.Equal(t, c.expected, actual.RealPath)
if actual.RealPath != c.expected {
t.Errorf("bad resolve path: '%s'!='%s'", actual.RealPath, c.expected)
}
}
})
}
+27 -46
View File
@@ -3,46 +3,28 @@ package source
import (
"fmt"
"github.com/mitchellh/hashstructure/v2"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
)
var _ hashstructure.Hashable = (*Location)(nil)
// Location represents a path relative to a particular filesystem resolved to a specific file.Reference. This struct is used as a key
// in content fetching to uniquely identify a file relative to a request (the VirtualPath).
// in content fetching to uniquely identify a file relative to a request (the VirtualPath). Note that the VirtualPath
// and ref are ignored fields when using github.com/mitchellh/hashstructure. The reason for this is to ensure that
// only the minimally expressible fields of a location are baked into the uniqueness of a Location. Since VirutalPath
// and ref are not captured in JSON output they cannot be included in this minimal definition.
type Location struct {
Coordinates
VirtualPath string // The path to the file which may or may not have hardlinks / symlinks
ref file.Reference // The file reference relative to the stereoscope.FileCatalog that has more information about this location.
RealPath string `json:"path"` // The path where all path ancestors have no hardlinks / symlinks
VirtualPath string `hash:"ignore" json:"-"` // The path to the file which may or may not have hardlinks / symlinks
FileSystemID string `json:"layerID,omitempty"` // An ID representing the filesystem. For container images this is a layer digest, directories or root filesystem this is blank.
ref file.Reference `hash:"ignore"` // The file reference relative to the stereoscope.FileCatalog that has more information about this location.
}
// NewLocation creates a new Location representing a path without denoting a filesystem or FileCatalog reference.
func NewLocation(realPath string) Location {
func NewLocation(path string) Location {
return Location{
Coordinates: Coordinates{
RealPath: realPath,
},
}
}
// NewVirtualLocation creates a new location for a path accessed by a virtual path (a path with a symlink or hardlink somewhere in the path)
func NewVirtualLocation(realPath, virtualPath string) Location {
return Location{
Coordinates: Coordinates{
RealPath: realPath,
},
VirtualPath: virtualPath,
}
}
// NewLocationFromCoordinates creates a new location for the given Coordinates.
func NewLocationFromCoordinates(coordinates Coordinates) Location {
return Location{
Coordinates: coordinates,
RealPath: path,
}
}
@@ -52,31 +34,25 @@ func NewLocationFromImage(virtualPath string, ref file.Reference, img *image.Ima
if err != nil {
log.Warnf("unable to find file catalog entry for ref=%+v", ref)
return Location{
Coordinates: Coordinates{
RealPath: string(ref.RealPath),
},
VirtualPath: virtualPath,
RealPath: string(ref.RealPath),
ref: ref,
}
}
return Location{
Coordinates: Coordinates{
RealPath: string(ref.RealPath),
FileSystemID: entry.Layer.Metadata.Digest,
},
VirtualPath: virtualPath,
ref: ref,
VirtualPath: virtualPath,
RealPath: string(ref.RealPath),
FileSystemID: entry.Layer.Metadata.Digest,
ref: ref,
}
}
// NewLocationFromDirectory creates a new Location representing the given path (extracted from the ref) relative to the given directory.
func NewLocationFromDirectory(responsePath string, ref file.Reference) Location {
return Location{
Coordinates: Coordinates{
RealPath: responsePath,
},
ref: ref,
RealPath: responsePath,
ref: ref,
}
}
@@ -98,8 +74,13 @@ func (l Location) String() string {
return fmt.Sprintf("Location<%s>", str)
}
func (l Location) Hash() (uint64, error) {
// since location is part of the package definition it is important that only coordinates are used during object
// hashing. (Location hash should be a pass-through for the coordinates and not include ref or VirtualPath.)
return hashstructure.Hash(l.ID(), hashstructure.FormatV2, nil)
func (l Location) ID() artifact.ID {
f, err := artifact.IDFromHash(l)
if err != nil {
// TODO: what to do in this case?
log.Warnf("unable to get fingerprint of location=%+v: %+v", l, err)
return ""
}
return f
}
+55 -122
View File
@@ -7,14 +7,10 @@ package source
import (
"fmt"
"io/ioutil"
"os"
"sync"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
"github.com/mholt/archiver/v3"
"github.com/spf13/afero"
)
@@ -22,10 +18,9 @@ import (
// in cataloging (based on the data source and configuration)
type Source struct {
Image *image.Image // the image object to be cataloged (image only)
DirectoryResolver *directoryResolver
Metadata Metadata
directoryResolver *directoryResolver
path string
mutex *sync.Mutex
Mutex *sync.Mutex
}
type sourceDetector func(string) (image.Source, string, error)
@@ -40,122 +35,75 @@ func New(userInput string, registryOptions *image.RegistryOptions) (*Source, fun
switch parsedScheme {
case FileScheme:
return generateFileSource(fs, location)
fileMeta, err := fs.Stat(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err)
}
if fileMeta.IsDir() {
return &Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err)
}
s, err := NewFromFile(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("could not populate source from path=%q: %w", location, err)
}
return &s, func() {}, nil
case DirectoryScheme:
return generateDirectorySource(fs, location)
fileMeta, err := fs.Stat(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err)
}
if !fileMeta.IsDir() {
return &Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err)
}
s, err := NewFromDirectory(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("could not populate source from path=%q: %w", location, err)
}
return &s, func() {}, nil
case ImageScheme:
return generateImageSource(location, userInput, imageSource, registryOptions)
img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions)
cleanup := stereoscope.Cleanup
if err != nil || img == nil {
return &Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err)
}
s, err := NewFromImage(img, location)
if err != nil {
return &Source{}, cleanup, fmt.Errorf("could not populate source with image: %w", err)
}
return &s, cleanup, nil
}
return &Source{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput)
}
func generateImageSource(location, userInput string, imageSource image.Source, registryOptions *image.RegistryOptions) (*Source, func(), error) {
img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions)
if err != nil {
log.Debugf("error parsing location: %s after detecting scheme; pulling image: %s", location, userInput)
// we may have been to aggressive reading the source hint
// try the input as supplied by the user if our initial parse failed
img, err = stereoscope.GetImageFromSource(userInput, imageSource, registryOptions)
}
cleanup := stereoscope.Cleanup
if err != nil || img == nil {
return &Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err)
}
s, err := NewFromImage(img, location)
if err != nil {
return &Source{}, cleanup, fmt.Errorf("could not populate source with image: %w", err)
}
return &s, cleanup, nil
}
func generateDirectorySource(fs afero.Fs, location string) (*Source, func(), error) {
fileMeta, err := fs.Stat(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err)
}
if !fileMeta.IsDir() {
return &Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err)
}
s, err := NewFromDirectory(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("could not populate source from path=%q: %w", location, err)
}
return &s, func() {}, nil
}
func generateFileSource(fs afero.Fs, location string) (*Source, func(), error) {
fileMeta, err := fs.Stat(location)
if err != nil {
return &Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err)
}
if fileMeta.IsDir() {
return &Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err)
}
s, cleanupFn := NewFromFile(location)
return &s, cleanupFn, nil
}
// NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively.
func NewFromDirectory(path string) (Source, error) {
return Source{
mutex: &sync.Mutex{},
Mutex: &sync.Mutex{},
Metadata: Metadata{
Scheme: DirectoryScheme,
Path: path,
},
path: path,
}, nil
}
// NewFromFile creates a new source object tailored to catalog a file.
func NewFromFile(path string) (Source, func()) {
analysisPath, cleanupFn := fileAnalysisPath(path)
// NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively.
func NewFromFile(path string) (Source, error) {
return Source{
mutex: &sync.Mutex{},
Mutex: &sync.Mutex{},
Metadata: Metadata{
Scheme: FileScheme,
Path: path,
},
path: analysisPath,
}, cleanupFn
}
// fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive
// contents have been made available. A cleanup function is provided for any temp files created (if any).
func fileAnalysisPath(path string) (string, func()) {
var analysisPath = path
var cleanupFn = func() {}
// if the given file is an archive (as indicated by the file extension and not MIME type) then unarchive it and
// use the contents as the source. Note: this does NOT recursively unarchive contents, only the given path is
// unarchived.
envelopedUnarchiver, err := archiver.ByExtension(path)
if unarchiver, ok := envelopedUnarchiver.(archiver.Unarchiver); err == nil && ok {
unarchivedPath, tmpCleanup, err := unarchiveToTmp(path, unarchiver)
if err != nil {
log.Warnf("file could not be unarchived: %+v", err)
} else {
log.Debugf("source path is an archive")
analysisPath = unarchivedPath
}
if tmpCleanup != nil {
cleanupFn = tmpCleanup
}
}
return analysisPath, cleanupFn
}, nil
}
// NewFromImage creates a new source object tailored to catalog a given container image, relative to the
@@ -177,16 +125,16 @@ func NewFromImage(img *image.Image, userImageStr string) (Source, error) {
func (s *Source) FileResolver(scope Scope) (FileResolver, error) {
switch s.Metadata.Scheme {
case DirectoryScheme, FileScheme:
s.mutex.Lock()
defer s.mutex.Unlock()
if s.directoryResolver == nil {
resolver, err := newDirectoryResolver(s.path)
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.DirectoryResolver == nil {
resolver, err := newDirectoryResolver(s.Metadata.Path)
if err != nil {
return nil, err
}
s.directoryResolver = resolver
s.DirectoryResolver = resolver
}
return s.directoryResolver, nil
return s.DirectoryResolver, nil
case ImageScheme:
switch scope {
case SquashedScope:
@@ -199,18 +147,3 @@ func (s *Source) FileResolver(scope Scope) (FileResolver, error) {
}
return nil, fmt.Errorf("unable to determine FilePathResolver with current scheme=%q", s.Metadata.Scheme)
}
func unarchiveToTmp(path string, unarchiver archiver.Unarchiver) (string, func(), error) {
tempDir, err := ioutil.TempDir("", "syft-archive-contents-")
if err != nil {
return "", func() {}, fmt.Errorf("unable to create tempdir for archive processing: %w", err)
}
cleanupFn := func() {
if err := os.RemoveAll(tempDir); err != nil {
log.Warnf("unable to cleanup archive tempdir: %+v", err)
}
}
return tempDir, cleanupFn, unarchiver.Unarchive(path, tempDir)
}
-162
View File
@@ -1,16 +1,8 @@
package source
import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"syscall"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/image"
@@ -97,80 +89,6 @@ func TestNewFromDirectory(t *testing.T) {
}
}
func TestNewFromFile(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expRefs int
}{
{
desc: "path detected",
input: "test-fixtures/path-detected",
inputPaths: []string{"/.vimrc"},
expRefs: 1,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, cleanup := NewFromFile(test.input)
if cleanup != nil {
t.Cleanup(cleanup)
}
assert.Equal(t, test.input, src.Metadata.Path)
assert.Equal(t, src.Metadata.Path, src.path)
resolver, err := src.FileResolver(SquashedScope)
require.NoError(t, err)
refs, err := resolver.FilesByPath(test.inputPaths...)
require.NoError(t, err)
assert.Len(t, refs, test.expRefs)
})
}
}
func TestNewFromFile_WithArchive(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expRefs int
}{
{
desc: "path detected",
input: "test-fixtures/path-detected",
inputPaths: []string{"/.vimrc"},
expRefs: 1,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
archivePath := setupArchiveTest(t, test.input)
src, cleanup := NewFromFile(archivePath)
if cleanup != nil {
t.Cleanup(cleanup)
}
assert.Equal(t, archivePath, src.Metadata.Path)
assert.NotEqual(t, src.Metadata.Path, src.path)
resolver, err := src.FileResolver(SquashedScope)
require.NoError(t, err)
refs, err := resolver.FilesByPath(test.inputPaths...)
require.NoError(t, err)
assert.Len(t, refs, test.expRefs)
})
}
}
func TestNewFromDirectoryShared(t *testing.T) {
testCases := []struct {
desc string
@@ -314,83 +232,3 @@ func TestFilesByGlob(t *testing.T) {
})
}
}
// createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
// setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
// which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
// (even if there's an error), and it should always be called.
func setupArchiveTest(t testing.TB, sourceDirPath string) string {
t.Helper()
archivePrefix, err := ioutil.TempFile("", "syft-archive-TEST-")
require.NoError(t, err)
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(archivePrefix.Name())
},
),
)
destinationArchiveFilePath := archivePrefix.Name() + ".tar"
t.Logf("archive path: %s", destinationArchiveFilePath)
createArchive(t, sourceDirPath, destinationArchiveFilePath)
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(destinationArchiveFilePath)
},
),
)
cwd, err := os.Getwd()
require.NoError(t, err)
t.Logf("running from: %s", cwd)
return destinationArchiveFilePath
}
func assertNoError(t testing.TB, fn func() error) func() {
return func() {
assert.NoError(t, fn())
}
}

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