Compare commits

...
Author SHA1 Message Date
Sambhav Kothari aebe843c6f Improve CycloneDX format output (#710)
* Improve CycloneDX format output

## Additions to CycloneDX output

* CPEs
* Authors
* Publishers
* External References (Website, Distribution, VCS)
* Description

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-01-19 11:43:16 -05:00
Alex Goodman 829e500aa9 Add additional PHP metadata (#753)
* add php related metadata

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* enable decoding of php metadata for syftjson format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add php metadata to json schema

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-01-19 11:42:16 -05:00
Toure Dunnon 814f2bf8b9 Update Syft formats for SyftJson (#752)
* Update Syft formats for SyftJson

This change will introduce omitempty struct tag to PackageCustomData.
This struct tag will cause null and empty values to be dropped on serialization
for consumers downstream.

Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>

* Updated the golden files for syftjson to allow for proper
test coverage.

Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2022-01-18 17:18:34 -05:00
Dan Luhring c61204f56d Add support for "file" source type in syftjson unmarshaling (#750)
* Add tests for image and directory syftjson source

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Add failing test case for file source unmarshaling

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Fix file source unmarshaling

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Add test case for unknown source type

Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2022-01-18 13:40:39 -05:00
Weston Steimel 46dcc84f1a support .sar for java ecosystem (#748)
Signed-off-by: Weston Steimel <weston.steimel@gmail.com>
2022-01-18 09:22:02 -05:00
Alex Goodman 86c3c1c531 Start developer documentation (#746)
* draft outline for developing docs

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update outline

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* list testing dependencies

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix header indention

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix title

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-01-14 06:17:25 -05:00
Keith Zantow f59af255e3 Align SPDX export more with SPDX 2.2 specification (#743) 2022-01-13 15:27:06 -05:00
Alex Goodman 706f291679 Replace distro type (#742)
* remove strong distro type

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* bump json schema to v3 (breaking distro shape)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix linting

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* allow for v2 decoding of distro idLikes field in v3 json decoder

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix casing in simple linux release name

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use discovered name as pretty name in simple linux release

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-01-12 12:13:42 -05:00
Christopher Angelo Phillips dfefd2ea4e update goreleaser with windows checksums (#740)
* update goreleaser with windows checksums

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* update format to be closer to our previous implementation

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* remove linux replacement

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* typo

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
2022-01-10 14:52:31 -05:00
Christopher Angelo Phillips b77ddfc29c bump stereoscope version to remove old containerd (#741)
* bump stereoscope version to remove old containerd

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* go mod tidy

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
2022-01-10 13:40:24 -05:00
Keith Zantow 5e5312c72d Add support for multiple output files in different formats (#732) 2022-01-06 17:52:20 -05:00
Alex Goodman 38c4b17847 Add support for searching for jars within archives (#734)
* add support for searching jars within archives

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add package cataloger config options

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* address review comments + factor out safeCopy helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update config docs regarding package archive search options

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* show that unindexed archive cataloging defaults to false

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove lies about -s

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* address review comments

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update search archive note about java

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-01-06 21:40:51 +00:00
Christopher Angelo Phillips 01dc78ccc3 683 windows filepath (#735)
Support Windows Directory Resolver
Add function that converts windows to posix functionality
Add function that converts posix to windows
Add build tags to remove windows developer environment errors
redact carriage return specific windows issues

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
2022-01-06 11:39:04 -05:00
Sambhav Kothari 2a7325a965 Fix CPE encode/decode when it contains special chars (#714)
* Fix CPE generation when the generated CPE contains invalid characters

Currently syft seems to generate invalid CPEs which do not
conform with the official CPE spec. This is because the underlying
nvdtools library is not a completely spec compliant implementation
and has some interesting bugs/issues.

The following are the list of issues I have encountered with nvdtools:

1. It parses strings which are not CPEs incorrectly as valid CPEs. This
messes up our filter function which is supposed to filter out any
incorrect CPEs we generate. In order to fix this, I have introduced
a new regex in the NewCPE function which follows the upstream spec and
filters out any incorrect CPEs.

2. Introduce wfn.WFNize for any cpe attributes we infer from packages.
This ensures that we are escaping and quoting any special characters
before putting them into CPEs. Note that nvdtools has yet another bug
in the WFNize function, specifically the "addSlashesAt" part of the
function which stops the loop as soon as it encounters ":" a valid
character for a WFN attribute after quoting, but the way nvdtools
handles it causes it to truncate strings that container ":". As a result
strings like "prefix:1.2" which would have been quoted as "prefix\:1.2"
end up becoming "prefix" instead causing loss of information and
incorrect CPEs being generated. As a result in such cases, we remove out
strings containing ":" in any part entirely for now. This is similar
to the way we were handling CPE filtering in the past with http urls as
vendor strings

3. Add special handling for version which contain ":" due to epochs in
debian and rpm. In this case, we strip out the parts before ":" i.e.
the epoch and only output the actual function. This ensures we are not
discarding valid version strings due to pt #.2.

In the future we should look at moving to a more spec compliant cpe
parsing library to avoid such shenanigans.

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Remove WFNize for input strings

WFNize seems to not be part of the standard as per
https://pkg.go.dev/github.com/facebookincubator/nvdtools@v0.1.4/wfn#WFNize
and seems to have bugs/issues with encode/decode cycles, so I am
just removing it at this point and relying on the CPE regex to filter
out invalid CPEs for now.

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Quote the string on decode to ensure consistent CPE string generation

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Add test cases for round-tripping the CPE and fix strip slashes

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Add comprehensive tests for cpe parsing

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Use strings.Builder instead of byte buffer

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-01-06 09:56:53 -05:00
Weston Steimel d9aa54cd00 support .par for java ecosystems (#727)
Signed-off-by: Weston Steimel <weston.steimel@gmail.com>
2022-01-04 16:40:27 -05:00
David Shay 697bad02ac Add arm64 support to install script (#729)
Signed-off-by: David Shay <davideshay@gmail.com>
2022-01-03 20:28:48 -05:00
Alex Goodman a18fbacae7 Revert "bump goreleaser to v1.2 (#720)" (#731)
This reverts commit 8535ee5b2e.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2022-01-03 15:19:00 +00:00
Sambhav Kothari 7168dc7473 Add a version flag (#722) 2021-12-23 18:56:07 -05:00
Jonas Galvão Xavier 211b188120 Add lpkg as java package format (#694)
* add lpkg support to java cataloger

linter clean up

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix comment formatting

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add filename test for lpkg

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* commment on lpkg file extension tests

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix comment typo

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix import format

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* simpler test validation

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>
2021-12-22 16:34:52 -08:00
Alex Goodman 7f8cb0bd80 remove unstable CLI test (bad binaries) (#721)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-22 15:55:37 -05:00
Alex Goodman 8535ee5b2e bump goreleaser to v1.2 (#720)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-22 20:37:48 +00:00
Christopher Angelo Phillips a689fb1213 update cp statement for core_utils_upgrade (#719)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-12-22 20:01:34 +00:00
Sambhav Kothari 4903b4d73f Bump syft JSON schema and add v2.0.2 (#716)
Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2021-12-22 13:55:34 -05:00
Dan Luhring 7de5e1288f Fix unhelpful error message for oci-archive scheme (#705)
* Improve error message

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Return error from stereoscope immediately

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Bump version of stereoscope

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Conditionally retry image retrieval

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Update error message for source construction failure

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Update stereoscope

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Retry image pull without predetermined image source

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Add comment to image pull source determination

Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-12-22 18:53:23 +00:00
Jonas Galvão Xavier 474ed682bc add version to logs (#715) 2021-12-21 23:18:49 -08:00
Keith Zantow b7979dbc7d exclusions does not need to be variadic (#711) 2021-12-21 10:13:15 -05:00
Sambhav KothariandChristopher Angelo Phillips cc20a8f341 Add tests for direct-url information and add it to the output purl (#708)
* add direct_url.json fields to python metadata

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* rename DirectURLOrigin struct; add stub for file

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add detection for direct_url.json

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* Add tests for direct-url information and add it to the output purl

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Update golden snapshot ids after adding new python package metadata field

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

* Add test names for packageurl tests

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

Co-authored-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-12-20 15:54:25 -05:00
Keith Zantow 006ba9b557 Add --exclude flag (#695) 2021-12-20 10:35:25 -05:00
Alex Goodman a27907659d Performance improvements around package ID (#698)
* set package ID in catalogers and improve hashing performance

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update setting ID + tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-16 08:55:53 -05:00
Alex Goodman 727b84ce0d prefer warning over erroring out when parsing java manifests (#688)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-14 12:17:05 -05:00
Dan Luhring 85ac5bcbf8 Handle extra empty lines in Java manifest parsing (#687)
* Add failing test for extra empty lines in manifest

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Handle extra empty lines in Java manifests

Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-12-14 11:53:50 -05:00
Alex Goodman 3d8c16649d Fix M1 install (#686)
* M1 install.sh script should use zip

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add arm64 binary extraction

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-14 15:54:46 +00:00
Davide Icardi ecbfbe6896 update readme for possible output types (#669)
Signed-off-by: Davide Icardi <davide.icardi@gmail.com>
2021-12-14 07:11:48 -05:00
Jonas Galvão XavierandAlex Goodman d3804d1a82 ignore target link files based on path (#667)
* ignore target link files based on path

log when files are actually indexed

add test for sym link resolution

golang test nits

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* nil catalog should act like an empty catalog

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove dir path filtering in favor of file type filtering

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* split out addPathToIndex into specialized functions

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add test for nul catalog enumeration

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* conditionally discover MIME types for file based on file resolver index

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* change logging around cataloging

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add tests to cover possible infinite symlink loop for resolver

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-13 20:49:11 -05:00
Keith Zantow 3a10ca7e6a Catalog java bundle-version if nothing else found (#677) 2021-12-13 17:38:25 -05:00
Dan Luhring f98868b55e Find Java package versions in additional manifest sections (#673)
* Add failing test for missing versions

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Look through all named sections for version

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Consistent installation of yajsv

Signed-off-by: Dan Luhring <dan+github@luhrings.com>

* Adjust output text for test assertion

Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-12-13 15:39:42 -05:00
Alex Goodman ab9fe53ff2 Recover from panics from the stdlib when cataloging malformed binaries (#663)
* recover from panics in stdlib binary parsing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add CLI test to cover regression case

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-10 11:49:50 -05:00
Alex Goodman da0b17b719 Add cataloging of macho multi-architecture binaries (#657)
* add cataloging within universal binaries

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update json test fixtures

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add comments + correct 32 bit multi arch magic check

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-08 16:25:24 -05:00
Alex Goodman c7752f0f6c fix panic from reading nonexistent reader (#658)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-08 20:28:47 +00:00
Bala Raman 7a359dc16b Adding AlmaLinux OS Support (#652)
* Adding AlmaLinux Support

Signed-off-by: Bala Raman <srbala@gmail.com>

* Adding AlmaLinux Support

Signed-off-by: Bala Raman <srbala@gmail.com>
2021-12-07 15:59:31 -05:00
Dan Luhring 7957294b26 Add section for community meetings (#655)
Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-12-06 12:18:34 -05:00
Jonas Galvão Xavier 5374a1dc6f add cyclone-json output format (#635)
* add cyclone json format

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* adapt format to sbom.SBOM structure

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* cycloneDX json output with official lib

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add cycloneDX 1.3 schema output in xml

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix lints errors

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* tidying go mod

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* remove cycloneDX 1.2 format

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* update cycloneDX xml schema

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix cyclone according to schema

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* use RFC 2141 URN form of uuid for serial number

add schema validation for cycloneDX 1.3 JSON output

add yajsv cli for JSON schema validation during tests

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* tidying go mod up

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* go get json schema validator

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* install yajsv without mess with go mod

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* reuse code between cycloneDX json & xml encoders

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add output options for cyclone XML

add bom.json to .gitignore

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add cyclone json format

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* adapt format to sbom.SBOM structure

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* cycloneDX json output with official lib

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add cycloneDX 1.3 schema output in xml

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix lints errors

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* tidying go mod

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* remove cycloneDX 1.2 format

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* update cycloneDX xml schema

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix cyclone according to schema

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* use RFC 2141 URN form of uuid for serial number

add schema validation for cycloneDX 1.3 JSON output

add yajsv cli for JSON schema validation during tests

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* tidying go mod up

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* go get json schema validator

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* install yajsv without mess with go mod

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* reuse code between cycloneDX json & xml encoders

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* add output options for cyclone XML

add bom.json to .gitignore

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix cyclone12xml removal

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* feedback changes

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* go mod tidy

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>
2021-12-03 17:06:23 -08:00
Alex Goodman 22c4b275e7 bump containerd and image-spec (#654)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-03 16:12:43 +00:00
Alex Goodman ffccfc8f3f update stereoscope to bump docker dependency (#648)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-02 11:18:00 -05:00
Alex Goodman 5aec918683 reduce relationship noise (#653)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-02 09:51:18 -05:00
Alex Goodman da62387545 Fix SPDX namespace value (#649)
* fix spdx namespace and add scheme range assertions

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* validate SPDX document name from source metadata

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* comment why namespace tests only check prefix

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-02 14:10:40 +00:00
Alex Goodman 21d1738b27 fix directory content fetching (#651)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-12-02 09:02:54 -05:00
Dan Luhring ed84e43d67 Remove webinar announcement (#650)
Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-12-01 19:07:10 +00:00
Alex Goodman fe616acd98 directory resolver should account for the proc cwd relative to the root path (#644)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 15:34:37 -05:00
Blaize KayeandAlex Goodman 6af132e088 Adds composer installed.json parser (#643)
* Adds installed.json functionality and tests

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Adds php-installed-cataloger

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Changes fallback logic

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Adds image tests for installed.json composer packages

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* tweak PHP cataloger names

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* unexport PHP types and fix CLI tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* rename PHP cataloger file

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 11:36:08 -05:00
Alex Goodman 00206233e6 use smaller jotframe event buffer (#647)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 11:23:16 -05:00
Alex Goodman bd9007fc0e Migrate SPDX-JSON relationships to SBOM model (#634)
* remove power-user document shape

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add power-user specific fields to syft-json format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* port remaining spdx-json relationships to sbom model

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add coordinate set

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add SBOM file path helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use internal mimetype helper in go binary cataloger

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add new package-of relationship

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update json schema to v2

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* replace power-user presenter with syft-json format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix tests and linting

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove "package-of" relationship (in favor of "contains")

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add tests for spdx22json format encoding enhancements

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update TODO and log entries

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* introduce sbom.Descriptor

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-23 14:54:17 -05:00
Alex Goodman e3b34813d7 ignore partybus unsubscribe errors (#641)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-22 16:07:55 -05:00
Dan Luhring f0b2d81599 Announce meetup on 2021-12-01 (#640)
Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-11-22 10:13:18 -05:00
Christopher Angelo Phillips 0849539729 Update concurrency primitives for relationships in command layer (#639)
* update concurrency primitives for relationships in command layer

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-20 03:44:17 -05:00
Christopher Angelo Phillips 4f0099583a promote catalog task pattern to all commands (#636)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-19 13:26:23 -05:00
Alex Goodman d76c868481 Catalog archive contents for single-file input (#637)
* add first-level archive processing when input is a file

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add license exception for github.com/xi2/xz

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* always return cleanup function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* change source.NewFromFile log entry to warn

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* ensure file source always has cleanup function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* ensure we are always preferring the unarchive cleanup function for source

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-19 14:16:25 +00:00
Alex GoodmanandDan Luhring e38cde35ed Introduce minimal source coordinates (#623)
* split source.Location and create source.Coordinates for minimal path addressing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* move coordinates into separate file

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* Update syft/source/coordinates.go

Co-authored-by: Dan Luhring <luhring@users.noreply.github.com>
2021-11-18 18:13:22 +00:00
Alex Goodman 9090c3a772 bump stereoscope, docker, and GGCR (#633)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-16 19:43:46 +00:00
Alex Goodman ef627d82ef Introduce relationships as first-class objects (#607)
* migrate pkg.ID and pkg.Relationship to artifact package

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* return relationships from tasks

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix more tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add artifact.Identifiable by Identity() method

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix linting

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove catalog ID assignment

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* adjust spdx helpers to use copy of packages

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* stabilize package ID relative to encode-decode format cycles

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* rename Identity() to ID()

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use zero value for nils in ID generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* enable source.Location to be identifiable

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* hoist up package relationship discovery to analysis stage

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update ownership-by-file-overlap relationship description

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add test reminders to put new relationships under test

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* adjust PHP composer.lock parser function to return relationships

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-16 14:14:13 -05:00
Christopher Angelo Phillips 45ea4177e8 update SPDX license list => 3.15 (#630)
* update SPDX license list 

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-15 14:41:46 -05:00
Alex Goodman e28b7b53b0 bump chronicle to v0.3.0 (#627)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-13 18:56:25 -08:00
Christopher Angelo Phillips fc39710d06 287 - Add retry for image fetch using user's input (#626)
* add fallback to user input if source hint fails

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* refactor for smaller functions

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-12 17:57:04 -05:00
Swathi Gangisetty e732f419f8 Add new distro and mapping to support Rocky Linux distro identification (#624)
Signed-off-by: Swathi Gangisetty <swathi@anchore.com>
2021-11-12 13:10:21 -08:00
NikitaandAlex Goodman 25835ef589 PHP parser composer.lock (#609)
* PHP parse composer.lock

Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com>

* rename PHP package type

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add PHP composer package support to source info SPDX helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update directory cataloger integration tests (make exception for images)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 14:08:41 -05:00
Christopher Angelo Phillips 319c6ee2eb document current draft for iana submission (#618)
* document current draft for IANA submission

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-10 11:28:47 -05:00
Alex Goodman 9c27fa7b0b use sbom.SBOM in ImportConfig (#621)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 18:39:06 -05:00
386 changed files with 37307 additions and 5724 deletions
+5
View File
@@ -15,3 +15,8 @@ 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
+14 -24
View File
@@ -13,28 +13,10 @@ builds:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
-w
-s
-extldflags '-static'
-X github.com/anchore/syft/internal/version.version={{.Version}}
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
- binary: syft
id: syft-win
env:
- CGO_ENABLED=0
goos:
- windows
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
@@ -68,14 +50,19 @@ builds:
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
archives:
- format: tar.gz
builds:
- syft # i.e. Linux only
- name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
id: syft
replacements:
windows: Windows
amd64: x86_64
format_overrides:
- goos: windows
format: zip
- format: zip # This is a hack for syft-macos! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
id: syft-zip
id: syft-exception
builds:
- syft-macos
- syft-win
signs:
- artifacts: checksum
@@ -109,8 +96,11 @@ brews:
- tap:
owner: anchore
name: homebrew-syft
ids:
- syft
homepage: *website
description: *description
license: "Apache License 2.0"
dockers:
- image_templates:
+179
View File
@@ -0,0 +1,179 @@
# Developing
## Getting started
In order to test and develop in this repo you will need the following dependencies installed:
- docker
- make
After cloning do the following:
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
2. run `make` to run linting, tests, and other verifications to make certain everything is working alright.
Checkout `make help` to see what other actions you can take.
The main make tasks for common static analysis and testing are `lint`, `lint-fix`, `unit`, `integration`, and `cli`.
## Levels of testing
- `unit`: The default level of test which is distributed throughout the repo are unit tests. Any `_test.go` file that
does not reside somewhere within the `/test` directory is a unit test. Other forms of testing should be organized in
the `/test` directory. These tests should focus on correctness of functionality in depth. % Test coverage metrics
only considers unit tests and no other forms of testing.
- `integration`: located within `test/integration`, these tests focus on the behavior surfaced by the common library
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
that integration tests will automatically fail if no test attempts to use that enum value. For more details see
the "Data diversity and freshness assertions" section below.
- `cli`: located with in `test/cli`, these are tests that test the correctness of application behavior from a
snapshot build. This should be used in cases where a unit or integration test will not do or if you are looking
for in-depth testing of code in the `cmd/` package (such as testing the proper behavior of application configuration,
CLI switches, and glue code before syft library calls).
- `acceptance`: located within `test/acceptance`, these are smoke-like tests that ensure that application packaging
and installation works as expected. For example, during release we provide RPM packages as a download artifact. We
also have an accompanying RPM acceptance test that installs the RPM from a snapshot build and ensures the output
of a syft invocation matches canned expected output. New acceptance tests should be added for each release artifact
and architecture supported (when possible).
### Data diversity and freshness assertions
It is important that tests against the codebase are flexible enough to begin failing when they do not cover "enough"
of the objects under test. "Cover" in this case does not mean that some percentage of the code has been executed
during testing, but instead that there is enough diversity of data input reflected in testing relative to the
definitions available.
For instance, consider an enum-like value like so:
```go
type Language string
const (
Java Language = "java"
JavaScript Language = "javascript"
Python Language = "python"
Ruby Language = "ruby"
Go Language = "go"
)
```
Say we have a test that exercises all the languages defined today:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that test all languages
}
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
})
}
}
```
Where each test case has a `inputFixturePath` that would result with packages from each language. This test is
brittle since it does not assert that all languages were exercised directly and future modifications (such as
adding a new language) won't be covered by any test cases.
To address this the enum-like object should have a definition of all objects that can be used in testing:
```go
type Language string
// const( Java Language = ..., ... )
var AllLanguages = []Language{
Java,
JavaScript,
Python,
Ruby,
Go,
Rust,
}
```
Allowing testing to automatically fail when adding a new language:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that (hopefully) covers all languages
}
// new stuff...
observedLanguages := strset.New()
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
// new stuff...
for _, actualPkg := range actual {
observedLanguages.Add(string(actualPkg.Language))
}
})
}
// new stuff...
for _, expectedLanguage := range pkg.AllLanguages {
if !observedLanguages.Contains(expectedLanguage) {
t.Errorf("failed to test language=%q", expectedLanguage)
}
}
}
```
This is a better test since it will fail when someone adds a new language but fails to write a test case that should
exercise that new language. This method is ideal for integration-level testing, where testing correctness in depth
is not needed (that is what unit tests are for) but instead testing in breadth to ensure that units are well integrated.
A similar case can be made for data freshness; if the quality of the results will be diminished if the input data
is not kept up to date then a test should be written (when possible) to assert any input data is not stale.
An example of this is the static list of licenses that is stored in `internal/spdxlicense` for use by the SPDX
presenters. This list is updated and published periodically by an external group and syft can grab and update this
list by running `go generate ./...` from the root of the repo.
An integration test has been written to grabs the latest license list version externally and compares that version
with the version generated in the codebase. If they differ, the test fails, indicating to someone that there is an
action needed to update it.
**_The key takeaway is to try and write tests that fail when data assumptions change and not just when code changes.**_
### Snapshot tests
The format objects make a lot of use of "snapshot" testing, where you save the expected output bytes from a call into the
git repository and during testing make a comparison of the actual bytes from the subject under test with the golden
copy saved in the repo. The "golden" files are stored in the `test-fixtures/snapshot` directory relative to the go
package under test and should always be updated by invoking `go test` on the specific test file with a specific CLI
update flag provided.
Many of the `Format` tests make use of this approach, where the raw SBOM report is saved in the repo and the test
compares that SBOM with what is generated from the latest presenter code. For instance, at the time of this writing
the CycloneDX presenter snapshots can be updated by running:
```bash
go test ./internal/formats -update-cyclonedx
```
These flags are defined at the top of the test files that have tests that use the snapshot files.
Snapshot testing is only as good as the manual verification of the golden snapshot file saved to the repo! Be careful
and diligent when updating these files.
## Architecture
TODO: outline:
- analysis creates a static SBOM which can be encoded and decoded.
- format objects, should strive to not add or enrich data in encoding that could otherwise be done during analysis
- pkg.Catalogers
- file catalogers
- source.Source
- file.Resolvers
- logger abstraction
- events / bus abstraction
+21 -23
View File
@@ -17,8 +17,8 @@ SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 62
# CI cache busting values; change these if you want CI to not use previous stored cache
INTEGRATION_CACHE_BUSTER="88738d2f"
CLI_CACHE_BUSTER="9a2c03cf"
INTEGRATION_CACHE_BUSTER="894d8ca"
CLI_CACHE_BUSTER="894d8ca"
BOOTSTRAP_CACHE="c7afb99ad"
## Build variables
@@ -37,10 +37,6 @@ 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
@@ -103,8 +99,9 @@ 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.2.0-beta
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
.PHONY: bootstrap-go
bootstrap-go:
@@ -139,7 +136,7 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
go mod tidy
.PHONY: check-licenses
check-licenses:
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
$(TEMPDIR)/bouncer check
check-go-mod-tidy:
@@ -194,6 +191,11 @@ 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
@@ -252,27 +254,19 @@ cli: $(SNAPSHOTDIR) ## Run CLI tests
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
go test -count=1 -v ./test/cli
.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'
.PHONY: changelog
changelog: clean-changelog CHANGELOG.md
@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-release ## Build and publish final binaries and packages. Intended to be run only on macOS.
release: clean-dist CHANGELOG.md ## 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
@@ -313,9 +307,13 @@ clean-snapshot:
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist:
clean-dist: clean-changelog
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
+61 -5
View File
@@ -11,11 +11,17 @@
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 our community meetings!
- Calendar: https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t
- Agenda: https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing (join [this group](https://groups.google.com/g/anchore-oss-community) for write access)
- All are welcome!
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
## Features
- Catalog container images and filesystems to discover packages and libraries.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR/PAR/SAR, Jenkins plugins JPI/HPI, Go modules)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
- Supports Docker and OCI image formats
- Direct support for [Grype](https://github.com/anchore/grype), a fast and powerful vulnerability matcher.
@@ -87,9 +93,27 @@ file:path/to/yourproject/file read directly from a path on disk (any si
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
```
### Excluding file paths
Syft can exclude files and paths from being scanned within a source by using glob expressions
with one or more `--exclude` parameters:
```
syft <source> --exclude './out/**/*.json' --exclude /etc
```
**Note:** in the case of _image scanning_, since the entire filesystem is scanned it is
possible to use absolute paths like `/etc` or `/usr/**/*.txt` whereas _directory scans_
exclude files _relative to the specified directory_. For example: scanning `/usr/foo` with
`--exclude ./package.json` would exclude `/usr/foo/package.json` and `--exclude '**/package.json'`
would exclude all `package.json` files under `/usr/foo`. For _directory scans_,
it is required to begin path expressions with `./`, `*/`, or `**/`, all of which
will be resolved _relative to the specified scan directory_. Keep in mind, your shell
may attempt to expand wildcards, so put those parameters in single quotes, like:
`'**/*.json'`.
### Output formats
The output format for Syft is configurable as well:
The output format for Syft is configurable as well using the
`-o` (or `--output`) option:
```
syft packages <image> -o <format>
@@ -98,11 +122,21 @@ syft packages <image> -o <format>
Where the `formats` available are:
- `json`: Use this to get as much information out of Syft as possible!
- `text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.3 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.3 specification](https://cyclonedx.org/specification/overview/).
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `table`: A columnar summary (default).
#### Multiple outputs
Syft can also output _multiple_ files in differing formats by appending
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
```shell
syft packages <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
```
## Private Registry Authentication
### Local Docker Credentials
@@ -197,8 +231,12 @@ Configuration search paths:
Configuration options (example values are the default):
```yaml
# the output format of the SBOM report (options: table, text, json)
# same as -o ; SYFT_OUTPUT env var
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
# same as -o, --output, and SYFT_OUTPUT env var
# to specify multiple output files in differing formats, use a list:
# output:
# - "json=<syft-json-output-file>"
# - "spdx-json=<spdx-json-output-file>"
output: "table"
# suppress all output (except for the SBOM report)
@@ -212,8 +250,26 @@ file: ""
# same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true
# a list of globs to exclude from scanning. same as --exclude ; for example:
# exclude:
# - "/etc/**"
# - "./out/**/*.json"
exclude:
# cataloging packages is exposed through the packages and power-user subcommands
package:
# search within archives that do contain a file index to search against (zip)
# note: for now this only applies to the java package cataloger
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
search-indexed-archives: true
# search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)
# note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed
# note: for now this only applies to the java package cataloger
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
search-unindexed-archives: false
cataloger:
# enable/disable cataloging of packages
# SYFT_PACKAGE_CATALOGER_ENABLED env var
+1
View File
@@ -11,6 +11,7 @@ import (
func checkForApplicationUpdate() {
if appConfig.CheckForAppUpdate {
log.Debugf("checking if new vesion of %s is available", internal.ApplicationName)
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
+44 -4
View File
@@ -1,17 +1,19 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
"sort"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
)
@@ -28,6 +30,8 @@ func init() {
initAppConfig,
initLogging,
logAppConfig,
checkForApplicationUpdate,
logAppVersion,
initEventBus,
)
}
@@ -48,7 +52,13 @@ func initCmdAliasBindings() {
panic(err)
}
if activeCmd == packagesCmd || activeCmd == rootCmd {
// enable all cataloger by default if power-user command is run
if activeCmd == powerUserCmd {
config.PowerUserCatalogerEnabledDefault()
}
switch activeCmd {
case packagesCmd, 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
@@ -58,7 +68,7 @@ func initCmdAliasBindings() {
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err)
}
} else {
default:
// 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 {
@@ -104,3 +114,33 @@ func initEventBus() {
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
}
func logAppVersion() {
versionInfo := version.FromBuild()
log.Infof("syft version: %s", versionInfo.Version)
var fields map[string]interface{}
bytes, err := json.Marshal(versionInfo)
if err != nil {
return
}
err = json.Unmarshal(bytes, &fields)
if err != nil {
return
}
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
for idx, field := range keys {
value := fields[field]
branch := "├──"
if idx == len(fields)-1 {
branch = "└──"
}
log.Debugf(" %s %s: %s", branch, field, value)
}
}
+2 -4
View File
@@ -14,7 +14,7 @@ import (
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:gocognit,funlen
// nolint: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,9 +41,7 @@ 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)
if err := subscription.Unsubscribe(); err != nil {
retErr = multierror.Append(retErr, err)
}
_ = subscription.Unsubscribe()
// 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
+5 -5
View File
@@ -50,7 +50,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.PresenterReady,
Type: event.Exit,
}
worker := func() <-chan error {
@@ -182,7 +182,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.PresenterReady,
Type: event.Exit,
}
worker := func() <-chan error {
@@ -251,8 +251,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.PresenterReady,
Error: fmt.Errorf("unable to create presenter"),
Type: event.Exit,
Error: fmt.Errorf("an exit error occured"),
}
worker := func() <-chan error {
@@ -376,7 +376,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.PresenterReady,
Type: event.Exit,
}
worker := func() <-chan error {
+72
View File
@@ -0,0 +1,72 @@
package cmd
import (
"fmt"
"strings"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/output"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/sbom"
"github.com/hashicorp/go-multierror"
)
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
outputOptions, err := parseOptions(outputs, defaultFile)
if err != nil {
return nil, err
}
writer, err := output.MakeWriter(outputOptions...)
if err != nil {
return nil, err
}
return writer, nil
}
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOptions(outputs []string, defaultFile string) (out []output.WriterOption, errs error) {
// always should have one option -- we generally get the default of "table", but just make sure
if len(outputs) == 0 {
outputs = append(outputs, string(format.TableOption))
}
for _, name := range outputs {
name = strings.TrimSpace(name)
// split to at most two parts for <format>=<file>
parts := strings.SplitN(name, "=", 2)
// the format option is the first part
name = parts[0]
// default to the --file or empty string if not specified
file := defaultFile
// If a file is specified as part of the output option, use that
if len(parts) > 1 {
file = parts[1]
}
option := format.ParseOption(name)
if option == format.UnknownFormatOption {
errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name))
continue
}
encoder := formats.ByOption(option)
if encoder == nil {
errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat))
continue
}
out = append(out, output.WriterOption{
Format: *encoder,
Path: file,
})
}
return out, errs
}
+78
View File
@@ -0,0 +1,78 @@
package cmd
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestOutputWriterConfig(t *testing.T) {
tmp := t.TempDir() + "/"
tests := []struct {
outputs []string
file string
err bool
expected []string
}{
{
outputs: []string{},
expected: []string{""},
},
{
outputs: []string{"json"},
expected: []string{""},
},
{
file: "test-1.json",
expected: []string{"test-1.json"},
},
{
outputs: []string{"json=test-2.json"},
expected: []string{"test-2.json"},
},
{
outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"},
expected: []string{"test-3-1.json", "test-3-2.json"},
},
{
outputs: []string{"text", "json=test-4.json"},
expected: []string{"", "test-4.json"},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) {
outputs := test.outputs
for i, val := range outputs {
outputs[i] = strings.Replace(val, "=", "="+tmp, 1)
}
file := test.file
if file != "" {
file = tmp + file
}
_, err := makeWriter(test.outputs, file)
if test.err {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
}
for _, expected := range test.expected {
if expected != "" {
assert.FileExists(t, tmp+expected)
} else if file != "" {
assert.FileExists(t, file)
} else {
assert.NoFileExists(t, expected)
}
}
})
}
}
+73 -60
View File
@@ -10,14 +10,13 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
@@ -51,8 +50,7 @@ const (
)
var (
packagesPresenterOpt format.Option
packagesCmd = &cobra.Command{
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
@@ -63,14 +61,7 @@ var (
Args: validateInputArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
// set the presenter
presenterOption := format.ParseOption(appConfig.Output)
if presenterOption == format.UnknownFormatOption {
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
}
packagesPresenterOpt = presenterOption
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
@@ -99,17 +90,17 @@ func setPackageFlags(flags *pflag.FlagSet) {
// Formatting & Input options //////////////////////////////////////////////
flags.StringP(
"scope", "s", source.SquashedScope.String(),
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringP(
"output", "o", string(format.TableOption),
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
flags.StringArrayP(
"output", "o", []string{string(format.TableOption)},
fmt.Sprintf("report output format, options=%v", format.AllOptions),
)
flags.StringP(
"file", "", "",
"file to write the report output to (default is STDOUT)",
"file to write the default report output to (default is STDOUT)",
)
// Upload options //////////////////////////////////////////////////////////
@@ -133,6 +124,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
"include dockerfile for upload to Anchore Enterprise",
)
flags.StringArrayP(
"exclude", "", nil,
"exclude paths from being scanned using a glob expression",
)
flags.Bool(
"overwrite-existing-image", false,
"overwrite an existing image during the upload to Anchore Enterprise",
@@ -159,6 +155,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
return err
}
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
return err
}
// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
@@ -201,26 +201,26 @@ func validateInputArgs(cmd *cobra.Command, args []string) error {
}
func packagesExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
reporter, closer, err := reportWriter()
defer func() {
if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
}()
writer, err := makeWriter(appConfig.Output, appConfig.File)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// could be an image or a directory, with or without a scheme
userInput := args[0]
return eventLoop(
packagesExecWorker(userInput),
packagesExecWorker(userInput, writer),
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet)...,
)
}
@@ -235,56 +235,70 @@ func isVerbose() (result bool) {
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
}
func packagesExecWorker(userInput string) <-chan error {
func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
f := formats.ByOption(packagesPresenterOpt)
if f == nil {
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
return
}
checkForApplicationUpdate()
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
tasks, err := tasks()
if err != nil {
errs <- fmt.Errorf("failed to determine image source: %w", err)
errs <- err
return
}
defer cleanup()
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %w", err)
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
return
}
if cleanup != nil {
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
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, src.Metadata, catalog, d); err != nil {
if err := runPackageSbomUpload(src, s); err != nil {
errs <- err
return
}
}
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: d,
},
Source: src.Metadata,
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: f.Presenter(sbomResult),
Type: event.Exit,
Value: func() error { return writer.Write(s) },
})
}()
return errs
}
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
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 {
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
@@ -319,9 +333,7 @@ func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Ca
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SourceMetadata: s,
Catalog: catalog,
Distro: d,
SBOM: s,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
Timeout: appConfig.Anchore.ImportTimeout,
@@ -330,5 +342,6 @@ func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Ca
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
}
+50 -33
View File
@@ -2,18 +2,21 @@ package cmd
import (
"fmt"
"sync"
"github.com/anchore/syft/syft/sbom"
"os"
"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/output"
"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"
@@ -21,6 +24,8 @@ 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)
@@ -69,68 +74,80 @@ func powerUserExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
reporter, closer, err := reportWriter()
defer func() {
if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
}()
writer, err := output.MakeWriter(output.WriterOption{
Format: syftjson.Format(),
Path: appConfig.File,
})
if err != nil {
return err
}
defer func() {
if err := writer.Close(); 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)
}()
return eventLoop(
powerUserExecWorker(userInput),
powerUserExecWorker(userInput, writer),
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet)...,
)
}
func powerUserExecWorker(userInput string) <-chan error {
func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
tasks, err := powerUserTasks()
appConfig.Secrets.Cataloger.Enabled = true
appConfig.FileMetadata.Cataloger.Enabled = true
appConfig.FileContents.Cataloger.Enabled = true
appConfig.FileClassification.Cataloger.Enabled = true
tasks, err := tasks()
if err != nil {
errs <- err
return
}
checkForApplicationUpdate()
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
if err != nil {
errs <- err
return
}
defer cleanup()
if cleanup != nil {
defer cleanup()
}
analysisResults := sbom.SBOM{
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: appConfig,
},
}
wg := &sync.WaitGroup{}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
wg.Add(1)
go func(task powerUserTask) {
defer wg.Done()
if err = task(&analysisResults.Artifacts, src); err != nil {
errs <- err
return
}
}(task)
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
wg.Wait()
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: poweruser.NewJSONPresenter(analysisResults, *appConfig),
Type: event.Exit,
Value: func() error { return writer.Write(s) },
})
}()
return errs
}
-33
View File
@@ -1,33 +0,0 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"github.com/anchore/syft/internal/log"
)
func reportWriter() (io.Writer, func() error, error) {
nop := func() error { return nil }
path := strings.TrimSpace(appConfig.File)
switch len(path) {
case 0:
return os.Stdout, nop, nil
default:
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
}
return reportFile, func() error {
log.Infof("report written to file=%q", path)
return reportFile.Close()
}, nil
}
}
+5 -1
View File
@@ -4,7 +4,9 @@ import (
"fmt"
"os"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -22,12 +24,14 @@ var rootCmd = &cobra.Command{
PreRunE: packagesCmd.PreRunE,
RunE: packagesCmd.RunE,
ValidArgsFunction: packagesCmd.ValidArgsFunction,
Version: version.FromBuild().Version,
}
func init() {
// set universal flags
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
// setting the version template to just print out the string since we already have a templatized version string
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
flag := "quiet"
rootCmd.PersistentFlags().BoolP(
flag, "q", false,
+58 -43
View File
@@ -4,25 +4,25 @@ import (
"crypto"
"fmt"
"github.com/anchore/syft/syft/sbom"
"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/source"
)
type powerUserTask func(*sbom.Artifacts, *source.Source) error
type task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func powerUserTasks() ([]powerUserTask, error) {
var tasks []powerUserTask
func tasks() ([]task, error) {
var tasks []task
generators := []func() (powerUserTask, error){
catalogPackagesTask,
catalogFileMetadataTask,
catalogFileDigestsTask,
catalogSecretsTask,
catalogFileClassificationsTask,
catalogContentsTask,
generators := []func() (task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
}
for _, generator := range generators {
@@ -30,6 +30,7 @@ func powerUserTasks() ([]powerUserTask, error) {
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
@@ -38,51 +39,51 @@ func powerUserTasks() ([]powerUserTask, error) {
return tasks, nil
}
func catalogPackagesTask() (powerUserTask, error) {
func generateCatalogPackagesTask() (task, error) {
if !appConfig.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *sbom.Artifacts, src *source.Source) error {
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.ToConfig())
if err != nil {
return err
return nil, err
}
results.PackageCatalog = packageCatalog
results.Distro = theDistro
results.LinuxDistribution = theDistro
return nil
return relationships, nil
}
return task, nil
}
func catalogFileMetadataTask() (powerUserTask, error) {
func generateCatalogFileMetadataTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := file.NewMetadataCataloger()
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileMetadata = result
return nil
return nil, nil
}
return task, nil
}
func catalogFileDigestsTask() (powerUserTask, error) {
func generateCatalogFileDigestsTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
@@ -111,24 +112,24 @@ func catalogFileDigestsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileDigests = result
return nil
return nil, nil
}
return task, nil
}
func catalogSecretsTask() (powerUserTask, error) {
func generateCatalogSecretsTask() (task, error) {
if !appConfig.Secrets.Cataloger.Enabled {
return nil, nil
}
@@ -143,24 +144,24 @@ func catalogSecretsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := secretsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.Secrets = result
return nil
return nil, nil
}
return task, nil
}
func catalogFileClassificationsTask() (powerUserTask, error) {
func generateCatalogFileClassificationsTask() (task, error) {
if !appConfig.FileClassification.Cataloger.Enabled {
return nil, nil
}
@@ -171,24 +172,24 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := classifierCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileClassifications = result
return nil
return nil, nil
}
return task, nil
}
func catalogContentsTask() (powerUserTask, error) {
func generateCatalogContentsTask() (task, error) {
if !appConfig.FileContents.Cataloger.Enabled {
return nil, nil
}
@@ -198,19 +199,33 @@ func catalogContentsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := contentsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileContents = result
return nil
return nil, nil
}
return task, nil
}
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
}
}
+22 -16
View File
@@ -3,50 +3,56 @@ module github.com/anchore/syft
go 1.16
require (
github.com/CycloneDX/cyclonedx-go v0.4.0
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
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-20211024152658-003132a67c10
github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718
// 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/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/docker/docker v20.10.11+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.4.1
github.com/google/go-cmp v0.5.6
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 v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.4.1
github.com/olekukonko/tablewriter v0.0.4
github.com/pelletier/go-toml v1.8.1
github.com/pelletier/go-toml v1.9.3
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.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.0
github.com/spf13/viper v1.8.1
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-20200730190914-3517092dd163
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/xeipuuv/gojsonschema v1.2.0
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
golang.org/x/mod v0.4.2
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
gopkg.in/yaml.v2 v2.4.0
)
+675 -455
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,7 +1,5 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2020-08-10T20:55:46Z.
#
usage() {
this=$1
@@ -23,7 +21,7 @@ EOF
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
@@ -67,8 +65,10 @@ execute() {
}
get_binaries() {
case "$PLATFORM" in
darwin/arm64) BINARIES="syft" ;;
darwin/amd64) BINARIES="syft" ;;
linux/amd64) BINARIES="syft" ;;
linux/arm64) BINARIES="syft" ;;
windows/amd64) BINARIES="syft" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
@@ -97,6 +97,9 @@ adjust_format() {
darwin) FORMAT=dmg ;;
windows) FORMAT=zip ;;
esac
case "${PLATFORM}" in
darwin/arm64) FORMAT=zip ;;
esac
true
}
adjust_os() {
@@ -244,7 +247,7 @@ extract_from_dmg() {
dmg_file=$1
mount_point="/Volumes/tmp-dmg"
hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}"
cp -fR "${mount_point}/" ./
cp -fR "${mount_point}/." ./
hdiutil detach -quiet -force "${mount_point}"
}
http_download_curl() {
+6 -11
View File
@@ -6,24 +6,19 @@ 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/pkg"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/sbom"
"github.com/antihax/optional"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
type ImportConfig struct {
ImageMetadata image.Metadata
SourceMetadata source.Metadata
Catalog *pkg.Catalog
Distro *distro.Distro
SBOM sbom.SBOM
Dockerfile []byte
OverwriteExistingUpload bool
Timeout uint
@@ -73,19 +68,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.SourceMetadata, cfg.Catalog, cfg.Distro, stage)
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM, 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.ImageMetadata.RawManifest, stage)
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.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.ImageMetadata.RawConfig, stage)
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage)
if err != nil {
return fmt.Errorf("failed to import Config: %w", err)
}
+64 -26
View File
@@ -8,55 +8,93 @@ import (
"fmt"
"net/http"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal/formats/syftjson"
"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/formats/syftjson"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/wagoodman/go-progress"
)
type packageSBOMImportAPI interface {
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
}
func packageSbomModel(srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
// importSBOM mirrors all elements found on the syftjson model format object relative to the anchore engine import schema.
type importSBOM struct {
Artifacts []syftjsonModel.Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []syftjsonModel.Relationship `json:"artifactRelationships"`
Files []syftjsonModel.File `json:"files,omitempty"` // note: must have omitempty
Secrets []syftjsonModel.Secrets `json:"secrets,omitempty"` // note: must have omitempty
Source syftjsonModel.Source `json:"source"` // Source represents the original object that was cataloged
Distro external.ImportDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor syftjsonModel.Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema syftjsonModel.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
}
// 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,
// toImportSBOMModel transforms the current sbom shape into what is needed for the current anchore import api shape.
func toImportSBOMModel(s sbom.SBOM) importSBOM {
m := syftjson.ToFormatModel(s)
var idLike string
if len(m.Distro.IDLike) > 0 {
idLike = m.Distro.IDLike[0]
}
err := syftjson.Format().Presenter(s).Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
var version = m.Distro.VersionID // note: version is intentionally not used as the default
if version == "" {
version = m.Distro.Version
}
var name = m.Distro.ID // note: name is intentionally not used as the default
if name == "" {
name = m.Distro.Name
}
return importSBOM{
Artifacts: m.Artifacts,
ArtifactRelationships: m.ArtifactRelationships,
Files: m.Files,
Secrets: m.Secrets,
Source: m.Source,
Distro: external.ImportDistribution{
Name: name,
Version: version,
IdLike: idLike,
},
Descriptor: m.Descriptor,
Schema: m.Schema,
}
}
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
doc := toImportSBOMModel(s)
enc := json.NewEncoder(&buf)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(&doc); err != nil {
return nil, fmt.Errorf("unable to encode import JSON model: %w", err)
}
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
var model external.ImagePackageManifest
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
return nil, fmt.Errorf("unable to convert JSON presenter output to import model: %w", err)
if err := json.Unmarshal(buf.Bytes(), &model); err != nil {
return nil, fmt.Errorf("unable to convert JSON output to import model: %w", err)
}
return &model, nil
}
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s sbom.SBOM, stage *progress.Stage) (string, error) {
log.Debug("importing package SBOM")
stage.Current = "package SBOM"
model, err := packageSbomModel(s, catalog, d)
model, err := packageSbomModel(s)
if err != nil {
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
}
+281 -147
View File
@@ -1,7 +1,6 @@
package anchore
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -9,16 +8,17 @@ import (
"strings"
"testing"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/formats/syftjson"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-progress"
)
@@ -29,98 +29,6 @@ func must(c pkg.CPE, e error) pkg.CPE {
return c
}
// this test is tailored towards the assumption that the import doc shape and the syft json shape are the same.
// TODO: replace this as the document shapes diverge.
func TestPackageSbomToModel(t *testing.T) {
m := source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Layers: []source.LayerMetadata{
{
MediaType: "layer-metadata-type!",
Digest: "layer-digest",
Size: 20,
},
},
Size: 10,
ManifestDigest: "sha256:digest!",
MediaType: "mediatype!",
Tags: nil,
},
}
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
p := pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: []source.Location{
{
RealPath: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
}
c := pkg.NewCatalog(p)
model, err := packageSbomModel(m, c, &d)
if err != nil {
t.Fatalf("unable to generate model from source material: %+v", err)
}
var modelJSON []byte
modelJSON, err = json.Marshal(&model)
if err != nil {
t.Fatalf("unable to marshal model: %+v", err)
}
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: c,
Distro: &d,
},
Source: m,
}
var buf bytes.Buffer
pres := syftjson.Format().Presenter(s)
if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err)
}
// unmarshal expected result
var expectedDoc syftjsonModel.Document
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
// unmarshal actual result
var actualDoc syftjsonModel.Document
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
for _, d := range deep.Equal(actualDoc, expectedDoc) {
if strings.HasSuffix(d, "<nil slice> != []") {
// do not consider nil vs empty collection semantics as a "difference"
continue
}
t.Errorf("diff: %+v", d)
}
}
type mockPackageSBOMImportAPI struct {
sessionID string
model external.ImagePackageManifest
@@ -141,63 +49,82 @@ func (m *mockPackageSBOMImportAPI) ImportImagePackages(ctx context.Context, sess
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
}
func TestPackageSbomImport(t *testing.T) {
catalog := pkg.NewCatalog(pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: []source.Location{
{
RealPath: "path",
FileSystemID: "layerID",
},
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "p-name",
Version: "p-version",
License: "p-license",
Author: "p-author",
AuthorEmail: "p-email",
Platform: "p-platform",
Files: []pkg.PythonFileRecord{
{
Path: "p-path",
Digest: &pkg.PythonFileDigest{
Algorithm: "p-alg",
Value: "p-digest",
func sbomFixture() sbom.SBOM {
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: []source.Location{
{
Coordinates: source.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
},
Size: "p-size",
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "p-name",
Version: "p-version",
License: "p-license",
Author: "p-author",
AuthorEmail: "p-email",
Platform: "p-platform",
Files: []pkg.PythonFileRecord{
{
Path: "p-path",
Digest: &pkg.PythonFileDigest{
Algorithm: "p-alg",
Value: "p-digest",
},
Size: "p-size",
},
},
SitePackagesRootPath: "p-site-packages-root",
TopLevelPackages: []string{"top-level"},
},
}),
LinuxDistribution: &linux.Release{
ID: "centos",
Version: "8.0",
VersionID: "8.0",
IDLike: []string{"rhel"},
},
SitePackagesRootPath: "p-site-packages-root",
TopLevelPackages: []string{"top-level"},
},
})
m := source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Layers: nil,
Size: 10,
ManifestDigest: "sha256:digest!",
MediaType: "mediatype!",
Tags: nil,
Relationships: []artifact.Relationship{
{
From: source.NewLocation("/place1"),
To: source.NewLocation("/place2"),
Type: artifact.ContainsRelationship,
},
},
Source: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Layers: nil,
Size: 10,
ManifestDigest: "sha256:digest!",
MediaType: "mediatype!",
Tags: nil,
},
},
}
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
}
theModel, err := packageSbomModel(m, catalog, &d)
func TestPackageSbomImport(t *testing.T) {
sbomResult := sbomFixture()
theModel, err := packageSbomModel(sbomResult)
if err != nil {
t.Fatalf("could not get sbom model: %+v", err)
}
@@ -236,7 +163,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, m, catalog, &d, &progress.Stage{})
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, sbomResult, &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
@@ -261,3 +188,210 @@ func TestPackageSbomImport(t *testing.T) {
})
}
}
type modelAssertion func(t *testing.T, model *external.ImagePackageManifest)
func Test_packageSbomModel(t *testing.T) {
fix := sbomFixture()
tests := []struct {
name string
sbom sbom.SBOM
traits []modelAssertion
}{
{
name: "distro: has single distro id-like",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
LinuxDistribution: &linux.Release{
Name: "centos-name",
ID: "centos-id",
IDLike: []string{
"centos-id-like-1",
},
Version: "version",
VersionID: "version-id",
},
},
},
traits: []modelAssertion{
hasDistroInfo("centos-id", "version-id", "centos-id-like-1"),
},
},
{
name: "distro: has multiple distro id-like",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
LinuxDistribution: &linux.Release{
Name: "centos-name",
ID: "centos-id",
IDLike: []string{
"centos-id-like-1",
"centos-id-like-2",
},
Version: "version",
VersionID: "version-id",
},
},
},
traits: []modelAssertion{
hasDistroInfo("centos-id", "version-id", "centos-id-like-1"),
},
},
{
name: "distro: has no distro id-like",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
LinuxDistribution: &linux.Release{
Name: "centos-name",
ID: "centos-id",
IDLike: []string{},
Version: "version",
VersionID: "version-id",
},
},
},
traits: []modelAssertion{
hasDistroInfo("centos-id", "version-id", ""),
},
},
{
name: "distro: has no version-id",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
LinuxDistribution: &linux.Release{
Name: "centos-name",
ID: "centos-id",
IDLike: []string{},
Version: "version",
VersionID: "",
},
},
},
traits: []modelAssertion{
hasDistroInfo("centos-id", "version", ""),
},
},
{
name: "distro: has no id",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
LinuxDistribution: &linux.Release{
Name: "centos-name",
ID: "",
IDLike: []string{},
Version: "version",
VersionID: "version-id",
},
},
},
traits: []modelAssertion{
hasDistroInfo("centos-name", "version-id", ""),
},
},
{
name: "should have expected packages",
sbom: fix,
traits: []modelAssertion{
func(t *testing.T, model *external.ImagePackageManifest) {
require.Len(t, model.Artifacts, 1)
modelPkg := model.Artifacts
modelBytes, err := json.Marshal(&modelPkg)
require.NoError(t, err)
fixPkg := syftjson.ToFormatModel(fix).Artifacts
fixBytes, err := json.Marshal(&fixPkg)
require.NoError(t, err)
assert.JSONEq(t, string(fixBytes), string(modelBytes))
},
},
},
{
name: "should have expected relationships",
sbom: fix,
traits: []modelAssertion{
func(t *testing.T, model *external.ImagePackageManifest) {
modelPkg := model.ArtifactRelationships
modelBytes, err := json.Marshal(&modelPkg)
require.NoError(t, err)
fixPkg := syftjson.ToFormatModel(fix).ArtifactRelationships
fixBytes, err := json.Marshal(&fixPkg)
require.NoError(t, err)
assert.JSONEq(t, string(fixBytes), string(modelBytes))
},
},
},
{
name: "should have expected schema",
sbom: fix,
traits: []modelAssertion{
func(t *testing.T, model *external.ImagePackageManifest) {
modelPkg := model.Schema
modelBytes, err := json.Marshal(&modelPkg)
require.NoError(t, err)
fixPkg := syftjson.ToFormatModel(fix).Schema
fixBytes, err := json.Marshal(&fixPkg)
require.NoError(t, err)
assert.JSONEq(t, string(fixBytes), string(modelBytes))
},
},
},
{
name: "should have expected descriptor",
sbom: fix,
traits: []modelAssertion{
func(t *testing.T, model *external.ImagePackageManifest) {
modelPkg := model.Descriptor
modelBytes, err := json.Marshal(&modelPkg)
require.NoError(t, err)
fixPkg := syftjson.ToFormatModel(fix).Descriptor
fixBytes, err := json.Marshal(&fixPkg)
require.NoError(t, err)
assert.JSONEq(t, string(fixBytes), string(modelBytes))
},
},
},
{
name: "should have expected source",
sbom: fix,
traits: []modelAssertion{
func(t *testing.T, model *external.ImagePackageManifest) {
modelPkg := model.Source
modelBytes, err := json.Marshal(&modelPkg)
require.NoError(t, err)
fixPkg := syftjson.ToFormatModel(fix).Source
fixBytes, err := json.Marshal(&fixPkg)
require.NoError(t, err)
assert.JSONEq(t, string(fixBytes), string(modelBytes))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := packageSbomModel(tt.sbom)
require.NoError(t, err)
for _, fn := range tt.traits {
fn(t, got)
}
})
}
}
func hasDistroInfo(name, version, idLike string) modelAssertion {
return func(t *testing.T, model *external.ImagePackageManifest) {
assert.Equal(t, name, model.Distro.Name)
assert.Equal(t, version, model.Distro.Version)
assert.Equal(t, idLike, model.Distro.IdLike)
}
}
+14 -2
View File
@@ -17,6 +17,8 @@ import (
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
var catalogerEnabledDefault = false
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
@@ -28,7 +30,7 @@ type parser interface {
// Application is the main syft application configuration.
type Application struct {
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
Output []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
@@ -36,12 +38,18 @@ type Application struct {
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Package packages `yaml:"package" json:"package" mapstructure:"package"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
}
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
func PowerUserCatalogerEnabledDefault() {
catalogerEnabledDefault = true
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
@@ -156,6 +164,10 @@ func (cfg *Application) parseLogLevelOption() error {
}
}
if cfg.Log.Level == "" {
cfg.Log.Level = cfg.Log.LevelOpt.String()
}
return nil
}
+6
View File
@@ -3,6 +3,8 @@ package config
import (
"fmt"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
@@ -12,6 +14,10 @@ type catalogerOptions struct {
ScopeOpt source.Scope `yaml:"-" json:"-"`
}
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
v.SetDefault("package.cataloger.enabled", true)
}
func (cfg *catalogerOptions) parseConfigValues() error {
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
+1 -1
View File
@@ -10,7 +10,7 @@ type fileClassification struct {
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", true)
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
+1 -1
View File
@@ -13,7 +13,7 @@ type fileContents struct {
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", true)
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
+1 -1
View File
@@ -11,7 +11,7 @@ type FileMetadata struct {
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", true)
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
-15
View File
@@ -1,15 +0,0 @@
package config
import "github.com/spf13/viper"
type packages struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg packages) loadDefaultValues(v *viper.Viper) {
v.SetDefault("package.cataloger.enabled", true)
}
func (cfg *packages) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
+33
View File
@@ -0,0 +1,33 @@
package config
import (
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/spf13/viper"
)
type pkg struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
}
func (cfg pkg) loadDefaultValues(v *viper.Viper) {
cfg.Cataloger.loadDefaultValues(v)
c := cataloger.DefaultSearchConfig()
v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives)
v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives)
}
func (cfg *pkg) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
func (cfg pkg) ToConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.SearchUnindexedArchives,
Scope: cfg.Cataloger.ScopeOpt,
},
}
}
+2 -1
View File
@@ -2,9 +2,10 @@ package config
import (
"fmt"
"github.com/anchore/stereoscope/pkg/image"
"testing"
"github.com/anchore/stereoscope/pkg/image"
"github.com/stretchr/testify/assert"
)
+1 -1
View File
@@ -15,7 +15,7 @@ type secrets struct {
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", true)
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
+2 -2
View File
@@ -4,7 +4,7 @@ const (
// ApplicationName is the non-capitalized name of the application (do not change this)
ApplicationName = "syft"
// JSONSchemaVersion is the current schema version output by the JSON presenter
// JSONSchemaVersion is the current schema version output by the JSON encoder
// 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 = "1.1.0"
JSONSchemaVersion = "3.0.1"
)
+19
View File
@@ -0,0 +1,19 @@
package file
import (
"errors"
"fmt"
"io"
)
const perFileReadLimit = 2 * GB
// safeCopy limits the copy from the reader. This is useful when extracting files from archives to
// protect against decompression bomb attacks.
func safeCopy(writer io.Writer, reader io.Reader) error {
numBytes, err := io.Copy(writer, io.LimitReader(reader, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package file
import (
"fmt"
"io/ioutil"
"path/filepath"
"github.com/bmatcuk/doublestar/v4"
"github.com/mholt/archiver/v3"
)
// ExtractGlobsFromTarToUniqueTempFile extracts paths matching the given globs within the given archive to a temporary directory, returning file openers for each file extracted.
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
// don't allow for full traversal, only select traversal from given paths
if len(globs) == 0 {
return results, nil
}
visitor := func(file archiver.File) error {
defer file.Close()
// ignore directories
if file.FileInfo.IsDir() {
return nil
}
// ignore any filename that doesn't match the given globs...
if !matchesAnyGlob(file.Name(), globs...) {
return nil
}
// we have a file we want to extract....
tempfilePrefix := filepath.Base(filepath.Clean(file.Name())) + "-"
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
if err := safeCopy(tempFile, file.ReadCloser); err != nil {
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", file.Name(), archivePath, err)
}
results[file.Name()] = Opener{path: tempFile.Name()}
return nil
}
return results, archiver.Walk(archivePath, visitor)
}
func matchesAnyGlob(name string, globs ...string) bool {
for _, glob := range globs {
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
return true
}
}
return false
}
+3
View File
@@ -1,3 +1,6 @@
//go:build !windows
// +build !windows
package file
import (
+3 -29
View File
@@ -3,9 +3,7 @@ package file
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -22,8 +20,6 @@ const (
GB
)
const perFileReadLimit = 2 * GB
type errZipSlipDetected struct {
Prefix string
JoinArgs []string
@@ -110,21 +106,10 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
}
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(tempFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
if err := safeCopy(tempFile, zippedFile); err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
// the file pointer is at the end due to the copy operation, reset back to the beginning
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
}
results[file.Name] = Opener{path: tempFile.Name()}
return nil
@@ -153,13 +138,7 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
}
var buffer bytes.Buffer
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
if err := safeCopy(&buffer, zippedFile); err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
@@ -228,12 +207,7 @@ func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) err
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
if err := safeCopy(outputFile, zippedFile); err != nil {
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
}
+3
View File
@@ -1,3 +1,6 @@
//go:build !windows
// +build !windows
package file
import (
+3
View File
@@ -1,3 +1,6 @@
//go:build !windows
// +build !windows
package file
import (
@@ -0,0 +1,32 @@
package cyclonedxhelpers
import (
"fmt"
"strings"
"github.com/anchore/syft/syft/pkg"
)
func Author(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.NpmPackageJSONMetadata:
return metadata.Author
case pkg.PythonPackageMetadata:
author := metadata.Author
if metadata.AuthorEmail != "" {
if author == "" {
return metadata.AuthorEmail
}
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
}
return author
case pkg.GemMetadata:
if len(metadata.Authors) > 0 {
return strings.Join(metadata.Authors, ",")
}
return ""
}
}
return ""
}
@@ -0,0 +1,87 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Author(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Authors: []string{
"auth1",
"auth2",
},
},
},
expected: "auth1,auth2",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from python - just name",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from python - just email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth@auth.gov",
},
{
name: "from python - both name and email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth <auth@auth.gov>",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Author(test.input))
})
}
}
@@ -0,0 +1,26 @@
package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
func Component(p pkg.Package) cyclonedx.Component {
return cyclonedx.Component{
Type: cyclonedx.ComponentTypeLibrary,
Name: p.Name,
Version: p.Version,
PackageURL: p.PURL,
Licenses: Licenses(p),
CPE: CPE(p),
Author: Author(p),
Publisher: Publisher(p),
Description: Description(p),
ExternalReferences: ExternalReferences(p),
Properties: Properties(p),
}
}
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
@@ -0,0 +1,12 @@
package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func CPE(p pkg.Package) string {
// Since the CPEs in a package are sorted by specificity
// we can extract the first CPE as the one to output in cyclonedx
if len(p.CPEs) > 0 {
return pkg.CPEString(p.CPEs[0])
}
return ""
}
@@ -0,0 +1,57 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_CPE(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{
CPEs: []pkg.CPE{},
},
expected: "",
},
{
name: "single CPE",
input: pkg.Package{
CPEs: []pkg.CPE{
testCPE,
},
},
expected: "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
},
{
name: "multiple CPEs",
input: pkg.Package{
CPEs: []pkg.CPE{
testCPE2,
testCPE,
},
},
expected: "cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, CPE(test.input))
})
}
}
@@ -0,0 +1,15 @@
package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func Description(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Description
case pkg.NpmPackageJSONMetadata:
return metadata.Description
}
}
return ""
}
@@ -0,0 +1,56 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Description(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Description(test.input))
})
}
}
@@ -0,0 +1,65 @@
package cyclonedxhelpers
import (
"fmt"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
refs := []cyclonedx.ExternalReference{}
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
if metadata.URL != "" {
refs = append(refs, cyclonedx.ExternalReference{
URL: metadata.URL,
Type: cyclonedx.ERTypeDistribution,
})
}
case pkg.CargoPackageMetadata:
if metadata.Source != "" {
refs = append(refs, cyclonedx.ExternalReference{
URL: metadata.Source,
Type: cyclonedx.ERTypeDistribution,
})
}
case pkg.NpmPackageJSONMetadata:
if metadata.URL != "" {
refs = append(refs, cyclonedx.ExternalReference{
URL: metadata.URL,
Type: cyclonedx.ERTypeDistribution,
})
}
if metadata.Homepage != "" {
refs = append(refs, cyclonedx.ExternalReference{
URL: metadata.Homepage,
Type: cyclonedx.ERTypeWebsite,
})
}
case pkg.GemMetadata:
if metadata.Homepage != "" {
refs = append(refs, cyclonedx.ExternalReference{
URL: metadata.Homepage,
Type: cyclonedx.ERTypeWebsite,
})
}
case pkg.PythonPackageMetadata:
if metadata.DirectURLOrigin != nil && metadata.DirectURLOrigin.URL != "" {
ref := cyclonedx.ExternalReference{
URL: metadata.DirectURLOrigin.URL,
Type: cyclonedx.ERTypeVCS,
}
if metadata.DirectURLOrigin.CommitID != "" {
ref.Comment = fmt.Sprintf("commit: %s", metadata.DirectURLOrigin.CommitID)
}
refs = append(refs, ref)
}
}
}
if len(refs) > 0 {
return &refs
}
return nil
}
@@ -0,0 +1,133 @@
package cyclonedxhelpers
import (
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_ExternalReferences(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected *[]cyclonedx.ExternalReference
}{
{
name: "no metadata",
input: pkg.Package{},
expected: nil,
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
URL: "http://a-place.gov",
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution},
},
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "http://a-place.gov",
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution},
},
},
{
name: "from cargo lock",
input: pkg.Package{
Name: "ansi_term",
Version: "0.12.1",
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
Licenses: nil,
Metadata: pkg.CargoPackageMetadata{
Name: "ansi_term",
Version: "0.12.1",
Source: "registry+https://github.com/rust-lang/crates.io-index",
Checksum: "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2",
Dependencies: []string{
"winapi",
},
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "registry+https://github.com/rust-lang/crates.io-index", Type: cyclonedx.ERTypeDistribution},
},
},
{
name: "from npm with homepage",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "http://a-place.gov",
Homepage: "http://homepage",
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution},
{URL: "http://homepage", Type: cyclonedx.ERTypeWebsite},
},
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Homepage: "http://a-place.gov",
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeWebsite},
},
},
{
name: "from python direct url",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{
URL: "http://a-place.gov",
},
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS},
},
},
{
name: "from python direct url with commit",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{
URL: "http://a-place.gov",
CommitID: "test",
},
},
},
expected: &[]cyclonedx.ExternalReference{
{URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS, Comment: "commit: test"},
},
},
{
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "",
},
},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, ExternalReferences(test.input))
})
}
}
@@ -0,0 +1,116 @@
package cyclonedxhelpers
import (
"time"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM := cyclonedx.NewBOM()
versionInfo := version.FromBuild()
// NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid):
// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source)
packages := s.Artifacts.PackageCatalog.Sorted()
components := make([]cyclonedx.Component, len(packages))
for i, p := range packages {
components[i] = Component(p)
}
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
cdxBOM.Components = &components
return cdxBOM
}
func toOSComponent(distro *linux.Release) []cyclonedx.Component {
if distro == nil {
return []cyclonedx.Component{}
}
eRefs := &[]cyclonedx.ExternalReference{}
if distro.BugReportURL != "" {
*eRefs = append(*eRefs, cyclonedx.ExternalReference{
URL: distro.BugReportURL,
Type: cyclonedx.ERTypeIssueTracker,
})
}
if distro.HomeURL != "" {
*eRefs = append(*eRefs, cyclonedx.ExternalReference{
URL: distro.HomeURL,
Type: cyclonedx.ERTypeWebsite,
})
}
if distro.SupportURL != "" {
*eRefs = append(*eRefs, cyclonedx.ExternalReference{
URL: distro.SupportURL,
Type: cyclonedx.ERTypeOther,
Comment: "support",
})
}
if distro.PrivacyPolicyURL != "" {
*eRefs = append(*eRefs, cyclonedx.ExternalReference{
URL: distro.PrivacyPolicyURL,
Type: cyclonedx.ERTypeOther,
Comment: "privacyPolicy",
})
}
if len(*eRefs) == 0 {
eRefs = nil
}
props := getCycloneDXProperties(*distro)
if len(*props) == 0 {
props = nil
}
return []cyclonedx.Component{
{
Type: cyclonedx.ComponentTypeOS,
Name: distro.Name,
Version: distro.Version,
CPE: distro.CPEName,
ExternalReferences: eRefs,
Properties: props,
},
}
}
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata {
return &cyclonedx.Metadata{
Timestamp: time.Now().Format(time.RFC3339),
Tools: &[]cyclonedx.Tool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
Component: toBomDescriptorComponent(srcMetadata),
}
}
func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
switch srcMetadata.Scheme {
case source.ImageScheme:
return &cyclonedx.Component{
Type: cyclonedx.ComponentTypeContainer,
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
}
case source.DirectoryScheme, source.FileScheme:
return &cyclonedx.Component{
Type: cyclonedx.ComponentTypeFile,
Name: srcMetadata.Path,
}
}
return nil
}
@@ -0,0 +1,24 @@
package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/pkg"
)
func Licenses(p pkg.Package) *cyclonedx.Licenses {
lc := cyclonedx.Licenses{}
for _, licenseName := range p.Licenses {
if value, exists := spdxlicense.ID(licenseName); exists {
lc = append(lc, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
ID: value,
},
})
}
}
if len(lc) > 0 {
return &lc
}
return nil
}
@@ -0,0 +1,83 @@
package cyclonedxhelpers
import (
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_License(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected *cyclonedx.Licenses
}{
{
name: "no licenses",
input: pkg.Package{},
expected: nil,
},
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: []string{
"made-up",
},
},
expected: nil,
},
{
name: "with SPDX license",
input: pkg.Package{
Licenses: []string{
"MIT",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}},
},
},
{
name: "with SPDX license expression",
input: pkg.Package{
Licenses: []string{
"MIT",
"GPL-3.0",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}},
{License: &cyclonedx.License{ID: "GPL-3.0"}},
},
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-3.0"}},
},
},
{
name: "debian to spdx conversion",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-2.0"}},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Licenses(test.input))
})
}
}
@@ -0,0 +1,68 @@
package cyclonedxhelpers
import (
"fmt"
"reflect"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
func Properties(p pkg.Package) *[]cyclonedx.Property {
props := []cyclonedx.Property{}
props = append(props, *getCycloneDXProperties(p)...)
if len(p.Locations) > 0 {
for _, l := range p.Locations {
props = append(props, *getCycloneDXProperties(l.Coordinates)...)
}
}
if hasMetadata(p) {
props = append(props, *getCycloneDXProperties(p.Metadata)...)
}
if len(props) > 0 {
return &props
}
return nil
}
func getCycloneDXProperties(m interface{}) *[]cyclonedx.Property {
props := []cyclonedx.Property{}
structValue := reflect.ValueOf(m)
// we can only handle top level structs as interfaces for now
if structValue.Kind() != reflect.Struct {
return &props
}
structType := structValue.Type()
for i := 0; i < structValue.NumField(); i++ {
if name, value := getCycloneDXPropertyName(structType.Field(i)), getCycloneDXPropertyValue(structValue.Field(i)); name != "" && value != "" {
props = append(props, cyclonedx.Property{
Name: name,
Value: value,
})
}
}
return &props
}
func getCycloneDXPropertyName(field reflect.StructField) string {
if value, exists := field.Tag.Lookup("cyclonedx"); exists {
return value
}
return ""
}
func getCycloneDXPropertyValue(field reflect.Value) string {
if field.IsZero() {
return ""
}
switch field.Kind() {
case reflect.String, reflect.Bool, reflect.Int, reflect.Float32, reflect.Float64, reflect.Complex128, reflect.Complex64:
if field.CanInterface() {
return fmt.Sprint(field.Interface())
}
return ""
case reflect.Ptr:
return getCycloneDXPropertyValue(reflect.Indirect(field))
}
return ""
}
@@ -0,0 +1,138 @@
package cyclonedxhelpers
import (
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
func Test_Properties(t *testing.T) {
epoch := 2
tests := []struct {
name string
input pkg.Package
expected *[]cyclonedx.Property
}{
{
name: "no metadata",
input: pkg.Package{},
expected: nil,
},
{
name: "from apk",
input: pkg.Package{
FoundBy: "cataloger",
Locations: []source.Location{
{Coordinates: source.Coordinates{RealPath: "test"}},
},
Metadata: pkg.ApkMetadata{
Package: "libc-utils",
OriginPackage: "libc-dev",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "0.7.2-r0",
License: "BSD",
Architecture: "x86_64",
URL: "http://alpinelinux.org",
Description: "Meta package to pull in correct libc",
Size: 0,
InstalledSize: 4096,
PullDependencies: "musl-utils",
PullChecksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
GitCommitOfAport: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
Files: []pkg.ApkFileRecord{},
},
},
expected: &[]cyclonedx.Property{
{Name: "foundBy", Value: "cataloger"},
{Name: "path", Value: "test"},
{Name: "originPackage", Value: "libc-dev"},
{Name: "installedSize", Value: "4096"},
{Name: "pullDependencies", Value: "musl-utils"},
{Name: "pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
{Name: "gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
},
},
{
name: "from dpkg",
input: pkg.Package{
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "tzdata",
Version: "2020a-0+deb10u1",
Source: "tzdata-dev",
SourceVersion: "1.0",
Architecture: "all",
InstalledSize: 3036,
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
Files: []pkg.DpkgFileRecord{},
},
},
expected: &[]cyclonedx.Property{
{Name: "metadataType", Value: "DpkgMetadata"},
{Name: "source", Value: "tzdata-dev"},
{Name: "sourceVersion", Value: "1.0"},
{Name: "installedSize", Value: "3036"},
},
},
{
name: "from go bin",
input: pkg.Package{
Name: "golang.org/x/net",
Version: "v0.0.0-20211006190231-62292e806868",
Language: pkg.Go,
Type: pkg.GoModulePkg,
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: "1.17",
Architecture: "amd64",
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
expected: &[]cyclonedx.Property{
{Name: "language", Value: pkg.Go.String()},
{Name: "type", Value: "go-module"},
{Name: "metadataType", Value: "GolangBinMetadata"},
{Name: "goCompiledVersion", Value: "1.17"},
{Name: "architecture", Value: "amd64"},
{Name: "h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
},
},
{
name: "from rpm",
input: pkg.Package{
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
Name: "dive",
Epoch: &epoch,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
Files: []pkg.RpmdbFileRecord{},
},
},
expected: &[]cyclonedx.Property{
{Name: "type", Value: "rpm"},
{Name: "metadataType", Value: "RpmdbMetadata"},
{Name: "epoch", Value: "2"},
{Name: "release", Value: "1"},
{Name: "sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
{Name: "size", Value: "12406784"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Properties(test.input))
})
}
}
@@ -0,0 +1,19 @@
package cyclonedxhelpers
import (
"github.com/anchore/syft/syft/pkg"
)
func Publisher(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Maintainer
case pkg.RpmdbMetadata:
return metadata.Vendor
case pkg.DpkgMetadata:
return metadata.Maintainer
}
}
return ""
}
@@ -0,0 +1,65 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Publisher(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
name: "from rpm",
input: pkg.Package{
Metadata: pkg.RpmdbMetadata{
Vendor: "auth",
},
},
expected: "auth",
},
{
name: "from dpkg",
input: pkg.Package{
Metadata: pkg.DpkgMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Publisher(test.input))
})
}
}
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Description(p *pkg.Package) string {
func Description(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@@ -14,10 +14,6 @@ func Description(p *pkg.Package) string {
return ""
}
func packageExists(p *pkg.Package) bool {
return p != nil
}
func hasMetadata(p *pkg.Package) bool {
return packageExists(p) && p.Metadata != nil
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
@@ -50,7 +50,7 @@ func Test_Description(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Description(&test.input))
assert.Equal(t, test.expected, Description(test.input))
})
}
}
@@ -0,0 +1,31 @@
package spdxhelpers
import (
"fmt"
"path"
"strings"
"github.com/anchore/syft/syft/source"
)
func DocumentName(srcMetadata source.Metadata) (string, error) {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanName(srcMetadata.ImageMetadata.UserInput), nil
case source.DirectoryScheme, source.FileScheme:
return cleanName(srcMetadata.Path), nil
}
return "", fmt.Errorf("unable to determine document name from scheme=%q", srcMetadata.Scheme)
}
func cleanName(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,73 @@
package spdxhelpers
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/source"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
)
func Test_DocumentName(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
tests := []struct {
name string
inputName string
srcMetadata source.Metadata
expected string
}{
{
name: "image",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "image-repo/name:tag",
ID: "id",
ManifestDigest: "digest",
},
},
expected: "image-repo/name-tag",
},
{
name: "directory",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.DirectoryScheme,
Path: "some/path/to/place",
},
expected: "some/path/to/place",
},
{
name: "file",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.FileScheme,
Path: "some/path/to/place",
},
expected: "some/path/to/place",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := DocumentName(test.srcMetadata)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual name %q", actual))
// track each scheme tested (passed or not)
testedSchemes.Add(string(test.srcMetadata.Scheme))
})
}
// assert all possible schemes were under test
assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test")
}
@@ -0,0 +1,45 @@
package spdxhelpers
import (
"fmt"
"net/url"
"path"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
func DocumentNameAndNamespace(srcMetadata source.Metadata) (string, string, error) {
name, err := DocumentName(srcMetadata)
if err != nil {
return "", "", err
}
return name, DocumentNamespace(name, srcMetadata), nil
}
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"
case source.FileScheme:
input = "file"
}
uniqueID := uuid.Must(uuid.NewRandom())
identifier := path.Join(input, uniqueID.String())
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
}
u := url.URL{
Scheme: "https",
Host: "anchore.com",
Path: path.Join(internal.ApplicationName, identifier),
}
return u.String()
}
@@ -0,0 +1,71 @@
package spdxhelpers
import (
"fmt"
"strings"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
)
func Test_documentNamespace(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
tests := []struct {
name string
inputName string
srcMetadata source.Metadata
expected string
}{
{
name: "image",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "image-repo/name:tag",
ID: "id",
ManifestDigest: "digest",
},
},
expected: "https://anchore.com/syft/image/my-name-",
},
{
name: "directory",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.DirectoryScheme,
Path: "some/path/to/place",
},
expected: "https://anchore.com/syft/dir/my-name-",
},
{
name: "file",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.FileScheme,
Path: "some/path/to/place",
},
expected: "https://anchore.com/syft/file/my-name-",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := DocumentNamespace(test.inputName, test.srcMetadata)
// note: since the namespace ends with a UUID we check the prefix
assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual namespace %q", actual))
// track each scheme tested (passed or not)
testedSchemes.Add(string(test.srcMetadata.Scheme))
})
}
// assert all possible schemes were under test
assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test")
}
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func DownloadLocation(p *pkg.Package) string {
func DownloadLocation(p pkg.Package) string {
// 3.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
@@ -48,7 +48,7 @@ func Test_DownloadLocation(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, DownloadLocation(&test.input))
assert.Equal(t, test.expected, DownloadLocation(test.input))
})
}
}
@@ -6,17 +6,13 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func ExternalRefs(p *pkg.Package) (externalRefs []model.ExternalRef) {
func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
externalRefs = make([]model.ExternalRef, 0)
if !packageExists(p) {
return externalRefs
}
for _, c := range p.CPEs {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.SecurityReferenceCategory,
ReferenceLocator: c.BindToFmtString(),
ReferenceLocator: pkg.CPEString(c),
ReferenceType: model.Cpe23ExternalRefType,
})
}
@@ -26,7 +26,7 @@ func Test_ExternalRefs(t *testing.T) {
expected: []model.ExternalRef{
{
ReferenceCategory: model.SecurityReferenceCategory,
ReferenceLocator: testCPE.BindToFmtString(),
ReferenceLocator: pkg.CPEString(testCPE),
ReferenceType: model.Cpe23ExternalRefType,
},
{
@@ -39,7 +39,7 @@ func Test_ExternalRefs(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, ExternalRefs(&test.input))
assert.ElementsMatch(t, test.expected, ExternalRefs(test.input))
})
}
}
@@ -1,51 +0,0 @@
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
}
@@ -2,7 +2,7 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Homepage(p *pkg.Package) string {
func Homepage(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.GemMetadata:
@@ -50,7 +50,7 @@ func Test_Homepage(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Homepage(&test.input))
assert.Equal(t, test.expected, Homepage(test.input))
})
}
}
@@ -7,7 +7,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func License(p *pkg.Package) string {
func License(p pkg.Package) string {
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Appendix IV;
@@ -17,7 +17,7 @@ func License(p *pkg.Package) string {
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if !packageExists(p) || len(p.Licenses) == 0 {
if len(p.Licenses) == 0 {
return "NONE"
}
@@ -67,7 +67,7 @@ func Test_License(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, License(&test.input))
assert.Equal(t, test.expected, License(test.input))
})
}
}
@@ -29,7 +29,7 @@ func Test_Originator(t *testing.T) {
},
},
},
expected: "auth1",
expected: "Person: auth1",
},
{
name: "from npm",
@@ -38,7 +38,7 @@ func Test_Originator(t *testing.T) {
Author: "auth",
},
},
expected: "auth",
expected: "Person: auth",
},
{
name: "from apk",
@@ -47,7 +47,7 @@ func Test_Originator(t *testing.T) {
Maintainer: "auth",
},
},
expected: "auth",
expected: "Person: auth",
},
{
name: "from python - just name",
@@ -56,7 +56,7 @@ func Test_Originator(t *testing.T) {
Author: "auth",
},
},
expected: "auth",
expected: "Person: auth",
},
{
name: "from python - just email",
@@ -65,7 +65,7 @@ func Test_Originator(t *testing.T) {
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth@auth.gov",
expected: "Person: auth@auth.gov",
},
{
name: "from python - both name and email",
@@ -75,7 +75,7 @@ func Test_Originator(t *testing.T) {
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth <auth@auth.gov>",
expected: "Person: auth (auth@auth.gov)",
},
{
name: "from rpm",
@@ -84,7 +84,7 @@ func Test_Originator(t *testing.T) {
Vendor: "auth",
},
},
expected: "auth",
expected: "Organization: auth",
},
{
name: "from dpkg",
@@ -93,7 +93,7 @@ func Test_Originator(t *testing.T) {
Maintainer: "auth",
},
},
expected: "auth",
expected: "Person: auth",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
@@ -108,7 +108,7 @@ func Test_Originator(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Originator(&test.input))
assert.Equal(t, test.expected, Originator(test.input))
})
}
}
@@ -6,31 +6,35 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Originator(p *pkg.Package) string {
// Originator needs to conform to the SPDX spec here:
// https://spdx.github.io/spdx-spec/package-information/#76-package-originator-field
// Available options are: <omit>, NOASSERTION, Person: <person>, Organization: <org>
func Originator(p pkg.Package) string {
if hasMetadata(p) {
author := ""
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Maintainer
author = metadata.Maintainer
case pkg.NpmPackageJSONMetadata:
return metadata.Author
author = metadata.Author
case pkg.PythonPackageMetadata:
author := metadata.Author
author = metadata.Author
if author == "" {
return metadata.AuthorEmail
author = metadata.AuthorEmail
} else if metadata.AuthorEmail != "" {
author = fmt.Sprintf("%s (%s)", author, metadata.AuthorEmail)
}
if metadata.AuthorEmail != "" {
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
}
return author
case pkg.GemMetadata:
if len(metadata.Authors) > 0 {
return metadata.Authors[0]
author = metadata.Authors[0]
}
return ""
case pkg.RpmdbMetadata:
return metadata.Vendor
return "Organization: " + metadata.Vendor
case pkg.DpkgMetadata:
return metadata.Maintainer
author = metadata.Maintainer
}
if author != "" {
return "Person: " + author
}
}
return ""
@@ -6,11 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func SourceInfo(p *pkg.Package) string {
if !packageExists(p) {
return ""
}
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
case pkg.RpmPkg:
@@ -31,6 +27,8 @@ 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,14 +19,8 @@ func Test_SourceInfo(t *testing.T) {
input: pkg.Package{
// note: no type given
Locations: []source.Location{
{
RealPath: "/a-place",
VirtualPath: "/b-place",
},
{
RealPath: "/c-place",
VirtualPath: "/d-place",
},
source.NewVirtualLocation("/a-place", "/b-place"),
source.NewVirtualLocation("/c-place", "/d-place"),
},
},
expected: []string{
@@ -124,6 +118,14 @@ 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 {
@@ -131,7 +133,7 @@ func Test_SourceInfo(t *testing.T) {
if test.input.Type != "" {
pkgTypes = append(pkgTypes, test.input.Type)
}
actual := SourceInfo(&test.input)
actual := SourceInfo(test.input)
for _, expected := range test.expected {
assert.Contains(t, actual, expected)
}
+54 -22
View File
@@ -2,14 +2,15 @@ package testutils
import (
"bytes"
"strings"
"testing"
"github.com/anchore/go-presenter"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
@@ -31,7 +32,7 @@ func FromSnapshot() ImageOption {
}
}
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
// grab the latest image contents and persist
@@ -39,11 +40,11 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
imagetest.UpdateGoldenFixtureImage(t, testImage)
}
err := pres.Present(&buffer)
err := format.Encode(&buffer, sbom)
assert.NoError(t, err)
actual := buffer.Bytes()
// replace the expected snapshot contents with the current presenter contents
// replace the expected snapshot contents with the current encoder contents
if updateSnapshot {
testutils.UpdateGoldenFileContents(t, actual)
}
@@ -51,6 +52,7 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which should be tested independently
redactors = append(redactors, carriageRedactor)
for _, r := range redactors {
actual = r(actual)
expected = r(expected)
@@ -64,14 +66,14 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
}
}
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
err := pres.Present(&buffer)
err := format.Encode(&buffer, sbom)
assert.NoError(t, err)
actual := buffer.Bytes()
// replace the expected snapshot contents with the current presenter contents
// replace the expected snapshot contents with the current encoder contents
if updateSnapshot {
testutils.UpdateGoldenFileContents(t, actual)
}
@@ -79,6 +81,7 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which should be tested independently
redactors = append(redactors, carriageRedactor)
for _, r := range redactors {
actual = r(actual)
expected = r(expected)
@@ -87,6 +90,8 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Logf("len: %d\nexpected: %s", len(expected), expected)
t.Logf("len: %d\nactual: %s", len(actual), actual)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
@@ -115,25 +120,42 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
src, err := source.NewFromImage(img, "user-image-input")
assert.NoError(t, err)
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &dist,
LinuxDistribution: &linux.Release{
PrettyName: "debian",
Name: "debian",
ID: "debian",
IDLike: []string{"like!"},
Version: "1.2.3",
VersionID: "1.2.3",
},
},
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",
},
},
}
}
func carriageRedactor(s []byte) []byte {
msg := strings.ReplaceAll(string(s), "\r\n", "\n")
return []byte(msg)
}
func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
@@ -154,7 +176,6 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
@@ -177,18 +198,31 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
func DirectoryInput(t testing.TB) sbom.SBOM {
catalog := newDirectoryCatalog()
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
src, err := source.NewFromDirectory("/some/path")
assert.NoError(t, err)
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &dist,
LinuxDistribution: &linux.Release{
PrettyName: "debian",
Name: "debian",
ID: "debian",
IDLike: []string{"like!"},
Version: "1.2.3",
VersionID: "1.2.3",
},
},
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",
},
},
}
}
@@ -197,13 +231,12 @@ func newDirectoryCatalog() *pkg.Catalog {
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
source.NewLocation("/some/path/pkg1"),
},
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
@@ -223,13 +256,12 @@ func newDirectoryCatalog() *pkg.Catalog {
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
source.NewLocation("/some/path/pkg1"),
},
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
@@ -1,26 +0,0 @@
package cyclonedx12xml
import (
"encoding/xml"
"io"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
enc := xml.NewEncoder(output)
enc.Indent("", " ")
_, err := output.Write([]byte(xml.Header))
if err != nil {
return err
}
err = enc.Encode(toFormatModel(s))
if err != nil {
return err
}
_, err = output.Write([]byte("\n"))
return err
}
@@ -1,31 +0,0 @@
package model
import (
"encoding/xml"
)
// Source: https://cyclonedx.org/ext/bom-descriptor/
// BomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
type BomDescriptor struct {
XMLName xml.Name `xml:"metadata"`
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
Tools []BomDescriptorTool `xml:"tools>tool"` // The tool used to create the BOM.
Component *BomDescriptorComponent `xml:"component"` // The component that the BOM describes.
}
// BomDescriptorTool represents the tool that created the BOM report.
type BomDescriptorTool struct {
XMLName xml.Name `xml:"tool"`
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
// TODO: hashes, author, manufacture, supplier
// TODO: add user-defined fields for the remaining build/version parameters
}
// BomDescriptorComponent represents the software/package being cataloged.
type BomDescriptorComponent struct {
XMLName xml.Name `xml:"component"`
Component
}
@@ -1,20 +0,0 @@
package model
import "encoding/xml"
// Component represents a single element in the CycloneDX BOM
type Component struct {
XMLName xml.Name `xml:"component"`
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
Name string `xml:"name"` // Required; The name of the component as defined by the project
Version string `xml:"version"` // Required; The version of the component as defined by the project
Description string `xml:"description,omitempty"` // A description of the component
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
}
@@ -1,17 +0,0 @@
package model
import (
"encoding/xml"
)
// Source: https://github.com/CycloneDX/specification
// Document represents a CycloneDX BOM Document.
type Document struct {
XMLName xml.Name `xml:"bom"`
XMLNs string `xml:"xmlns,attr"`
Version int `xml:"version,attr"`
SerialNumber string `xml:"serialNumber,attr"`
BomDescriptor *BomDescriptor `xml:"metadata"` // The BOM descriptor extension
Components []Component `xml:"components>component"` // The BOM contents
}
@@ -1,10 +0,0 @@
package model
import "encoding/xml"
// License represents a single software license for a Component
type License struct {
XMLName xml.Name `xml:"license"`
ID string `xml:"id,omitempty"` // A valid SPDX license ID
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
}
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
<metadata>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="file">
<name>/some/path</name>
<version></version>
</component>
</metadata>
<components>
<component type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
<license>
<name>MIT</name>
</license>
</licenses>
<purl>a-purl-2</purl>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<purl>a-purl-2</purl>
</component>
</components>
</bom>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
<metadata>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component>
</metadata>
<components>
<component type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
<license>
<name>MIT</name>
</license>
</licenses>
<purl>a-purl-1</purl>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<purl>a-purl-2</purl>
</component>
</components>
</bom>
@@ -1,95 +0,0 @@
package cyclonedx12xml
import (
"encoding/xml"
"time"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/cyclonedx12xml/model"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// toFormatModel creates and populates a new in-memory representation of a CycloneDX 1.2 document
func toFormatModel(s sbom.SBOM) model.Document {
versionInfo := version.FromBuild()
doc := model.Document{
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
Version: 1,
SerialNumber: uuid.New().URN(),
BomDescriptor: toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source),
}
// attach components
for _, p := range s.Artifacts.PackageCatalog.Sorted() {
doc.Components = append(doc.Components, toComponent(p))
}
return doc
}
func toComponent(p *pkg.Package) model.Component {
return model.Component{
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
PackageURL: p.PURL,
Licenses: toLicenses(p.Licenses),
}
}
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *model.BomDescriptor {
return &model.BomDescriptor{
XMLName: xml.Name{},
Timestamp: time.Now().Format(time.RFC3339),
Tools: []model.BomDescriptorTool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
Component: toBomDescriptorComponent(srcMetadata),
}
}
func toBomDescriptorComponent(srcMetadata source.Metadata) *model.BomDescriptorComponent {
switch srcMetadata.Scheme {
case source.ImageScheme:
return &model.BomDescriptorComponent{
Component: model.Component{
Type: "container",
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
},
}
case source.DirectoryScheme:
return &model.BomDescriptorComponent{
Component: model.Component{
Type: "file",
Name: srcMetadata.Path,
},
}
}
return nil
}
func toLicenses(licenses []string) *[]model.License {
if len(licenses) == 0 {
return nil
}
var result []model.License
for _, licenseName := range licenses {
result = append(result, model.License{
Name: licenseName,
})
}
return &result
}
@@ -0,0 +1,18 @@
package cyclonedx13json
import (
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON)
enc.SetPretty(true)
err := enc.Encode(bom)
return err
}
@@ -0,0 +1,41 @@
package cyclonedx13json
import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
func TestCycloneDxDirectoryEncoder(t *testing.T) {
testutils.AssertEncoderAgainstGoldenSnapshot(t,
Format(),
testutils.DirectoryInput(t),
*updateCycloneDx,
cycloneDxRedactor,
)
}
func TestCycloneDxImageEncoder(t *testing.T) {
testImage := "image-simple"
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
Format(),
testutils.ImageInput(t, testImage),
testImage,
*updateCycloneDx,
cycloneDxRedactor,
)
}
func cycloneDxRedactor(s []byte) []byte {
serialPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
sha256Pattern := regexp.MustCompile(`sha256:[A-Fa-f0-9]{64}`)
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, sha256Pattern} {
s = pattern.ReplaceAll(s, []byte("redacted"))
}
return s
}
@@ -0,0 +1,12 @@
package cyclonedx13json
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.CycloneDxJSONOption,
encoder,
nil,
nil,
)
}
@@ -0,0 +1,103 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950",
"version": 1,
"metadata": {
"timestamp": "2022-01-14T22:47:00Z",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "[not provided]"
}
],
"component": {
"type": "file",
"name": "/some/path",
"version": ""
}
},
"components": [
{
"type": "library",
"name": "package-1",
"version": "1.0.1",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"value": "the-cataloger-1"
},
{
"name": "language",
"value": "python"
},
{
"name": "type",
"value": "python"
},
{
"name": "metadataType",
"value": "PythonPackageMetadata"
},
{
"name": "path",
"value": "/some/path/pkg1"
}
]
},
{
"type": "library",
"name": "package-2",
"version": "2.0.1",
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"value": "the-cataloger-2"
},
{
"name": "type",
"value": "deb"
},
{
"name": "metadataType",
"value": "DpkgMetadata"
},
{
"name": "path",
"value": "/some/path/pkg1"
}
]
},
{
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
"properties": [
{
"name": "prettyName",
"value": "debian"
},
{
"name": "id",
"value": "debian"
},
{
"name": "versionID",
"value": "1.2.3"
}
]
}
]
}
@@ -0,0 +1,111 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0",
"version": 1,
"metadata": {
"timestamp": "2022-01-14T22:47:00Z",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "[not provided]"
}
],
"component": {
"type": "container",
"name": "user-image-input",
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
}
},
"components": [
{
"type": "library",
"name": "package-1",
"version": "1.0.1",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
"purl": "a-purl-1",
"properties": [
{
"name": "foundBy",
"value": "the-cataloger-1"
},
{
"name": "language",
"value": "python"
},
{
"name": "type",
"value": "python"
},
{
"name": "metadataType",
"value": "PythonPackageMetadata"
},
{
"name": "path",
"value": "/somefile-1.txt"
},
{
"name": "layerID",
"value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
}
]
},
{
"type": "library",
"name": "package-2",
"version": "2.0.1",
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"value": "the-cataloger-2"
},
{
"name": "type",
"value": "deb"
},
{
"name": "metadataType",
"value": "DpkgMetadata"
},
{
"name": "path",
"value": "/somefile-2.txt"
},
{
"name": "layerID",
"value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
}
]
},
{
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
"properties": [
{
"name": "prettyName",
"value": "debian"
},
{
"name": "id",
"value": "debian"
},
{
"name": "versionID",
"value": "1.2.3"
}
]
}
]
}
@@ -0,0 +1,18 @@
package cyclonedx13xml
import (
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML)
enc.SetPretty(true)
err := enc.Encode(bom)
return err
}
@@ -1,4 +1,4 @@
package cyclonedx12xml
package cyclonedx13xml
import (
"flag"
@@ -8,20 +8,22 @@ import (
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
func TestCycloneDxDirectoryPresenter(t *testing.T) {
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(testutils.DirectoryInput(t)),
func TestCycloneDxDirectoryEncoder(t *testing.T) {
testutils.AssertEncoderAgainstGoldenSnapshot(t,
Format(),
testutils.DirectoryInput(t),
*updateCycloneDx,
cycloneDxRedactor,
)
}
func TestCycloneDxImagePresenter(t *testing.T) {
func TestCycloneDxImageEncoder(t *testing.T) {
testImage := "image-simple"
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
Format().Presenter(testutils.ImageInput(t, testImage)),
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
Format(),
testutils.ImageInput(t, testImage),
testImage,
*updateCycloneDx,
cycloneDxRedactor,
@@ -31,8 +33,9 @@ func TestCycloneDxImagePresenter(t *testing.T) {
func cycloneDxRedactor(s []byte) []byte {
serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
sha256Pattern := regexp.MustCompile(`sha256:[A-Fa-f0-9]{64}`)
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} {
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, sha256Pattern} {
s = pattern.ReplaceAll(s, []byte("redacted"))
}
return s
@@ -1,10 +1,10 @@
package cyclonedx12xml
package cyclonedx13xml
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.CycloneDxOption,
format.CycloneDxXMLOption,
encoder,
nil,
nil,
@@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-2.txt
@@ -0,0 +1 @@
this file has contents
@@ -0,0 +1 @@
file-2 contents!
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:16a426e7-fcc7-4b94-abd2-66c67569cc44" version="1">
<metadata>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="file">
<name>/some/path</name>
<version></version>
</component>
</metadata>
<components>
<component type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
<license>
<id>MIT</id>
</license>
</licenses>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/some/path/pkg1</property>
</properties>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/some/path/pkg1</property>
</properties>
</component>
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
</properties>
</component>
</components>
</bom>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:a3d776c6-a2ca-4116-8b99-b3538dd1a460" version="1">
<metadata>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component>
</metadata>
<components>
<component type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
<license>
<id>MIT</id>
</license>
</licenses>
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
<purl>a-purl-1</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/somefile-1.txt</property>
<property name="layerID">sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab</property>
</properties>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/somefile-2.txt</property>
<property name="layerID">sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67</property>
</properties>
</component>
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
</properties>
</component>
</components>
</bom>

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