Compare commits

...
Author SHA1 Message Date
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
103 changed files with 5491 additions and 1810 deletions
+1
View File
@@ -101,6 +101,7 @@ bootstrap-tools: $(TEMPDIR)
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
.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:
+6 -9
View File
@@ -11,15 +11,11 @@
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
### Join us at the virtual OSS meetup!
### Join our community meetings!
When: December 1, 2021 @ 11am-noon PT (2-3pm ET)
We’ve got security experts from VMware and Anchore who will share their tips, tricks, and lessons learned on securing containers using [Syft](https://github.com/anchore/syft), [Grype](https://github.com/anchore/grype), and [Cartographer](https://github.com/vmware-tanzu/cartographer).
Register now: https://get.anchore.com/anchore-oss-meetup-dec-1-2021/
---
- 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)
@@ -108,7 +104,8 @@ 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).
+3 -2
View File
@@ -3,6 +3,7 @@ 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/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
@@ -12,13 +13,13 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89
// 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 v20.10.10+incompatible
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
+16 -5
View File
@@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CycloneDX/cyclonedx-go v0.4.0 h1:Wz4QZ9B4RXGWIWTypVLEOVJgOdFfy5mcS5PGNzUkZxU=
github.com/CycloneDX/cyclonedx-go v0.4.0/go.mod h1:rmRcf//gT7PIzovatusbWi377xqCg1FS4jyST0GH20E=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@@ -78,6 +80,7 @@ github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2
github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=
github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -108,8 +111,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d h1:ieKMmJk1IcmdviUv/KGO1DAk0yFxoGwDpwGq8ckVeVU=
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d/go.mod h1:Zzs5pLx2ZtUctlER7bNDAucDmsc8RO2g/oL3for1tVA=
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89 h1:pcMFN7wjIQIxdZm8NA0R3JiRRFn5Eyx9mhagHOVS4Bw=
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89/go.mod h1:FNm1rtauEjkC3elA3jr3bJkU+/4QiovApwJdPnHQ9x0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
@@ -137,11 +140,14 @@ github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQm
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -192,13 +198,15 @@ github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM=
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw=
github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@@ -233,6 +241,7 @@ github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDG
github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
@@ -290,8 +299,9 @@ github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TT
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM=
github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -651,8 +661,9 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7 h1:axgApq2XShTLwQii2zAnIkMPlhGVHbAXHUcHezu5G/k=
github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+5 -3
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,6 +65,7 @@ execute() {
}
get_binaries() {
case "$PLATFORM" in
darwin/arm64) BINARIES="syft" ;;
darwin/amd64) BINARIES="syft" ;;
linux/amd64) BINARIES="syft" ;;
windows/amd64) BINARIES="syft" ;;
@@ -97,6 +96,9 @@ adjust_format() {
darwin) FORMAT=dmg ;;
windows) FORMAT=zip ;;
esac
case "${PLATFORM}" in
darwin/arm64) FORMAT=zip ;;
esac
true
}
adjust_os() {
+1 -1
View File
@@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON presenter
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "2.0.0"
JSONSchemaVersion = "2.0.1"
)
@@ -0,0 +1,93 @@
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/pkg"
"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] = toComponent(p)
}
cdxBOM.Components = &components
return cdxBOM
}
// 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 toComponent(p pkg.Package) cyclonedx.Component {
return cyclonedx.Component{
Type: cyclonedx.ComponentTypeLibrary,
Name: p.Name,
Version: p.Version,
PackageURL: p.PURL,
Licenses: toLicenses(p.Licenses),
}
}
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
}
func toLicenses(ls []string) *cyclonedx.Licenses {
if len(ls) == 0 {
return nil
}
lc := make(cyclonedx.Licenses, len(ls))
for i, licenseName := range ls {
lc[i] = cyclonedx.LicenseChoice{
License: &cyclonedx.License{
Name: licenseName,
},
}
}
return &lc
}
@@ -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")
}
@@ -87,6 +87,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))
}
}
@@ -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,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,39 @@
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 presenters")
func TestCycloneDxDirectoryPresenter(t *testing.T) {
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(testutils.DirectoryInput(t)),
*updateCycloneDx,
cycloneDxRedactor,
)
}
func TestCycloneDxImagePresenter(t *testing.T) {
testImage := "image-simple"
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
Format().Presenter(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]))`)
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} {
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,42 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:a81dc685-cf22-48e0-bda5-65ea1a8bca5b",
"version": 1,
"metadata": {
"timestamp": "2021-12-03T13:17:26-08:00",
"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": {
"name": "MIT"
}
}
],
"purl": "a-purl-2"
},
{
"type": "library",
"name": "package-2",
"version": "2.0.1",
"purl": "a-purl-2"
}
]
}
@@ -0,0 +1,42 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:2156ac1f-c838-4e93-8dc5-a3874ffeb967",
"version": 1,
"metadata": {
"timestamp": "2021-12-03T13:17:26-08:00",
"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": {
"name": "MIT"
}
}
],
"purl": "a-purl-1"
},
{
"type": "library",
"name": "package-2",
"version": "2.0.1",
"purl": "a-purl-2"
}
]
}
@@ -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"
@@ -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!
@@ -1,7 +1,7 @@
<?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">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:7b1c3b1d-ea3b-4022-9dcc-80f4b4cbce36" version="1">
<metadata>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@@ -31,4 +31,4 @@
<purl>a-purl-2</purl>
</component>
</components>
</bom>
</bom>
@@ -1,7 +1,7 @@
<?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">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:66bda3e1-888a-4d43-b906-7fd96d428753" version="1">
<metadata>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@@ -31,4 +31,4 @@
<purl>a-purl-2</purl>
</component>
</components>
</bom>
</bom>
+4 -2
View File
@@ -3,7 +3,8 @@ package formats
import (
"bytes"
"github.com/anchore/syft/internal/formats/cyclonedx12xml"
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
"github.com/anchore/syft/internal/formats/syftjson"
@@ -17,7 +18,8 @@ func All() []format.Format {
return []format.Format{
syftjson.Format(),
table.Format(),
cyclonedx12xml.Format(),
cyclonedx13xml.Format(),
cyclonedx13json.Format(),
spdx22json.Format(),
spdx22tagvalue.Format(),
text.Format(),
+5 -4
View File
@@ -7,15 +7,16 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const anchoreNamespace = "https://anchore.com/syft"
func encoder(output io.Writer, s sbom.SBOM) error {
doc := toFormatModel(s)
doc, err := toFormatModel(s)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
return enc.Encode(doc)
}
@@ -3,7 +3,7 @@
"name": "/some/path",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-11-17T19:35:54.834877Z",
"created": "2021-12-01T15:08:29.469369Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
@@ -11,7 +11,7 @@
"licenseListVersion": "3.15"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https:/anchore.com/syft/dir/some/path-65e2226e-a61e-4ed1-81bb-56022e1ff1eb",
"documentNamespace": "https://anchore.com/syft/dir/some/path-f4586501-2da6-4541-a8e9-232b32f25e9a",
"packages": [
{
"SPDXID": "SPDXRef-2a115ac97d018a0e",
@@ -3,7 +3,7 @@
"name": "user-image-input",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-11-17T19:35:57.761372Z",
"created": "2021-12-01T15:08:29.476498Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
@@ -11,7 +11,7 @@
"licenseListVersion": "3.15"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-5383918f-ec96-4aa9-b756-ad16e1ada31e",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-e3b7637c-9b2f-4005-a683-58e60f979082",
"packages": [
{
"SPDXID": "SPDXRef-888661d4f0362f02",
+8 -48
View File
@@ -2,7 +2,6 @@ package spdx22json
import (
"fmt"
"path"
"path/filepath"
"sort"
"strings"
@@ -19,14 +18,16 @@ import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
func toFormatModel(s sbom.SBOM) model.Document {
name := documentName(s.Source)
func toFormatModel(s sbom.SBOM) (*model.Document, error) {
name, namespace, err := spdxhelpers.DocumentNameAndNamespace(s.Source)
if err != nil {
return nil, err
}
return model.Document{
return &model.Document{
Element: model.Element{
SPDXID: model.ElementID("DOCUMENT").String(),
Name: name,
@@ -42,52 +43,11 @@ func toFormatModel(s sbom.SBOM) model.Document {
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: documentNamespace(name, s.Source),
DocumentNamespace: namespace,
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
}
}
func documentName(srcMetadata source.Metadata) string {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
case source.DirectoryScheme:
return cleanSPDXName(srcMetadata.Path)
}
// TODO: is this alright?
return uuid.Must(uuid.NewRandom()).String()
}
func cleanSPDXName(name string) string {
// remove # according to specification
name = strings.ReplaceAll(name, "#", "-")
// remove : for url construction
name = strings.ReplaceAll(name, ":", "-")
// clean relative pathing
return path.Clean(name)
}
func documentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
input = "image"
case source.DirectoryScheme:
input = "dir"
}
uniqueID := uuid.Must(uuid.NewRandom())
identifier := path.Join(input, uniqueID.String())
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
}
return path.Join(anchoreNamespace, identifier)
}, nil
}
func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []model.Package {
+5 -2
View File
@@ -8,6 +8,9 @@ import (
)
func encoder(output io.Writer, s sbom.SBOM) error {
model := toFormatModel(s)
return tvsaver.Save2_2(&model, output)
model, err := toFormatModel(s)
if err != nil {
return err
}
return tvsaver.Save2_2(model, output)
}
@@ -32,6 +32,10 @@ func TestSPDXTagValueImagePresenter(t *testing.T) {
func spdxTagValueRedactor(s []byte) []byte {
// each SBOM reports the time it was generated, which is not useful during snapshot testing
s = regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
s = regexp.MustCompile(`DocumentNamespace: https://anchore.com/syft/.*`).ReplaceAll(s, []byte("redacted"))
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
return regexp.MustCompile(`LicenseListVersion: .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -1,11 +1,12 @@
SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentNamespace: https://anchore.com/syft/image/
LicenseListVersion: 3.13
DocumentName: /some/path
DocumentNamespace: https://anchore.com/syft/dir/some/path-22f5732a-cab0-4376-9b79-15e413049500
LicenseListVersion: 3.15
Creator: Organization: Anchore, Inc
Creator: Tool: syft-[not provided]
Created: 2021-06-23T17:49:25Z
Created: 2021-12-01T15:08:43Z
##### Package: package-2
@@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input
LicenseListVersion: 3.13
DocumentNamespace: https://anchore.com/syft/image/user-image-input-ce4d4ae5-9d79-4f84-a410-361e394c2908
LicenseListVersion: 3.15
Creator: Organization: Anchore, Inc
Creator: Tool: syft-[not provided]
Created: 2021-06-23T17:49:25Z
Created: 2021-12-01T15:08:44Z
##### Package: package-2
@@ -16,8 +16,12 @@ import (
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
// nolint:funlen
func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
return spdx.Document2_2{
func toFormatModel(s sbom.SBOM) (*spdx.Document2_2, error) {
name, namespace, err := spdxhelpers.DocumentNameAndNamespace(s.Source)
if err != nil {
return nil, err
}
return &spdx.Document2_2{
CreationInfo: &spdx.CreationInfo2_2{
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
// Cardinality: mandatory, one
@@ -33,7 +37,7 @@ func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
// 2.4: Document Name
// Cardinality: mandatory, one
DocumentName: s.Source.ImageMetadata.UserInput,
DocumentName: name,
// 2.5: Document Namespace
// Cardinality: mandatory, one
@@ -52,7 +56,7 @@ func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
// In many cases, the URI will point to a web accessible document, but this should not be assumed
// to be the case.
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", s.Source.ImageMetadata.UserInput),
DocumentNamespace: namespace,
// 2.6: External Document References
// Cardinality: optional, one or many
@@ -82,7 +86,7 @@ func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
DocumentComment: "",
},
Packages: toFormatPackages(s.Artifacts.PackageCatalog),
}
}, nil
}
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
@@ -83,7 +83,7 @@
}
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "2.0.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
}
}
@@ -179,7 +179,7 @@
}
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "2.0.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
}
}
@@ -104,7 +104,7 @@
}
},
"schema": {
"version": "2.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
"version": "2.0.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
}
}
@@ -202,6 +202,11 @@ func toSourceModel(src source.Metadata) (model.Source, error) {
Type: "directory",
Target: src.Path,
}, nil
case source.FileScheme:
return model.Source{
Type: "file",
Target: src.Path,
}, nil
default:
return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
@@ -0,0 +1,84 @@
package syftjson
import (
"testing"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_toSourceModel(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
tests := []struct {
name string
src source.Metadata
expected model.Source
}{
{
name: "directory",
src: source.Metadata{
Scheme: source.DirectoryScheme,
Path: "some/path",
},
expected: model.Source{
Type: "directory",
Target: "some/path",
},
},
{
name: "file",
src: source.Metadata{
Scheme: source.FileScheme,
Path: "some/path",
},
expected: model.Source{
Type: "file",
Target: "some/path",
},
},
{
name: "image",
src: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
expected: model.Source{
Type: "image",
Target: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// track each scheme tested (passed or not)
testedSchemes.Add(string(test.src.Scheme))
// assert the model transformation is correct
actual, err := toSourceModel(test.src)
require.NoError(t, err)
assert.Equal(t, test.expected, actual)
})
}
// assert all possible schemes were under test
assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test")
}
@@ -40,6 +40,11 @@ func toSyftSourceData(s model.Source) *source.Metadata {
Scheme: source.DirectoryScheme,
Path: s.Target.(string),
}
case "file":
return &source.Metadata{
Scheme: source.FileScheme,
Path: s.Target.(string),
}
case "image":
return &source.Metadata{
Scheme: source.ImageScheme,
@@ -0,0 +1,81 @@
package syftjson
import (
"testing"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/source"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
)
func Test_toSyftSourceData(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
tests := []struct {
name string
src model.Source
expected source.Metadata
}{
{
name: "directory",
expected: source.Metadata{
Scheme: source.DirectoryScheme,
Path: "some/path",
},
src: model.Source{
Type: "directory",
Target: "some/path",
},
},
{
name: "file",
expected: source.Metadata{
Scheme: source.FileScheme,
Path: "some/path",
},
src: model.Source{
Type: "file",
Target: "some/path",
},
},
{
name: "image",
expected: source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
src: model.Source{
Type: "image",
Target: source.ImageMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// assert the model transformation is correct
actual := toSyftSourceData(test.src)
assert.Equal(t, test.expected, *actual)
// track each scheme tested (passed or not)
testedSchemes.Add(string(test.expected.Scheme))
})
}
// assert all possible schemes were under test
assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test")
}
+1 -1
View File
@@ -16,7 +16,7 @@ func encoder(output io.Writer, s sbom.SBOM) error {
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
switch s.Source.Scheme {
case source.DirectoryScheme:
case source.DirectoryScheme, source.FileScheme:
fmt.Fprintf(w, "[Path: %s]\n", s.Source.Path)
case source.ImageScheme:
fmt.Fprintln(w, "[Image]")
+2 -1
View File
@@ -1 +1,2 @@
bom.xml
bom.xml
bom.json
+3 -1
View File
@@ -2,4 +2,6 @@
.PHONY: validate-schema
validate-schema:
go run ../../main.go ubuntu:latest -vv -o cyclonedx > bom.xml
xmllint --noout --schema ./cyclonedx.xsd bom.xml
xmllint --noout --schema ./cyclonedx.xsd bom.xml
go run ../../main.go ubuntu:latest -vv -o cyclonedx-json > bom.json
../../.tmp/yajsv -s bom-1.3.schema.json bom.json
File diff suppressed because it is too large Load Diff
+269 -56
View File
@@ -16,13 +16,13 @@ limitations under the License.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
xmlns:bom="http://cyclonedx.org/schema/bom/1.2"
xmlns:bom="http://cyclonedx.org/schema/bom/1.3"
xmlns:spdx="http://cyclonedx.org/schema/spdx"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/bom/1.2"
targetNamespace="http://cyclonedx.org/schema/bom/1.3"
vc:minVersion="1.0"
vc:maxVersion="1.1"
version="1.2">
version="1.3">
<xs:import namespace="http://cyclonedx.org/schema/spdx" schemaLocation="spdx.xsd"/>
@@ -32,9 +32,6 @@ limitations under the License.
<url>https://cyclonedx.org/</url>
<license uri="http://www.apache.org/licenses/LICENSE-2.0"
version="2.0">Apache License, Version 2.0</license>
<authors>
<author>Steve Springett</author>
</authors>
</xs:documentation>
</xs:annotation>
@@ -71,15 +68,23 @@ limitations under the License.
<xs:documentation>The component that the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="manufacture" type="bom:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<xs:element name="manufacture" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The organization that manufactured the component that the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The organization that supplied the component that the BOM describes. The
supplier may often be the manufacture, but may also be a distributor or repackager.</xs:documentation>
supplier may often be the manufacturer, but may also be a distributor or repackager.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document properties in a key/value store.
This provides flexibility to include data not officially supported in the standard
without having to use additional namespaces or create extensions.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
@@ -181,14 +186,14 @@ limitations under the License.
<xs:documentation>The name of the contact</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The email address of the contact. Multiple email addresses are allowed.</xs:documentation>
<xs:documentation>The email address of the contact.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The phone number of the contact. Multiple phone numbers are allowed.</xs:documentation>
<xs:documentation>The phone number of the contact.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
@@ -231,7 +236,7 @@ limitations under the License.
<xs:element name="supplier" type="bom:organizationalEntity" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The organization that supplied the component. The supplier may often
be the manufacture, but may also be a distributor or repackager.</xs:documentation>
be the manufacturer, but may also be a distributor or repackager.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="author" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
@@ -282,19 +287,7 @@ limitations under the License.
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:choice>
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A valid SPDX license expression.
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
<xs:element name="copyright" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>An optional copyright notice informing users of the underlying claims to
@@ -349,6 +342,13 @@ limitations under the License.
component or to the project the component describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document properties in a key/value store.
This provides flexibility to include data not officially supported in the standard
without having to use additional namespaces or create extensions.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="components" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
@@ -370,6 +370,11 @@ limitations under the License.
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="evidence" type="bom:componentEvidenceType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document evidence collected through various forms of extraction or analysis.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
@@ -512,45 +517,45 @@ limitations under the License.
<xs:simpleType name="classification">
<xs:restriction base="xs:string">
<xs:enumeration value="application">
<xs:annotation>
<xs:documentation>A software application. Refer to https://en.wikipedia.org/wiki/Application_software
<xs:annotation>
<xs:documentation>A software application. Refer to https://en.wikipedia.org/wiki/Application_software
for information about applications.</xs:documentation>
</xs:annotation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="framework">
<xs:annotation>
<xs:documentation>A software framework. Refer to https://en.wikipedia.org/wiki/Software_framework
for information on how frameworks vary slightly from libraries.</xs:documentation>
for information on how frameworks vary slightly from libraries.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="library">
<xs:annotation>
<xs:documentation>A software library. Refer to https://en.wikipedia.org/wiki/Library_(computing)
for information about libraries. All third-party and open source reusable components will likely
be a library. If the library also has key features of a framework, then it should be classified
as a framework. If not, or is unknown, then specifying library is recommended.</xs:documentation>
for information about libraries. All third-party and open source reusable components will likely
be a library. If the library also has key features of a framework, then it should be classified
as a framework. If not, or is unknown, then specifying library is recommended.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="container">
<xs:annotation>
<xs:documentation>A packaging and/or runtime format, not specific to any particular technology,
which isolates software inside the container from software outside of a container through
virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization</xs:documentation>
which isolates software inside the container from software outside of a container through
virtualization technology. Refer to https://en.wikipedia.org/wiki/OS-level_virtualization</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="operating-system">
<xs:annotation>
<xs:documentation>A software operating system without regard to deployment model
(i.e. installed on physical hardware, virtual machine, image, etc) Refer to
https://en.wikipedia.org/wiki/Operating_system</xs:documentation>
(i.e. installed on physical hardware, virtual machine, image, etc) Refer to
https://en.wikipedia.org/wiki/Operating_system</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="device">
<xs:annotation>
<xs:documentation>A hardware device such as a processor, or chip-set. A hardware device
containing firmware should include a component for the physical hardware itself, and another
component of type 'firmware' or 'operating-system' (whichever is relevant), describing
information about the software running on the device.</xs:documentation>
containing firmware should include a component for the physical hardware itself, and another
component of type 'firmware' or 'operating-system' (whichever is relevant), describing
information about the software running on the device.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="firmware">
@@ -562,7 +567,7 @@ limitations under the License.
<xs:enumeration value="file">
<xs:annotation>
<xs:documentation>A computer file. Refer to https://en.wikipedia.org/wiki/Computer_file
for information about files.</xs:documentation>
for information about files.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
@@ -782,6 +787,13 @@ limitations under the License.
<xs:documentation xml:lang="en">An optional comment describing the external reference</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="hash" type="bom:hashType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="type" type="bom:externalReferenceType" use="required">
<xs:annotation>
@@ -927,7 +939,7 @@ limitations under the License.
<xs:enumeration value="backport">
<xs:annotation>
<xs:documentation>A patch which takes code from a newer version of software and applies
it to older versions of the same software. Refer to https://en.wikipedia.org/wiki/Backporting</xs:documentation>
it to older versions of the same software. Refer to https://en.wikipedia.org/wiki/Backporting</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="cherry-pick">
@@ -1255,24 +1267,19 @@ limitations under the License.
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:choice>
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A valid SPDX license expression.
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
<xs:element name="externalReferences" type="bom:externalReferences" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document external references related to the service.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document properties in a key/value store.
This provides flexibility to include data not officially supported in the standard
without having to use additional namespaces or create extensions.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="services" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
@@ -1349,6 +1356,199 @@ limitations under the License.
</xs:restriction>
</xs:simpleType>
<xs:complexType name="licenseChoiceType">
<xs:choice>
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A valid SPDX license expression.
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:complexType name="copyrightsType">
<xs:sequence>
<xs:element name="text" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="componentEvidenceType">
<xs:sequence>
<xs:element name="licenses" type="bom:licenseChoiceType" minOccurs="0" maxOccurs="1"/>
<xs:element name="copyright" type="bom:copyrightsType" minOccurs="0" maxOccurs="1"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="compositionsType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="composition" type="bom:compositionType"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="compositionType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="aggregate" type="bom:aggregateType" default="not_specified">
<xs:annotation>
<xs:documentation>Specifies an aggregate type that describe how complete a relationship is.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="assemblies" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
The bom-ref identifiers of the components or services being described. Assemblies refer to
nested relationships whereby a constituent part may include other constituent parts. References
do not cascade to child parts. References are explicit for the specified constituent part only.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="assembly" type="bom:bomReferenceType"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="dependencies" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
The bom-ref identifiers of the components or services being described. Dependencies refer to a
relationship whereby an independent constituent part requires another independent constituent
part. References do not cascade to transitive dependencies. References are explicit for the
specified dependency only.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="dependency" type="bom:bomReferenceType"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="aggregateType">
<xs:restriction base="xs:string">
<xs:enumeration value="complete">
<xs:annotation>
<xs:documentation>The relationship is complete. No further relationships including constituent components, services, or dependencies exist.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="incomplete">
<xs:annotation>
<xs:documentation>The relationship is incomplete. Additional relationships exist and may include constituent components, services, or dependencies.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="incomplete_first_party_only">
<xs:annotation>
<xs:documentation>The relationship is incomplete. Only relationships for first-party components, services, or their dependencies are represented.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="incomplete_third_party_only">
<xs:annotation>
<xs:documentation>The relationship is incomplete. Only relationships for third-party components, services, or their dependencies are represented.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="unknown">
<xs:annotation>
<xs:documentation>The relationship may be complete or incomplete. This usually signifies a 'best-effort' to obtain constituent components, services, or dependencies but the completeness is inconclusive.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="not_specified">
<xs:annotation>
<xs:documentation>The relationship completeness is not specified.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="bomReferenceType">
<xs:attribute name="ref" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>References a component or service by the its bom-ref attribute</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="propertiesType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="property" type="bom:propertyType"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="propertyType">
<xs:annotation>
<xs:documentation>Specifies an individual property with a name and value.</xs:documentation>
</xs:annotation>
<xs:simpleContent>
<xs:extension base="xs:normalizedString">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name of the property. Duplicate names are allowed, each potentially having a different value.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:element name="bom">
<xs:complexType>
<xs:sequence>
@@ -1378,6 +1578,19 @@ limitations under the License.
<xs:documentation>Provides the ability to document dependency relationships.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="compositions" type="bom:compositionsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Compositions describe constituent parts (including components, services, and dependency relationships) and their completeness.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="properties" type="bom:propertiesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document properties in a name-value store.
This provides flexibility to include data not officially supported in the standard
without having to use additional namespaces or create extensions. Unlike key-value
stores, properties support duplicate names, each potentially having different values.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
+1226 -1146
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -35,6 +35,7 @@ type artifactMetadataContainer struct {
Python pkg.PythonPackageMetadata
Rpm pkg.RpmdbMetadata
Cargo pkg.CargoPackageMetadata
Go pkg.GolangBinMetadata
}
func main() {
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -6,10 +6,9 @@ import (
"os"
"testing"
hashiVer "github.com/hashicorp/go-version"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/source"
hashiVer "github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
@@ -128,6 +127,11 @@ func TestIdentifyDistro(t *testing.T) {
Type: RockyLinux,
Version: "8.4.0",
},
{
fixture: "test-fixtures/os/almalinux",
Type: AlmaLinux,
Version: "8.4.0",
},
}
observedDistros := internal.NewStringSet()
@@ -0,0 +1,15 @@
NAME="AlmaLinux"
VERSION="8.4 (Electric Cheetah)"
ID="almalinux"
ID_LIKE="rhel centos fedora"
VERSION_ID="8.4"
PLATFORM_ID="platform:el8"
PRETTY_NAME="AlmaLinux 8.4 (Electric Cheetah)"
ANSI_COLOR="0;34"
CPE_NAME="cpe:/o:almalinux:almalinux:8.4:GA"
HOME_URL="https://almalinux.org/"
DOCUMENTATION_URL="https://wiki.almalinux.org/"
BUG_REPORT_URL="https://bugs.almalinux.org/"
ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8"
ALMALINUX_MANTISBT_PROJECT_VERSION="8.4"
+3
View File
@@ -22,6 +22,7 @@ const (
Windows Type = "windows"
Mariner Type = "mariner"
RockyLinux Type = "rockylinux"
AlmaLinux Type = "almalinux"
)
// All contains all Linux distribution options
@@ -42,6 +43,7 @@ var All = []Type{
Windows,
Mariner,
RockyLinux,
AlmaLinux,
}
// IDMapping connects a distro ID like "ubuntu" to a Distro type
@@ -62,6 +64,7 @@ var IDMapping = map[string]Type{
"windows": Windows,
"mariner": Mariner,
"rocky": RockyLinux,
"almalinux": AlmaLinux,
}
// String returns the string representation of the given Linux distribution.
+6 -6
View File
@@ -18,7 +18,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-libpython3.7.so",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/libpython3.7.so",
location: "libpython3.7.so",
expected: []Classification{
{
Class: "python-binary",
@@ -32,7 +32,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-python3.6",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/python3.6",
location: "python3.6",
expected: []Classification{
{
Class: "python-binary",
@@ -46,7 +46,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-patchlevel.h",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/patchlevel.h",
location: "patchlevel.h",
expected: []Classification{
{
Class: "cpython-source",
@@ -60,7 +60,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-go",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/go",
location: "go",
expected: []Classification{
{
Class: "go-binary",
@@ -74,7 +74,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-go-hint",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/VERSION",
location: "VERSION",
expected: []Classification{
{
Class: "go-binary-hint",
@@ -88,7 +88,7 @@ func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
{
name: "positive-busybox",
fixtureDir: "test-fixtures/classifiers/positive",
location: "test-fixtures/classifiers/positive/busybox",
location: "busybox",
expected: []Classification{
{
Class: "busybox-binary",
+10 -4
View File
@@ -7,7 +7,8 @@ const (
JSONOption Option = "json"
TextOption Option = "text"
TableOption Option = "table"
CycloneDxOption Option = "cyclonedx"
CycloneDxXMLOption Option = "cyclonedx"
CycloneDxJSONOption Option = "cyclonedx-json"
SPDXTagValueOption Option = "spdx-tag-value"
SPDXJSONOption Option = "spdx-json"
)
@@ -16,7 +17,8 @@ var AllOptions = []Option{
JSONOption,
TextOption,
TableOption,
CycloneDxOption,
CycloneDxXMLOption,
CycloneDxJSONOption,
SPDXTagValueOption,
SPDXJSONOption,
}
@@ -31,8 +33,12 @@ func ParseOption(userStr string) Option {
return TextOption
case string(TableOption):
return TableOption
case string(CycloneDxOption), "cyclone", "cyclone-dx":
return CycloneDxOption
case string(CycloneDxXMLOption), "cyclone", "cyclone-dx", "cyclone-dx-xml", "cyclone-xml":
// NOTE(jonasagx): setting "cyclone" to XML by default for retro-compatibility.
// If we want to show no preference between XML and JSON please remove it.
return CycloneDxXMLOption
case string(CycloneDxJSONOption), "cyclone-json", "cyclone-dx-json":
return CycloneDxJSONOption
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
return SPDXTagValueOption
case string(SPDXJSONOption), "spdxjson":
+4
View File
@@ -103,6 +103,10 @@ func (c *Catalog) Enumerate(types ...Type) <-chan Package {
channel := make(chan Package)
go func() {
defer close(channel)
if c == nil {
// we should allow enumerating from a catalog that was never created (which will result in no packages enumerated)
return
}
for ty, ids := range c.idsByType {
if len(types) != 0 {
found := false
+7
View File
@@ -3,6 +3,8 @@ package pkg
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/syft/source"
@@ -144,3 +146,8 @@ func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
}
}
func TestCatalog_EnumerateNilCatalog(t *testing.T) {
var c *Catalog
assert.Empty(t, c.Enumerate())
}
+5 -3
View File
@@ -51,6 +51,7 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
var errs error
for _, theCataloger := range catalogers {
// find packages from the underlying raw data
log.Debugf("cataloging with %q", theCataloger.Name())
packages, relationships, err := theCataloger.Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
@@ -59,7 +60,7 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
catalogedPackages := len(packages)
log.Debugf("package cataloger %q discovered %d packages", theCataloger.Name(), catalogedPackages)
log.Debugf("discovered %d packages", catalogedPackages)
packagesDiscovered.N += int64(catalogedPackages)
for _, p := range packages {
@@ -111,8 +112,9 @@ func packageFileOwnershipRelationships(p pkg.Package, resolver source.FilePathRe
}
if len(locations) == 0 {
// TODO: this is a known-unknown that could later be persisted in the SBOM (or as a validation failure)
log.Warnf("unable to find location which a package claims ownership of: %s", path)
// ideally we want to warn users about missing files from a package, however, it is very common for
// container image authors to delete files that are not needed in order to keep image sizes small. Adding
// a warning here would be needlessly noisy (even for popular base images).
continue
}
+1 -1
View File
@@ -138,7 +138,7 @@ func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Lo
if conffilesReader != nil && conffilesLocation != nil {
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
// attach the file list
files = append(files, parseDpkgConffileInfo(md5Reader)...)
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
// keep a record of the file where this was discovered
locations = append(locations, *conffilesLocation)
@@ -40,12 +40,12 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
for _, location := range fileMatches {
r, err := resolver.FileContentsByLocation(location)
if err != nil {
return pkgs, nil, fmt.Errorf("failed to resolve file contents by location: %w", err)
return pkgs, nil, fmt.Errorf("failed to resolve file contents by location=%q: %w", location.RealPath, err)
}
goPkgs, err := parseGoBin(location, r)
goPkgs, err := parseGoBin(location, r, openExe)
if err != nil {
log.Warnf("could not parse possible go binary: %+v", err)
log.Warnf("could not parse possible go binary at %q: %+v", location.RealPath, err)
}
internal.CloseAndLogError(r, location.RealPath)
+87 -4
View File
@@ -14,6 +14,7 @@ import (
"debug/pe"
"fmt"
"io"
"strings"
)
// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
@@ -24,6 +25,9 @@ type exe interface {
// ReadData reads and returns up to size byte starting at virtual address addr.
ReadData(addr, size uint64) ([]byte, error)
// ArchName returns a string that represents the CPU architecture of the executable.
ArchName() string
// DataStart returns the writable data segment start address.
DataStart() uint64
}
@@ -32,7 +36,7 @@ type exe interface {
// we changed this signature from accpeting a string
// to a ReadCloser so we could adapt the code to the
// stereoscope api. We removed the file open methods.
func openExe(file io.ReadCloser) (exe, error) {
func openExe(file io.ReadCloser) ([]exe, error) {
/*
f, err := os.Open(file)
if err != nil {
@@ -57,7 +61,7 @@ func openExe(file io.ReadCloser) (exe, error) {
return nil, err
}
return &elfExe{file, e}, nil
return []exe{&elfExe{file, e}}, nil
}
if bytes.HasPrefix(data, []byte("MZ")) {
@@ -65,7 +69,7 @@ func openExe(file io.ReadCloser) (exe, error) {
if err != nil {
return nil, err
}
return &peExe{file, e}, nil
return []exe{&peExe{file, e}}, nil
}
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
@@ -73,7 +77,20 @@ func openExe(file io.ReadCloser) (exe, error) {
if err != nil {
return nil, err
}
return &machoExe{file, e}, nil
return []exe{&machoExe{file, e}}, nil
}
// adding macho multi-architecture support (both for 64bit and 32 bit)... this case is not in the stdlib yet
if bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBE")) || bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBF")) {
fatExe, err := macho.NewFatFile(f)
if err != nil {
return nil, err
}
var exes []exe
for _, arch := range fatExe.Arches {
exes = append(exes, &machoExe{file, arch.File})
}
return exes, nil
}
return nil, fmt.Errorf("unrecognized executable format")
@@ -90,6 +107,14 @@ func (x *elfExe) Close() error {
return x.os.Close()
}
func (x *elfExe) ArchName() string {
return cleanElfArch(x.f.Machine)
}
func cleanElfArch(machine elf.Machine) string {
return strings.TrimPrefix(strings.ToLower(machine.String()), "em_")
}
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
for _, prog := range x.f.Progs {
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
@@ -132,6 +157,56 @@ func (x *peExe) Close() error {
return x.os.Close()
}
func (x *peExe) ArchName() string {
// from: debug/pe/pe.go
switch x.f.Machine {
case pe.IMAGE_FILE_MACHINE_AM33:
return "amd33"
case pe.IMAGE_FILE_MACHINE_AMD64:
return "amd64"
case pe.IMAGE_FILE_MACHINE_ARM:
return "arm"
case pe.IMAGE_FILE_MACHINE_ARMNT:
return "armnt"
case pe.IMAGE_FILE_MACHINE_ARM64:
return "arm64"
case pe.IMAGE_FILE_MACHINE_EBC:
return "ebc"
case pe.IMAGE_FILE_MACHINE_I386:
return "i386"
case pe.IMAGE_FILE_MACHINE_IA64:
return "ia64"
case pe.IMAGE_FILE_MACHINE_M32R:
return "m32r"
case pe.IMAGE_FILE_MACHINE_MIPS16:
return "mips16"
case pe.IMAGE_FILE_MACHINE_MIPSFPU:
return "mipsfpu"
case pe.IMAGE_FILE_MACHINE_MIPSFPU16:
return "mipsfpu16"
case pe.IMAGE_FILE_MACHINE_POWERPC:
return "ppc"
case pe.IMAGE_FILE_MACHINE_POWERPCFP:
return "ppcfp"
case pe.IMAGE_FILE_MACHINE_R4000:
return "r4000"
case pe.IMAGE_FILE_MACHINE_SH3:
return "sh3"
case pe.IMAGE_FILE_MACHINE_SH3DSP:
return "sh3dsp"
case pe.IMAGE_FILE_MACHINE_SH4:
return "sh4"
case pe.IMAGE_FILE_MACHINE_SH5:
return "sh5"
case pe.IMAGE_FILE_MACHINE_THUMB:
return "thumb"
case pe.IMAGE_FILE_MACHINE_WCEMIPSV2:
return "wcemipsv2"
default:
return fmt.Sprintf("unknown-pe-machine-%d", x.f.Machine)
}
}
func (x *peExe) imageBase() uint64 {
switch oh := x.f.OptionalHeader.(type) {
case *pe.OptionalHeader32:
@@ -193,6 +268,14 @@ func (x *machoExe) Close() error {
return x.os.Close()
}
func (x *machoExe) ArchName() string {
return cleanMachoArch(x.f.Cpu)
}
func cleanMachoArch(cpu macho.Cpu) string {
return strings.TrimPrefix(strings.ToLower(cpu.String()), "cpu")
}
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
+43
View File
@@ -0,0 +1,43 @@
package golang
import (
"debug/elf"
"debug/macho"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_cleanElfArch(t *testing.T) {
tests := []struct {
machine elf.Machine
want string
}{
{
machine: elf.EM_X86_64,
want: "x86_64",
},
}
for _, test := range tests {
t.Run(test.machine.String(), func(t *testing.T) {
assert.Equalf(t, test.want, cleanElfArch(test.machine), "cleanElfArch(%v)", test.machine)
})
}
}
func Test_cleanMachoArch(t *testing.T) {
tests := []struct {
cpu macho.Cpu
want string
}{
{
cpu: macho.CpuAmd64,
want: "amd64",
},
}
for _, test := range tests {
t.Run(test.cpu.String(), func(t *testing.T) {
assert.Equalf(t, test.want, cleanMachoArch(test.cpu), "cleanMachoArch(%v)", test.cpu)
})
}
}
+22 -7
View File
@@ -2,6 +2,7 @@ package golang
import (
"bufio"
"fmt"
"io"
"strings"
@@ -14,19 +15,32 @@ const (
replaceIdentifier = "=>"
)
func parseGoBin(location source.Location, reader io.ReadCloser) ([]pkg.Package, error) {
type exeOpener func(file io.ReadCloser) ([]exe, error)
func parseGoBin(location source.Location, reader io.ReadCloser, opener exeOpener) (pkgs []pkg.Package, err error) {
var exes []exe
// it has been found that there are stdlib paths within openExe that can panic. We want to prevent this behavior
// bubbling up and halting execution. For this reason we try to recover from any panic and return an error.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic while parse go binary at %q: %+v", location.RealPath, r)
}
}()
// Identify if bin was compiled by go
x, err := openExe(reader)
exes, err = opener(reader)
if err != nil {
return nil, err
return pkgs, err
}
goVersion, mod := findVers(x)
return buildGoPkgInfo(location, mod, goVersion), nil
for _, x := range exes {
goVersion, mod := findVers(x)
pkgs = append(pkgs, buildGoPkgInfo(location, mod, goVersion, x.ArchName())...)
}
return pkgs, err
}
func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Package {
func buildGoPkgInfo(location source.Location, mod, goVersion, arch string) []pkg.Package {
pkgsSlice := make([]pkg.Package, 0)
scanner := bufio.NewScanner(strings.NewReader(mod))
@@ -52,6 +66,7 @@ func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Packa
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goVersion,
H1Digest: fields[3],
Architecture: arch,
},
})
}
+36 -7
View File
@@ -1,6 +1,7 @@
package golang
import (
"io"
"testing"
"github.com/anchore/syft/syft/pkg"
@@ -8,9 +9,11 @@ import (
"github.com/stretchr/testify/assert"
)
const goCompiledVersion = "1.17"
func TestBuildGoPkgInfo(t *testing.T) {
const (
goCompiledVersion = "1.17"
archDetails = "amd64"
)
tests := []struct {
name string
mod string
@@ -43,6 +46,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
Architecture: archDetails,
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
},
},
@@ -62,6 +66,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
Architecture: archDetails,
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
},
},
@@ -92,6 +97,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
Architecture: archDetails,
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
@@ -111,6 +117,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
Architecture: archDetails,
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
},
},
@@ -130,6 +137,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
Architecture: archDetails,
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
},
},
@@ -137,17 +145,38 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
location := source.Location{
Coordinates: source.Coordinates{
RealPath: "/a-path",
FileSystemID: "layer-id",
},
}
pkgs := buildGoPkgInfo(location, tt.mod, goCompiledVersion)
assert.Equal(t, tt.expected, pkgs)
pkgs := buildGoPkgInfo(location, test.mod, goCompiledVersion, archDetails)
assert.Equal(t, test.expected, pkgs)
})
}
}
func Test_parseGoBin_recoversFromPanic(t *testing.T) {
freakOut := func(file io.ReadCloser) ([]exe, error) {
panic("baaahhh!")
}
tests := []struct {
name string
wantPkgs []pkg.Package
wantErr assert.ErrorAssertionFunc
}{
{
name: "recovers from panic",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgs, err := parseGoBin(source.NewLocation("some/path"), nil, freakOut)
assert.Error(t, err)
assert.Nil(t, pkgs)
})
}
}
+75 -33
View File
@@ -19,48 +19,66 @@ const manifestGlob = "/META-INF/MANIFEST.MF"
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
var manifest pkg.JavaManifest
sections := []map[string]string{
make(map[string]string),
var sections []map[string]string
currentSection := func() int {
return len(sections) - 1
}
currentSection := 0
scanner := bufio.NewScanner(reader)
var lastKey string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
// empty lines denote section separators
if strings.TrimSpace(line) == "" {
currentSection++
// we don't want to allocate a new section map that wont necessarily be used, do that once there is
// we don't want to allocate a new section map that won't necessarily be used, do that once there is
// a non-empty line to process
// do not process line continuations after this
lastKey = ""
continue
} else if currentSection >= len(sections) {
sections = append(sections, make(map[string]string))
}
if line[0] == ' ' {
// this is a continuation
if lastKey == "" {
return nil, fmt.Errorf("found continuation with no previous key (%s)", line)
}
sections[currentSection][lastKey] += strings.TrimSpace(line)
} else {
// this is a new key-value pair
idx := strings.Index(line, ":")
if idx == -1 {
return nil, fmt.Errorf("unable to split java manifest key-value pairs: %q", line)
log.Warnf("java manifest %q: found continuation with no previous key: %q", path, line)
continue
}
key := strings.TrimSpace(line[0:idx])
value := strings.TrimSpace(line[idx+1:])
sections[currentSection][key] = value
sections[currentSection()][lastKey] += strings.TrimSpace(line)
// keep track of key for potential future continuations
lastKey = key
continue
}
// this is a new key-value pair
idx := strings.Index(line, ":")
if idx == -1 {
log.Warnf("java manifest %q: unable to split java manifest key-value pairs: %q", path, line)
continue
}
key := strings.TrimSpace(line[0:idx])
value := strings.TrimSpace(line[idx+1:])
if key == "" {
// don't attempt to add new keys or sections unless there is a non-empty key
continue
}
if lastKey == "" {
// we're entering a new section
sections = append(sections, make(map[string]string))
}
sections[currentSection()][key] = value
// keep track of key for potential future continuations
lastKey = key
}
if err := scanner.Err(); err != nil {
@@ -75,7 +93,7 @@ func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error)
name, ok := s["Name"]
if !ok {
// per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
// this should never happen. If it does we want to know about it, but not necessarily stop
// this should never happen. If it does, we want to know about it, but not necessarily stop
// cataloging entirely... for this reason we only log.
log.Warnf("java manifest section found without a name: %s", path)
name = strconv.Itoa(i)
@@ -115,16 +133,40 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
}
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
var version string
switch {
case filenameObj.version != "":
version = filenameObj.version
case manifest.Main["Implementation-Version"] != "":
version = manifest.Main["Implementation-Version"]
case manifest.Main["Specification-Version"] != "":
version = manifest.Main["Specification-Version"]
case manifest.Main["Plugin-Version"] != "":
version = manifest.Main["Plugin-Version"]
if v := filenameObj.version; v != "" {
return v
}
return version
if manifest == nil {
return ""
}
fieldNames := []string{
"Implementation-Version",
"Specification-Version",
"Plugin-Version",
"Bundle-Version",
}
for _, fieldName := range fieldNames {
if v := fieldValueFromManifest(*manifest, fieldName); v != "" {
return v
}
}
return ""
}
func fieldValueFromManifest(manifest pkg.JavaManifest, fieldName string) string {
if value := manifest.Main[fieldName]; value != "" {
return value
}
for _, section := range manifest.NamedSections {
if value := section[fieldName]; value != "" {
return value
}
}
return ""
}
@@ -5,6 +5,8 @@ import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
@@ -56,6 +58,30 @@ func TestParseJavaManifest(t *testing.T) {
},
},
},
{
fixture: "test-fixtures/manifest/extra-empty-lines",
expected: pkg.JavaManifest{
Main: map[string]string{
"Manifest-Version": "1.0",
"Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3",
},
NamedSections: map[string]map[string]string{
"thing-1": {
"Built-By": "?",
},
"thing-2": {
"Built-By": "someone!",
},
"2": {
"Other": "things",
},
"3": {
"Last": "item",
},
},
},
},
{
fixture: "test-fixtures/manifest/continuation",
expected: pkg.JavaManifest{
@@ -145,5 +171,94 @@ func TestSelectName(t *testing.T) {
}
})
}
}
func TestSelectVersion(t *testing.T) {
tests := []struct {
name string
manifest pkg.JavaManifest
archive archiveFilename
expected string
}{
{
name: "Get name from Implementation-Version",
archive: archiveFilename{},
manifest: pkg.JavaManifest{
Main: map[string]string{
"Implementation-Version": "1.8.2",
},
},
expected: "1.8.2",
},
{
name: "Implementation-Version takes precedence over Specification-Version",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Implementation-Version": "1.8.2",
"Specification-Version": "1.0",
},
},
expected: "1.8.2",
},
{
name: "Implementation-Version found outside the main section",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Manifest-Version": "1.0",
"Ant-Version": "Apache Ant 1.8.2",
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
},
NamedSections: map[string]map[string]string{
"org/apache/tools/ant/taskdefs/optional/": {
"Implementation-Version": "1.8.2",
},
},
},
expected: "1.8.2",
},
{
name: "Implementation-Version takes precedence over Specification-Version in subsequent section",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Manifest-Version": "1.0",
"Ant-Version": "Apache Ant 1.8.2",
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
"Specification-Version": "2.0",
},
NamedSections: map[string]map[string]string{
"org/apache/tools/ant/taskdefs/optional/": {
"Specification-Version": "1.8",
},
"some-other-section": {
"Implementation-Version": "1.8.2",
},
},
},
expected: "1.8.2",
},
{
name: "Implementation-Version takes precedence over Specification-Version in subsequent section",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Manifest-Version": "1.0",
"Ant-Version": "Apache Ant 1.8.2",
"Created-By": "1.5.0_22-b03 (Sun Microsystems Inc.)",
},
NamedSections: map[string]map[string]string{
"some-other-section": {
"Bundle-Version": "1.11.28",
},
},
},
expected: "1.11.28",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := selectVersion(&test.manifest, test.archive)
assert.Equal(t, test.expected, result)
})
}
}
@@ -0,0 +1,22 @@
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3
Name: thing-1
Built-By: ?
.
Name: thing-2
Built-By: someone!
:
Other: things
junk
:
Last: item
@@ -208,7 +208,14 @@ func (p PackageJSON) hasNameAndVersionValues() bool {
return p.Name != "" && p.Version != ""
}
func pathContainsNodeModulesDirectory(path string) bool {
isNodeModulesPath, _ := regexp.MatchString("[\\/]node_modules[\\/]", path)
return isNodeModulesPath
// this supports both windows and unix paths
var filepathSeparator = regexp.MustCompile(`[\\/]`)
func pathContainsNodeModulesDirectory(p string) bool {
for _, subPath := range filepathSeparator.Split(p, -1) {
if subPath == "node_modules" {
return true
}
}
return false
}
@@ -6,6 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestParsePackageJSON(t *testing.T) {
@@ -160,3 +161,50 @@ func TestParsePackageJSON_Partial(t *testing.T) { // see https://github.com/anch
t.Errorf("no packages should've been returned (but got %d packages)", actualCount)
}
}
func Test_pathContainsNodeModulesDirectory(t *testing.T) {
tests := []struct {
path string
expected bool
}{
// positive
{
path: "something/node_modules/package",
expected: true,
},
{
path: "node_modules/package",
expected: true,
},
{
path: "something/node_modules",
expected: true,
},
{
path: "\\something\\node_modules\\",
expected: true,
},
{
path: "\\something\\node_modules",
expected: true,
},
// negative
{
path: "something/node_bogus_modules",
expected: false,
},
{
path: "something/node_modules_bogus",
expected: false,
},
{
path: "something/node_bogus_modules/package",
expected: false,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
assert.Equal(t, test.expected, pathContainsNodeModulesDirectory(test.path))
})
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package pkg
// GolangBinMetadata represents all captured data for a Golang Binary
type GolangBinMetadata struct {
GoCompiledVersion string
H1Digest string
GoCompiledVersion string `json:"goCompiledVersion"`
Architecture string `json:"architecture"`
H1Digest string `json:"h1Digest"`
}
+147 -89
View File
@@ -9,9 +9,10 @@ import (
"path/filepath"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
@@ -21,23 +22,21 @@ import (
var unixSystemRuntimePrefixes = []string{
"/proc",
"/sys",
"/dev",
"/sys",
}
var ErrIgnoreIrregularFile = errors.New("ignoring irregular file type")
var _ FileResolver = (*directoryResolver)(nil)
type pathFilterFn func(string) bool
type pathFilterFn func(string, os.FileInfo) bool
// directoryResolver implements path and content access for the directory data source.
type directoryResolver struct {
path string
cwdRelativeToRoot string
cwd string
fileTree *filetree.FileTree
metadata map[file.ID]FileMetadata
path string
currentWdRelativeToRoot string
currentWd string
fileTree *filetree.FileTree
metadata map[file.ID]FileMetadata
// TODO: wire up to report these paths in the json report
pathFilterFns []pathFilterFn
refsByMIMEType map[string][]file.Reference
@@ -45,39 +44,41 @@ type directoryResolver struct {
}
func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) {
cwd, err := os.Getwd()
currentWd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
}
var cwdRelRoot string
var currentWdRelRoot string
if path.IsAbs(root) {
cwdRelRoot, err = filepath.Rel(cwd, root)
currentWdRelRoot, err = filepath.Rel(currentWd, root)
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
}
} else {
currentWdRelRoot = filepath.Clean(root)
}
if pathFilters == nil {
pathFilters = []pathFilterFn{isUnixSystemRuntimePath}
pathFilters = []pathFilterFn{isUnallowableFileType, isUnixSystemRuntimePath}
}
resolver := directoryResolver{
path: root,
cwd: cwd,
cwdRelativeToRoot: cwdRelRoot,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
path: root,
currentWd: currentWd,
currentWdRelativeToRoot: currentWdRelRoot,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
}
func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]string, error) {
log.Infof("indexing filesystem path=%q", root)
log.Debugf("indexing filesystem path=%q", root)
var roots []string
var err error
@@ -93,11 +94,8 @@ func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]st
// but continue forth with index regardless if the given root path exists or not.
fi, err := os.Stat(root)
if err != nil && fi != nil && !fi.IsDir() {
newRoot, err := r.addPathToIndex(root, fi)
if err = r.handleFileAccessErr(root, err); err != nil {
return nil, fmt.Errorf("unable to index path: %w", err)
}
// note: we want to index the path regardless of an error stat-ing the path
newRoot := r.indexPath(root, fi, nil)
if newRoot != "" {
roots = append(roots, newRoot)
}
@@ -108,102 +106,133 @@ func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]st
func(path string, info os.FileInfo, err error) error {
stager.Current = path
newRoot, indexErr := r.indexPath(path, info, err)
newRoot := r.indexPath(path, info, err)
if newRoot != "" {
roots = append(roots, newRoot)
}
return indexErr
return nil
})
}
func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error) (string, error) {
func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error) string {
// ignore any path which a filter function returns true
for _, filterFn := range r.pathFilterFns {
if filterFn(path) {
return "", nil
if filterFn(path, info) {
return ""
}
}
if err = r.handleFileAccessErr(path, err); err != nil {
return "", err
if r.isFileAccessErr(path, err) {
return ""
}
// link cycles could cause a revisit --we should not allow this
if r.fileTree.HasPath(file.Path(path)) {
return "", nil
return ""
}
if info == nil {
// walk may not be able to provide a FileInfo object, don't allow for this to stop indexing; keep track of the paths and continue.
r.errPaths[path] = fmt.Errorf("no file info observable at path=%q", path)
return "", nil
return ""
}
newRoot, err := r.addPathToIndex(path, info)
if err = r.handleFileAccessErr(path, err); err != nil {
return "", fmt.Errorf("unable to index path: %w", err)
if r.isFileAccessErr(path, err) {
return ""
}
return newRoot, nil
return newRoot
}
func (r *directoryResolver) handleFileAccessErr(path string, err error) error {
if errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist) || errors.Is(err, ErrIgnoreIrregularFile) {
// don't allow for permission errors to stop indexing, keep track of the paths and continue.
func (r *directoryResolver) isFileAccessErr(path string, err error) bool {
// don't allow for errors to stop indexing, keep track of the paths and continue.
if err != nil {
log.Warnf("unable to access path=%q: %+v", path, err)
r.errPaths[path] = err
return nil
} else if err != nil {
return fmt.Errorf("unable to access path=%q: %w", path, err)
return true
}
return nil
return false
}
func (r directoryResolver) addPathToIndex(p string, info os.FileInfo) (string, error) {
var ref *file.Reference
var err error
var newRoot string
switch newFileTypeFromMode(info.Mode()) {
switch t := newFileTypeFromMode(info.Mode()); t {
case SymbolicLink:
linkTarget, err := os.Readlink(p)
if err != nil {
return "", fmt.Errorf("unable to readlink for path=%q: %w", p, err)
}
ref, err = r.fileTree.AddSymLink(file.Path(p), file.Path(linkTarget))
if err != nil {
return "", err
}
targetAbsPath := linkTarget
if !filepath.IsAbs(targetAbsPath) {
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
}
newRoot = targetAbsPath
return r.addSymlinkToIndex(p, info)
case Directory:
ref, err = r.fileTree.AddDir(file.Path(p))
if err != nil {
return "", err
}
case IrregularFile:
return "", ErrIgnoreIrregularFile
return "", r.addDirectoryToIndex(p, info)
case RegularFile:
return "", r.addFileToIndex(p, info)
default:
ref, err = r.fileTree.AddFile(file.Path(p))
if err != nil {
return "", err
}
return "", fmt.Errorf("unsupported file type: %s", t)
}
}
func (r directoryResolver) addDirectoryToIndex(p string, info os.FileInfo) error {
ref, err := r.fileTree.AddDir(file.Path(p))
if err != nil {
return err
}
metadata := fileMetadataFromPath(p, info)
location := NewLocationFromDirectory(p, *ref)
metadata := fileMetadataFromPath(p, info, r.isInIndex(location))
r.addFileMetadataToIndex(ref, metadata)
return nil
}
func (r directoryResolver) addFileToIndex(p string, info os.FileInfo) error {
ref, err := r.fileTree.AddFile(file.Path(p))
if err != nil {
return err
}
location := NewLocationFromDirectory(p, *ref)
metadata := fileMetadataFromPath(p, info, r.isInIndex(location))
r.addFileMetadataToIndex(ref, metadata)
return nil
}
func (r directoryResolver) addSymlinkToIndex(p string, info os.FileInfo) (string, error) {
var usedInfo = info
linkTarget, err := os.Readlink(p)
if err != nil {
return "", fmt.Errorf("unable to readlink for path=%q: %w", p, err)
}
// note: if the link is not absolute (e.g, /dev/stderr -> fd/2 ) we need to resolve it relative to the directory
// in question (e.g. resolve to /dev/fd/2)
if !filepath.IsAbs(linkTarget) {
linkTarget = filepath.Join(filepath.Dir(p), linkTarget)
}
ref, err := r.fileTree.AddSymLink(file.Path(p), file.Path(linkTarget))
if err != nil {
return "", err
}
targetAbsPath := linkTarget
if !filepath.IsAbs(targetAbsPath) {
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
}
location := NewLocationFromDirectory(p, *ref)
metadata := fileMetadataFromPath(p, usedInfo, r.isInIndex(location))
r.addFileMetadataToIndex(ref, metadata)
return targetAbsPath, nil
}
func (r directoryResolver) addFileMetadataToIndex(ref *file.Reference, metadata FileMetadata) {
if ref != nil {
r.refsByMIMEType[metadata.MIMEType] = append(r.refsByMIMEType[metadata.MIMEType], *ref)
if metadata.MIMEType != "" {
r.refsByMIMEType[metadata.MIMEType] = append(r.refsByMIMEType[metadata.MIMEType], *ref)
}
r.metadata[ref.ID()] = metadata
}
r.metadata[ref.ID()] = metadata
return newRoot, nil
}
func (r directoryResolver) requestPath(userPath string) (string, error) {
@@ -212,7 +241,7 @@ func (r directoryResolver) requestPath(userPath string) (string, error) {
userPath = path.Join(r.path, userPath)
} else {
// ensure we take into account any relative difference between the root path and the CWD for relative requests
userPath = path.Join(r.cwdRelativeToRoot, userPath)
userPath = path.Join(r.currentWdRelativeToRoot, userPath)
}
var err error
@@ -227,7 +256,7 @@ func (r directoryResolver) responsePath(path string) string {
// always return references relative to the request path (not absolute path)
if filepath.IsAbs(path) {
// we need to account for the cwd relative to the running process and the given root for the directory resolver
prefix := filepath.Clean(filepath.Join(r.cwd, r.cwdRelativeToRoot))
prefix := filepath.Clean(filepath.Join(r.currentWd, r.currentWdRelativeToRoot))
return strings.TrimPrefix(path, prefix+string(filepath.Separator))
}
return path
@@ -283,8 +312,6 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
exists, ref, err := r.fileTree.File(file.Path(userStrPath))
if err == nil && exists {
references = append(references, NewLocationFromDirectory(r.responsePath(userStrPath), *ref))
} else {
log.Warnf("path (%s) not found in file tree: Exists: %t Err:%+v", userStrPath, exists, err)
}
}
@@ -326,7 +353,23 @@ func (r *directoryResolver) RelativeFileByPath(_ Location, path string) *Locatio
// FileContentsByLocation fetches file contents for a single file reference relative to a directory.
// If the path does not exist an error is returned.
func (r directoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
return file.NewLazyReadCloser(location.RealPath), nil
if location.ref.RealPath == "" {
return nil, errors.New("empty path given")
}
if !r.isInIndex(location) {
// this is in cases where paths have been explicitly excluded from the tree index. In which case
// we should DENY all content requests. Why? These paths have been indicated to be inaccessible (either
// by preference or these files are not readable by the current user).
return nil, fmt.Errorf("file content is inaccessible path=%q", location.ref.RealPath)
}
return file.NewLazyReadCloser(string(location.ref.RealPath)), nil
}
func (r directoryResolver) isInIndex(location Location) bool {
if location.ref.RealPath == "" {
return false
}
return r.fileTree.HasPath(location.ref.RealPath, filetree.FollowBasenameLinks)
}
func (r *directoryResolver) AllLocations() <-chan Location {
@@ -361,10 +404,25 @@ func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error)
return locations, nil
}
func isUnixSystemRuntimePath(path string) bool {
func isUnixSystemRuntimePath(path string, _ os.FileInfo) bool {
return internal.HasAnyOfPrefixes(path, unixSystemRuntimePrefixes...)
}
func isUnallowableFileType(_ string, info os.FileInfo) bool {
if info == nil {
// we can't filter out by filetype for non-existent files
return false
}
switch newFileTypeFromMode(info.Mode()) {
case CharacterDevice, Socket, BlockDevice, FIFONode, IrregularFile:
return true
// note: symlinks that point to these files may still get by. We handle this later in processing to help prevent
// against infinite links traversal.
}
return false
}
func indexAllRoots(root string, indexer func(string, *progress.Stage) ([]string, error)) error {
// why account for multiple roots? To cover cases when there is a symlink that references above the root path,
// in which case we need to additionally index where the link resolves to. it's for this reason why the filetree
+330 -95
View File
@@ -1,14 +1,15 @@
package source
import (
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
@@ -19,6 +20,55 @@ import (
"github.com/wagoodman/go-progress"
)
func TestDirectoryResolver_FilesByPath_relativeRoot(t *testing.T) {
cases := []struct {
name string
relativeRoot string
input string
expected []string
}{
{
name: "should find a file from an absolute input",
relativeRoot: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path",
relativeRoot: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
expected: []string{
"image-symlinks/file-1.txt",
},
},
{
name: "should find a file from a relative path (root above cwd)",
relativeRoot: "../",
input: "sbom/sbom.go",
expected: []string{
"sbom/sbom.go",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver, err := newDirectoryResolver(c.relativeRoot)
assert.NoError(t, err)
refs, err := resolver.FilesByPath(c.input)
require.NoError(t, err)
assert.Len(t, refs, len(c.expected))
s := strset.New()
for _, actual := range refs {
s.Add(actual.RealPath)
}
assert.ElementsMatch(t, c.expected, s.List())
})
}
}
func TestDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) {
cases := []struct {
name string
@@ -85,23 +135,16 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
{
name: "finds a file (relative)",
root: "./test-fixtures/",
input: "test-fixtures/image-symlinks/file-1.txt",
expected: "test-fixtures/image-symlinks/file-1.txt",
input: "image-symlinks/file-1.txt",
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
name: "finds a file with relative indirection",
root: "./test-fixtures/../test-fixtures",
input: "test-fixtures/image-symlinks/file-1.txt",
expected: "test-fixtures/image-symlinks/file-1.txt",
refCount: 1,
},
{
// note: this is asserting the old behavior is not supported
name: "relative lookup with wrong path fails",
root: "./test-fixtures/",
input: "image-symlinks/file-1.txt",
refCount: 0,
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
name: "managed non-existing files (relative)",
@@ -113,7 +156,7 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
name: "finds a file (absolute)",
root: "./test-fixtures/",
input: "/image-symlinks/file-1.txt",
expected: "test-fixtures/image-symlinks/file-1.txt",
expected: "image-symlinks/file-1.txt",
refCount: 1,
},
{
@@ -158,17 +201,17 @@ func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) {
}{
{
name: "finds multiple files",
input: []string{"test-fixtures/image-symlinks/file-1.txt", "test-fixtures/image-symlinks/file-2.txt"},
input: []string{"image-symlinks/file-1.txt", "image-symlinks/file-2.txt"},
refCount: 2,
},
{
name: "skips non-existing files",
input: []string{"test-fixtures/image-symlinks/bogus.txt", "test-fixtures/image-symlinks/file-1.txt"},
input: []string{"image-symlinks/bogus.txt", "image-symlinks/file-1.txt"},
refCount: 1,
},
{
name: "does not return anything for non-existing directories",
input: []string{"test-fixtures/non-existing/bogus.txt", "test-fixtures/non-existing/file-1.txt"},
input: []string{"non-existing/bogus.txt", "non-existing/file-1.txt"},
refCount: 0,
},
}
@@ -210,7 +253,7 @@ func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, refs, 1)
assert.Equal(t, "test-fixtures/image-symlinks/file-1.txt", refs[0].RealPath)
assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath)
}
func TestDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
@@ -218,95 +261,128 @@ func TestDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
resolver, err := newDirectoryResolver("test-fixtures/system_paths/target")
assert.NoError(t, err)
// ensure the correct filter function is wired up by default
expectedFn := reflect.ValueOf(isUnixSystemRuntimePath)
expectedFn := reflect.ValueOf(isUnallowableFileType)
actualFn := reflect.ValueOf(resolver.pathFilterFns[0])
assert.Equal(t, expectedFn.Pointer(), actualFn.Pointer())
// all paths should be found (non filtering matches a path)
refs, err := resolver.FilesByGlob("**/place")
locations, err := resolver.FilesByGlob("**/place")
assert.NoError(t, err)
// 4: within target/
// 1: target/link --> relative path to "place"
// 1: outside_root/link_target/place
assert.Len(t, refs, 6)
assert.Len(t, locations, 6)
// ensure that symlink indexing outside of root worked
testLocation := "test-fixtures/system_paths/outside_root/link_target/place"
ok := false
test_location := "test-fixtures/system_paths/outside_root/link_target/place"
for _, actual_loc := range refs {
if test_location == actual_loc.RealPath {
for _, location := range locations {
if strings.HasSuffix(location.RealPath, testLocation) {
ok = true
}
}
if !ok {
t.Fatalf("could not find test location=%q", test_location)
t.Fatalf("could not find test location=%q", testLocation)
}
}
func TestDirectoryResolverUsesPathFilterFunction(t *testing.T) {
// let's make certain that the index honors the filter function
filter := func(s string) bool {
// a dummy function that works for testing purposes
return strings.Contains(s, "dev/place") || strings.Contains(s, "proc/place") || strings.Contains(s, "sys/place")
}
var _ fs.FileInfo = (*testFileInfo)(nil)
resolver, err := newDirectoryResolver("test-fixtures/system_paths/target", filter)
assert.NoError(t, err)
// ensure the correct filter function is wired up by default
expectedFn := reflect.ValueOf(filter)
actualFn := reflect.ValueOf(resolver.pathFilterFns[0])
assert.Equal(t, expectedFn.Pointer(), actualFn.Pointer())
assert.Len(t, resolver.pathFilterFns, 1)
refs, err := resolver.FilesByGlob("**/place")
assert.NoError(t, err)
// target/home/place + target/link/.../place + outside_root/.../place
assert.Len(t, refs, 3)
type testFileInfo struct {
mode os.FileMode
}
func Test_isUnixSystemRuntimePath(t *testing.T) {
func (t testFileInfo) Name() string {
panic("implement me")
}
func (t testFileInfo) Size() int64 {
panic("implement me")
}
func (t testFileInfo) Mode() fs.FileMode {
return t.mode
}
func (t testFileInfo) ModTime() time.Time {
panic("implement me")
}
func (t testFileInfo) IsDir() bool {
panic("implement me")
}
func (t testFileInfo) Sys() interface{} {
panic("implement me")
}
func Test_isUnallowableFileType(t *testing.T) {
tests := []struct {
path string
name string
info os.FileInfo
expected bool
}{
{
path: "proc/place",
name: "regular file",
info: testFileInfo{
mode: 0,
},
expected: false,
},
{
path: "/proc/place",
expected: true,
},
{
path: "/proc",
expected: true,
},
{
path: "/pro/c",
name: "dir",
info: testFileInfo{
mode: os.ModeDir,
},
expected: false,
},
{
path: "/pro",
name: "symlink",
info: testFileInfo{
mode: os.ModeSymlink,
},
expected: false,
},
{
path: "/dev",
name: "socket",
info: testFileInfo{
mode: os.ModeSocket,
},
expected: true,
},
{
path: "/sys",
name: "named pipe",
info: testFileInfo{
mode: os.ModeNamedPipe,
},
expected: true,
},
{
path: "/something/sys",
expected: false,
name: "char device",
info: testFileInfo{
mode: os.ModeCharDevice,
},
expected: true,
},
{
name: "block device",
info: testFileInfo{
mode: os.ModeDevice,
},
expected: true,
},
{
name: "irregular",
info: testFileInfo{
mode: os.ModeIrregular,
},
expected: true,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
assert.Equal(t, test.expected, isUnixSystemRuntimePath(test.path))
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, isUnallowableFileType("dont/care", test.info))
})
}
}
@@ -345,9 +421,7 @@ func Test_directoryResolver_index(t *testing.T) {
// note: the index uses absolute paths, so assertions MUST keep this in mind
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("could not get working dir: %+v", err)
}
require.NoError(t, err)
p := file.Path(path.Join(cwd, test.path))
assert.Equal(t, true, r.fileTree.HasPath(p))
@@ -365,32 +439,27 @@ func Test_handleFileAccessErr(t *testing.T) {
tests := []struct {
name string
input error
expectedErr error
expectedPathTracked bool
}{
{
name: "permission error does not propagate",
input: os.ErrPermission,
expectedPathTracked: true,
expectedErr: nil,
},
{
name: "file does not exist error does not propagate",
input: os.ErrNotExist,
expectedPathTracked: true,
expectedErr: nil,
},
{
name: "non-permission errors propagate",
name: "non-permission errors are tracked",
input: os.ErrInvalid,
expectedPathTracked: false,
expectedErr: os.ErrInvalid,
expectedPathTracked: true,
},
{
name: "non-errors ignored",
input: nil,
expectedPathTracked: false,
expectedErr: nil,
},
}
@@ -400,7 +469,7 @@ func Test_handleFileAccessErr(t *testing.T) {
errPaths: make(map[string]error),
}
p := "a/place"
assert.ErrorIs(t, r.handleFileAccessErr(p, test.input), test.expectedErr)
assert.Equal(t, r.isFileAccessErr(p, test.input), test.expectedPathTracked)
_, exists := r.errPaths[p]
assert.Equal(t, test.expectedPathTracked, exists)
})
@@ -493,7 +562,7 @@ func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
{
fixturePath: "./test-fixtures/image-simple",
mimeType: "text/plain",
expectedPaths: strset.New("test-fixtures/image-simple/file-1.txt", "test-fixtures/image-simple/file-2.txt", "test-fixtures/image-simple/target/really/nested/file-3.txt", "test-fixtures/image-simple/Dockerfile"),
expectedPaths: strset.New("file-1.txt", "file-2.txt", "target/really/nested/file-3.txt", "Dockerfile"),
},
}
for _, test := range tests {
@@ -511,28 +580,194 @@ func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
}
}
func Test_ignoreIrregularFiles(t *testing.T) {
// NOTE: craeting a pipe/fifo file on demand since git doesn't let me
// commit one. The directory resolver should ignore it (and any other
// file that is neither regular, a symlink or directory) when indexing a directory
dir := "./test-fixtures/irregular-files"
f := "f.fifo"
err := syscall.Mknod(filepath.Join(dir, f), syscall.S_IFIFO|0666, 0)
func Test_IndexingNestedSymLinks(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-simple")
assert.NoError(t, err)
defer func() {
err := os.Remove(filepath.Join(dir, f))
assert.NoError(t, err)
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// check that we can access the same file via 1 symlink
locations, err = resolver.FilesByPath("./link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
}
func Test_IndexingNestedSymLinks_ignoredIndexes(t *testing.T) {
filterFn := func(path string, _ os.FileInfo) bool {
return strings.HasSuffix(path, string(filepath.Separator)+"readme")
}
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-simple", filterFn)
assert.NoError(t, err)
var testingLocations []Location
// the path to the real file is PRUNED from the index, so we should NOT expect a location returned
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Empty(t, locations)
// check that we can access the same file via 1 symlink
locations, err = resolver.FilesByPath("./link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
testingLocations = append(testingLocations, locations...)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
testingLocations = append(testingLocations, locations...)
// check that we CANNOT get contents from any of the link locations
for _, location := range testingLocations {
contentReader, err := resolver.FileContentsByLocation(location)
assert.Errorf(t, err, "expected an error for getting content from a location not in the index")
assert.Nil(t, contentReader)
}
}
func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-roots/root")
assert.NoError(t, err)
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// check that we can access the same file via 2 symlinks (link_to_link_to_readme -> link_to_readme -> readme)
locations, err = resolver.FilesByPath("./link_to_link_to_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
}
func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
location Location
expects string
err bool
}{
{
name: "use file reference for content requests",
location: NewLocationFromDirectory("some/place", file.Reference{
RealPath: file.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")),
}),
expects: "this file has contents",
},
{
name: "error on empty file reference",
location: NewLocationFromDirectory("doesn't matter", file.Reference{}),
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r, err := newDirectoryResolver(".")
require.NoError(t, err)
actual, err := r.FileContentsByLocation(test.location)
if test.err {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
if test.expects != "" {
b, err := ioutil.ReadAll(actual)
require.NoError(t, err)
assert.Equal(t, test.expects, string(b))
}
})
}
}
func Test_isUnixSystemRuntimePath(t *testing.T) {
tests := []struct {
path string
expected bool
}{
{
path: "proc/place",
expected: false,
},
{
path: "/proc/place",
expected: true,
},
{
path: "/proc",
expected: true,
},
{
path: "/pro/c",
expected: false,
},
{
path: "/pro",
expected: false,
},
{
path: "/dev",
expected: true,
},
{
path: "/sys",
expected: true,
},
{
path: "/something/sys",
expected: false,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
assert.Equal(t, test.expected, isUnixSystemRuntimePath(test.path, nil))
})
}
}
func Test_SymlinkLoopWithGlobsShouldResolve(t *testing.T) {
test := func(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-loop")
require.NoError(t, err)
locations, err := resolver.FilesByGlob("**/file.target")
require.NoError(t, err)
// Note: I'm not certain that this behavior is correct, but it is not an infinite loop (which is the point of the test)
// - block/loop0/file.target
// - devices/loop0/file.target
// - devices/loop0/subsystem/loop0/file.target
assert.Len(t, locations, 3)
}
testWithTimeout(t, 5*time.Second, test)
}
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
done := make(chan bool)
go func() {
test(t)
done <- true
}()
fileRefs, err := ioutil.ReadDir(dir)
assert.NoError(t, err)
assert.Len(t, fileRefs, 2) // two files (f.fifo and readme)
resolver, err := newDirectoryResolver(dir)
assert.NoError(t, err)
assert.Len(t, resolver.fileTree.AllFiles(), 1)
rp := resolver.fileTree.AllFiles()[0].RealPath
assert.True(t, strings.Contains(string(rp), filepath.Join(dir, "readme")))
select {
case <-time.After(timeout):
t.Fatal("test timed out")
case <-done:
}
}
+19 -14
View File
@@ -3,10 +3,10 @@ package source
import (
"os"
"github.com/anchore/syft/internal/log"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/stereoscope/pkg/image"
)
@@ -37,19 +37,24 @@ func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata,
}, nil
}
func fileMetadataFromPath(path string, info os.FileInfo) FileMetadata {
func fileMetadataFromPath(path string, info os.FileInfo, withMIMEType bool) FileMetadata {
var mimeType string
uid, gid := GetXid(info)
f, err := os.Open(path)
if err != nil {
// TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns
f = nil
} else {
defer func() {
if err := f.Close(); err != nil {
log.Warnf("unable to close file while obtaining metadata: %s", path)
}
}()
if withMIMEType {
f, err := os.Open(path)
if err != nil {
// TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns
f = nil
} else {
defer func() {
if err := f.Close(); err != nil {
log.Warnf("unable to close file while obtaining metadata: %s", path)
}
}()
}
mimeType = file.MIMEType(f)
}
return FileMetadata{
@@ -58,6 +63,6 @@ func fileMetadataFromPath(path string, info os.FileInfo) FileMetadata {
// unsupported across platforms
UserID: uid,
GroupID: gid,
MIMEType: file.MIMEType(f),
MIMEType: mimeType,
}
}
+54
View File
@@ -0,0 +1,54 @@
package source
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_fileMetadataFromPath(t *testing.T) {
tests := []struct {
path string
withMIMEType bool
expectedType string
expectedMIMEType string
}{
{
path: "test-fixtures/symlinks-simple/readme",
withMIMEType: true,
expectedType: "RegularFile",
expectedMIMEType: "text/plain",
},
{
path: "test-fixtures/symlinks-simple/link_to_new_readme",
withMIMEType: true,
expectedType: "SymbolicLink",
expectedMIMEType: "text/plain",
},
{
path: "test-fixtures/symlinks-simple/readme",
withMIMEType: false,
expectedType: "RegularFile",
expectedMIMEType: "",
},
{
path: "test-fixtures/symlinks-simple/link_to_new_readme",
withMIMEType: false,
expectedType: "SymbolicLink",
expectedMIMEType: "",
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
info, err := os.Lstat(test.path)
require.NoError(t, err)
actual := fileMetadataFromPath(test.path, info, test.withMIMEType)
assert.Equal(t, test.expectedMIMEType, actual.MIMEType)
assert.Equal(t, test.expectedType, string(actual.Type))
})
}
}
+21 -8
View File
@@ -6,8 +6,7 @@ import (
)
const (
UnknownFileType FileType = "UnknownFileType"
RegularFile FileType = "RegularFile"
RegularFile FileType = "RegularFile"
// IrregularFile is how syft defines files that are neither regular, symbolic or directory.
// For ref: the seven standard Unix file types are regular, directory, symbolic link,
// FIFO special, block special, character special, and socket as defined by POSIX.
@@ -18,6 +17,7 @@ const (
BlockDevice FileType = "BlockDevice"
Directory FileType = "Directory"
FIFONode FileType = "FIFONode"
Socket FileType = "Socket"
)
type FileType string
@@ -39,19 +39,32 @@ func newFileTypeFromTarHeaderTypeFlag(flag byte) FileType {
case tar.TypeFifo:
return FIFONode
}
return UnknownFileType
return IrregularFile
}
// TODO: fill in more types from mod...
func newFileTypeFromMode(mode os.FileMode) FileType {
switch {
case mode&os.ModeSymlink == os.ModeSymlink:
case isSet(mode, os.ModeSymlink):
return SymbolicLink
case isSet(mode, os.ModeIrregular):
return IrregularFile
case isSet(mode, os.ModeCharDevice):
return CharacterDevice
case isSet(mode, os.ModeDevice):
return BlockDevice
case isSet(mode, os.ModeNamedPipe):
return FIFONode
case isSet(mode, os.ModeSocket):
return Socket
case mode.IsDir():
return Directory
case !mode.IsRegular():
return IrregularFile
default:
case mode.IsRegular():
return RegularFile
default:
return IrregularFile
}
}
func isSet(mode, field os.FileMode) bool {
return mode&field != 0
}
+6
View File
@@ -23,6 +23,12 @@ const (
FileScheme Scheme = "FileScheme"
)
var AllSchemes = []Scheme{
DirectoryScheme,
ImageScheme,
FileScheme,
}
func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
switch {
case strings.HasPrefix(userInput, "dir:"):
+9 -7
View File
@@ -55,19 +55,19 @@ func TestNewFromDirectory(t *testing.T) {
{
desc: "path detected",
input: "test-fixtures",
inputPaths: []string{"test-fixtures/path-detected/.vimrc"},
inputPaths: []string{"path-detected/.vimrc"},
expRefs: 1,
},
{
desc: "directory ignored",
input: "test-fixtures",
inputPaths: []string{"test-fixtures/path-detected"},
inputPaths: []string{"path-detected"},
expRefs: 0,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
inputPaths: []string{"test-fixtures/no-path-detected"},
inputPaths: []string{"no-path-detected"},
expRefs: 0,
},
}
@@ -184,21 +184,21 @@ func TestNewFromDirectoryShared(t *testing.T) {
desc: "path detected",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"test-fixtures/path-detected/.vimrc"},
inputPaths: []string{"path-detected/.vimrc"},
expRefs: 1,
},
{
desc: "directory ignored",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"test-fixtures/path-detected"},
inputPaths: []string{"path-detected"},
expRefs: 0,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
notExist: "foobar/",
inputPaths: []string{"test-fixtures/no-path-detected"},
inputPaths: []string{"no-path-detected"},
expRefs: 0,
},
}
@@ -306,7 +306,9 @@ func TestFilesByGlob(t *testing.T) {
t.Errorf("could not get resolver error: %+v", err)
}
contents, err := resolver.FilesByGlob(test.glob)
if err != nil {
t.Errorf("could not get files by glob: %s+v", err)
}
if len(contents) != test.expected {
t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
}
@@ -0,0 +1 @@
this mimics a partial layout on a linux system within /sys/devices/virtual to help with ensuring globbing for files does not end up in a infinite loop
+1
View File
@@ -0,0 +1 @@
../devices/loop0
@@ -0,0 +1 @@
contents!
@@ -0,0 +1 @@
../../block
@@ -0,0 +1 @@
../root/readme
@@ -0,0 +1 @@
../outside/link_to_readme
@@ -0,0 +1 @@
contents!
@@ -0,0 +1 @@
./link_to_new_readme
@@ -0,0 +1 @@
./readme
+1 -1
View File
@@ -24,7 +24,7 @@ func TestDirectoryScanCompletesWithinTimeout(t *testing.T) {
select {
case <-done:
break
case <-time.After(5 * time.Second):
case <-time.After(10 * time.Second):
t.Fatalf("directory scan is taking too long")
}
+26 -11
View File
@@ -6,7 +6,8 @@ import (
)
func TestPackagesCmdFlags(t *testing.T) {
request := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
badBinariesImage := "docker-archive:" + getFixtureImage(t, "image-bad-binaries")
tests := []struct {
name string
@@ -25,18 +26,32 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "json-output-flag",
args: []string{"packages", "-o", "json", request},
args: []string{"packages", "-o", "json", coverageImage},
assertions: []traitAssertion{
assertJsonReport,
assertSuccessfulReturnCode,
},
},
{
name: "regression-survive-bad-binaries",
// this image has all sorts of rich binaries from the clang-13 test suite that should do pretty bad things
// to the go cataloger binary path. We should NEVER let a panic stop the cataloging process for these
// specific cases.
// this is more of an integration test, however, to assert the output we want to see from the application
// a CLI test is much easier.
args: []string{"packages", "-vv", badBinariesImage},
assertions: []traitAssertion{
assertInOutput("could not parse possible go binary"),
assertSuccessfulReturnCode,
},
},
{
name: "output-env-binding",
env: map[string]string{
"SYFT_OUTPUT": "json",
},
args: []string{"packages", request},
args: []string{"packages", coverageImage},
assertions: []traitAssertion{
assertJsonReport,
assertSuccessfulReturnCode,
@@ -44,7 +59,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "table-output-flag",
args: []string{"packages", "-o", "table", request},
args: []string{"packages", "-o", "table", coverageImage},
assertions: []traitAssertion{
assertTableReport,
assertSuccessfulReturnCode,
@@ -52,7 +67,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "default-output-flag",
args: []string{"packages", request},
args: []string{"packages", coverageImage},
assertions: []traitAssertion{
assertTableReport,
assertSuccessfulReturnCode,
@@ -60,7 +75,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "squashed-scope-flag",
args: []string{"packages", "-o", "json", "-s", "squashed", request},
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage},
assertions: []traitAssertion{
assertPackageCount(20),
assertSuccessfulReturnCode,
@@ -68,7 +83,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "all-layers-scope-flag",
args: []string{"packages", "-o", "json", "-s", "all-layers", request},
args: []string{"packages", "-o", "json", "-s", "all-layers", coverageImage},
assertions: []traitAssertion{
assertPackageCount(22),
assertSuccessfulReturnCode,
@@ -76,7 +91,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "all-layers-scope-flag-by-env",
args: []string{"packages", "-o", "json", request},
args: []string{"packages", "-o", "json", coverageImage},
env: map[string]string{
"SYFT_PACKAGE_CATALOGER_SCOPE": "all-layers",
},
@@ -87,7 +102,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "attempt-upload-on-cli-switches",
args: []string{"packages", "-vv", "-H", "localhost:8080", "-u", "the-username", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", "--overwrite-existing-image", request},
args: []string{"packages", "-vv", "-H", "localhost:8080", "-u", "the-username", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", "--overwrite-existing-image", coverageImage},
env: map[string]string{
"SYFT_ANCHORE_PATH": "path/to/api",
"SYFT_ANCHORE_PASSWORD": "the-password",
@@ -108,7 +123,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "dockerfile-without-upload-is-invalid",
args: []string{"packages", "-vv", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", request},
args: []string{"packages", "-vv", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", coverageImage},
assertions: []traitAssertion{
assertNotInOutput("uploading results to localhost:8080"),
@@ -118,7 +133,7 @@ func TestPackagesCmdFlags(t *testing.T) {
},
{
name: "attempt-upload-with-env-host-set",
args: []string{"packages", "-vv", request},
args: []string{"packages", "-vv", coverageImage},
env: map[string]string{
"SYFT_ANCHORE_HOST": "localhost:8080",
},

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