diff --git a/README.md b/README.md
index c06ded7225..8160865c2a 100644
--- a/README.md
+++ b/README.md
@@ -198,6 +198,7 @@ Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
- container image, local filesystem and remote git repository
- Supply chain security (SBOM support)
- Support CycloneDX
+ - Support SPDX
# Integrations
- [GitHub Actions][action]
diff --git a/docs/docs/index.md b/docs/docs/index.md
index c8c0bd568d..bac14b33b6 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -57,6 +57,7 @@ See [Integrations][integrations] for details.
- remote git repository
- [SBOM][sbom] (Software Bill of Materials) support
- CycloneDX
+ - SPDX
Please see [LICENSE][license] for Trivy licensing information.
diff --git a/docs/docs/references/cli/sbom.md b/docs/docs/references/cli/sbom.md
index b03f6da1c4..5e5c9be4d9 100644
--- a/docs/docs/references/cli/sbom.md
+++ b/docs/docs/references/cli/sbom.md
@@ -7,13 +7,20 @@ NAME:
USAGE:
trivy sbom [command options] ARTIFACT
+DESCRIPTION:
+ ARTIFACT can be a container image, file path/directory, git repository or container image archive. See examples.
+
OPTIONS:
--output value, -o value output file name [$TRIVY_OUTPUT]
--clear-cache, -c clear image caches without scanning (default: false) [$TRIVY_CLEAR_CACHE]
--ignorefile value specify .trivyignore file (default: ".trivyignore") [$TRIVY_IGNOREFILE]
--timeout value timeout (default: 5m0s) [$TRIVY_TIMEOUT]
--severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL") [$TRIVY_SEVERITY]
+ --offline-scan do not issue API requests to identify dependencies (default: false) [$TRIVY_OFFLINE_SCAN]
+ --db-repository value OCI repository to retrieve trivy-db from (default: "ghcr.io/aquasecurity/trivy-db") [$TRIVY_DB_REPOSITORY]
+ --skip-files value specify the file paths to skip traversal (accepts multiple inputs) [$TRIVY_SKIP_FILES]
+ --skip-dirs value specify the directories where the traversal is skipped (accepts multiple inputs) [$TRIVY_SKIP_DIRS]
--artifact-type value, --type value input artifact type (image, fs, repo, archive) (default: "image") [$TRIVY_ARTIFACT_TYPE]
- --sbom-format value, --format value SBOM format (cyclonedx) (default: "cyclonedx") [$TRIVY_SBOM_FORMAT]
+ --sbom-format value, --format value SBOM format (cyclonedx, spdx, spdx-json) (default: "cyclonedx") [$TRIVY_SBOM_FORMAT]
--help, -h show help (default: false)
```
diff --git a/docs/docs/sbom/index.md b/docs/docs/sbom/index.md
index 22a77b5d20..82e37b38cb 100644
--- a/docs/docs/sbom/index.md
+++ b/docs/docs/sbom/index.md
@@ -1,7 +1,9 @@
# SBOM
+
Trivy currently supports the following SBOM formats.
- [CycloneDX][cyclonedx]
+- [SPDX][spdx]
To generate SBOM, you can use the `--format` option for each subcommand such as `image` and `fs`.
@@ -188,4 +190,5 @@ $ trivy sbom --artifact-type repo github.com/aquasecurity/trivy-ci-test
$ trivy sbom --artifact-type archive alpine.tar
```
-[cyclonedx]: cyclonedx.md
\ No newline at end of file
+[cyclonedx]: cyclonedx.md
+[spdx]: spdx.md
diff --git a/docs/docs/sbom/spdx.md b/docs/docs/sbom/spdx.md
new file mode 100644
index 0000000000..4868f136c8
--- /dev/null
+++ b/docs/docs/sbom/spdx.md
@@ -0,0 +1,297 @@
+# SPDX
+
+Trivy generates reports in the [SPDX][spdx] format.
+
+You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `spdx` with the `--format` option.
+
+```
+$ trivy image --format spdx --output result.spdx alpine:3.15
+```
+
+
+Result
+
+```
+$ cat result.spdx
+SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: alpine:3.15
+DocumentNamespace: http://aquasecurity.github.io/trivy/container_image/alpine:3.15-bebf6b19-a94c-4e2c-af44-065f63923f48
+Creator: Organization: aquasecurity
+Creator: Tool: trivy
+Created: 2022-04-28T07:32:57.142806Z
+
+##### Package: zlib
+
+PackageName: zlib
+SPDXID: SPDXRef-12bc938ac028a5e1
+PackageVersion: 1.2.12-r0
+FilesAnalyzed: false
+PackageLicenseConcluded: Zlib
+PackageLicenseDeclared: Zlib
+
+##### Package: apk-tools
+
+PackageName: apk-tools
+SPDXID: SPDXRef-26c274652190d87f
+PackageVersion: 2.12.7-r3
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-only
+PackageLicenseDeclared: GPL-2.0-only
+
+##### Package: libretls
+
+PackageName: libretls
+SPDXID: SPDXRef-2b021966d19a8211
+PackageVersion: 3.3.4-r3
+FilesAnalyzed: false
+PackageLicenseConcluded: ISC AND (BSD-3-Clause OR MIT)
+PackageLicenseDeclared: ISC AND (BSD-3-Clause OR MIT)
+
+##### Package: busybox
+
+PackageName: busybox
+SPDXID: SPDXRef-317ce3476703f20d
+PackageVersion: 1.34.1-r5
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-only
+PackageLicenseDeclared: GPL-2.0-only
+
+##### Package: libcrypto1.1
+
+PackageName: libcrypto1.1
+SPDXID: SPDXRef-34f407fb4dbd67f4
+PackageVersion: 1.1.1n-r0
+FilesAnalyzed: false
+PackageLicenseConcluded: OpenSSL
+PackageLicenseDeclared: OpenSSL
+
+##### Package: libc-utils
+
+PackageName: libc-utils
+SPDXID: SPDXRef-4bbc1cb449d54083
+PackageVersion: 0.7.2-r3
+FilesAnalyzed: false
+PackageLicenseConcluded: BSD-2-Clause AND BSD-3-Clause
+PackageLicenseDeclared: BSD-2-Clause AND BSD-3-Clause
+
+##### Package: alpine-keys
+
+PackageName: alpine-keys
+SPDXID: SPDXRef-a3bdd174be1456b6
+PackageVersion: 2.4-r1
+FilesAnalyzed: false
+PackageLicenseConcluded: MIT
+PackageLicenseDeclared: MIT
+
+##### Package: ca-certificates-bundle
+
+PackageName: ca-certificates-bundle
+SPDXID: SPDXRef-ac6472ba26fb991c
+PackageVersion: 20211220-r0
+FilesAnalyzed: false
+PackageLicenseConcluded: MPL-2.0 AND MIT
+PackageLicenseDeclared: MPL-2.0 AND MIT
+
+##### Package: libssl1.1
+
+PackageName: libssl1.1
+SPDXID: SPDXRef-b2d1b1d70fe90f7d
+PackageVersion: 1.1.1n-r0
+FilesAnalyzed: false
+PackageLicenseConcluded: OpenSSL
+PackageLicenseDeclared: OpenSSL
+
+##### Package: scanelf
+
+PackageName: scanelf
+SPDXID: SPDXRef-c617077ba6649520
+PackageVersion: 1.3.3-r0
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-only
+PackageLicenseDeclared: GPL-2.0-only
+
+##### Package: musl
+
+PackageName: musl
+SPDXID: SPDXRef-ca80b810029cde0e
+PackageVersion: 1.2.2-r7
+FilesAnalyzed: false
+PackageLicenseConcluded: MIT
+PackageLicenseDeclared: MIT
+
+##### Package: alpine-baselayout
+
+PackageName: alpine-baselayout
+SPDXID: SPDXRef-d782e64751ba9faa
+PackageVersion: 3.2.0-r18
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-only
+PackageLicenseDeclared: GPL-2.0-only
+
+##### Package: musl-utils
+
+PackageName: musl-utils
+SPDXID: SPDXRef-e5e8a237f6162e22
+PackageVersion: 1.2.2-r7
+FilesAnalyzed: false
+PackageLicenseConcluded: MIT BSD GPL2+
+PackageLicenseDeclared: MIT BSD GPL2+
+
+##### Package: ssl_client
+
+PackageName: ssl_client
+SPDXID: SPDXRef-fdf0ce84f6337be4
+PackageVersion: 1.34.1-r5
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-only
+PackageLicenseDeclared: GPL-2.0-only
+```
+
+
+
+SPDX-JSON format is also supported by using `spdx-json` with the `--format` option.
+
+```
+$ trivy image --format spdx-json --output result.spdx.json alpine:3.15
+```
+
+
+Result
+
+```
+$ cat result.spdx.json | jq .
+{
+ "SPDXID": "SPDXRef-DOCUMENT",
+ "creationInfo": {
+ "created": "2022-04-28T08:16:55.328255Z",
+ "creators": [
+ "Tool: trivy",
+ "Organization: aquasecurity"
+ ]
+ },
+ "dataLicense": "CC0-1.0",
+ "documentNamespace": "http://aquasecurity.github.io/trivy/container_image/alpine:3.15-d9549e3a-a4c5-4ee3-8bde-8c78d451fbe7",
+ "name": "alpine:3.15",
+ "packages": [
+ {
+ "SPDXID": "SPDXRef-12bc938ac028a5e1",
+ "filesAnalyzed": false,
+ "licenseConcluded": "Zlib",
+ "licenseDeclared": "Zlib",
+ "name": "zlib",
+ "versionInfo": "1.2.12-r0"
+ },
+ {
+ "SPDXID": "SPDXRef-26c274652190d87f",
+ "filesAnalyzed": false,
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseDeclared": "GPL-2.0-only",
+ "name": "apk-tools",
+ "versionInfo": "2.12.7-r3"
+ },
+ {
+ "SPDXID": "SPDXRef-2b021966d19a8211",
+ "filesAnalyzed": false,
+ "licenseConcluded": "ISC AND (BSD-3-Clause OR MIT)",
+ "licenseDeclared": "ISC AND (BSD-3-Clause OR MIT)",
+ "name": "libretls",
+ "versionInfo": "3.3.4-r3"
+ },
+ {
+ "SPDXID": "SPDXRef-317ce3476703f20d",
+ "filesAnalyzed": false,
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseDeclared": "GPL-2.0-only",
+ "name": "busybox",
+ "versionInfo": "1.34.1-r5"
+ },
+ {
+ "SPDXID": "SPDXRef-34f407fb4dbd67f4",
+ "filesAnalyzed": false,
+ "licenseConcluded": "OpenSSL",
+ "licenseDeclared": "OpenSSL",
+ "name": "libcrypto1.1",
+ "versionInfo": "1.1.1n-r0"
+ },
+ {
+ "SPDXID": "SPDXRef-4bbc1cb449d54083",
+ "filesAnalyzed": false,
+ "licenseConcluded": "BSD-2-Clause AND BSD-3-Clause",
+ "licenseDeclared": "BSD-2-Clause AND BSD-3-Clause",
+ "name": "libc-utils",
+ "versionInfo": "0.7.2-r3"
+ },
+ {
+ "SPDXID": "SPDXRef-a3bdd174be1456b6",
+ "filesAnalyzed": false,
+ "licenseConcluded": "MIT",
+ "licenseDeclared": "MIT",
+ "name": "alpine-keys",
+ "versionInfo": "2.4-r1"
+ },
+ {
+ "SPDXID": "SPDXRef-ac6472ba26fb991c",
+ "filesAnalyzed": false,
+ "licenseConcluded": "MPL-2.0 AND MIT",
+ "licenseDeclared": "MPL-2.0 AND MIT",
+ "name": "ca-certificates-bundle",
+ "versionInfo": "20211220-r0"
+ },
+ {
+ "SPDXID": "SPDXRef-b2d1b1d70fe90f7d",
+ "filesAnalyzed": false,
+ "licenseConcluded": "OpenSSL",
+ "licenseDeclared": "OpenSSL",
+ "name": "libssl1.1",
+ "versionInfo": "1.1.1n-r0"
+ },
+ {
+ "SPDXID": "SPDXRef-c617077ba6649520",
+ "filesAnalyzed": false,
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseDeclared": "GPL-2.0-only",
+ "name": "scanelf",
+ "versionInfo": "1.3.3-r0"
+ },
+ {
+ "SPDXID": "SPDXRef-ca80b810029cde0e",
+ "filesAnalyzed": false,
+ "licenseConcluded": "MIT",
+ "licenseDeclared": "MIT",
+ "name": "musl",
+ "versionInfo": "1.2.2-r7"
+ },
+ {
+ "SPDXID": "SPDXRef-d782e64751ba9faa",
+ "filesAnalyzed": false,
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseDeclared": "GPL-2.0-only",
+ "name": "alpine-baselayout",
+ "versionInfo": "3.2.0-r18"
+ },
+ {
+ "SPDXID": "SPDXRef-e5e8a237f6162e22",
+ "filesAnalyzed": false,
+ "licenseConcluded": "MIT BSD GPL2+",
+ "licenseDeclared": "MIT BSD GPL2+",
+ "name": "musl-utils",
+ "versionInfo": "1.2.2-r7"
+ },
+ {
+ "SPDXID": "SPDXRef-fdf0ce84f6337be4",
+ "filesAnalyzed": false,
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseDeclared": "GPL-2.0-only",
+ "name": "ssl_client",
+ "versionInfo": "1.34.1-r5"
+ }
+ ],
+ "spdxVersion": "SPDX-2.2"
+}
+```
+
+
+
+[spdx]: https://spdx.dev/wp-content/uploads/sites/41/2020/08/SPDX-specification-2-2.pdf
diff --git a/go.mod b/go.mod
index 44e414aa97..dc8bf2feac 100644
--- a/go.mod
+++ b/go.mod
@@ -155,6 +155,7 @@ require (
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
+ github.com/spdx/tools-golang v0.3.0
github.com/spf13/cast v1.4.1 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/tmccombs/hcl2json v0.3.4 // indirect
@@ -198,5 +199,7 @@ require (
sigs.k8s.io/yaml v1.3.0 // indirect
)
+require github.com/mitchellh/hashstructure/v2 v2.0.2
+
// To resolve CVE-2022-23648
replace github.com/containerd/containerd v1.5.9 => github.com/containerd/containerd v1.5.10
diff --git a/go.sum b/go.sum
index c80b6c2f8b..caa4304f28 100644
--- a/go.sum
+++ b/go.sum
@@ -1183,6 +1183,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -1468,6 +1470,9 @@ github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34c
github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
+github.com/spdx/tools-golang v0.3.0 h1:rtm+DHk3aAt74Fh0Wgucb4pCxjXV8SqHCPEb2iBd30k=
+github.com/spdx/tools-golang v0.3.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
diff --git a/mkdocs.yml b/mkdocs.yml
index bcb92ab4ff..1bc42ae1cb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,125 +7,126 @@ repo_url: https://github.com/aquasecurity/trivy
edit_uri: ""
nav:
- - HOME: index.md
- - Getting started:
+ - HOME: index.md
+ - Getting started:
- Overview: getting-started/overview.md
- Installation: getting-started/installation.md
- Quick Start: getting-started/quickstart.md
- Further Reading: getting-started/further.md
- - Docs:
- - Overview: docs/index.md
- - Vulnerability:
- - Scanning:
- - Overview: docs/vulnerability/scanning/index.md
- - Container Image: docs/vulnerability/scanning/image.md
- - Filesystem: docs/vulnerability/scanning/filesystem.md
- - Rootfs: docs/vulnerability/scanning/rootfs.md
- - Git Repository: docs/vulnerability/scanning/git-repository.md
- - Detection:
- - OS Packages: docs/vulnerability/detection/os.md
- - Language-specific Packages: docs/vulnerability/detection/language.md
- - Data Sources: docs/vulnerability/detection/data-source.md
- - Supported: docs/vulnerability/detection/supported.md
- - Examples:
- - Vulnerability Filtering: docs/vulnerability/examples/filter.md
- - Report Formats: docs/vulnerability/examples/report.md
- - Vulnerability DB: docs/vulnerability/examples/db.md
- - Cache: docs/vulnerability/examples/cache.md
- - Others: docs/vulnerability/examples/others.md
- - Distributions: docs/vulnerability/distributions.md
- - Languages:
- - Go: docs/vulnerability/languages/golang.md
- - Misconfiguration:
- - Scanning:
- - Overview: docs/misconfiguration/index.md
- - Infrastructure as Code: docs/misconfiguration/iac.md
- - Filesystem: docs/misconfiguration/filesystem.md
- - Policy:
- - Built-in Policies: docs/misconfiguration/policy/builtin.md
- - Exceptions: docs/misconfiguration/policy/exceptions.md
- - Custom Policies:
- - Overview: docs/misconfiguration/custom/index.md
- - Data: docs/misconfiguration/custom/data.md
- - Combine: docs/misconfiguration/custom/combine.md
- - Testing: docs/misconfiguration/custom/testing.md
- - Debugging Policies: docs/misconfiguration/custom/debug.md
- - Examples: docs/misconfiguration/custom/examples.md
- - Options:
- - Policy: docs/misconfiguration/options/policy.md
- - Filtering: docs/misconfiguration/options/filter.md
- - Report Formats: docs/misconfiguration/options/report.md
- - Others: docs/misconfiguration/options/others.md
- - Comparison:
- - vs Conftest: docs/misconfiguration/comparison/conftest.md
- - vs tfsec: docs/misconfiguration/comparison/tfsec.md
- - vs cfsec: docs/misconfiguration/comparison/cfsec.md
- - Secret:
- - Scanning: docs/secret/scanning.md
- - Configuration: docs/secret/configuration.md
- - Examples: docs/secret/examples.md
- - SBOM:
- - Overview: docs/sbom/index.md
- - CycloneDX: docs/sbom/cyclonedx.md
- - Integrations:
- - Overview: docs/integrations/index.md
- - GitHub Actions: docs/integrations/github-actions.md
- - CircleCI: docs/integrations/circleci.md
- - Travis CI: docs/integrations/travis-ci.md
- - GitLab CI: docs/integrations/gitlab-ci.md
- - Bitbucket Pipelines: docs/integrations/bitbucket.md
- - AWS CodePipeline: docs/integrations/aws-codepipeline.md
- - AWS Security Hub: docs/integrations/aws-security-hub.md
- - Advanced:
- - Plugins: docs/advanced/plugins.md
- - Air-Gapped Environment: docs/advanced/air-gap.md
- - Container Image:
- - Embed in Dockerfile: docs/advanced/container/embed-in-dockerfile.md
- - Unpacked container image filesystem: docs/advanced/container/unpacked-filesystem.md
- - OCI Image: docs/advanced/container/oci.md
- - Podman: docs/advanced/container/podman.md
- - Private Docker Registries:
- - Overview: docs/advanced/private-registries/index.md
- - Docker Hub: docs/advanced/private-registries/docker-hub.md
- - AWS ECR (Elastic Container Registry): docs/advanced/private-registries/ecr.md
- - GCR (Google Container Registry): docs/advanced/private-registries/gcr.md
- - ACR (Azure Container Registry): docs/advanced/private-registries/acr.md
- - Self-Hosted: docs/advanced/private-registries/self.md
- - References:
- - CLI:
- - Overview: docs/references/cli/index.md
- - Image: docs/references/cli/image.md
- - Config: docs/references/cli/config.md
- - Filesystem: docs/references/cli/fs.md
- - Rootfs: docs/references/cli/rootfs.md
- - Repository: docs/references/cli/repo.md
- - Client: docs/references/cli/client.md
- - Server: docs/references/cli/server.md
- - Plugins: docs/references/cli/plugins.md
- - SBOM: docs/references/cli/sbom.md
- - Modes:
- - Standalone: docs/references/modes/standalone.md
- - Client/Server: docs/references/modes/client-server.md
- - Troubleshooting: docs/references/troubleshooting.md
- - Community:
- - Tools: community/tools.md
- - References: community/references.md
- - CKS Reference: community/cks.md
- - Credits: community/credit.md
- - How to contribute:
- - Issues: community/contribute/issue.md
- - Pull Requests: community/contribute/pr.md
- - Maintainer:
- - Help Wanted: community/maintainer/help-wanted.md
- - Triage: community/maintainer/triage.md
+ - Docs:
+ - Overview: docs/index.md
+ - Vulnerability:
+ - Scanning:
+ - Overview: docs/vulnerability/scanning/index.md
+ - Container Image: docs/vulnerability/scanning/image.md
+ - Filesystem: docs/vulnerability/scanning/filesystem.md
+ - Rootfs: docs/vulnerability/scanning/rootfs.md
+ - Git Repository: docs/vulnerability/scanning/git-repository.md
+ - Detection:
+ - OS Packages: docs/vulnerability/detection/os.md
+ - Language-specific Packages: docs/vulnerability/detection/language.md
+ - Data Sources: docs/vulnerability/detection/data-source.md
+ - Supported: docs/vulnerability/detection/supported.md
+ - Examples:
+ - Vulnerability Filtering: docs/vulnerability/examples/filter.md
+ - Report Formats: docs/vulnerability/examples/report.md
+ - Vulnerability DB: docs/vulnerability/examples/db.md
+ - Cache: docs/vulnerability/examples/cache.md
+ - Others: docs/vulnerability/examples/others.md
+ - Distributions: docs/vulnerability/distributions.md
+ - Languages:
+ - Go: docs/vulnerability/languages/golang.md
+ - Misconfiguration:
+ - Scanning:
+ - Overview: docs/misconfiguration/index.md
+ - Infrastructure as Code: docs/misconfiguration/iac.md
+ - Filesystem: docs/misconfiguration/filesystem.md
+ - Policy:
+ - Built-in Policies: docs/misconfiguration/policy/builtin.md
+ - Exceptions: docs/misconfiguration/policy/exceptions.md
+ - Custom Policies:
+ - Overview: docs/misconfiguration/custom/index.md
+ - Data: docs/misconfiguration/custom/data.md
+ - Combine: docs/misconfiguration/custom/combine.md
+ - Testing: docs/misconfiguration/custom/testing.md
+ - Debugging Policies: docs/misconfiguration/custom/debug.md
+ - Examples: docs/misconfiguration/custom/examples.md
+ - Options:
+ - Policy: docs/misconfiguration/options/policy.md
+ - Filtering: docs/misconfiguration/options/filter.md
+ - Report Formats: docs/misconfiguration/options/report.md
+ - Others: docs/misconfiguration/options/others.md
+ - Comparison:
+ - vs Conftest: docs/misconfiguration/comparison/conftest.md
+ - vs tfsec: docs/misconfiguration/comparison/tfsec.md
+ - vs cfsec: docs/misconfiguration/comparison/cfsec.md
+ - Secret:
+ - Scanning: docs/secret/scanning.md
+ - Configuration: docs/secret/configuration.md
+ - Examples: docs/secret/examples.md
+ - SBOM:
+ - Overview: docs/sbom/index.md
+ - CycloneDX: docs/sbom/cyclonedx.md
+ - SPDX: docs/sbom/spdx.md
+ - Integrations:
+ - Overview: docs/integrations/index.md
+ - GitHub Actions: docs/integrations/github-actions.md
+ - CircleCI: docs/integrations/circleci.md
+ - Travis CI: docs/integrations/travis-ci.md
+ - GitLab CI: docs/integrations/gitlab-ci.md
+ - Bitbucket Pipelines: docs/integrations/bitbucket.md
+ - AWS CodePipeline: docs/integrations/aws-codepipeline.md
+ - AWS Security Hub: docs/integrations/aws-security-hub.md
+ - Advanced:
+ - Plugins: docs/advanced/plugins.md
+ - Air-Gapped Environment: docs/advanced/air-gap.md
+ - Container Image:
+ - Embed in Dockerfile: docs/advanced/container/embed-in-dockerfile.md
+ - Unpacked container image filesystem: docs/advanced/container/unpacked-filesystem.md
+ - OCI Image: docs/advanced/container/oci.md
+ - Podman: docs/advanced/container/podman.md
+ - Private Docker Registries:
+ - Overview: docs/advanced/private-registries/index.md
+ - Docker Hub: docs/advanced/private-registries/docker-hub.md
+ - AWS ECR (Elastic Container Registry): docs/advanced/private-registries/ecr.md
+ - GCR (Google Container Registry): docs/advanced/private-registries/gcr.md
+ - ACR (Azure Container Registry): docs/advanced/private-registries/acr.md
+ - Self-Hosted: docs/advanced/private-registries/self.md
+ - References:
+ - CLI:
+ - Overview: docs/references/cli/index.md
+ - Image: docs/references/cli/image.md
+ - Config: docs/references/cli/config.md
+ - Filesystem: docs/references/cli/fs.md
+ - Rootfs: docs/references/cli/rootfs.md
+ - Repository: docs/references/cli/repo.md
+ - Client: docs/references/cli/client.md
+ - Server: docs/references/cli/server.md
+ - Plugins: docs/references/cli/plugins.md
+ - SBOM: docs/references/cli/sbom.md
+ - Modes:
+ - Standalone: docs/references/modes/standalone.md
+ - Client/Server: docs/references/modes/client-server.md
+ - Troubleshooting: docs/references/troubleshooting.md
+ - Community:
+ - Tools: community/tools.md
+ - References: community/references.md
+ - CKS Reference: community/cks.md
+ - Credits: community/credit.md
+ - How to contribute:
+ - Issues: community/contribute/issue.md
+ - Pull Requests: community/contribute/pr.md
+ - Maintainer:
+ - Help Wanted: community/maintainer/help-wanted.md
+ - Triage: community/maintainer/triage.md
theme:
- name: material
- language: 'en'
- logo: imgs/logo-white.svg
- features:
- - navigation.tabs
- - navigation.tabs.sticky
- - navigation.sections
+ name: material
+ language: "en"
+ logo: imgs/logo-white.svg
+ features:
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.sections
markdown_extensions:
- pymdownx.highlight
@@ -148,4 +149,4 @@ extra:
plugins:
- search
- - macros
\ No newline at end of file
+ - macros
diff --git a/pkg/commands/app.go b/pkg/commands/app.go
index 1125f34f04..aa1d063e63 100644
--- a/pkg/commands/app.go
+++ b/pkg/commands/app.go
@@ -816,7 +816,7 @@ func NewSbomCommand() *cli.Command {
Name: "sbom-format",
Aliases: []string{"format"},
Value: "cyclonedx",
- Usage: "SBOM format (cyclonedx)",
+ Usage: "SBOM format (cyclonedx, spdx, spdx-json)",
EnvVars: []string{"TRIVY_SBOM_FORMAT"},
},
},
diff --git a/pkg/commands/option/report.go b/pkg/commands/option/report.go
index df5536aca4..1c50fb4263 100644
--- a/pkg/commands/option/report.go
+++ b/pkg/commands/option/report.go
@@ -5,13 +5,13 @@ import (
"os"
"strings"
- "github.com/aquasecurity/trivy/pkg/types"
-
"github.com/urfave/cli/v2"
"go.uber.org/zap"
+ "golang.org/x/exp/slices"
"golang.org/x/xerrors"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
+ "github.com/aquasecurity/trivy/pkg/types"
)
// ReportOption holds the options for reporting scan results
@@ -137,8 +137,8 @@ func (c *ReportOption) populateSecurityChecks() error {
}
func (c *ReportOption) forceListAllPkgs(logger *zap.SugaredLogger) bool {
- if c.Format == "cyclonedx" && !c.ListAllPkgs {
- logger.Debugf("'--format cyclonedx' automatically enables '--list-all-pkgs'.")
+ if slices.Contains(supportedSbomFormats, c.Format) && !c.ListAllPkgs {
+ logger.Debugf("'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.")
return true
}
return false
diff --git a/pkg/commands/option/report_test.go b/pkg/commands/option/report_test.go
index fb9741324f..6062e2e8d5 100644
--- a/pkg/commands/option/report_test.go
+++ b/pkg/commands/option/report_test.go
@@ -103,7 +103,7 @@ func TestReportReportConfig_Init(t *testing.T) {
},
args: []string{"centos:7"},
logs: []string{
- "'--format cyclonedx' automatically enables '--list-all-pkgs'.",
+ "'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.",
"Severities: CRITICAL",
},
want: ReportOption{
diff --git a/pkg/commands/option/sbom.go b/pkg/commands/option/sbom.go
index 519505fe92..261efdb533 100644
--- a/pkg/commands/option/sbom.go
+++ b/pkg/commands/option/sbom.go
@@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
)
-var supportedSbomFormats = []string{"cyclonedx"}
+var supportedSbomFormats = []string{"cyclonedx", "spdx", "spdx-json"}
// SbomOption holds the options for SBOM generation
type SbomOption struct {
diff --git a/pkg/report/spdx/spdx.go b/pkg/report/spdx/spdx.go
new file mode 100644
index 0000000000..faf28ad61b
--- /dev/null
+++ b/pkg/report/spdx/spdx.go
@@ -0,0 +1,170 @@
+package spdx
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/hashstructure/v2"
+ "github.com/spdx/tools-golang/jsonsaver"
+ "github.com/spdx/tools-golang/spdx"
+ "github.com/spdx/tools-golang/tvsaver"
+ "golang.org/x/xerrors"
+ "k8s.io/utils/clock"
+
+ ftypes "github.com/aquasecurity/fanal/types"
+ "github.com/aquasecurity/trivy/pkg/types"
+)
+
+const (
+ SPDXVersion = "SPDX-2.2"
+ DataLicense = "CC0-1.0"
+ SPDXIdentifier = "DOCUMENT"
+ DocumentNamespace = "http://aquasecurity.github.io/trivy"
+ CreatorOrganization = "aquasecurity"
+ CreatorTool = "trivy"
+)
+
+type Writer struct {
+ output io.Writer
+ version string
+ *options
+}
+
+type newUUID func() uuid.UUID
+
+type options struct {
+ format spdx.Document2_1
+ clock clock.Clock
+ newUUID newUUID
+ spdxFormat string
+}
+
+type option func(*options)
+
+type spdxSaveFunction func(*spdx.Document2_2, io.Writer) error
+
+func WithClock(clock clock.Clock) option {
+ return func(opts *options) {
+ opts.clock = clock
+ }
+}
+
+func WithNewUUID(newUUID newUUID) option {
+ return func(opts *options) {
+ opts.newUUID = newUUID
+ }
+}
+
+func NewWriter(output io.Writer, version string, spdxFormat string, opts ...option) Writer {
+ o := &options{
+ format: spdx.Document2_1{},
+ clock: clock.RealClock{},
+ newUUID: uuid.New,
+ spdxFormat: spdxFormat,
+ }
+
+ for _, opt := range opts {
+ opt(o)
+ }
+
+ return Writer{
+ output: output,
+ version: version,
+ options: o,
+ }
+}
+
+func (cw Writer) Write(report types.Report) error {
+ spdxDoc, err := cw.convertToBom(report, cw.version)
+ if err != nil {
+ return xerrors.Errorf("failed to convert bom: %w", err)
+ }
+
+ var saveFunc spdxSaveFunction
+ if cw.spdxFormat != "spdx-json" {
+ saveFunc = tvsaver.Save2_2
+ } else {
+ saveFunc = jsonsaver.Save2_2
+ }
+
+ if err = saveFunc(spdxDoc, cw.output); err != nil {
+ return xerrors.Errorf("failed to save bom: %w", err)
+ }
+ return nil
+}
+
+func (cw *Writer) convertToBom(r types.Report, version string) (*spdx.Document2_2, error) {
+ packages := make(map[spdx.ElementID]*spdx.Package2_2)
+
+ for _, result := range r.Results {
+ for _, pkg := range result.Packages {
+ spdxPackage, err := pkgToSpdxPackage(result.Type, r.Metadata, pkg)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to parse pkg: %w", err)
+ }
+ packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage
+ }
+ }
+
+ return &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: SPDXVersion,
+ DataLicense: DataLicense,
+ SPDXIdentifier: SPDXIdentifier,
+ DocumentName: r.ArtifactName,
+ DocumentNamespace: getDocumentNamespace(r, cw),
+ CreatorOrganizations: []string{CreatorOrganization},
+ CreatorTools: []string{CreatorTool},
+ Created: cw.clock.Now().UTC().Format(time.RFC3339Nano),
+ },
+ Packages: packages,
+ }, nil
+}
+
+func pkgToSpdxPackage(t string, meta types.Metadata, pkg ftypes.Package) (spdx.Package2_2, error) {
+ var spdxPackage spdx.Package2_2
+ license := getLicense(pkg)
+
+ pkgID, err := getPackageID(pkg)
+ if err != nil {
+ return spdx.Package2_2{}, xerrors.Errorf("failed to get %s package ID: %w", pkg.Name, err)
+ }
+
+ spdxPackage.PackageSPDXIdentifier = spdx.ElementID(pkgID)
+ spdxPackage.PackageName = pkg.Name
+ spdxPackage.PackageVersion = pkg.Version
+
+ // The Declared License is what the authors of a project believe govern the package
+ spdxPackage.PackageLicenseConcluded = license
+
+ // The Concluded License field is the license the SPDX file creator believes governs the package
+ spdxPackage.PackageLicenseDeclared = license
+
+ return spdxPackage, nil
+}
+
+func getLicense(p ftypes.Package) string {
+ if p.License == "" {
+ return "NONE"
+ }
+
+ return p.License
+}
+
+func getDocumentNamespace(r types.Report, cw *Writer) string {
+ return DocumentNamespace + "/" + string(r.ArtifactType) + "/" + r.ArtifactName + "-" + cw.newUUID().String()
+}
+
+func getPackageID(p ftypes.Package) (string, error) {
+ f, err := hashstructure.Hash(p, hashstructure.FormatV2, &hashstructure.HashOptions{
+ ZeroNil: true,
+ SlicesAsSets: true,
+ })
+ if err != nil {
+ return "", xerrors.Errorf("could not build package ID for package=%+v: %+v", p, err)
+ }
+
+ return fmt.Sprintf("%x", f), nil
+}
diff --git a/pkg/report/spdx/spdx_test.go b/pkg/report/spdx/spdx_test.go
new file mode 100644
index 0000000000..d3a6c2ce6d
--- /dev/null
+++ b/pkg/report/spdx/spdx_test.go
@@ -0,0 +1,395 @@
+package spdx_test
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+ "time"
+
+ fos "github.com/aquasecurity/fanal/analyzer/os"
+ ftypes "github.com/aquasecurity/fanal/types"
+ "github.com/aquasecurity/trivy/pkg/report"
+ reportSpdx "github.com/aquasecurity/trivy/pkg/report/spdx"
+ "github.com/aquasecurity/trivy/pkg/types"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/uuid"
+ "github.com/spdx/tools-golang/jsonloader"
+ "github.com/spdx/tools-golang/spdx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ fake "k8s.io/utils/clock/testing"
+)
+
+func TestWriter_Write(t *testing.T) {
+ testCases := []struct {
+ name string
+ inputReport types.Report
+ wantSBOM *spdx.Document2_2
+ }{
+ {
+ name: "happy path for container scan",
+ inputReport: types.Report{
+ SchemaVersion: report.SchemaVersion,
+ ArtifactName: "rails:latest",
+ ArtifactType: ftypes.ArtifactContainerImage,
+ Metadata: types.Metadata{
+ Size: 1024,
+ OS: &ftypes.OS{
+ Family: fos.CentOS,
+ Name: "8.3.2011",
+ Eosl: true,
+ },
+ ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
+ RepoTags: []string{"rails:latest"},
+ DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"},
+ RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"},
+ ImageConfig: v1.ConfigFile{
+ Architecture: "arm64",
+ },
+ },
+ Results: types.Results{
+ {
+ Target: "rails:latest (centos 8.3.2011)",
+ Class: types.ClassOSPkg,
+ Type: fos.CentOS,
+ Packages: []ftypes.Package{
+ {
+ Name: "binutils",
+ Version: "2.30",
+ Release: "93.el8",
+ Epoch: 0,
+ Arch: "aarch64",
+ SrcName: "binutils",
+ SrcVersion: "2.30",
+ SrcRelease: "93.el8",
+ SrcEpoch: 0,
+ Modularitylabel: "",
+ License: "GPLv3+",
+ },
+ },
+ },
+ {
+ Target: "app/subproject/Gemfile.lock",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Bundler,
+ Packages: []ftypes.Package{
+ {
+ Name: "actionpack",
+ Version: "7.0.0",
+ },
+ {
+ Name: "actioncontroller",
+ Version: "7.0.0",
+ },
+ },
+ },
+ {
+ Target: "app/Gemfile.lock",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Bundler,
+ Packages: []ftypes.Package{
+ {
+ Name: "actionpack",
+ Version: "7.0.0",
+ },
+ },
+ },
+ },
+ },
+ wantSBOM: &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: "DOCUMENT",
+ DocumentName: "rails:latest",
+ DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000001",
+ CreatorOrganizations: []string{"aquasecurity"},
+ CreatorTools: []string{"trivy"},
+ Created: "2021-08-25T12:20:30.000000005Z",
+ ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{},
+ },
+ Packages: map[spdx.ElementID]*spdx.Package2_2{
+ spdx.ElementID("e27d088813d330a4"): {
+ PackageSPDXIdentifier: spdx.ElementID("e27d088813d330a4"),
+ PackageName: "actioncontroller",
+ PackageVersion: "7.0.0",
+ PackageLicenseConcluded: "NONE",
+ PackageLicenseDeclared: "NONE",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ spdx.ElementID("163ff5a6292fef8c"): {
+ PackageSPDXIdentifier: spdx.ElementID("163ff5a6292fef8c"),
+ PackageName: "actionpack",
+ PackageVersion: "7.0.0",
+ PackageLicenseConcluded: "NONE",
+ PackageLicenseDeclared: "NONE",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ spdx.ElementID("a4aded544ebeda0a"): {
+ PackageSPDXIdentifier: spdx.ElementID("a4aded544ebeda0a"),
+ PackageName: "binutils",
+ PackageVersion: "2.30",
+ PackageLicenseConcluded: "GPLv3+",
+ PackageLicenseDeclared: "GPLv3+",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ },
+ UnpackagedFiles: nil,
+ OtherLicenses: nil,
+ Relationships: nil,
+ Annotations: nil,
+ Reviews: nil,
+ },
+ },
+ {
+ name: "happy path for local container scan",
+ inputReport: types.Report{
+ SchemaVersion: report.SchemaVersion,
+ ArtifactName: "centos:latest",
+ ArtifactType: ftypes.ArtifactContainerImage,
+ Metadata: types.Metadata{
+ Size: 1024,
+ OS: &ftypes.OS{
+ Family: fos.CentOS,
+ Name: "8.3.2011",
+ Eosl: true,
+ },
+ ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
+ RepoTags: []string{"centos:latest"},
+ RepoDigests: []string{},
+ ImageConfig: v1.ConfigFile{
+ Architecture: "arm64",
+ },
+ },
+ Results: types.Results{
+ {
+ Target: "centos:latest (centos 8.3.2011)",
+ Class: types.ClassOSPkg,
+ Type: fos.CentOS,
+ Packages: []ftypes.Package{
+ {
+ Name: "acl",
+ Version: "2.2.53",
+ Release: "1.el8",
+ Epoch: 1,
+ Arch: "aarch64",
+ SrcName: "acl",
+ SrcVersion: "2.2.53",
+ SrcRelease: "1.el8",
+ SrcEpoch: 1,
+ Modularitylabel: "",
+ License: "GPLv2+",
+ },
+ },
+ },
+ {
+ Target: "Ruby",
+ Class: types.ClassLangPkg,
+ Type: ftypes.GemSpec,
+ Packages: []ftypes.Package{
+ {
+ Name: "actionpack",
+ Version: "7.0.0",
+ Layer: ftypes.Layer{
+ DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488",
+ },
+ FilePath: "tools/project-john/specifications/actionpack.gemspec",
+ },
+ {
+ Name: "actionpack",
+ Version: "7.0.1",
+ Layer: ftypes.Layer{
+ DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488",
+ },
+ FilePath: "tools/project-doe/specifications/actionpack.gemspec",
+ },
+ },
+ },
+ },
+ },
+ wantSBOM: &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: "DOCUMENT",
+ DocumentName: "centos:latest",
+ DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000001",
+ CreatorOrganizations: []string{"aquasecurity"},
+ CreatorTools: []string{"trivy"},
+ Created: "2021-08-25T12:20:30.000000005Z",
+ ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{},
+ },
+ Packages: map[spdx.ElementID]*spdx.Package2_2{
+ spdx.ElementID("d963712012fcbc8c"): {
+ PackageSPDXIdentifier: spdx.ElementID("d963712012fcbc8c"),
+ PackageName: "acl",
+ PackageVersion: "2.2.53",
+ PackageLicenseConcluded: "GPLv2+",
+ PackageLicenseDeclared: "GPLv2+",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ spdx.ElementID("17ba95e087440896"): {
+ PackageSPDXIdentifier: spdx.ElementID("17ba95e087440896"),
+ PackageName: "actionpack",
+ PackageVersion: "7.0.0",
+ PackageLicenseConcluded: "NONE",
+ PackageLicenseDeclared: "NONE",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ spdx.ElementID("50ac94ac1875540"): {
+ PackageSPDXIdentifier: spdx.ElementID("50ac94ac1875540"),
+ PackageName: "actionpack",
+ PackageVersion: "7.0.1",
+ PackageLicenseConcluded: "NONE",
+ PackageLicenseDeclared: "NONE",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ },
+ UnpackagedFiles: nil,
+ OtherLicenses: nil,
+ Relationships: nil,
+ Annotations: nil,
+ Reviews: nil,
+ },
+ },
+ {
+ name: "happy path for fs scan",
+ inputReport: types.Report{
+ SchemaVersion: report.SchemaVersion,
+ ArtifactName: "masahiro331/CVE-2021-41098",
+ ArtifactType: ftypes.ArtifactFilesystem,
+ Results: types.Results{
+ {
+ Target: "Gemfile.lock",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Bundler,
+ Packages: []ftypes.Package{
+ {
+ Name: "actioncable",
+ Version: "6.1.4.1",
+ },
+ },
+ },
+ },
+ },
+ wantSBOM: &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: "DOCUMENT",
+ DocumentName: "masahiro331/CVE-2021-41098",
+ DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000001",
+ CreatorOrganizations: []string{"aquasecurity"},
+ CreatorTools: []string{"trivy"},
+ Created: "2021-08-25T12:20:30.000000005Z",
+ ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{},
+ },
+ Packages: map[spdx.ElementID]*spdx.Package2_2{
+ spdx.ElementID("5c11ed655628960c"): {
+ PackageSPDXIdentifier: spdx.ElementID("5c11ed655628960c"),
+ PackageName: "actioncable",
+ PackageVersion: "6.1.4.1",
+ PackageLicenseConcluded: "NONE",
+ PackageLicenseDeclared: "NONE",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ },
+ },
+ },
+ {
+ name: "happy path aggregate results",
+ inputReport: types.Report{
+ SchemaVersion: report.SchemaVersion,
+ ArtifactName: "test-aggregate",
+ ArtifactType: ftypes.ArtifactRemoteRepository,
+ Results: types.Results{
+ {
+ Target: "Node.js",
+ Class: types.ClassLangPkg,
+ Type: ftypes.NodePkg,
+ Packages: []ftypes.Package{
+ {
+ Name: "ruby-typeprof",
+ Version: "0.20.1",
+ License: "MIT",
+ Layer: ftypes.Layer{
+ DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e",
+ },
+ FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json",
+ },
+ },
+ },
+ },
+ },
+ wantSBOM: &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: "DOCUMENT",
+ DocumentName: "test-aggregate",
+ DocumentNamespace: "http://aquasecurity.github.io/trivy/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000001",
+ CreatorOrganizations: []string{"aquasecurity"},
+ CreatorTools: []string{"trivy"},
+ Created: "2021-08-25T12:20:30.000000005Z",
+ ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{},
+ },
+ Packages: map[spdx.ElementID]*spdx.Package2_2{
+ spdx.ElementID("983b94af8413fe04"): {
+ PackageSPDXIdentifier: spdx.ElementID("983b94af8413fe04"),
+ PackageName: "ruby-typeprof",
+ PackageVersion: "0.20.1",
+ PackageLicenseConcluded: "MIT",
+ PackageLicenseDeclared: "MIT",
+ IsFilesAnalyzedTagPresent: true,
+ },
+ },
+ },
+ },
+ {
+ name: "happy path empty",
+ inputReport: types.Report{
+ SchemaVersion: report.SchemaVersion,
+ ArtifactName: "empty/path",
+ ArtifactType: ftypes.ArtifactFilesystem,
+ Results: types.Results{},
+ },
+ wantSBOM: &spdx.Document2_2{
+ CreationInfo: &spdx.CreationInfo2_2{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: "DOCUMENT",
+ DocumentName: "empty/path",
+ DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000001",
+ CreatorOrganizations: []string{"aquasecurity"},
+ CreatorTools: []string{"trivy"},
+ Created: "2021-08-25T12:20:30.000000005Z",
+ ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{},
+ },
+ Packages: nil,
+ },
+ },
+ }
+
+ clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var count int
+ newUUID := func() uuid.UUID {
+ count++
+ return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count)))
+ }
+
+ output := bytes.NewBuffer(nil)
+ writer := reportSpdx.NewWriter(output, "dev", "spdx-json", reportSpdx.WithClock(clock), reportSpdx.WithNewUUID(newUUID))
+
+ err := writer.Write(tc.inputReport)
+ require.NoError(t, err)
+
+ got, err := jsonloader.Load2_2(output)
+ require.NoError(t, err)
+
+ assert.Equal(t, *tc.wantSBOM, *got)
+ })
+ }
+}
diff --git a/pkg/report/writer.go b/pkg/report/writer.go
index 5310da71ab..946bca8d20 100644
--- a/pkg/report/writer.go
+++ b/pkg/report/writer.go
@@ -11,6 +11,7 @@ import (
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report/cyclonedx"
+ "github.com/aquasecurity/trivy/pkg/report/spdx"
"github.com/aquasecurity/trivy/pkg/types"
)
@@ -50,6 +51,8 @@ func Write(report types.Report, option Option) error {
case "cyclonedx":
// TODO: support xml format option with cyclonedx writer
writer = cyclonedx.NewWriter(option.Output, option.AppVersion)
+ case "spdx", "spdx-json":
+ writer = spdx.NewWriter(option.Output, option.AppVersion, option.Format)
case "template":
// We keep `sarif.tpl` template working for backward compatibility for a while.
if strings.HasPrefix(option.OutputTemplate, "@") && strings.HasSuffix(option.OutputTemplate, "sarif.tpl") {