mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 14:47:22 +02:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf11f0e3d | ||
|
|
628c2e44a5 | ||
|
|
aba0de6e65 | ||
|
|
cb0278f3bc | ||
|
|
b519340b86 | ||
|
|
2356539ebe | ||
|
|
1537e73083 | ||
|
|
52adfcbd44 | ||
|
|
a3f0d659da | ||
|
|
8a0fa5d3ad | ||
|
|
253faf5652 | ||
|
|
69d2b1ba3c | ||
|
|
b08a11e46d | ||
|
|
fef951c29b | ||
|
|
a906b9a03a |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+23
-5
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+5
-5
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+142
-84
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]+)$`,
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user