feat(report): add support for SPDX (#2059)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Shira Cohen
2022-05-02 13:44:26 +03:00
committed by GitHub
parent 6e2453c2d6
commit 6601d2957a
15 changed files with 1010 additions and 124 deletions

View File

@@ -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]

View File

@@ -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.

View File

@@ -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)
```

View File

@@ -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
[cyclonedx]: cyclonedx.md
[spdx]: spdx.md

297
docs/docs/sbom/spdx.md Normal file
View File

@@ -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
```
<details>
<summary>Result</summary>
```
$ 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
```
</details>
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
```
<details>
<summary>Result</summary>
```
$ 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"
}
```
</details>
[spdx]: https://spdx.dev/wp-content/uploads/sites/41/2020/08/SPDX-specification-2-2.pdf

3
go.mod
View File

@@ -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

5
go.sum
View File

@@ -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=

View File

@@ -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
- macros

View File

@@ -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"},
},
},

View File

@@ -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

View File

@@ -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{

View File

@@ -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 {

170
pkg/report/spdx/spdx.go Normal file
View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

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