From 5b821d3b134c249872840ac6dfd83105c38df6e7 Mon Sep 17 00:00:00 2001 From: Masahiro331 Date: Mon, 4 Jul 2022 02:03:21 +0900 Subject: [PATCH] feat(sbom): add cyclonedx sbom scan (#2203) Co-authored-by: knqyf263 --- docs/docs/references/cli/sbom.md | 55 +- docs/docs/sbom/cyclonedx.md | 29 + docs/docs/sbom/index.md | 35 +- go.mod | 2 +- go.sum | 4 +- integration/sbom_test.go | 96 ++++ .../testdata/centos-7-cyclonedx.json.golden | 526 ++++++++++++++++++ .../fixtures/sbom/centos-7-cyclonedx.json | 140 +++++ .../fluentd-multiple-lockfiles-cyclonedx.json | 169 ++++++ ...d-multiple-lockfiles-cyclonedx.json.golden | 346 ++++++++++++ pkg/commands/app.go | 10 +- pkg/commands/artifact/config.go | 2 +- pkg/commands/artifact/fs.go | 4 +- pkg/commands/artifact/image.go | 2 +- pkg/commands/artifact/inject.go | 13 + pkg/commands/artifact/repository.go | 2 +- pkg/commands/artifact/run.go | 57 +- pkg/commands/artifact/sbom.go | 23 +- pkg/commands/artifact/wire_gen.go | 49 +- pkg/commands/option/sbom.go | 2 + pkg/detector/library/detect.go | 1 + pkg/detector/ospkg/alma/alma.go | 1 + pkg/detector/ospkg/alpine/alpine.go | 1 + pkg/detector/ospkg/amazon/amazon.go | 1 + pkg/detector/ospkg/debian/debian.go | 1 + pkg/detector/ospkg/mariner/mariner.go | 1 + pkg/detector/ospkg/oracle/oracle.go | 1 + pkg/detector/ospkg/photon/photon.go | 1 + pkg/detector/ospkg/redhat/redhat.go | 1 + pkg/detector/ospkg/rocky/rocky.go | 1 + pkg/detector/ospkg/suse/suse.go | 1 + pkg/detector/ospkg/ubuntu/ubuntu.go | 1 + pkg/fanal/artifact/remote/git.go | 2 +- pkg/fanal/artifact/sbom/sbom.go | 125 +++++ pkg/fanal/artifact/sbom/sbom_test.go | 182 ++++++ pkg/fanal/artifact/sbom/testdata/bom.json | 235 ++++++++ .../artifact/sbom/testdata/os-only-bom.json | 74 +++ pkg/fanal/image/image.go | 12 +- pkg/fanal/types/artifact.go | 4 + pkg/fanal/types/sbom.go | 33 ++ pkg/module/serialize/types_easyjson.go | 15 +- pkg/purl/purl.go | 79 ++- pkg/purl/purl_test.go | 99 ++++ pkg/report/cyclonedx/cyclonedx.go | 25 +- pkg/sbom/cyclonedx/marshal.go | 106 +++- pkg/sbom/cyclonedx/marshal_test.go | 256 ++++++++- pkg/sbom/cyclonedx/testdata/happy/bom.json | 235 ++++++++ .../cyclonedx/testdata/happy/empty-bom.json | 48 ++ .../happy/independent-library-bom.json | 60 ++ .../cyclonedx/testdata/happy/os-only-bom.json | 74 +++ .../testdata/happy/unrelated-bom.json | 64 +++ .../cyclonedx/testdata/sad/invalid-purl.json | 52 ++ pkg/sbom/cyclonedx/unmarshal.go | 328 +++++++++++ pkg/sbom/cyclonedx/unmarshal_test.go | 214 +++++++ pkg/sbom/sbom.go | 67 +++ pkg/scanner/scan.go | 22 +- pkg/types/report.go | 3 + pkg/types/vulnerability.go | 1 + 58 files changed, 3892 insertions(+), 101 deletions(-) create mode 100644 integration/sbom_test.go create mode 100644 integration/testdata/centos-7-cyclonedx.json.golden create mode 100644 integration/testdata/fixtures/sbom/centos-7-cyclonedx.json create mode 100644 integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json create mode 100644 integration/testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden create mode 100644 pkg/fanal/artifact/sbom/sbom.go create mode 100644 pkg/fanal/artifact/sbom/sbom_test.go create mode 100644 pkg/fanal/artifact/sbom/testdata/bom.json create mode 100644 pkg/fanal/artifact/sbom/testdata/os-only-bom.json create mode 100644 pkg/fanal/types/sbom.go create mode 100644 pkg/sbom/cyclonedx/testdata/happy/bom.json create mode 100644 pkg/sbom/cyclonedx/testdata/happy/empty-bom.json create mode 100644 pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json create mode 100644 pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json create mode 100644 pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json create mode 100644 pkg/sbom/cyclonedx/testdata/sad/invalid-purl.json create mode 100644 pkg/sbom/cyclonedx/unmarshal.go create mode 100644 pkg/sbom/cyclonedx/unmarshal_test.go create mode 100644 pkg/sbom/sbom.go diff --git a/docs/docs/references/cli/sbom.md b/docs/docs/references/cli/sbom.md index 6c2fc99eae53..c30a22b9cf38 100644 --- a/docs/docs/references/cli/sbom.md +++ b/docs/docs/references/cli/sbom.md @@ -2,26 +2,45 @@ ```bash NAME: - trivy sbom - generate SBOM for an artifact + trivy sbom - scan SBOM for vulnerabilities USAGE: - trivy sbom [command options] ARTIFACT - -DESCRIPTION: - ARTIFACT can be a container image, file path/directory, git repository or container image archive. See examples. + trivy sbom [command options] SBOM 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] - --insecure allow insecure server connections when using SSL (default: false) [$TRIVY_INSECURE] - --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, spdx, spdx-json) (default: "cyclonedx") [$TRIVY_SBOM_FORMAT] - --help, -h show help (default: false) + --cache-backend value cache backend (e.g. redis://localhost:6379) (default: "fs") [$TRIVY_CACHE_BACKEND] + --cache-ttl value cache TTL when using redis as cache backend (default: 0s) [$TRIVY_CACHE_TTL] + --clear-cache, -c clear image caches without scanning (default: false) [$TRIVY_CLEAR_CACHE] + --custom-headers value custom headers in client/server mode (accepts multiple inputs) [$TRIVY_CUSTOM_HEADERS] + --db-repository value OCI repository to retrieve trivy-db from (default: "ghcr.io/aquasecurity/trivy-db") [$TRIVY_DB_REPOSITORY] + --download-db-only download/update vulnerability database but don't run a scan (default: false) [$TRIVY_DOWNLOAD_DB_ONLY] + --exit-code value Exit code when vulnerabilities were found (default: 0) [$TRIVY_EXIT_CODE] + --format value, -f value format (table, json, sarif, template, cyclonedx, spdx, spdx-json, github) (default: "table") [$TRIVY_FORMAT] + --ignore-policy value specify the Rego file to evaluate each vulnerability [$TRIVY_IGNORE_POLICY] + --ignore-unfixed display only fixed vulnerabilities (default: false) [$TRIVY_IGNORE_UNFIXED] + --ignorefile value specify .trivyignore file (default: ".trivyignore") [$TRIVY_IGNOREFILE] + --input value, -i value input file path instead of image name [$TRIVY_INPUT] + --insecure allow insecure server connections when using SSL (default: false) [$TRIVY_INSECURE] + --list-all-pkgs enabling the option will output all packages regardless of vulnerability (default: false) [$TRIVY_LIST_ALL_PKGS] + --no-progress suppress progress bar (default: false) [$TRIVY_NO_PROGRESS] + --offline-scan do not issue API requests to identify dependencies (default: false) [$TRIVY_OFFLINE_SCAN] + --output value, -o value output file name [$TRIVY_OUTPUT] + --reset remove all caches and database (default: false) [$TRIVY_RESET] + --security-checks value comma-separated list of what security issues to detect (vuln,config,secret) (default: "vuln") [$TRIVY_SECURITY_CHECKS] + --server value server address [$TRIVY_SERVER] + --severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL") [$TRIVY_SEVERITY] + --skip-db-update, --skip-update skip updating vulnerability database (default: false) [$TRIVY_SKIP_UPDATE, $TRIVY_SKIP_DB_UPDATE] + --skip-dirs value specify the directories where the traversal is skipped (accepts multiple inputs) [$TRIVY_SKIP_DIRS] + --skip-files value specify the file paths to skip traversal (accepts multiple inputs) [$TRIVY_SKIP_FILES] + --template value, -t value output template [$TRIVY_TEMPLATE] + --timeout value timeout (default: 5m0s) [$TRIVY_TIMEOUT] + --token value for authentication in client/server mode [$TRIVY_TOKEN] + --token-header value specify a header name for token in client/server mode (default: "Trivy-Token") [$TRIVY_TOKEN_HEADER] + +EXAMPLES: + - Scan CycloneDX and show the result in tables: + $ trivy sbom /path/to/report.cdx + + - Scan CycloneDX and generate a CycloneDX report: + $ trivy sbom --format cyclonedx /path/to/report.cdx ``` diff --git a/docs/docs/sbom/cyclonedx.md b/docs/docs/sbom/cyclonedx.md index ddf575b8b0e2..e703b602d655 100644 --- a/docs/docs/sbom/cyclonedx.md +++ b/docs/docs/sbom/cyclonedx.md @@ -1,5 +1,6 @@ # CycloneDX +## Reporting Trivy generates JSON reports in the [CycloneDX][cyclonedx] format. Note that XML format is not supported at the moment. @@ -230,4 +231,32 @@ $ cat result.json | jq . +## Scanning +Trivy can take CycloneDX as an input and scan for vulnerabilities. +To scan SBOM, you can use the `sbom` subcommand and pass the path to your CycloneDX report. + +```bash +$ trivy sbom /path/to/cyclonedx.json + +cyclonedx.json (alpine 3.7.1) +========================= +Total: 3 (CRITICAL: 3) + +┌─────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ curl │ CVE-2018-14618 │ CRITICAL │ 7.61.0-r0 │ 7.61.1-r0 │ curl: NTLM password overflow via integer overflow │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2018-14618 │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ libbz2 │ CVE-2019-12900 │ CRITICAL │ 1.0.6-r6 │ 1.0.6-r7 │ bzip2: out-of-bounds write in function BZ2_decompress │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-12900 │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ sqlite-libs │ CVE-2019-8457 │ CRITICAL │ 3.21.0-r1 │ 3.25.3-r1 │ sqlite: heap out-of-bound read in function rtreenode() │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-8457 │ +└─────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘ +``` + +!!! note + If you want to generate a CycloneDX report from a CycloneDX input, please be aware that the output stores references to your original CycloneDX report and contains only detected vulnerabilities, not components. + [cyclonedx]: https://cyclonedx.org/ \ No newline at end of file diff --git a/docs/docs/sbom/index.md b/docs/docs/sbom/index.md index 5af032917e9e..cf5a69657496 100644 --- a/docs/docs/sbom/index.md +++ b/docs/docs/sbom/index.md @@ -1,6 +1,7 @@ # SBOM -Trivy currently supports the following SBOM formats. +## Reporting +Trivy can generate the following SBOM formats. - [CycloneDX][cyclonedx] - [SPDX][spdx] @@ -175,5 +176,37 @@ $ trivy fs --format cyclonedx --output result.json /app/myproject +## Scanning +Trivy also can take the following SBOM formats as an input and scan for vulnerabilities. + +- CycloneDX + +To scan SBOM, you can use the `sbom` subcommand and pass the path to the SBOM. + +```bash +$ trivy sbom /path/to/cyclonedx.json + +cyclonedx.json (alpine 3.7.1) +========================= +Total: 3 (CRITICAL: 3) + +┌─────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ curl │ CVE-2018-14618 │ CRITICAL │ 7.61.0-r0 │ 7.61.1-r0 │ curl: NTLM password overflow via integer overflow │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2018-14618 │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ libbz2 │ CVE-2019-12900 │ CRITICAL │ 1.0.6-r6 │ 1.0.6-r7 │ bzip2: out-of-bounds write in function BZ2_decompress │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-12900 │ +├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ sqlite-libs │ CVE-2019-8457 │ CRITICAL │ 3.21.0-r1 │ 3.25.3-r1 │ sqlite: heap out-of-bound read in function rtreenode() │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-8457 │ +└─────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘ +``` + + +!!! note + CycloneDX XML and SPDX are not supported at the moment. + [cyclonedx]: cyclonedx.md [spdx]: spdx.md diff --git a/go.mod b/go.mod index 46e4fa0732b9..dfd44f1851c5 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/hashicorp/go-getter v1.6.2 github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d - github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 + github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 github.com/kylelemons/godebug v1.1.0 github.com/mailru/easyjson v0.7.6 github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 diff --git a/go.sum b/go.sum index 78663d48b360..6f98ebe1d371 100644 --- a/go.sum +++ b/go.sum @@ -943,8 +943,8 @@ github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GX github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c= github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= -github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 h1:HDjRqotkViMNcGMGicb7cgxklx8OwnjtCBmyWEqrRvM= -github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 h1:aC6MEAs3PE3lWD7lqrJfDxHd6hcced9R4JTZu85cJwU= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= github.com/knqyf263/go-rpmdb v0.0.0-20220607073645-842f01763e21 h1:3E1B04qBvkGmr6oXPSwLpuAF0wekN67CKseKGRjj6Yo= github.com/knqyf263/go-rpmdb v0.0.0-20220607073645-842f01763e21/go.mod h1:zp6SMcRd0GB+uwNJjr+DkrNZdQZ4er2HMO6KyD0vIGU= github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc= diff --git a/integration/sbom_test.go b/integration/sbom_test.go new file mode 100644 index 000000000000..eb5b0ddb7888 --- /dev/null +++ b/integration/sbom_test.go @@ -0,0 +1,96 @@ +//go:build integration +// +build integration + +package integration + +import ( + "io" + "os" + "path/filepath" + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/commands" +) + +func TestCycloneDX(t *testing.T) { + type args struct { + input string + format string + artifactType string + } + tests := []struct { + name string + args args + golden string + }{ + { + name: "centos7-bom by trivy", + args: args{ + input: "testdata/fixtures/sbom/centos-7-cyclonedx.json", + format: "cyclonedx", + artifactType: "cyclonedx", + }, + golden: "testdata/centos-7-cyclonedx.json.golden", + }, + { + name: "fluentd-multiple-lockfiles-bom by trivy", + args: args{ + input: "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json", + format: "cyclonedx", + artifactType: "cyclonedx", + }, + golden: "testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden", + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "trivy", "--cache-dir", cacheDir, "sbom", "--skip-db-update", "--format", tt.args.format, + } + + // Setup the output file + outputFile := filepath.Join(t.TempDir(), "output.json") + if *update { + outputFile = tt.golden + } + + osArgs = append(osArgs, "--output", outputFile) + osArgs = append(osArgs, tt.args.input) + + // Setup CLI App + app := commands.NewApp("dev") + app.Writer = io.Discard + + // Run "trivy sbom" + assert.Nil(t, app.Run(osArgs)) + + // Compare want and got + want := decodeCycloneDX(t, tt.golden) + got := decodeCycloneDX(t, outputFile) + assert.Equal(t, want, got) + }) + } +} + +func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM { + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + bom := cdx.NewBOM() + decoder := cdx.NewBOMDecoder(f, cdx.BOMFileFormatJSON) + err = decoder.Decode(bom) + require.NoError(t, err) + + bom.Metadata.Timestamp = "" + + return bom +} diff --git a/integration/testdata/centos-7-cyclonedx.json.golden b/integration/testdata/centos-7-cyclonedx.json.golden new file mode 100644 index 000000000000..30d2f12fc8e6 --- /dev/null +++ b/integration/testdata/centos-7-cyclonedx.json.golden @@ -0,0 +1,526 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2022-07-03T08:45:54+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "urn:uuid:1455c02d-64ca-453e-a5df-ddfb70a7c804/1", + "type": "container", + "name": "integration/testdata/fixtures/images/centos-7.tar.gz" + } + }, + "vulnerabilities": [ + { + "id": "CVE-2019-18276", + "ratings": [ + { + "source": { + "name": "cbl-mariner" + }, + "severity": "high" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.2, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.8, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "low" + }, + { + "source": { + "name": "photon" + }, + "severity": "high" + }, + { + "source": { + "name": "redhat" + }, + "score": 7.8, + "severity": "low", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "low" + } + ], + "cwes": [ + 273 + ], + "description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "advisories": [ + { + "url": "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-18276" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276" + }, + { + "url": "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2019-18276.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2021-1679.html" + }, + { + "url": "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-18276" + }, + { + "url": "https://security.gentoo.org/glsa/202105-34" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20200430-0003/" + }, + { + "url": "https://www.youtube.com/watch?v=-wGtxJ8opa8" + } + ], + "published": "2019-11-28T01:15:00+00:00", + "updated": "2021-05-26T12:15:00+00:00", + "affects": [ + { + "ref": "urn:cdx:1455c02d-64ca-453e-a5df-ddfb70a7c804/1#pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "versions": [ + { + "version": "4.2.46-31.el7", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2019-1559", + "ratings": [ + { + "source": { + "name": "amazon" + }, + "severity": "medium" + }, + { + "source": { + "name": "arch-linux" + }, + "severity": "medium" + }, + { + "source": { + "name": "nvd" + }, + "score": 4.3, + "severity": "medium", + "method": "CVSSv2", + "vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N" + }, + { + "source": { + "name": "nvd" + }, + "score": 5.9, + "severity": "medium", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "medium" + }, + { + "source": { + "name": "redhat" + }, + "score": 5.9, + "severity": "medium", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "medium" + } + ], + "cwes": [ + 203 + ], + "description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "advisories": [ + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html" + }, + { + "url": "http://www.securityfocus.com/bid/107174" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2304" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2437" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2439" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2471" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3929" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3931" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-1559" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559" + }, + { + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e" + }, + { + "url": "https://github.com/RUB-NDS/TLS-Padding-Oracles" + }, + { + "url": "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2019-1559.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2019-2471.html" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/" + }, + { + "url": "https://security.gentoo.org/glsa/201903-10" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190301-0001/" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190301-0002/" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190423-0002/" + }, + { + "url": "https://support.f5.com/csp/article/K18549143" + }, + { + "url": "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS" + }, + { + "url": "https://ubuntu.com/security/notices/USN-3899-1" + }, + { + "url": "https://ubuntu.com/security/notices/USN-4376-2" + }, + { + "url": "https://usn.ubuntu.com/3899-1/" + }, + { + "url": "https://usn.ubuntu.com/4376-2/" + }, + { + "url": "https://www.debian.org/security/2019/dsa-4400" + }, + { + "url": "https://www.openssl.org/news/secadv/20190226.txt" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + }, + { + "url": "https://www.tenable.com/security/tns-2019-02" + }, + { + "url": "https://www.tenable.com/security/tns-2019-03" + } + ], + "published": "2019-02-27T23:29:00+00:00", + "updated": "2021-01-20T15:15:00+00:00", + "affects": [ + { + "ref": "urn:cdx:1455c02d-64ca-453e-a5df-ddfb70a7c804/1#pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "versions": [ + { + "version": "1:1.0.2k-16.el7", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2018-0734", + "ratings": [ + { + "source": { + "name": "amazon" + }, + "severity": "medium" + }, + { + "source": { + "name": "arch-linux" + }, + "severity": "low" + }, + { + "source": { + "name": "cbl-mariner" + }, + "severity": "medium" + }, + { + "source": { + "name": "nvd" + }, + "score": 4.3, + "severity": "medium", + "method": "CVSSv2", + "vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N" + }, + { + "source": { + "name": "nvd" + }, + "score": 5.9, + "severity": "medium", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "low" + }, + { + "source": { + "name": "photon" + }, + "severity": "medium" + }, + { + "source": { + "name": "redhat" + }, + "score": 5.1, + "severity": "low", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "low" + } + ], + "cwes": [ + 327 + ], + "description": "The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p).", + "advisories": [ + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html" + }, + { + "url": "http://www.securityfocus.com/bid/105758" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2304" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3700" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3932" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3933" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3935" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2018-0734" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734" + }, + { + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac" + }, + { + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f" + }, + { + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2018-0734.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2019-3700.html" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/" + }, + { + "url": "https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-0734" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20181105-0002/" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190118-0002/" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190423-0002/" + }, + { + "url": "https://ubuntu.com/security/notices/USN-3840-1" + }, + { + "url": "https://usn.ubuntu.com/3840-1/" + }, + { + "url": "https://www.debian.org/security/2018/dsa-4348" + }, + { + "url": "https://www.debian.org/security/2018/dsa-4355" + }, + { + "url": "https://www.openssl.org/news/secadv/20181030.txt" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuapr2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html" + }, + { + "url": "https://www.tenable.com/security/tns-2018-16" + }, + { + "url": "https://www.tenable.com/security/tns-2018-17" + } + ], + "published": "2018-10-30T12:29:00+00:00", + "updated": "2020-08-24T17:37:00+00:00", + "affects": [ + { + "ref": "urn:cdx:1455c02d-64ca-453e-a5df-ddfb70a7c804/1#pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "versions": [ + { + "version": "1:1.0.2k-16.el7", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json new file mode 100644 index 000000000000..098563a80025 --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json @@ -0,0 +1,140 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:1455c02d-64ca-453e-a5df-ddfb70a7c804", + "version": 1, + "metadata": { + "timestamp": "2022-06-14T15:08:48+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "d0d41e30-9650-489d-948d-425ff2ed63d2", + "type": "container", + "name": "integration/testdata/fixtures/images/centos-7.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "type": "library", + "name": "bash", + "version": "4.2.46-31.el7", + "licenses": [ + { + "expression": "GPLv3+" + } + ], + "purl": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.2.46" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "31.el7" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810", + "type": "library", + "name": "openssl-libs", + "version": "1:1.0.2k-16.el7", + "licenses": [ + { + "expression": "OpenSSL" + } + ], + "purl": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.0.2k" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "16.el7" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + { + "bom-ref": "0175f732-df9d-4bb8-9f56-870898e3ff89", + "type": "operating-system", + "name": "centos", + "version": "7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "centos" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "0175f732-df9d-4bb8-9f56-870898e3ff89", + "dependsOn": [ + "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810" + ] + }, + { + "ref": "d0d41e30-9650-489d-948d-425ff2ed63d2", + "dependsOn": [ + "0175f732-df9d-4bb8-9f56-870898e3ff89" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json b/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json new file mode 100644 index 000000000000..ea9bf391f6c8 --- /dev/null +++ b/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json @@ -0,0 +1,169 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:31ee662c-480e-4f63-9765-23ea8afc754d", + "version": 1, + "metadata": { + "timestamp": "2022-06-14T15:10:14+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "type": "container", + "name": "integration/testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "type": "library", + "name": "bash", + "version": "5.0-4", + "purl": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.0-4" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "type": "library", + "name": "libidn2-0", + "version": "2.0.5-1", + "purl": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "libidn2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.0.5-1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + } + ] + }, + { + "bom-ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "type": "operating-system", + "name": "debian", + "version": "10.2", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec", + "type": "library", + "name": "activesupport", + "version": "6.0.2.1", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:gem/activesupport@6.0.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gemspec" + } + ] + } + ], + "dependencies": [ + { + "ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "dependsOn": [ + "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2" + ] + }, + { + "ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "dependsOn": [ + "353f2470-9c8b-4647-9d0d-96d893838dc8", + "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden b/integration/testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden new file mode 100644 index 000000000000..ff7e9626536a --- /dev/null +++ b/integration/testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden @@ -0,0 +1,346 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2022-07-03T08:45:54+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "urn:uuid:31ee662c-480e-4f63-9765-23ea8afc754d/1", + "type": "container", + "name": "integration/testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz" + } + }, + "vulnerabilities": [ + { + "id": "CVE-2020-8165", + "source": { + "name": "ghsa", + "url": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Arubygems" + }, + "ratings": [ + { + "source": { + "name": "ghsa" + }, + "severity": "high" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + }, + { + "source": { + "name": "nvd" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 9.8, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "cwes": [ + 502 + ], + "description": "A deserialization of untrusted data vulnerability exists in rails \u003c 5.2.4.3, rails \u003c 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE.", + "advisories": [ + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00031.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00034.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2020-8165" + }, + { + "url": "https://github.com/advisories/GHSA-2p68-f74v-9wc6" + }, + { + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2020-8165.yml" + }, + { + "url": "https://groups.google.com/forum/#!msg/rubyonrails-security/bv6fW4S0Y1c/KnkEqM7AAQAJ" + }, + { + "url": "https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c" + }, + { + "url": "https://groups.google.com/g/rubyonrails-security/c/bv6fW4S0Y1c" + }, + { + "url": "https://hackerone.com/reports/413388" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/06/msg00022.html" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/07/msg00013.html" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-8165" + }, + { + "url": "https://weblog.rubyonrails.org/2020/5/18/Rails-5-2-4-3-and-6-0-3-1-have-been-released/" + }, + { + "url": "https://www.debian.org/security/2020/dsa-4766" + } + ], + "published": "2020-06-19T18:15:00+00:00", + "updated": "2020-10-17T12:15:00+00:00", + "affects": [ + { + "ref": "urn:cdx:31ee662c-480e-4f63-9765-23ea8afc754d/1#pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec", + "versions": [ + { + "version": "6.0.2.1", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2019-18276", + "source": { + "name": "debian", + "url": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "ratings": [ + { + "source": { + "name": "cbl-mariner" + }, + "severity": "high" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.2, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.8, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "low" + }, + { + "source": { + "name": "photon" + }, + "severity": "high" + }, + { + "source": { + "name": "redhat" + }, + "score": 7.8, + "severity": "low", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "low" + } + ], + "cwes": [ + 273 + ], + "description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "advisories": [ + { + "url": "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-18276" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276" + }, + { + "url": "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2019-18276.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2021-1679.html" + }, + { + "url": "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-18276" + }, + { + "url": "https://security.gentoo.org/glsa/202105-34" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20200430-0003/" + }, + { + "url": "https://www.youtube.com/watch?v=-wGtxJ8opa8" + } + ], + "published": "2019-11-28T01:15:00+00:00", + "updated": "2021-05-26T12:15:00+00:00", + "affects": [ + { + "ref": "urn:cdx:31ee662c-480e-4f63-9765-23ea8afc754d/1#pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "versions": [ + { + "version": "5.0-4", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2019-18224", + "source": { + "name": "debian", + "url": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "ratings": [ + { + "source": { + "name": "amazon" + }, + "severity": "medium" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + }, + { + "source": { + "name": "nvd" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 5.6, + "severity": "medium", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "medium" + } + ], + "cwes": [ + 787 + ], + "description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "advisories": [ + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-18224" + }, + { + "url": "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224" + }, + { + "url": "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c" + }, + { + "url": "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/" + }, + { + "url": "https://seclists.org/bugtraq/2020/Feb/4" + }, + { + "url": "https://security.gentoo.org/glsa/202003-63" + }, + { + "url": "https://ubuntu.com/security/notices/USN-4168-1" + }, + { + "url": "https://usn.ubuntu.com/4168-1/" + }, + { + "url": "https://www.debian.org/security/2020/dsa-4613" + } + ], + "published": "2019-10-21T17:15:00+00:00", + "updated": "2019-10-29T19:15:00+00:00", + "affects": [ + { + "ref": "urn:cdx:31ee662c-480e-4f63-9765-23ea8afc754d/1#pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "versions": [ + { + "version": "2.0.5-1", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/pkg/commands/app.go b/pkg/commands/app.go index a180fd515870..d4e8f44352eb 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/samber/lo" "github.com/urfave/cli/v2" "github.com/aquasecurity/trivy-db/pkg/metadata" @@ -929,7 +930,6 @@ func NewSbomCommand() *cli.Command { &templateFlag, &formatFlag, &inputFlag, - &severityFlag, &outputFlag, &exitCodeFlag, &skipDBUpdateFlag, @@ -940,6 +940,7 @@ func NewSbomCommand() *cli.Command { &ignoreUnfixedFlag, &ignoreFileFlag, &timeoutFlag, + &severityFlag, &ignorePolicy, &listAllPackages, &cacheBackendFlag, @@ -950,10 +951,17 @@ func NewSbomCommand() *cli.Command { &offlineScan, &insecureFlag, &dbRepositoryFlag, + lo.ToPtr(withValue(securityChecksFlag, types.SecurityCheckVulnerability)), // Enable only vulnerability scanning stringSliceFlag(skipFiles), stringSliceFlag(skipDirs), + // for client/server + &remoteServer, + &token, + &tokenHeader, + &customHeaders, + // deprecated options &cli.StringFlag{ Name: "artifact-type", diff --git a/pkg/commands/artifact/config.go b/pkg/commands/artifact/config.go index 85c247a8624f..bc970eca2fbe 100644 --- a/pkg/commands/artifact/config.go +++ b/pkg/commands/artifact/config.go @@ -23,5 +23,5 @@ func ConfigRun(ctx *cli.Context) error { opt.SecurityChecks = []string{types.SecurityCheckConfig} // Run filesystem command internally - return run(ctx.Context, opt, filesystemArtifact) + return run(ctx.Context, opt, TargetFilesystem) } diff --git a/pkg/commands/artifact/fs.go b/pkg/commands/artifact/fs.go index e66840742d01..7324e0b19b51 100644 --- a/pkg/commands/artifact/fs.go +++ b/pkg/commands/artifact/fs.go @@ -29,10 +29,10 @@ func filesystemRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.S // FilesystemRun runs scan on filesystem for language-specific dependencies and config files func FilesystemRun(ctx *cli.Context) error { - return Run(ctx, filesystemArtifact) + return Run(ctx, TargetFilesystem) } // RootfsRun runs scan on rootfs. func RootfsRun(ctx *cli.Context) error { - return Run(ctx, rootfsArtifact) + return Run(ctx, TargetRootfs) } diff --git a/pkg/commands/artifact/image.go b/pkg/commands/artifact/image.go index ec001b250cf1..618e41957e59 100644 --- a/pkg/commands/artifact/image.go +++ b/pkg/commands/artifact/image.go @@ -66,5 +66,5 @@ func archiveRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scan // ImageRun runs scan on container image func ImageRun(ctx *cli.Context) error { - return Run(ctx, containerImageArtifact) + return Run(ctx, TargetContainerImage) } diff --git a/pkg/commands/artifact/inject.go b/pkg/commands/artifact/inject.go index d24774d14663..bd4193c11aad 100644 --- a/pkg/commands/artifact/inject.go +++ b/pkg/commands/artifact/inject.go @@ -49,6 +49,12 @@ func initializeRepositoryScanner(ctx context.Context, url string, artifactCache return scanner.Scanner{}, nil, nil } +func initializeSBOMScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, + localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, func(), error) { + wire.Build(scanner.StandaloneSBOMSet) + return scanner.Scanner{}, nil, nil +} + ///////////////// // Client/Server ///////////////// @@ -76,3 +82,10 @@ func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifac wire.Build(scanner.RemoteFilesystemSet) return scanner.Scanner{}, nil, nil } + +// initializeRemoteSBOMScanner is for sbom scanning in client/server mode +func initializeRemoteSBOMScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, + remoteScanOptions client.ScannerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) { + wire.Build(scanner.RemoteSBOMSet) + return scanner.Scanner{}, nil, nil +} diff --git a/pkg/commands/artifact/repository.go b/pkg/commands/artifact/repository.go index de5d897f1cee..8d718a17e59b 100644 --- a/pkg/commands/artifact/repository.go +++ b/pkg/commands/artifact/repository.go @@ -20,5 +20,5 @@ func repositoryStandaloneScanner(ctx context.Context, conf ScannerConfig) (scann // RepositoryRun runs scan on repository func RepositoryRun(ctx *cli.Context) error { - return Run(ctx, repositoryArtifact) + return Run(ctx, TargetRepository) } diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index c39424a37973..3f3d9345acf5 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -28,21 +28,21 @@ import ( "github.com/aquasecurity/trivy/pkg/utils" ) -type ArtifactType string +// TargetKind represents what kind of artifact Trivy scans +type TargetKind string const ( - containerImageArtifact ArtifactType = "image" - filesystemArtifact ArtifactType = "fs" - rootfsArtifact ArtifactType = "rootfs" - repositoryArtifact ArtifactType = "repo" - imageArchiveArtifact ArtifactType = "archive" - sbomArtifact ArtifactType = "sbom" + TargetContainerImage TargetKind = "image" + TargetFilesystem TargetKind = "fs" + TargetRootfs TargetKind = "rootfs" + TargetRepository TargetKind = "repo" + TargetImageArchive TargetKind = "archive" + TargetSBOM TargetKind = "sbom" ) var ( defaultPolicyNamespaces = []string{"appshield", "defsec", "builtin"} - - SkipScan = errors.New("skip subsequent processes") + SkipScan = errors.New("skip subsequent processes") ) // InitializeScanner defines the initialize function signature of scanner @@ -116,10 +116,6 @@ func NewRunner(cliOption Option, opts ...runnerOption) (Runner, error) { return nil, xerrors.Errorf("cache error: %w", err) } - if err = r.initDB(cliOption); err != nil { - return nil, xerrors.Errorf("DB error: %w", err) - } - // Initialize WASM modules m, err := module.NewManager(cliOption.Context.Context) if err != nil { @@ -215,11 +211,24 @@ func (r *runner) ScanSBOM(ctx context.Context, opt Option) (types.Report, error) opt.ReportOption.VulnType = []string{types.VulnTypeOS, types.VulnTypeLibrary} opt.ReportOption.SecurityChecks = []string{types.SecurityCheckVulnerability} - // TODO: implement SBOM scanning - return types.Report{}, nil + var s InitializeScanner + if opt.RemoteAddr == "" { + // Scan cycloneDX in standalone mode + s = sbomStandaloneScanner + } else { + // Scan cycloneDX in client/server mode + s = sbomRemoteScanner + } + + return r.scanArtifact(ctx, opt, s) } func (r *runner) scanArtifact(ctx context.Context, opt Option, initializeScanner InitializeScanner) (types.Report, error) { + // Update the vulnerability database if needed. + if err := r.initDB(opt); err != nil { + return types.Report{}, xerrors.Errorf("DB error: %w", err) + } + report, err := scan(ctx, opt, initializeScanner, r.cache) if err != nil { return types.Report{}, xerrors.Errorf("scan error: %w", err) @@ -328,16 +337,16 @@ func (r *runner) initCache(c Option) error { } // Run performs artifact scanning -func Run(cliCtx *cli.Context, artifactType ArtifactType) error { +func Run(cliCtx *cli.Context, targetKind TargetKind) error { opt, err := InitOption(cliCtx) if err != nil { return xerrors.Errorf("InitOption: %w", err) } - return run(cliCtx.Context, opt, artifactType) + return run(cliCtx.Context, opt, targetKind) } -func run(ctx context.Context, opt Option, artifactType ArtifactType) (err error) { +func run(ctx context.Context, opt Option, targetKind TargetKind) (err error) { ctx, cancel := context.WithTimeout(ctx, opt.Timeout) defer cancel() @@ -357,24 +366,24 @@ func run(ctx context.Context, opt Option, artifactType ArtifactType) (err error) defer r.Close(ctx) var report types.Report - switch artifactType { - case containerImageArtifact, imageArchiveArtifact: + switch targetKind { + case TargetContainerImage, TargetImageArchive: if report, err = r.ScanImage(ctx, opt); err != nil { return xerrors.Errorf("image scan error: %w", err) } - case filesystemArtifact: + case TargetFilesystem: if report, err = r.ScanFilesystem(ctx, opt); err != nil { return xerrors.Errorf("filesystem scan error: %w", err) } - case rootfsArtifact: + case TargetRootfs: if report, err = r.ScanRootfs(ctx, opt); err != nil { return xerrors.Errorf("rootfs scan error: %w", err) } - case repositoryArtifact: + case TargetRepository: if report, err = r.ScanRepository(ctx, opt); err != nil { return xerrors.Errorf("repository scan error: %w", err) } - case sbomArtifact: + case TargetSBOM: if report, err = r.ScanSBOM(ctx, opt); err != nil { return xerrors.Errorf("sbom scan error: %w", err) } diff --git a/pkg/commands/artifact/sbom.go b/pkg/commands/artifact/sbom.go index 72b9719045c5..f3efc25351ef 100644 --- a/pkg/commands/artifact/sbom.go +++ b/pkg/commands/artifact/sbom.go @@ -1,10 +1,31 @@ package artifact import ( + "context" + "github.com/urfave/cli/v2" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/scanner" ) // SBOMRun scans SBOM for vulnerabilities func SBOMRun(ctx *cli.Context) error { - return Run(ctx, sbomArtifact) + return Run(ctx, TargetSBOM) +} + +func sbomStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) + } + return s, cleanup, nil +} + +func sbomRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeRemoteSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) + } + return s, cleanup, nil } diff --git a/pkg/commands/artifact/wire_gen.go b/pkg/commands/artifact/wire_gen.go index b434232cd6d9..16cfc1d33b86 100644 --- a/pkg/commands/artifact/wire_gen.go +++ b/pkg/commands/artifact/wire_gen.go @@ -8,7 +8,6 @@ package artifact import ( "context" - "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy/pkg/detector/ospkg" "github.com/aquasecurity/trivy/pkg/fanal/applier" @@ -16,6 +15,7 @@ import ( image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" local2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/local" "github.com/aquasecurity/trivy/pkg/fanal/artifact/remote" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" "github.com/aquasecurity/trivy/pkg/fanal/cache" "github.com/aquasecurity/trivy/pkg/fanal/image" "github.com/aquasecurity/trivy/pkg/fanal/types" @@ -35,7 +35,8 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach config := db.Config{} client := vulnerability.NewClient(config) localScanner := local.NewScanner(applierApplier, detector, client) - typesImage, cleanup, err := image.NewContainerImage(ctx, imageName, dockerOpt) + v := _wireValue + typesImage, cleanup, err := image.NewContainerImage(ctx, imageName, dockerOpt, v...) if err != nil { return scanner.Scanner{}, nil, err } @@ -50,6 +51,10 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach }, nil } +var ( + _wireValue = []image.Option(nil) +) + // initializeArchiveScanner is for container image archive scanning in standalone mode // e.g. docker save -o alpine.tar alpine:3.15 func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, error) { @@ -102,12 +107,28 @@ func initializeRepositoryScanner(ctx context.Context, url string, artifactCache }, nil } +func initializeSBOMScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, func(), error) { + applierApplier := applier.NewApplier(localArtifactCache) + detector := ospkg.Detector{} + config := db.Config{} + client := vulnerability.NewClient(config) + localScanner := local.NewScanner(applierApplier, detector, client) + artifactArtifact, err := sbom.NewArtifact(filePath, artifactCache, artifactOption) + if err != nil { + return scanner.Scanner{}, nil, err + } + scannerScanner := scanner.NewScanner(localScanner, artifactArtifact) + return scannerScanner, func() { + }, nil +} + // initializeRemoteDockerScanner is for container image scanning in client/server mode // e.g. dockerd, container registry, podman, etc. func initializeRemoteDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, dockerOpt types.DockerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) { - v := _wireValue + v := _wireValue2 clientScanner := client.NewScanner(remoteScanOptions, v...) - typesImage, cleanup, err := image.NewContainerImage(ctx, imageName, dockerOpt) + v2 := _wireValue3 + typesImage, cleanup, err := image.NewContainerImage(ctx, imageName, dockerOpt, v2...) if err != nil { return scanner.Scanner{}, nil, err } @@ -123,13 +144,14 @@ func initializeRemoteDockerScanner(ctx context.Context, imageName string, artifa } var ( - _wireValue = []client.Option(nil) + _wireValue2 = []client.Option(nil) + _wireValue3 = []image.Option(nil) ) // initializeRemoteArchiveScanner is for container image archive scanning in client/server mode // e.g. docker save -o alpine.tar alpine:3.15 func initializeRemoteArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, artifactOption artifact.Option) (scanner.Scanner, error) { - v := _wireValue + v := _wireValue2 clientScanner := client.NewScanner(remoteScanOptions, v...) typesImage, err := image.NewArchiveImage(filePath) if err != nil { @@ -145,7 +167,7 @@ func initializeRemoteArchiveScanner(ctx context.Context, filePath string, artifa // initializeRemoteFilesystemScanner is for filesystem scanning in client/server mode func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) { - v := _wireValue + v := _wireValue2 clientScanner := client.NewScanner(remoteScanOptions, v...) artifactArtifact, err := local2.NewArtifact(path, artifactCache, artifactOption) if err != nil { @@ -155,3 +177,16 @@ func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifac return scannerScanner, func() { }, nil } + +// initializeRemoteSBOMScanner is for sbom scanning in client/server mode +func initializeRemoteSBOMScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) { + v := _wireValue2 + clientScanner := client.NewScanner(remoteScanOptions, v...) + artifactArtifact, err := sbom.NewArtifact(path, artifactCache, artifactOption) + if err != nil { + return scanner.Scanner{}, nil, err + } + scannerScanner := scanner.NewScanner(clientScanner, artifactArtifact) + return scannerScanner, func() { + }, nil +} diff --git a/pkg/commands/option/sbom.go b/pkg/commands/option/sbom.go index c4b86f3abb89..f515954c330a 100644 --- a/pkg/commands/option/sbom.go +++ b/pkg/commands/option/sbom.go @@ -3,6 +3,7 @@ package option import ( "github.com/urfave/cli/v2" "go.uber.org/zap" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/report" ) @@ -33,6 +34,7 @@ func (c *SbomOption) Init(ctx *cli.Context, logger *zap.SugaredLogger) error { if c.ArtifactType != "" || c.SbomFormat != "" { logger.Error("'trivy sbom' is now for scanning SBOM. " + "See https://github.com/aquasecurity/trivy/discussions/2407 for the detail") + return xerrors.New("'--artifact-type' and '--sbom-format' are no longer available") } return nil diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go index 240181bb85f3..7c69ac85645f 100644 --- a/pkg/detector/library/detect.go +++ b/pkg/detector/library/detect.go @@ -33,6 +33,7 @@ func detect(driver Driver, libs []ftypes.Package) ([]types.DetectedVulnerability for i := range vulns { vulns[i].Layer = lib.Layer vulns[i].PkgPath = lib.FilePath + vulns[i].Ref = lib.Ref } vulnerabilities = append(vulnerabilities, vulns...) } diff --git a/pkg/detector/ospkg/alma/alma.go b/pkg/detector/ospkg/alma/alma.go index a7d8a55c5414..669dcf2e101a 100644 --- a/pkg/detector/ospkg/alma/alma.go +++ b/pkg/detector/ospkg/alma/alma.go @@ -89,6 +89,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa PkgName: pkg.Name, InstalledVersion: installed, FixedVersion: fixedVersion.String(), + Ref: pkg.Ref, Layer: pkg.Layer, DataSource: adv.DataSource, } diff --git a/pkg/detector/ospkg/alpine/alpine.go b/pkg/detector/ospkg/alpine/alpine.go index 69b90bc69f96..dfa4d177f771 100644 --- a/pkg/detector/ospkg/alpine/alpine.go +++ b/pkg/detector/ospkg/alpine/alpine.go @@ -125,6 +125,7 @@ func (s *Scanner) Detect(osVer string, repo *ftypes.Repository, pkgs []ftypes.Pa InstalledVersion: installed, FixedVersion: adv.FixedVersion, Layer: pkg.Layer, + Ref: pkg.Ref, Custom: adv.Custom, DataSource: adv.DataSource, }) diff --git a/pkg/detector/ospkg/amazon/amazon.go b/pkg/detector/ospkg/amazon/amazon.go index dc1fbb383243..9be939183e49 100644 --- a/pkg/detector/ospkg/amazon/amazon.go +++ b/pkg/detector/ospkg/amazon/amazon.go @@ -102,6 +102,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa PkgName: pkg.Name, InstalledVersion: installed, FixedVersion: adv.FixedVersion, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/detector/ospkg/debian/debian.go b/pkg/detector/ospkg/debian/debian.go index 779a29168e98..751b82e7b77a 100644 --- a/pkg/detector/ospkg/debian/debian.go +++ b/pkg/detector/ospkg/debian/debian.go @@ -104,6 +104,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa PkgName: pkg.Name, InstalledVersion: installed, FixedVersion: adv.FixedVersion, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/detector/ospkg/mariner/mariner.go b/pkg/detector/ospkg/mariner/mariner.go index d064febd5e81..c4737190331d 100644 --- a/pkg/detector/ospkg/mariner/mariner.go +++ b/pkg/detector/ospkg/mariner/mariner.go @@ -53,6 +53,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa VulnerabilityID: adv.VulnerabilityID, PkgName: pkg.Name, InstalledVersion: installed, + Ref: pkg.Ref, Layer: pkg.Layer, DataSource: adv.DataSource, } diff --git a/pkg/detector/ospkg/oracle/oracle.go b/pkg/detector/ospkg/oracle/oracle.go index 6d110d7647d9..a86b988bee4d 100644 --- a/pkg/detector/ospkg/oracle/oracle.go +++ b/pkg/detector/ospkg/oracle/oracle.go @@ -86,6 +86,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa VulnerabilityID: adv.VulnerabilityID, PkgName: pkg.Name, InstalledVersion: installed, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/detector/ospkg/photon/photon.go b/pkg/detector/ospkg/photon/photon.go index b0889da2fe95..833e6c4f19f0 100644 --- a/pkg/detector/ospkg/photon/photon.go +++ b/pkg/detector/ospkg/photon/photon.go @@ -79,6 +79,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa VulnerabilityID: adv.VulnerabilityID, PkgName: pkg.Name, InstalledVersion: installed, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/detector/ospkg/redhat/redhat.go b/pkg/detector/ospkg/redhat/redhat.go index be3c4e28ef5a..e459acfe6f86 100644 --- a/pkg/detector/ospkg/redhat/redhat.go +++ b/pkg/detector/ospkg/redhat/redhat.go @@ -157,6 +157,7 @@ func (s *Scanner) detect(osVer string, pkg ftypes.Package) ([]types.DetectedVuln VulnerabilityID: vulnID, PkgName: pkg.Name, InstalledVersion: utils.FormatVersion(pkg), + Ref: pkg.Ref, Layer: pkg.Layer, SeveritySource: vulnerability.RedHat, Vulnerability: dbTypes.Vulnerability{ diff --git a/pkg/detector/ospkg/rocky/rocky.go b/pkg/detector/ospkg/rocky/rocky.go index 77eea6c94a8e..5be4fead6c35 100644 --- a/pkg/detector/ospkg/rocky/rocky.go +++ b/pkg/detector/ospkg/rocky/rocky.go @@ -89,6 +89,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa PkgName: pkg.Name, InstalledVersion: installed, FixedVersion: fixedVersion.String(), + Ref: pkg.Ref, Layer: pkg.Layer, DataSource: adv.DataSource, } diff --git a/pkg/detector/ospkg/suse/suse.go b/pkg/detector/ospkg/suse/suse.go index 72e71fd74de9..1241a3e94434 100644 --- a/pkg/detector/ospkg/suse/suse.go +++ b/pkg/detector/ospkg/suse/suse.go @@ -131,6 +131,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa VulnerabilityID: adv.VulnerabilityID, PkgName: pkg.Name, InstalledVersion: installed, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/detector/ospkg/ubuntu/ubuntu.go b/pkg/detector/ospkg/ubuntu/ubuntu.go index b8ea36e46bce..fe3d13e1fb06 100644 --- a/pkg/detector/ospkg/ubuntu/ubuntu.go +++ b/pkg/detector/ospkg/ubuntu/ubuntu.go @@ -114,6 +114,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa PkgName: pkg.Name, InstalledVersion: installed, FixedVersion: adv.FixedVersion, + Ref: pkg.Ref, Layer: pkg.Layer, Custom: adv.Custom, DataSource: adv.DataSource, diff --git a/pkg/fanal/artifact/remote/git.go b/pkg/fanal/artifact/remote/git.go index ac7afe4073aa..eabbd8619d9e 100644 --- a/pkg/fanal/artifact/remote/git.go +++ b/pkg/fanal/artifact/remote/git.go @@ -90,7 +90,7 @@ func newURL(rawurl string) (*url.URL, error) { return nil, xerrors.Errorf("url parse error: %w", err) } // "https://" can be omitted - // e.g. github.com/aquasecurity/fanal + // e.g. github.com/aquasecurity/trivy if u.Scheme == "" { u.Scheme = "https" } diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go new file mode 100644 index 000000000000..3356dbee0f20 --- /dev/null +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -0,0 +1,125 @@ +package sbom + +import ( + "context" + "crypto/sha256" + "encoding/json" + "io" + "os" + "path/filepath" + + digest "github.com/opencontainers/go-digest" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/cache" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" +) + +type Artifact struct { + filePath string + cache cache.ArtifactCache + analyzer analyzer.AnalyzerGroup + handlerManager handler.Manager + + artifactOption artifact.Option + configScannerOption config.ScannerOption +} + +func NewArtifact(filePath string, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) { + return Artifact{ + filePath: filepath.Clean(filePath), + cache: c, + artifactOption: opt, + }, nil +} + +func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { + f, err := os.Open(a.filePath) + if err != nil { + return types.ArtifactReference{}, xerrors.Errorf("failed to open sbom file error: %w", err) + } + defer f.Close() + + // Format auto-detection + format, err := sbom.DetectFormat(f) + if err != nil { + return types.ArtifactReference{}, xerrors.Errorf("failed to detect SBOM format: %w", err) + } + log.Logger.Infof("Detected SBOM format: %s", format) + + // Rewind the SBOM file + if _, err = f.Seek(0, io.SeekStart); err != nil { + return types.ArtifactReference{}, xerrors.Errorf("seek error: %w", err) + } + + var unmarshaler sbom.Unmarshaler + switch format { + case sbom.FormatCycloneDXJSON: + unmarshaler = cyclonedx.NewJSONUnmarshaler() + default: + return types.ArtifactReference{}, xerrors.Errorf("%s scanning is not yet supported", format) + + } + bom, err := unmarshaler.Unmarshal(f) + if err != nil { + return types.ArtifactReference{}, xerrors.Errorf("failed to unmarshal: %w", err) + } + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: bom.OS, + PackageInfos: bom.Packages, + Applications: bom.Applications, + } + + cacheKey, err := a.calcCacheKey(blobInfo) + if err != nil { + return types.ArtifactReference{}, xerrors.Errorf("failed to calculate a cache key: %w", err) + } + + if err = a.cache.PutBlob(cacheKey, blobInfo); err != nil { + return types.ArtifactReference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) + } + + var artifactType types.ArtifactType + switch format { + case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML: + artifactType = types.ArtifactCycloneDX + } + + return types.ArtifactReference{ + Name: a.filePath, + Type: artifactType, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + + // Keep an original report + CycloneDX: bom.CycloneDX, + }, nil +} + +func (a Artifact) Clean(reference types.ArtifactReference) error { + return a.cache.DeleteBlobs(reference.BlobIDs) +} + +func (a Artifact) calcCacheKey(blobInfo types.BlobInfo) (string, error) { + // calculate hash of JSON and use it as pseudo artifactID and blobID + h := sha256.New() + if err := json.NewEncoder(h).Encode(blobInfo); err != nil { + return "", xerrors.Errorf("json error: %w", err) + } + + d := digest.NewDigest(digest.SHA256, h) + cacheKey, err := cache.CalcKey(d.String(), a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) + if err != nil { + return "", xerrors.Errorf("cache key: %w", err) + } + + return cacheKey, nil +} diff --git a/pkg/fanal/artifact/sbom/sbom_test.go b/pkg/fanal/artifact/sbom/sbom_test.go new file mode 100644 index 000000000000..a6a338d9da99 --- /dev/null +++ b/pkg/fanal/artifact/sbom/sbom_test.go @@ -0,0 +1,182 @@ +package sbom_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestArtifact_Inspect(t *testing.T) { + tests := []struct { + name string + filePath string + putBlobExpectation cache.ArtifactCachePutBlobExpectation + want types.ArtifactReference + wantErr string + }{ + { + name: "happy path", + filePath: "testdata/bom.json", + putBlobExpectation: cache.ArtifactCachePutBlobExpectation{ + Args: cache.ArtifactCachePutBlobArgs{ + BlobID: "sha256:21f10e5ab97c37f6c4d6a45815cd5db10e9539d5db8614d3b1d8890111d7a2b8", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: &types.OS{ + Family: "alpine", + Name: "3.16.0", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: []types.Package{ + { + Name: "musl", Version: "1.2.3-r0", SrcName: "musl", SrcVersion: "1.2.3-r0", Licenses: []string{"MIT"}, + Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + Layer: types.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Libraries: []types.Package{ + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Libraries: []types.Package{ + { + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Ref: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "jar", + FilePath: "", + Libraries: []types.Package{ + { + Name: "org.codehaus.mojo:child-project", + Ref: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + Version: "1.0", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "node-pkg", + FilePath: "", + Libraries: []types.Package{ + { + Name: "bootstrap", + Version: "5.0.2", + Ref: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + Licenses: []string{"MIT"}, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + }, + }, + }, + Returns: cache.ArtifactCachePutBlobReturns{}, + }, + want: types.ArtifactReference{ + Name: "testdata/bom.json", + Type: types.ArtifactCycloneDX, + ID: "sha256:21f10e5ab97c37f6c4d6a45815cd5db10e9539d5db8614d3b1d8890111d7a2b8", + BlobIDs: []string{ + "sha256:21f10e5ab97c37f6c4d6a45815cd5db10e9539d5db8614d3b1d8890111d7a2b8", + }, + }, + }, + { + name: "sad path with no such directory", + filePath: "./testdata/unknown.json", + wantErr: "no such file or directory", + }, + { + name: "sad path PutBlob returns an error", + filePath: "testdata/os-only-bom.json", + putBlobExpectation: cache.ArtifactCachePutBlobExpectation{ + Args: cache.ArtifactCachePutBlobArgs{ + BlobID: "sha256:05a4e94bb5503e437108210c90849a977ea0b9b83e4e8606aabc9647b2a5256c", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: &types.OS{ + Family: "alpine", + Name: "3.16.0", + }, + PackageInfos: []types.PackageInfo{ + {}, + }, + }, + }, + Returns: cache.ArtifactCachePutBlobReturns{ + Err: errors.New("error"), + }, + }, + wantErr: "failed to store blob", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := new(cache.MockArtifactCache) + c.ApplyPutBlobExpectation(tt.putBlobExpectation) + + a, err := sbom.NewArtifact(tt.filePath, c, artifact.Option{}) + require.NoError(t, err) + + got, err := a.Inspect(context.Background()) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + // Not compare the original CycloneDX report + got.CycloneDX = nil + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/artifact/sbom/testdata/bom.json b/pkg/fanal/artifact/sbom/testdata/bom.json new file mode 100644 index 000000000000..9afa62bbeea9 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/bom.json @@ -0,0 +1,235 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "musl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.3-r0" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ] + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "type": "library", + "name": "org.codehaus.mojo:child-project", + "version": "1.0", + "purl": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/maven/target/child-project-1.0.jar" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "jar" + } + ] + }, + { + "bom-ref": "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "type": "library", + "name": "bootstrap", + "version": "5.0.2", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:npm/bootstrap@5.0.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/app/package.json" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "node-pkg" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "composer" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "type": "library", + "name": "github.com/package-url/packageurl-go", + "version": "v0.1.1-0.20220203205134-d70459300c8a", + "purl": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "type": "application", + "name": "app/gobinary/gobinary", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [ + "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + ] + }, + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "dependsOn": [ + "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a" + ] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "100925ff-7c0a-470f-a725-8fb973b40e7b", + "1a111e6b-a682-470e-8b0e-aaa49d93cd39" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/sbom/testdata/os-only-bom.json b/pkg/fanal/artifact/sbom/testdata/os-only-bom.json new file mode 100644 index 000000000000..86e2cf0c2500 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/os-only-bom.json @@ -0,0 +1,74 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/image/image.go b/pkg/fanal/image/image.go index 32daad5846f1..7ac9e0af80cb 100644 --- a/pkg/fanal/image/image.go +++ b/pkg/fanal/image/image.go @@ -18,33 +18,33 @@ type options struct { remote bool } -type option func(*options) +type Option func(*options) -func DisableDockerd() option { +func DisableDockerd() Option { return func(opts *options) { opts.dockerd = false } } -func DisablePodman() option { +func DisablePodman() Option { return func(opts *options) { opts.podman = false } } -func DisableContainerd() option { +func DisableContainerd() Option { return func(opts *options) { opts.containerd = false } } -func DisableRemote() option { +func DisableRemote() Option { return func(opts *options) { opts.remote = false } } -func NewContainerImage(ctx context.Context, imageName string, option types.DockerOption, opts ...option) (types.Image, func(), error) { +func NewContainerImage(ctx context.Context, imageName string, option types.DockerOption, opts ...Option) (types.Image, func(), error) { o := &options{ dockerd: true, podman: true, diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go index 2058e33ca4a9..a424f8e5f71e 100644 --- a/pkg/fanal/types/artifact.go +++ b/pkg/fanal/types/artifact.go @@ -94,6 +94,7 @@ const ( ArtifactContainerImage ArtifactType = "container_image" ArtifactFilesystem ArtifactType = "filesystem" ArtifactRemoteRepository ArtifactType = "repository" + ArtifactCycloneDX ArtifactType = "cyclonedx" ) // ArtifactReference represents a reference of container image, local filesystem and repository @@ -103,6 +104,9 @@ type ArtifactReference struct { ID string BlobIDs []string ImageMetadata ImageMetadata + + // SBOM + CycloneDX *CycloneDX } type ImageMetadata struct { diff --git a/pkg/fanal/types/sbom.go b/pkg/fanal/types/sbom.go new file mode 100644 index 000000000000..51856c64642b --- /dev/null +++ b/pkg/fanal/types/sbom.go @@ -0,0 +1,33 @@ +package types + +// CycloneDX re-defines only necessary fields from cyclondx/cyclonedx-go +// cf. https://github.com/CycloneDX/cyclonedx-go/blob/de6bc07025d148badc8f6699ccb556744a5f4070/cyclonedx.go#L58-L77 +// +// The encoding/xml package that cyclondx-go depends on cannot be imported due to some limitations in TinyGo. +// cf. https://tinygo.org/docs/reference/lang-support/stdlib/ +type CycloneDX struct { + // JSON specific fields + BOMFormat string `json:"bomFormat" xml:"-"` + SpecVersion string `json:"specVersion" xml:"-"` + + SerialNumber string `json:"serialNumber,omitempty" xml:"serialNumber,attr,omitempty"` + Version int `json:"version" xml:"version,attr"` + Metadata Metadata `json:"metadata,omitempty" xml:"metadata,omitempty"` + Components []Component `json:"components,omitempty" xml:"components>component,omitempty"` +} + +type Metadata struct { + Timestamp string `json:"timestamp,omitempty" xml:"timestamp,omitempty"` + Component Component `json:"component,omitempty" xml:"component,omitempty"` +} + +type Component struct { + BOMRef string `json:"bom-ref,omitempty" xml:"bom-ref,attr,omitempty"` + MIMEType string `json:"mime-type,omitempty" xml:"mime-type,attr,omitempty"` + Type ComponentType `json:"type" xml:"type,attr"` + Name string `json:"name" xml:"name"` + Version string `json:"version,omitempty" xml:"version,omitempty"` + PackageURL string `json:"purl,omitempty" xml:"purl,omitempty"` +} + +type ComponentType string diff --git a/pkg/module/serialize/types_easyjson.go b/pkg/module/serialize/types_easyjson.go index cb97844bee70..c7276dc7d56f 100644 --- a/pkg/module/serialize/types_easyjson.go +++ b/pkg/module/serialize/types_easyjson.go @@ -4,14 +4,13 @@ package serialize import ( json "encoding/json" - time "time" - types2 "github.com/aquasecurity/trivy-db/pkg/types" types1 "github.com/aquasecurity/trivy/pkg/fanal/types" types "github.com/aquasecurity/trivy/pkg/types" easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" + time "time" ) // suppress unused package warning @@ -1303,6 +1302,8 @@ func easyjson6601e8cdDecodeGithubComAquasecurityTrivyPkgTypes(in *jlexer.Lexer, out.SeveritySource = types2.SourceID(in.String()) case "PrimaryURL": out.PrimaryURL = string(in.String()) + case "Ref": + out.Ref = string(in.String()) case "DataSource": if in.IsNull() { in.Skip() @@ -1556,6 +1557,16 @@ func easyjson6601e8cdEncodeGithubComAquasecurityTrivyPkgTypes(out *jwriter.Write } out.String(string(in.PrimaryURL)) } + if in.Ref != "" { + const prefix string = ",\"Ref\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Ref)) + } if in.DataSource != nil { const prefix string = ",\"DataSource\":" if first { diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go index 00515f49b7e8..fa8637b08c1c 100644 --- a/pkg/purl/purl.go +++ b/pkg/purl/purl.go @@ -5,7 +5,8 @@ import ( "strings" cn "github.com/google/go-containerregistry/pkg/name" - "github.com/package-url/packageurl-go" + version "github.com/knqyf263/go-rpm-version" + packageurl "github.com/package-url/packageurl-go" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" @@ -24,6 +25,75 @@ type PackageURL struct { FilePath string } +func FromString(purl string) (*PackageURL, error) { + p, err := packageurl.FromString(purl) + if err != nil { + return nil, xerrors.Errorf("failed to parse purl: %w", err) + } + + return &PackageURL{ + PackageURL: p, + }, nil +} + +func (p *PackageURL) Package() *ftypes.Package { + pkg := &ftypes.Package{ + Name: p.Name, + Version: p.Version, + } + for _, q := range p.Qualifiers { + switch q.Key { + case "arch": + pkg.Arch = q.Value + case "modularitylabel": + pkg.Modularitylabel = q.Value + } + } + + if p.Type == packageurl.TypeRPM { + rpmVer := version.NewVersion(p.Version) + pkg.Release = rpmVer.Release() + pkg.Version = rpmVer.Version() + pkg.Epoch = rpmVer.Epoch() + } + + // TODO: replace with packageurl.TypeApk once they add it. + // Return of packages without Namespace. + // OS packages does not have namespace. + if p.Namespace == "" || p.Type == packageurl.TypeRPM || p.Type == packageurl.TypeDebian || p.Type == string(analyzer.TypeApk) { + return pkg + } + + if p.Type == packageurl.TypeMaven { + // Maven package separate ":" + // e.g. org.springframework:spring-core + pkg.Name = strings.Join([]string{p.Namespace, p.Name}, ":") + } else { + pkg.Name = strings.Join([]string{p.Namespace, p.Name}, "/") + } + + return pkg +} + +// AppType returns an application type in Trivy +func (p *PackageURL) AppType() string { + switch p.Type { + case packageurl.TypeComposer: + return string(analyzer.TypeComposer) + case packageurl.TypeMaven: + return string(analyzer.TypeJar) + case packageurl.TypeGem: + return string(analyzer.TypeGemSpec) + case packageurl.TypePyPi: + return string(analyzer.TypePythonPkg) + case packageurl.TypeGolang: + return string(analyzer.TypeGoBinary) + case packageurl.TypeNPM: + return string(analyzer.TypeNodePkg) + } + return p.Type +} + func (purl PackageURL) BOMRef() string { // 'bom-ref' must be unique within BOM, but PURLs may conflict // when the same packages are installed in an artifact. @@ -50,7 +120,7 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (Packa ptype := purlType(t) name := pkg.Name - version := utils.FormatVersion(pkg) + ver := utils.FormatVersion(pkg) namespace := "" switch ptype { @@ -87,7 +157,7 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (Packa } return PackageURL{ - PackageURL: *packageurl.NewPackageURL(ptype, namespace, name, version, qualifiers, ""), + PackageURL: *packageurl.NewPackageURL(ptype, namespace, name, ver, qualifiers, ""), FilePath: pkg.FilePath, }, nil } @@ -163,11 +233,10 @@ func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualif family = "sles" } - distro := fmt.Sprintf("%s-%s", family, fos.Name) qualifiers := packageurl.Qualifiers{ { Key: "distro", - Value: distro, + Value: fmt.Sprintf("%s-%s", family, fos.Name), }, } diff --git a/pkg/purl/purl_test.go b/pkg/purl/purl_test.go index 001634d4ccea..46171d1777ae 100644 --- a/pkg/purl/purl_test.go +++ b/pkg/purl/purl_test.go @@ -308,3 +308,102 @@ func TestNewPackageURL(t *testing.T) { }) } } + +func TestFromString(t *testing.T) { + + testCases := []struct { + name string + purl string + want purl.PackageURL + wantErr string + }{ + { + name: "happy path for maven", + purl: "pkg:maven/org.springframework/spring-core@5.0.4.RELEASE", + want: purl.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Version: "5.0.4.RELEASE", + Name: "spring-core", + Qualifiers: packageurl.Qualifiers{}, + }, + FilePath: "", + }, + }, + { + name: "happy path for npm", + purl: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + want: purl.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bootstrap", + Version: "5.0.2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "file_path", + Value: "app/app/package.json", + }, + }, + }, + }, + }, + { + name: "happy path for apk", + purl: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?distro=3.14.2", + want: purl.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: string(analyzer.TypeApk), + Namespace: "alpine", + Name: "alpine-baselayout", + Version: "3.2.0-r16", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.14.2", + }, + }, + }, + }, + }, + { + name: "happy path for rpm", + purl: "pkg:rpm/redhat/containers-common@0.1.14", + want: purl.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "containers-common", + Version: "0.1.14", + Qualifiers: packageurl.Qualifiers{}, + }, + }, + }, + { + name: "bad rpm", + purl: "pkg:rpm/redhat/a--@1.0.0", + want: purl.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "a--", + Version: "1.0.0", + Qualifiers: packageurl.Qualifiers{}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pkg, err := purl.FromString(tc.purl) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.want, *pkg, tc.name) + }) + } +} diff --git a/pkg/report/cyclonedx/cyclonedx.go b/pkg/report/cyclonedx/cyclonedx.go index ef8971569a17..7e8e70bf2015 100644 --- a/pkg/report/cyclonedx/cyclonedx.go +++ b/pkg/report/cyclonedx/cyclonedx.go @@ -6,6 +6,8 @@ import ( cdx "github.com/CycloneDX/cyclonedx-go" "golang.org/x/xerrors" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" "github.com/aquasecurity/trivy/pkg/types" ) @@ -17,22 +19,35 @@ type Writer struct { marshaler *cyclonedx.Marshaler } -func NewWriter(output io.Writer, version string) Writer { +func NewWriter(output io.Writer, appVersion string) Writer { return Writer{ output: output, format: cdx.BOMFileFormatJSON, - marshaler: cyclonedx.NewMarshaler(version), + marshaler: cyclonedx.NewMarshaler(appVersion), } } // Write writes the results in CycloneDX format func (w Writer) Write(report types.Report) error { - bom, err := w.marshaler.Marshal(report) + var bom *cdx.BOM + var err error + + // When the input is CycloneDX, only vulnerabilities will be stored in CycloneDX. + // Each vulnerability has a reference to a component in the original CycloneDX. + // e.g. "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#jackson-databind-2.8.0" + if report.ArtifactType == ftypes.ArtifactCycloneDX { + log.Logger.Info("Components will not be exported in the CycloneDX report as the input is CycloneDX") + bom, err = w.marshaler.MarshalVulnerabilities(report) + } else { + bom, err = w.marshaler.Marshal(report) + } if err != nil { - return xerrors.Errorf("failed to convert bom: %w", err) + return xerrors.Errorf("CycloneDX marshal error: %w", err) } - if err = cdx.NewBOMEncoder(w.output, w.format).Encode(bom); err != nil { + encoder := cdx.NewBOMEncoder(w.output, w.format) + encoder.SetPretty(true) + if err = encoder.Encode(bom); err != nil { return xerrors.Errorf("failed to encode bom: %w", err) } diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 0781bb5b60b7..c71f9695e209 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -1,6 +1,7 @@ package cyclonedx import ( + "fmt" "sort" "strconv" "strings" @@ -36,6 +37,7 @@ const ( PropertyRepoTag = "RepoTag" // Package properties + PropertyPkgType = "PkgType" PropertySrcName = "SrcName" PropertySrcVersion = "SrcVersion" PropertySrcRelease = "SrcRelease" @@ -49,10 +51,14 @@ const ( timeLayout = "2006-01-02T15:04:05+00:00" ) +var ( + ErrInvalidBOMLink = xerrors.New("invalid bomLink format error") +) + type Marshaler struct { - version string - clock clock.Clock - newUUID newUUID + appVersion string // Trivy version + clock clock.Clock + newUUID newUUID } type newUUID func() uuid.UUID @@ -73,9 +79,9 @@ func WithNewUUID(newUUID newUUID) marshalOption { func NewMarshaler(version string, opts ...marshalOption) *Marshaler { e := &Marshaler{ - version: version, - clock: clock.RealClock{}, - newUUID: uuid.New, + appVersion: version, + clock: clock.RealClock{}, + newUUID: uuid.New, } for _, opt := range opts { @@ -94,27 +100,88 @@ func (e *Marshaler) Marshal(report types.Report) (*cdx.BOM, error) { return nil, xerrors.Errorf("failed to parse metadata component: %w", err) } - bom.Metadata = &cdx.Metadata{ + bom.Metadata = e.cdxMetadata() + bom.Metadata.Component = metadataComponent + + bom.Components, bom.Dependencies, bom.Vulnerabilities, err = e.marshalComponents(report, bom.Metadata.Component.BOMRef) + if err != nil { + return nil, xerrors.Errorf("failed to parse components: %w", err) + } + + return bom, nil +} + +// MarshalVulnerabilities converts the Trivy report to the CycloneDX format only with vulnerabilities. +// The output refers to another CycloneDX SBOM. +func (e *Marshaler) MarshalVulnerabilities(report types.Report) (*cdx.BOM, error) { + vulnMap := map[string]cdx.Vulnerability{} + for _, result := range report.Results { + for _, vuln := range result.Vulnerabilities { + ref, err := externalRef(report.CycloneDX.SerialNumber, vuln.Ref) + if err != nil { + return nil, err + } + if v, ok := vulnMap[vuln.VulnerabilityID]; ok { + *v.Affects = append(*v.Affects, cdxAffects(ref, vuln.InstalledVersion)) + } else { + vulnMap[vuln.VulnerabilityID] = toCdxVulnerability(ref, vuln) + } + } + } + vulns := maps.Values(vulnMap) + sort.Slice(vulns, func(i, j int) bool { + return vulns[i].ID > vulns[j].ID + }) + + bom := cdx.NewBOM() + bom.Metadata = e.cdxMetadata() + + // Fill the detected vulnerabilities + bom.Vulnerabilities = &vulns + + // Use the original component as is + bom.Metadata.Component = &cdx.Component{ + Name: report.CycloneDX.Metadata.Component.Name, + Version: report.CycloneDX.Metadata.Component.Version, + Type: cdx.ComponentType(report.CycloneDX.Metadata.Component.Type), + } + + // Overwrite the bom ref as it must be the BOM ref of the original CycloneDX. + // e.g. + // "metadata" : { + // "timestamp" : "2022-07-02T00:00:00Z", + // "component" : { + // "name" : "Acme Product", + // "version": "2.4.0", + // "type" : "application", + // "bom-ref" : "urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1" + // } + // }, + bom.Metadata.Component.BOMRef = fmt.Sprintf("%s/%d", report.CycloneDX.SerialNumber, report.CycloneDX.Version) + return bom, nil +} + +func (e *Marshaler) cdxMetadata() *cdx.Metadata { + return &cdx.Metadata{ Timestamp: e.clock.Now().UTC().Format(timeLayout), Tools: &[]cdx.Tool{ { Vendor: "aquasecurity", Name: "trivy", - Version: e.version, + Version: e.appVersion, }, }, - Component: metadataComponent, } +} - bom.Components, bom.Dependencies, bom.Vulnerabilities, err = e.parseComponents(report, bom.Metadata.Component.BOMRef) - if err != nil { - return nil, xerrors.Errorf("failed to parse components: %w", err) +func externalRef(bomLink string, bomRef string) (string, error) { + if !strings.HasPrefix(bomLink, "urn:uuid:") { + return "", xerrors.Errorf("%q: %w", bomLink, ErrInvalidBOMLink) } - - return bom, nil + return fmt.Sprintf("%s/%d#%s", strings.Replace(bomLink, "uuid", "cdx", 1), cdx.BOMFileFormatJSON, bomRef), nil } -func (e *Marshaler) parseComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability, error) { +func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability, error) { var components []cdx.Component var dependencies []cdx.Dependency var metadataDependencies []cdx.Dependency @@ -326,12 +393,12 @@ func (e Marshaler) resultToCdxComponent(r types.Result, osFound *ftypes.OS) cdx. return component } -func pkgToCdxComponent(t string, meta types.Metadata, pkg ftypes.Package) (cdx.Component, error) { - pu, err := purl.NewPackageURL(t, meta, pkg) +func pkgToCdxComponent(pkgType string, meta types.Metadata, pkg ftypes.Package) (cdx.Component, error) { + pu, err := purl.NewPackageURL(pkgType, meta, pkg) if err != nil { return cdx.Component{}, xerrors.Errorf("failed to new package purl: %w", err) } - properties := cdxProperties(pkg) + properties := cdxProperties(pkgType, pkg) component := cdx.Component{ Type: cdx.ComponentTypeLibrary, Name: pkg.Name, @@ -351,11 +418,12 @@ func pkgToCdxComponent(t string, meta types.Metadata, pkg ftypes.Package) (cdx.C return component, nil } -func cdxProperties(pkg ftypes.Package) *[]cdx.Property { +func cdxProperties(pkgType string, pkg ftypes.Package) *[]cdx.Property { props := []struct { name string value string }{ + {PropertyPkgType, pkgType}, {PropertyFilePath, pkg.FilePath}, {PropertySrcName, pkg.SrcName}, {PropertySrcVersion, pkg.SrcVersion}, diff --git a/pkg/sbom/cyclonedx/marshal_test.go b/pkg/sbom/cyclonedx/marshal_test.go index 9368ea60d815..4ae980b4817b 100644 --- a/pkg/sbom/cyclonedx/marshal_test.go +++ b/pkg/sbom/cyclonedx/marshal_test.go @@ -23,7 +23,7 @@ import ( "github.com/aquasecurity/trivy/pkg/types" ) -func TestWriter_Write(t *testing.T) { +func TestMarshaler_Marshal(t *testing.T) { tests := []struct { name string inputReport types.Report @@ -203,6 +203,10 @@ func TestWriter_Write(t *testing.T) { }, PackageURL: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "centos", + }, { Name: "aquasecurity:trivy:SrcName", Value: "binutils", @@ -239,6 +243,12 @@ func TestWriter_Write(t *testing.T) { Name: "actionpack", Version: "7.0.0", PackageURL: "pkg:gem/actionpack@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, }, { BOMRef: "pkg:gem/actioncontroller@7.0.0", @@ -246,6 +256,12 @@ func TestWriter_Write(t *testing.T) { Name: "actioncontroller", Version: "7.0.0", PackageURL: "pkg:gem/actioncontroller@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, }, { BOMRef: "3ff14136-e09f-4df9-80ea-000000000003", @@ -594,6 +610,10 @@ func TestWriter_Write(t *testing.T) { }, PackageURL: "pkg:rpm/centos/acl@1:2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011", Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "centos", + }, { Name: "aquasecurity:trivy:SrcName", Value: "acl", @@ -635,6 +655,10 @@ func TestWriter_Write(t *testing.T) { Version: "7.0.0", PackageURL: "pkg:gem/actionpack@7.0.0", Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "gemspec", + }, { Name: "aquasecurity:trivy:FilePath", Value: "tools/project-john/specifications/actionpack.gemspec", @@ -652,6 +676,10 @@ func TestWriter_Write(t *testing.T) { Version: "7.0.1", PackageURL: "pkg:gem/actionpack@7.0.1", Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "gemspec", + }, { Name: "aquasecurity:trivy:FilePath", Value: "tools/project-doe/specifications/actionpack.gemspec", @@ -818,6 +846,12 @@ func TestWriter_Write(t *testing.T) { Name: "actioncable", Version: "6.1.4.1", PackageURL: "pkg:gem/actioncable@6.1.4.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, }, { BOMRef: "3ff14136-e09f-4df9-80ea-000000000003", @@ -919,6 +953,10 @@ func TestWriter_Write(t *testing.T) { cdx.LicenseChoice{Expression: "MIT"}, }, Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "node-pkg", + }, { Name: "aquasecurity:trivy:FilePath", Value: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", @@ -1008,3 +1046,219 @@ func TestWriter_Write(t *testing.T) { }) } } + +func TestMarshaler_MarshalVulnerabilities(t *testing.T) { + tests := []struct { + name string + inputReport types.Report + want *cdx.BOM + }{ + { + name: "happy path for cyclonedx scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "cyclonedx.json", + ArtifactType: ftypes.ArtifactCycloneDX, + 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", + }, + }, + CycloneDX: &ftypes.CycloneDX{ + SerialNumber: "urn:uuid:f08a6ccd-4dce-4759-bd84-c626675d60a7", + Version: 1, + Metadata: ftypes.Metadata{ + Component: ftypes.Component{ + Type: ftypes.ComponentType(cdx.ComponentTypeApplication), + Name: "centos:8", + }, + }, + }, + Results: types.Results{ + { + Target: "rails:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: fos.CentOS, + Packages: []ftypes.Package{ + { + Name: "binutils", + Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Version: "2.30", + Release: "93.el8", + Epoch: 0, + Arch: "aarch64", + SrcName: "binutils", + SrcVersion: "2.30", + SrcRelease: "93.el8", + SrcEpoch: 0, + Modularitylabel: "", + Licenses: []string{"GPLv3+"}, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2018-20623", + PkgName: "binutils", + InstalledVersion: "2.30-93.el8", + Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Layer: ftypes.Layer{ + DiffID: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a", + }, + SeveritySource: vulnerability.RedHatOVAL, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2018-20623", + DataSource: &dtypes.DataSource{ + ID: vulnerability.RedHatOVAL, + Name: "Red Hat OVAL v2", + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "binutils: Use-after-free in the error function", + Description: "In GNU Binutils 2.31.1, there is a use-after-free in the error function in elfcomm.c when called from the process_archive function in readelf.c via a crafted ELF file.", + Severity: dtypes.SeverityMedium.String(), + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.NVD: dtypes.SeverityMedium, + vulnerability.RedHatOVAL: dtypes.SeverityMedium, + }, + CweIDs: []string{"CWE-416"}, + CVSS: dtypes.VendorCVSS{ + vulnerability.NVD: dtypes.CVSS{ + V2Vector: "AV:N/AC:M/Au:N/C:N/I:N/A:P", + V3Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", + V2Score: 4.3, + V3Score: 5.5, + }, + vulnerability.RedHatOVAL: dtypes.CVSS{ + V3Vector: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", + V3Score: 5.3, + }, + }, + References: []string{ + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00072.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-11/msg00008.html", + }, + PublishedDate: lo.ToPtr(time.Date(2018, 12, 31, 19, 29, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2019, 10, 31, 1, 15, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.4", + BOMFormat: "CycloneDX", + SpecVersion: "1.4", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &[]cdx.Tool{ + { + Name: "trivy", + Vendor: "aquasecurity", + Version: "dev", + }, + }, + Component: &cdx.Component{ + Name: "centos:8", + Type: cdx.ComponentTypeApplication, + BOMRef: "urn:uuid:f08a6ccd-4dce-4759-bd84-c626675d60a7/1", + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{ + { + ID: "CVE-2018-20623", + Source: &cdx.Source{ + Name: string(vulnerability.RedHatOVAL), + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + Ratings: &[]cdx.VulnerabilityRating{ + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + URL: "", + }, + Score: lo.ToPtr(4.3), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv2, + Vector: "AV:N/AC:M/Au:N/C:N/I:N/A:P", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + URL: "", + }, + Score: lo.ToPtr(5.5), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv3, + Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.RedHatOVAL), + URL: "", + }, + Score: lo.ToPtr(5.3), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv3, + Vector: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", + }, + }, + CWEs: &[]int{ + 416, + }, + Description: "In GNU Binutils 2.31.1, there is a use-after-free in the error function in elfcomm.c when called from the process_archive function in readelf.c via a crafted ELF file.", + Advisories: &[]cdx.Advisory{ + { + URL: "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00072.html", + }, + { + URL: "http://lists.opensuse.org/opensuse-security-announce/2019-11/msg00008.html", + }, + }, + Published: "2018-12-31T19:29:00+00:00", + Updated: "2019-10-31T01:15:00+00:00", + Affects: &[]cdx.Affects{ + { + Ref: "urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1#pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Range: &[]cdx.AffectedVersions{ + { + Version: "2.30-93.el8", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + }, + }, + }, + }, + }, + } + + clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + + for _, tt := range tests { + t.Run(tt.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))) + } + + marshaler := cyclonedx.NewMarshaler("dev", cyclonedx.WithClock(clock), cyclonedx.WithNewUUID(newUUID)) + got, err := marshaler.MarshalVulnerabilities(tt.inputReport) + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/sbom/cyclonedx/testdata/happy/bom.json b/pkg/sbom/cyclonedx/testdata/happy/bom.json new file mode 100644 index 000000000000..9afa62bbeea9 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/bom.json @@ -0,0 +1,235 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "musl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.3-r0" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ] + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "type": "library", + "name": "org.codehaus.mojo:child-project", + "version": "1.0", + "purl": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/maven/target/child-project-1.0.jar" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "jar" + } + ] + }, + { + "bom-ref": "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "type": "library", + "name": "bootstrap", + "version": "5.0.2", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:npm/bootstrap@5.0.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/app/package.json" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "node-pkg" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "composer" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "type": "library", + "name": "github.com/package-url/packageurl-go", + "version": "v0.1.1-0.20220203205134-d70459300c8a", + "purl": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "type": "application", + "name": "app/gobinary/gobinary", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [ + "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + ] + }, + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "dependsOn": [ + "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a" + ] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "100925ff-7c0a-470f-a725-8fb973b40e7b", + "1a111e6b-a682-470e-8b0e-aaa49d93cd39" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json b/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json new file mode 100644 index 000000000000..bfac954d0f15 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "dependencies": [ + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a" + } + ] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json b/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json new file mode 100644 index 000000000000..e0faa0433d8c --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json @@ -0,0 +1,60 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/core@1.13.1", + "type": "library", + "name": "pear/core", + "version": "1.13.1", + "purl": "pkg:composer/pear/core@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:composer/pear/core@1.13.1", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json b/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json new file mode 100644 index 000000000000..86e2cf0c2500 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json @@ -0,0 +1,74 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json b/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json new file mode 100644 index 000000000000..b6483caca912 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json @@ -0,0 +1,64 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0" + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock" + } + ], + "dependencies": [ + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "pkg:composer/pear/log@1.13.1" + }, + { + "ref": "pkg:composer/pear/pear_exception@v1.0.0" + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/sad/invalid-purl.json b/pkg/sbom/cyclonedx/testdata/sad/invalid-purl.json new file mode 100644 index 000000000000..8a40bd35fcf9 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/sad/invalid-purl.json @@ -0,0 +1,52 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/core@1.13.1", + "type": "library", + "name": "pear/core", + "version": "1.13.1", + "purl": "pkg:composer/pear/core@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "invalid-purl-format" + } + ], + "dependencies": [ + { + "ref": "pkg:composer/pear/core@1.13.1", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go new file mode 100644 index 000000000000..6aad2d1a62fe --- /dev/null +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -0,0 +1,328 @@ +package cyclonedx + +import ( + "io" + "sort" + "strconv" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +type Unmarshaler struct { + format cdx.BOMFileFormat + + dependencies map[string][]string + components map[string]cdx.Component +} + +func NewJSONUnmarshaler() sbom.Unmarshaler { + return &Unmarshaler{ + format: cdx.BOMFileFormatJSON, + } +} + +func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { + bom := cdx.NewBOM() + decoder := cdx.NewBOMDecoder(r, u.format) + if err := decoder.Decode(bom); err != nil { + return sbom.SBOM{}, xerrors.Errorf("CycloneDX decode error: %w", err) + } + + u.dependencies = dependencyMap(bom.Dependencies) + u.components = componentMap(bom.Metadata, bom.Components) + + var ( + osInfo *ftypes.OS + apps []ftypes.Application + pkgInfos []ftypes.PackageInfo + seen = make(map[string]struct{}) + ) + for bomRef := range u.dependencies { + component := u.components[bomRef] + switch component.Type { + case cdx.ComponentTypeOS: // OS info and OS packages + osInfo = toOS(component) + pkgInfo, err := u.parseOSPkgs(component, seen) + if err != nil { + return sbom.SBOM{}, xerrors.Errorf("failed to parse os packages: %w", err) + } + pkgInfos = append(pkgInfos, pkgInfo) + case cdx.ComponentTypeApplication: // It would be a lock file in a CycloneDX report generated by Trivy + if lookupProperty(component.Properties, PropertyType) == "" { + continue + } + app, err := u.parseLangPkgs(component, seen) + if err != nil { + return sbom.SBOM{}, xerrors.Errorf("failed to parse language packages: %w", err) + } + apps = append(apps, *app) + case cdx.ComponentTypeLibrary: + // It is an individual package not associated with any lock files and should be processed later. + // e.g. .gemspec, .egg and .wheel + continue + } + } + + var libComponents []cdx.Component + for ref, component := range u.components { + if _, ok := seen[ref]; ok { + continue + } + if component.Type == cdx.ComponentTypeLibrary { + libComponents = append(libComponents, component) + } + } + + aggregatedApps, err := aggregateLangPkgs(libComponents) + if err != nil { + return sbom.SBOM{}, xerrors.Errorf("failed to aggregate packages: %w", err) + } + apps = append(apps, aggregatedApps...) + + sort.Slice(apps, func(i, j int) bool { + if apps[i].Type != apps[j].Type { + return apps[i].Type < apps[j].Type + } + return apps[i].FilePath < apps[j].FilePath + }) + + var metadata ftypes.Metadata + if bom.Metadata != nil { + metadata.Timestamp = bom.Metadata.Timestamp + if bom.Metadata.Component != nil { + metadata.Component = toTrivyCdxComponent(fromPtr(bom.Metadata.Component)) + } + } + + var components []ftypes.Component + for _, c := range fromPtr(bom.Components) { + components = append(components, toTrivyCdxComponent(c)) + } + + return sbom.SBOM{ + OS: osInfo, + Packages: pkgInfos, + Applications: apps, + + // Keep the original SBOM + CycloneDX: &ftypes.CycloneDX{ + BOMFormat: bom.BOMFormat, + SpecVersion: bom.SpecVersion, + SerialNumber: bom.SerialNumber, + Version: bom.Version, + Metadata: metadata, + Components: components, + }, + }, nil +} + +func (u *Unmarshaler) parseOSPkgs(component cdx.Component, seen map[string]struct{}) (ftypes.PackageInfo, error) { + components := u.walkDependencies(component.BOMRef) + pkgs, err := parsePkgs(components, seen) + if err != nil { + return ftypes.PackageInfo{}, xerrors.Errorf("failed to parse os package: %w", err) + } + + return ftypes.PackageInfo{ + Packages: pkgs, + }, nil +} + +func (u *Unmarshaler) parseLangPkgs(component cdx.Component, seen map[string]struct{}) (*ftypes.Application, error) { + components := u.walkDependencies(component.BOMRef) + components = lo.UniqBy(components, func(c cdx.Component) string { + return c.BOMRef + }) + + app := toApplication(component) + pkgs, err := parsePkgs(components, seen) + if err != nil { + return nil, xerrors.Errorf("failed to parse language-specific packages: %w", err) + } + app.Libraries = pkgs + + return app, nil +} + +func parsePkgs(components []cdx.Component, seen map[string]struct{}) ([]ftypes.Package, error) { + var pkgs []ftypes.Package + for _, com := range components { + seen[com.BOMRef] = struct{}{} + _, pkg, err := toPackage(com) + if err != nil { + return nil, xerrors.Errorf("failed to parse language package: %w", err) + } + pkgs = append(pkgs, *pkg) + } + return pkgs, nil +} + +// walkDependencies takes all nested dependencies of the root component. +// +// e.g. Library A, B, C, D and E will be returned as dependencies of Application 1. +// type: Application 1 +// - type: Library A +// - type: Library B +// - type: Application 2 +// - type: Library C +// - type: Application 3 +// - type: Library D +// - type: Library E +func (u *Unmarshaler) walkDependencies(rootRef string) []cdx.Component { + var components []cdx.Component + for _, dep := range u.dependencies[rootRef] { + component, ok := u.components[dep] + if !ok { + continue + } + + // Take only 'Libraries' + if component.Type == cdx.ComponentTypeLibrary { + components = append(components, component) + } + + components = append(components, u.walkDependencies(dep)...) + } + return components +} + +func componentMap(metadata *cdx.Metadata, components *[]cdx.Component) map[string]cdx.Component { + cmap := make(map[string]cdx.Component) + + for _, component := range fromPtr(components) { + cmap[component.BOMRef] = component + } + if metadata != nil { + cmap[metadata.Component.BOMRef] = *metadata.Component + } + return cmap +} + +func dependencyMap(deps *[]cdx.Dependency) map[string][]string { + depMap := make(map[string][]string) + + for _, dep := range fromPtr(deps) { + if _, ok := depMap[dep.Ref]; ok { + continue + } + + var refs []string + for _, d := range fromPtr(dep.Dependencies) { + refs = append(refs, d.Ref) + } + + depMap[dep.Ref] = refs + } + return depMap +} + +func aggregateLangPkgs(libs []cdx.Component) ([]ftypes.Application, error) { + pkgMap := map[string][]ftypes.Package{} + for _, lib := range libs { + appType, pkg, err := toPackage(lib) + if err != nil { + return nil, xerrors.Errorf("failed to parse purl to package: %w", err) + } + + pkgMap[appType] = append(pkgMap[appType], *pkg) + } + + var apps []ftypes.Application + for appType, pkgs := range pkgMap { + sort.Slice(pkgs, func(i, j int) bool { + return pkgs[i].Name < pkgs[j].Name + }) + apps = append(apps, ftypes.Application{ + Type: appType, + Libraries: pkgs, + }) + } + return apps, nil +} + +func toOS(component cdx.Component) *ftypes.OS { + return &ftypes.OS{ + Family: component.Name, + Name: component.Version, + } +} + +func toApplication(component cdx.Component) *ftypes.Application { + return &ftypes.Application{ + Type: lookupProperty(component.Properties, PropertyType), + FilePath: component.Name, + } +} + +func toPackage(component cdx.Component) (string, *ftypes.Package, error) { + p, err := purl.FromString(component.PackageURL) + if err != nil { + return "", nil, xerrors.Errorf("failed to parse purl: %w", err) + } + + pkg := p.Package() + pkg.Ref = component.BOMRef + + for _, license := range fromPtr(component.Licenses) { + pkg.Licenses = append(pkg.Licenses, license.Expression) + } + + for _, prop := range fromPtr(component.Properties) { + if strings.HasPrefix(prop.Name, Namespace) { + switch strings.TrimPrefix(prop.Name, Namespace) { + case PropertySrcName: + pkg.SrcName = prop.Value + case PropertySrcVersion: + pkg.SrcVersion = prop.Value + case PropertySrcRelease: + pkg.SrcRelease = prop.Value + case PropertySrcEpoch: + pkg.SrcEpoch, err = strconv.Atoi(prop.Value) + if err != nil { + return "", nil, xerrors.Errorf("failed to parse source epoch: %w", err) + } + case PropertyModularitylabel: + pkg.Modularitylabel = prop.Value + case PropertyLayerDiffID: + pkg.Layer.DiffID = prop.Value + } + } + } + + return p.AppType(), pkg, nil +} + +func toTrivyCdxComponent(component cdx.Component) ftypes.Component { + return ftypes.Component{ + BOMRef: component.BOMRef, + MIMEType: component.MIMEType, + Type: ftypes.ComponentType(component.Type), + Name: component.Name, + Version: component.Version, + PackageURL: component.PackageURL, + } +} + +func lookupProperty(properties *[]cdx.Property, key string) string { + for _, p := range fromPtr(properties) { + if p.Name == Namespace+key { + return p.Value + } + } + return "" +} + +func fromPtr[T any](ptr *T) T { + if ptr == nil { + var t T + return t + } + return *ptr +} diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go new file mode 100644 index 000000000000..30bf35589083 --- /dev/null +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -0,0 +1,214 @@ +package cyclonedx_test + +import ( + "os" + "testing" + + "github.com/aquasecurity/trivy/pkg/sbom" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" +) + +func TestUnmarshaler_Unmarshal(t *testing.T) { + tests := []struct { + name string + inputFile string + want sbom.SBOM + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/happy/bom.json", + want: sbom.SBOM{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + Packages: []ftypes.PackageInfo{ + { + Packages: []ftypes.Package{ + { + Name: "musl", Version: "1.2.3-r0", SrcName: "musl", SrcVersion: "1.2.3-r0", Licenses: []string{"MIT"}, + Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + Layer: ftypes.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Libraries: []ftypes.Package{ + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Libraries: []ftypes.Package{ + { + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Ref: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "jar", + Libraries: []ftypes.Package{ + { + Name: "org.codehaus.mojo:child-project", + Ref: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + Version: "1.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "node-pkg", + FilePath: "", + Libraries: []ftypes.Package{ + { + Name: "bootstrap", + Version: "5.0.2", + Ref: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for unrelated bom", + inputFile: "testdata/happy/unrelated-bom.json", + want: sbom.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Libraries: []ftypes.Package{ + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + { + name: "happy path for independent library bom", + inputFile: "testdata/happy/independent-library-bom.json", + want: sbom.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Libraries: []ftypes.Package{ + { + Name: "pear/core", + Version: "1.13.1", + Ref: "pkg:composer/pear/core@1.13.1", + }, + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + { + name: "happy path only os component", + inputFile: "testdata/happy/os-only-bom.json", + want: sbom.SBOM{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + Packages: []ftypes.PackageInfo{ + {}, + }, + }, + }, + { + name: "happy path empty component", + inputFile: "testdata/happy/empty-bom.json", + want: sbom.SBOM{}, + }, + { + name: "sad path invalid purl", + inputFile: "testdata/sad/invalid-purl.json", + wantErr: "failed to parse purl", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + unmarshaler := cyclonedx.NewJSONUnmarshaler() + got, err := unmarshaler.Unmarshal(f) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + // Not compare the CycloneDX field + got.CycloneDX = nil + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go new file mode 100644 index 000000000000..928f1623edba --- /dev/null +++ b/pkg/sbom/sbom.go @@ -0,0 +1,67 @@ +package sbom + +import ( + "encoding/json" + "encoding/xml" + "io" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type SBOM struct { + OS *types.OS + Packages []types.PackageInfo + Applications []types.Application + + CycloneDX *types.CycloneDX +} + +type Unmarshaler interface { + Unmarshal(io.Reader) (SBOM, error) +} + +type Format string + +const ( + FormatCycloneDXJSON = "cyclonedx-json" + FormatCycloneDXXML = "cyclonedx-xml" + FormatSPDXJSON = "spdx-json" + FormatSPDXXML = "spdx-xml" + FormatUnknown = "unknown" +) + +func DetectFormat(r io.ReadSeeker) (Format, error) { + type cyclonedx struct { + // XML specific field + XMLNS string `json:"-" xml:"xmlns,attr"` + + // JSON specific field + BOMFormat string `json:"bomFormat" xml:"-"` + } + + // Try CycloneDX JSON + var cdxBom cyclonedx + if err := json.NewDecoder(r).Decode(&cdxBom); err == nil { + if cdxBom.BOMFormat == "CycloneDX" { + return FormatCycloneDXJSON, nil + } + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return FormatUnknown, xerrors.Errorf("seek error: %w", err) + } + + // Try CycloneDX XML + if err := xml.NewDecoder(r).Decode(&cdxBom); err == nil { + if strings.HasPrefix(cdxBom.XMLNS, "http://cyclonedx.org") { + return FormatCycloneDXXML, nil + } + } + + // TODO: implement SPDX + + return FormatUnknown, nil +} diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index 53e79b035b79..9afe5dd0aa26 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -10,6 +10,7 @@ import ( aimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" flocal "github.com/aquasecurity/trivy/pkg/fanal/artifact/local" "github.com/aquasecurity/trivy/pkg/fanal/artifact/remote" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" "github.com/aquasecurity/trivy/pkg/fanal/image" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" @@ -32,6 +33,7 @@ var StandaloneSuperSet = wire.NewSet( // StandaloneDockerSet binds docker dependencies var StandaloneDockerSet = wire.NewSet( + wire.Value([]image.Option(nil)), // optional functions image.NewContainerImage, aimage.NewArtifact, StandaloneSuperSet, @@ -56,6 +58,12 @@ var StandaloneRepositorySet = wire.NewSet( StandaloneSuperSet, ) +// StandaloneSBOMSet binds sbom dependencies +var StandaloneSBOMSet = wire.NewSet( + sbom.NewArtifact, + StandaloneSuperSet, +) + ///////////////// // Client/Server ///////////////// @@ -74,9 +82,16 @@ var RemoteFilesystemSet = wire.NewSet( RemoteSuperSet, ) +// RemoteSBOMSet binds sbom dependencies for client/server mode +var RemoteSBOMSet = wire.NewSet( + sbom.NewArtifact, + RemoteSuperSet, +) + // RemoteDockerSet binds remote docker dependencies var RemoteDockerSet = wire.NewSet( aimage.NewArtifact, + wire.Value([]image.Option(nil)), // optional functions image.NewContainerImage, RemoteSuperSet, ) @@ -137,14 +152,17 @@ func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (t ArtifactName: artifactInfo.Name, ArtifactType: artifactInfo.Type, Metadata: types.Metadata{ - OS: osFound, + OS: osFound, + + // Container image ImageID: artifactInfo.ImageMetadata.ID, DiffIDs: artifactInfo.ImageMetadata.DiffIDs, RepoTags: artifactInfo.ImageMetadata.RepoTags, RepoDigests: artifactInfo.ImageMetadata.RepoDigests, ImageConfig: artifactInfo.ImageMetadata.ConfigFile, }, - Results: results, + CycloneDX: artifactInfo.CycloneDX, + Results: results, }, nil } diff --git a/pkg/types/report.go b/pkg/types/report.go index 56a59bde8092..c298292867f3 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -15,6 +15,9 @@ type Report struct { ArtifactType ftypes.ArtifactType `json:",omitempty"` Metadata Metadata `json:",omitempty"` Results Results `json:",omitempty"` + + // SBOM + CycloneDX *ftypes.CycloneDX `json:"-"` // Just for internal usage, not exported in JSON } // Metadata represents a metadata of artifact diff --git a/pkg/types/vulnerability.go b/pkg/types/vulnerability.go index ff746f12915b..205967a63188 100644 --- a/pkg/types/vulnerability.go +++ b/pkg/types/vulnerability.go @@ -17,6 +17,7 @@ type DetectedVulnerability struct { Layer ftypes.Layer `json:",omitempty"` SeveritySource types.SourceID `json:",omitempty"` PrimaryURL string `json:",omitempty"` + Ref string `json:",omitempty"` // DataSource holds where the advisory comes from DataSource *types.DataSource `json:",omitempty"`