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") {