feat(sbom): add cyclonedx sbom scan (#2203)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Masahiro331
2022-07-04 02:03:21 +09:00
committed by GitHub
parent f0720f3ce5
commit 5b821d3b13
58 changed files with 3896 additions and 105 deletions

View File

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

View File

@@ -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 .
</details>
## 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/

View File

@@ -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
</details>
## 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

2
go.mod
View File

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

4
go.sum
View File

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

96
integration/sbom_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

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

View File

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

33
pkg/fanal/types/sbom.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,19 +100,10 @@ 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{
Timestamp: e.clock.Now().UTC().Format(timeLayout),
Tools: &[]cdx.Tool{
{
Vendor: "aquasecurity",
Name: "trivy",
Version: e.version,
},
},
Component: metadataComponent,
}
bom.Metadata = e.cdxMetadata()
bom.Metadata.Component = metadataComponent
bom.Components, bom.Dependencies, bom.Vulnerabilities, err = e.parseComponents(report, bom.Metadata.Component.BOMRef)
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)
}
@@ -114,7 +111,77 @@ func (e *Marshaler) Marshal(report types.Report) (*cdx.BOM, error) {
return bom, nil
}
func (e *Marshaler) parseComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability, error) {
// 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.appVersion,
},
},
}
}
func externalRef(bomLink string, bomRef string) (string, error) {
if !strings.HasPrefix(bomLink, "urn:uuid:") {
return "", xerrors.Errorf("%q: %w", bomLink, ErrInvalidBOMLink)
}
return fmt.Sprintf("%s/%d#%s", strings.Replace(bomLink, "uuid", "cdx", 1), cdx.BOMFileFormatJSON, bomRef), nil
}
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},

View File

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

View File

@@ -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": []
}

View File

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

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

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

View File

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

67
pkg/sbom/sbom.go Normal file
View File

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

View File

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

View File

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

View File

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