mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
feat(report): add support for CycloneDX (#1081)
Co-authored-by: tspearconquest <81998567+tspearconquest@users.noreply.github.com> Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
@@ -185,6 +185,8 @@ Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
|
|||||||
- **Suitable for CI** such as GitHub Actions, Jenkins, GitLab CI, etc.
|
- **Suitable for CI** such as GitHub Actions, Jenkins, GitLab CI, etc.
|
||||||
- Support multiple targets
|
- Support multiple targets
|
||||||
- container image, local filesystem and remote git repository
|
- container image, local filesystem and remote git repository
|
||||||
|
- Supply chain security (SBOM support)
|
||||||
|
- Support CycloneDX
|
||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
- [GitHub Actions][action]
|
- [GitHub Actions][action]
|
||||||
|
|||||||
175
docs/advanced/sbom/cyclonedx.md
Normal file
175
docs/advanced/sbom/cyclonedx.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# CycloneDX
|
||||||
|
Trivy generates JSON reports in the [CycloneDX][cyclonedx] format.
|
||||||
|
Note that XML format is not supported at the moment.
|
||||||
|
|
||||||
|
|
||||||
|
You can specify `cyclonedx` with the `--format` option.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ trivy image --format cyclonedx --output result.json alpine:3.15
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Result</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat result.json | jq .
|
||||||
|
{
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.3",
|
||||||
|
"serialNumber": "urn:uuid:2be5773d-7cd3-4b4b-90a5-e165474ddace",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2022-02-22T15:11:40.270597Z",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"vendor": "aquasecurity",
|
||||||
|
"name": "trivy",
|
||||||
|
"version": "dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64",
|
||||||
|
"type": "container",
|
||||||
|
"name": "alpine:3.15",
|
||||||
|
"version": "",
|
||||||
|
"purl": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SchemaVersion",
|
||||||
|
"value": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:ImageID",
|
||||||
|
"value": "sha256:c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:RepoDigest",
|
||||||
|
"value": "alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:DiffID",
|
||||||
|
"value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:RepoTag",
|
||||||
|
"value": "alpine:3.15"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0",
|
||||||
|
"type": "library",
|
||||||
|
"name": "alpine-baselayout",
|
||||||
|
"version": "3.2.0-r18",
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"expression": "GPL-2.0-only"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"purl": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SrcName",
|
||||||
|
"value": "alpine-baselayout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SrcVersion",
|
||||||
|
"value": "3.2.0-r18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDigest",
|
||||||
|
"value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDiffID",
|
||||||
|
"value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...(snip)...
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0",
|
||||||
|
"type": "library",
|
||||||
|
"name": "zlib",
|
||||||
|
"version": "1.2.11-r3",
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"expression": "Zlib"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"purl": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SrcName",
|
||||||
|
"value": "zlib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SrcVersion",
|
||||||
|
"value": "1.2.11-r3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDigest",
|
||||||
|
"value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDiffID",
|
||||||
|
"value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bom-ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37",
|
||||||
|
"type": "operating-system",
|
||||||
|
"name": "alpine",
|
||||||
|
"version": "3.15.0",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:Type",
|
||||||
|
"value": "alpine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:Class",
|
||||||
|
"value": "os-pkgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37",
|
||||||
|
"dependsOn": [
|
||||||
|
"pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/alpine-keys@2.4-r1?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/apk-tools@2.12.7-r3?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/busybox@1.34.1-r3?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/ca-certificates-bundle@20191127-r7?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/libc-utils@0.7.2-r3?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/libcrypto1.1@1.1.1l-r7?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/libretls@3.3.4-r2?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/libssl1.1@1.1.1l-r7?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/musl@1.2.2-r7?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/musl-utils@1.2.2-r7?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/scanelf@1.3.3-r0?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/ssl_client@1.34.1-r3?distro=3.15.0",
|
||||||
|
"pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64",
|
||||||
|
"dependsOn": [
|
||||||
|
"3da6a469-964d-4b4e-b67d-e94ec7c88d37"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
!!! caution
|
||||||
|
It doesn't support vulnerabilities yet, but installed packages.
|
||||||
|
|
||||||
|
[cyclonedx]: https://cyclonedx.org/
|
||||||
@@ -55,6 +55,8 @@ See [Integrations][integrations] for details.
|
|||||||
- An image directory compliant with [OCI Image Format][oci]
|
- An image directory compliant with [OCI Image Format][oci]
|
||||||
- local filesystem and rootfs
|
- local filesystem and rootfs
|
||||||
- remote git repository
|
- remote git repository
|
||||||
|
- SBOM (Software Bill of Materials) support
|
||||||
|
- CycloneDX
|
||||||
|
|
||||||
Please see [LICENSE][license] for Trivy licensing information.
|
Please see [LICENSE][license] for Trivy licensing information.
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,6 +3,7 @@ module github.com/aquasecurity/trivy
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/CycloneDX/cyclonedx-go v0.4.0
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||||
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
||||||
@@ -28,6 +29,7 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.8.2 // indirect
|
github.com/goccy/go-yaml v1.8.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2
|
github.com/golang/protobuf v1.5.2
|
||||||
github.com/google/go-containerregistry v0.7.1-0.20211214010025-a65b7844a475
|
github.com/google/go-containerregistry v0.7.1-0.20211214010025-a65b7844a475
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
github.com/google/wire v0.5.0
|
github.com/google/wire v0.5.0
|
||||||
github.com/hashicorp/go-getter v1.5.11
|
github.com/hashicorp/go-getter v1.5.11
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -141,6 +141,8 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
|
|||||||
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
|
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
|
||||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/CycloneDX/cyclonedx-go v0.4.0 h1:Wz4QZ9B4RXGWIWTypVLEOVJgOdFfy5mcS5PGNzUkZxU=
|
||||||
|
github.com/CycloneDX/cyclonedx-go v0.4.0/go.mod h1:rmRcf//gT7PIzovatusbWi377xqCg1FS4jyST0GH20E=
|
||||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||||
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||||
@@ -315,6 +317,8 @@ github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6
|
|||||||
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
||||||
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
||||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||||
|
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
|
||||||
|
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||||
github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw=
|
github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw=
|
||||||
github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
|
github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
|
||||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -32,6 +34,7 @@ type csArgs struct {
|
|||||||
Input string
|
Input string
|
||||||
ClientToken string
|
ClientToken string
|
||||||
ClientTokenHeader string
|
ClientTokenHeader string
|
||||||
|
ListAllPackages bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientServer(t *testing.T) {
|
func TestClientServer(t *testing.T) {
|
||||||
@@ -322,6 +325,55 @@ func TestClientServerWithTemplate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientServerWithCycloneDX(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args csArgs
|
||||||
|
wantComponentsCount int
|
||||||
|
wantDependenciesCount int
|
||||||
|
wantDependsOnCount []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fluentd with RubyGems with CycloneDX format",
|
||||||
|
args: csArgs{
|
||||||
|
Format: "cyclonedx",
|
||||||
|
Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
|
||||||
|
},
|
||||||
|
wantComponentsCount: 161,
|
||||||
|
wantDependenciesCount: 2,
|
||||||
|
wantDependsOnCount: []int{
|
||||||
|
105,
|
||||||
|
56,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app, addr, cacheDir := setup(t, setupOptions{})
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, "")
|
||||||
|
|
||||||
|
// Run Trivy client
|
||||||
|
err := app.Run(osArgs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := os.Open(outputFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var got cdx.BOM
|
||||||
|
err = json.NewDecoder(f).Decode(&got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, tt.wantComponentsCount, len(*got.Components))
|
||||||
|
assert.EqualValues(t, tt.wantDependenciesCount, len(*got.Dependencies))
|
||||||
|
for i, dep := range *got.Dependencies {
|
||||||
|
assert.EqualValues(t, tt.wantDependsOnCount[i], len(*dep.Dependencies))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientServerWithToken(t *testing.T) {
|
func TestClientServerWithToken(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ nav:
|
|||||||
- Overview: advanced/index.md
|
- Overview: advanced/index.md
|
||||||
- Plugins: advanced/plugins.md
|
- Plugins: advanced/plugins.md
|
||||||
- Air-Gapped Environment: advanced/air-gap.md
|
- Air-Gapped Environment: advanced/air-gap.md
|
||||||
|
- SBOM:
|
||||||
|
- CycloneDX: advanced/sbom/cyclonedx.md
|
||||||
- Integrations:
|
- Integrations:
|
||||||
- Overview: advanced/integrations/index.md
|
- Overview: advanced/integrations/index.md
|
||||||
- GitHub Actions: advanced/integrations/github-actions.md
|
- GitHub Actions: advanced/integrations/github-actions.md
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestOption_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --template enabled without --format",
|
name: "invalid option combination: --template enabled without --format",
|
||||||
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
@@ -116,7 +116,7 @@ func TestOption_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --template and --format json",
|
name: "invalid option combination: --template and --format json",
|
||||||
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format json is specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
@@ -136,7 +136,7 @@ func TestOption_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --format template without --template",
|
name: "invalid option combination: --format template without --template",
|
||||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
|
"'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestConfig_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --template enabled without --format",
|
name: "invalid option combination: --template enabled without --format",
|
||||||
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
@@ -162,7 +162,7 @@ func TestConfig_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --template and --format json",
|
name: "invalid option combination: --template and --format json",
|
||||||
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format json is specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
@@ -183,7 +183,7 @@ func TestConfig_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --format template without --template",
|
name: "invalid option combination: --format template without --template",
|
||||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
|
"'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
@@ -203,7 +203,7 @@ func TestConfig_Init(t *testing.T) {
|
|||||||
name: "invalid option combination: --format template without --template",
|
name: "invalid option combination: --format template without --template",
|
||||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
|
"'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.",
|
||||||
},
|
},
|
||||||
want: Option{
|
want: Option{
|
||||||
ReportOption: option.ReportOption{
|
ReportOption: option.ReportOption{
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
// ImageOption holds the options for scanning images
|
// ImageOption holds the options for scanning images
|
||||||
type ImageOption struct {
|
type ImageOption struct {
|
||||||
ScanRemovedPkgs bool
|
ScanRemovedPkgs bool
|
||||||
ListAllPkgs bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImageOption is the factory method to return ImageOption
|
// NewImageOption is the factory method to return ImageOption
|
||||||
func NewImageOption(c *cli.Context) ImageOption {
|
func NewImageOption(c *cli.Context) ImageOption {
|
||||||
return ImageOption{
|
return ImageOption{
|
||||||
ScanRemovedPkgs: c.Bool("removed-pkgs"),
|
ScanRemovedPkgs: c.Bool("removed-pkgs"),
|
||||||
ListAllPkgs: c.Bool("list-all-pkgs"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type ReportOption struct {
|
|||||||
SecurityChecks []string
|
SecurityChecks []string
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
Severities []dbTypes.Severity
|
Severities []dbTypes.Severity
|
||||||
|
ListAllPkgs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReportOption is the factory method to return ReportOption
|
// NewReportOption is the factory method to return ReportOption
|
||||||
@@ -51,31 +52,35 @@ func NewReportOption(c *cli.Context) ReportOption {
|
|||||||
IgnoreFile: c.String("ignorefile"),
|
IgnoreFile: c.String("ignorefile"),
|
||||||
IgnoreUnfixed: c.Bool("ignore-unfixed"),
|
IgnoreUnfixed: c.Bool("ignore-unfixed"),
|
||||||
ExitCode: c.Int("exit-code"),
|
ExitCode: c.Int("exit-code"),
|
||||||
|
ListAllPkgs: c.Bool("list-all-pkgs"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the ReportOption
|
// Init initializes the ReportOption
|
||||||
func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
||||||
var err error
|
|
||||||
|
|
||||||
if c.Template != "" {
|
if c.Template != "" {
|
||||||
if c.Format == "" {
|
if c.Format == "" {
|
||||||
logger.Warn("--template is ignored because --format template is not specified. Use --template option with --format template option.")
|
logger.Warn("'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.")
|
||||||
} else if c.Format != "template" {
|
} else if c.Format != "template" {
|
||||||
logger.Warnf("--template is ignored because --format %s is specified. Use --template option with --format template option.", c.Format)
|
logger.Warnf("'--template' is ignored because '--format %s' is specified. Use '--template' option with '--format template' option.", c.Format)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.Format == "template" {
|
||||||
|
logger.Warn("'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.Format == "template" && c.Template == "" {
|
|
||||||
logger.Warn("--format template is ignored because --template not is specified. Specify --template option when you use --format template.")
|
if c.forceListAllPkgs(logger) {
|
||||||
|
c.ListAllPkgs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Severities = splitSeverity(logger, c.severities)
|
c.Severities = splitSeverity(logger, c.severities)
|
||||||
|
|
||||||
if err = c.populateVulnTypes(); err != nil {
|
if err := c.populateVulnTypes(); err != nil {
|
||||||
return xerrors.Errorf("vuln type: %w", err)
|
return xerrors.Errorf("vuln type: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.populateSecurityChecks(); err != nil {
|
if err := c.populateSecurityChecks(); err != nil {
|
||||||
return xerrors.Errorf("security checks: %w", err)
|
return xerrors.Errorf("security checks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +91,7 @@ func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
|||||||
|
|
||||||
// The output is os.Stdout by default
|
// The output is os.Stdout by default
|
||||||
if c.output != "" {
|
if c.output != "" {
|
||||||
|
var err error
|
||||||
if output, err = os.Create(c.output); err != nil {
|
if output, err = os.Create(c.output); err != nil {
|
||||||
return xerrors.Errorf("failed to create an output file: %w", err)
|
return xerrors.Errorf("failed to create an output file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -124,6 +130,14 @@ func (c *ReportOption) populateSecurityChecks() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ReportOption) forceListAllPkgs(logger *zap.SugaredLogger) bool {
|
||||||
|
if c.Format == "cyclonedx" && !c.ListAllPkgs {
|
||||||
|
logger.Debugf("'--format cyclonedx' automatically enables '--list-all-pkgs'.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func splitSeverity(logger *zap.SugaredLogger, severity string) []dbTypes.Severity {
|
func splitSeverity(logger *zap.SugaredLogger, severity string) []dbTypes.Severity {
|
||||||
logger.Debugf("Severities: %s", severity)
|
logger.Debugf("Severities: %s", severity)
|
||||||
var severities []dbTypes.Severity
|
var severities []dbTypes.Severity
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
severities string
|
severities string
|
||||||
IgnoreFile string
|
IgnoreFile string
|
||||||
IgnoreUnfixed bool
|
IgnoreUnfixed bool
|
||||||
|
listAllPksgs bool
|
||||||
ExitCode int
|
ExitCode int
|
||||||
VulnType []string
|
VulnType []string
|
||||||
Output *os.File
|
Output *os.File
|
||||||
Severities []dbTypes.Severity
|
Severities []dbTypes.Severity
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -70,6 +72,49 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
Output: os.Stdout,
|
Output: os.Stdout,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with an cyclonedx",
|
||||||
|
fields: fields{
|
||||||
|
severities: "CRITICAL",
|
||||||
|
vulnType: "os,library",
|
||||||
|
securityChecks: "vuln",
|
||||||
|
Format: "cyclonedx",
|
||||||
|
listAllPksgs: true,
|
||||||
|
},
|
||||||
|
args: []string{"centos:7"},
|
||||||
|
want: ReportOption{
|
||||||
|
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
|
||||||
|
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
|
||||||
|
SecurityChecks: []string{types.SecurityCheckVulnerability},
|
||||||
|
Format: "cyclonedx",
|
||||||
|
Output: os.Stdout,
|
||||||
|
ListAllPkgs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with an cyclonedx option list-all-pkgs is false",
|
||||||
|
fields: fields{
|
||||||
|
severities: "CRITICAL",
|
||||||
|
vulnType: "os,library",
|
||||||
|
securityChecks: "vuln",
|
||||||
|
Format: "cyclonedx",
|
||||||
|
listAllPksgs: false,
|
||||||
|
debug: true,
|
||||||
|
},
|
||||||
|
args: []string{"centos:7"},
|
||||||
|
logs: []string{
|
||||||
|
"'--format cyclonedx' automatically enables '--list-all-pkgs'.",
|
||||||
|
"Severities: CRITICAL",
|
||||||
|
},
|
||||||
|
want: ReportOption{
|
||||||
|
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
|
||||||
|
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
|
||||||
|
SecurityChecks: []string{types.SecurityCheckVulnerability},
|
||||||
|
Format: "cyclonedx",
|
||||||
|
Output: os.Stdout,
|
||||||
|
ListAllPkgs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid option combination: --template enabled without --format",
|
name: "invalid option combination: --template enabled without --format",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
@@ -80,7 +125,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: ReportOption{
|
want: ReportOption{
|
||||||
Output: os.Stdout,
|
Output: os.Stdout,
|
||||||
@@ -101,7 +146,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--template is ignored because --format json is specified. Use --template option with --format template option.",
|
"'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.",
|
||||||
},
|
},
|
||||||
want: ReportOption{
|
want: ReportOption{
|
||||||
Format: "json",
|
Format: "json",
|
||||||
@@ -122,7 +167,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||||
logs: []string{
|
logs: []string{
|
||||||
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
|
"'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.",
|
||||||
},
|
},
|
||||||
want: ReportOption{
|
want: ReportOption{
|
||||||
Format: "template",
|
Format: "template",
|
||||||
@@ -135,7 +180,12 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
core, obs := observer.New(zap.InfoLevel)
|
level := zap.InfoLevel
|
||||||
|
if tt.fields.debug {
|
||||||
|
level = zap.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
core, obs := observer.New(level)
|
||||||
logger := zap.New(core)
|
logger := zap.New(core)
|
||||||
|
|
||||||
set := flag.NewFlagSet("test", 0)
|
set := flag.NewFlagSet("test", 0)
|
||||||
@@ -151,9 +201,9 @@ func TestReportReportConfig_Init(t *testing.T) {
|
|||||||
IgnoreFile: tt.fields.IgnoreFile,
|
IgnoreFile: tt.fields.IgnoreFile,
|
||||||
IgnoreUnfixed: tt.fields.IgnoreUnfixed,
|
IgnoreUnfixed: tt.fields.IgnoreUnfixed,
|
||||||
ExitCode: tt.fields.ExitCode,
|
ExitCode: tt.fields.ExitCode,
|
||||||
|
ListAllPkgs: tt.fields.listAllPksgs,
|
||||||
Output: tt.fields.Output,
|
Output: tt.fields.Output,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.Init(os.Stdout, logger.Sugar())
|
err := c.Init(os.Stdout, logger.Sugar())
|
||||||
|
|
||||||
// tests log messages
|
// tests log messages
|
||||||
|
|||||||
112
pkg/purl/purl.go
112
pkg/purl/purl.go
@@ -2,7 +2,6 @@ package purl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cn "github.com/google/go-containerregistry/pkg/name"
|
cn "github.com/google/go-containerregistry/pkg/name"
|
||||||
@@ -20,15 +19,36 @@ const (
|
|||||||
TypeOCI = "oci"
|
TypeOCI = "oci"
|
||||||
)
|
)
|
||||||
|
|
||||||
// nolint: gocyclo
|
type PackageURL struct {
|
||||||
func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packageurl.PackageURL, error) {
|
packageurl.PackageURL
|
||||||
ptype := purlType(t)
|
FilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// In that case, we prefer to make PURLs unique by adding file paths,
|
||||||
|
// rather than using UUIDs, even if it is not PURL technically.
|
||||||
|
// ref. https://cyclonedx.org/use-cases/#dependency-graph
|
||||||
|
if purl.FilePath != "" {
|
||||||
|
purl.Qualifiers = append(purl.Qualifiers,
|
||||||
|
packageurl.Qualifier{
|
||||||
|
Key: "file_path",
|
||||||
|
Value: purl.FilePath,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return purl.PackageURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gocyclo
|
||||||
|
func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (PackageURL, error) {
|
||||||
var qualifiers packageurl.Qualifiers
|
var qualifiers packageurl.Qualifiers
|
||||||
if metadata.OS != nil {
|
if metadata.OS != nil {
|
||||||
qualifiers = parseQualifier(pkg, metadata.OS.Name)
|
qualifiers = parseQualifier(pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ptype := purlType(t)
|
||||||
name := pkg.Name
|
name := pkg.Name
|
||||||
version := utils.FormatVersion(pkg)
|
version := utils.FormatVersion(pkg)
|
||||||
namespace := ""
|
namespace := ""
|
||||||
@@ -41,7 +61,7 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packa
|
|||||||
case packageurl.TypeDebian:
|
case packageurl.TypeDebian:
|
||||||
qualifiers = append(qualifiers, parseDeb(metadata.OS)...)
|
qualifiers = append(qualifiers, parseDeb(metadata.OS)...)
|
||||||
namespace = metadata.OS.Family
|
namespace = metadata.OS.Family
|
||||||
case string(analyzer.TypeApk): // TODO: replace with packageurl.TypeApk
|
case string(analyzer.TypeApk): // TODO: replace with packageurl.TypeApk once they add it.
|
||||||
qualifiers = append(qualifiers, parseApk(metadata.OS)...)
|
qualifiers = append(qualifiers, parseApk(metadata.OS)...)
|
||||||
namespace = metadata.OS.Family
|
namespace = metadata.OS.Family
|
||||||
case packageurl.TypeMaven:
|
case packageurl.TypeMaven:
|
||||||
@@ -55,15 +75,23 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packa
|
|||||||
case packageurl.TypeNPM:
|
case packageurl.TypeNPM:
|
||||||
namespace, name = parseNpm(name)
|
namespace, name = parseNpm(name)
|
||||||
case packageurl.TypeOCI:
|
case packageurl.TypeOCI:
|
||||||
return parseOCI(metadata)
|
purl, err := parseOCI(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, err
|
||||||
|
}
|
||||||
|
return PackageURL{PackageURL: purl}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return *packageurl.NewPackageURL(ptype, namespace, name, version, qualifiers, ""), nil
|
return PackageURL{
|
||||||
|
PackageURL: *packageurl.NewPackageURL(ptype, namespace, name, version, qualifiers, ""),
|
||||||
|
FilePath: pkg.FilePath,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#oci
|
||||||
func parseOCI(metadata types.Metadata) (packageurl.PackageURL, error) {
|
func parseOCI(metadata types.Metadata) (packageurl.PackageURL, error) {
|
||||||
if len(metadata.RepoDigests) == 0 {
|
if len(metadata.RepoDigests) == 0 {
|
||||||
return packageurl.PackageURL{}, xerrors.New("repository digests empty error")
|
return *packageurl.NewPackageURL("", "", "", "", nil, ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
digest, err := cn.NewDigest(metadata.RepoDigests[0])
|
digest, err := cn.NewDigest(metadata.RepoDigests[0])
|
||||||
@@ -99,6 +127,7 @@ func parseApk(fos *ftypes.OS) packageurl.Qualifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#deb
|
||||||
func parseDeb(fos *ftypes.OS) packageurl.Qualifiers {
|
func parseDeb(fos *ftypes.OS) packageurl.Qualifiers {
|
||||||
distro := fmt.Sprintf("%s-%s", fos.Family, fos.Name)
|
distro := fmt.Sprintf("%s-%s", fos.Family, fos.Name)
|
||||||
return packageurl.Qualifiers{
|
return packageurl.Qualifiers{
|
||||||
@@ -109,6 +138,7 @@ func parseDeb(fos *ftypes.OS) packageurl.Qualifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#rpm
|
||||||
func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualifiers) {
|
func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualifiers) {
|
||||||
// SLES string has whitespace
|
// SLES string has whitespace
|
||||||
family := fos.Family
|
family := fos.Family
|
||||||
@@ -117,7 +147,6 @@ func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualif
|
|||||||
}
|
}
|
||||||
|
|
||||||
distro := fmt.Sprintf("%s-%s", family, fos.Name)
|
distro := fmt.Sprintf("%s-%s", family, fos.Name)
|
||||||
|
|
||||||
qualifiers := packageurl.Qualifiers{
|
qualifiers := packageurl.Qualifiers{
|
||||||
{
|
{
|
||||||
Key: "distro",
|
Key: "distro",
|
||||||
@@ -134,54 +163,36 @@ func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualif
|
|||||||
return family, qualifiers
|
return family, qualifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#maven
|
||||||
func parseMaven(pkgName string) (string, string) {
|
func parseMaven(pkgName string) (string, string) {
|
||||||
var namespace string
|
// The group id is the "namespace" and the artifact id is the "name".
|
||||||
name := strings.ReplaceAll(pkgName, ":", "/")
|
name := strings.ReplaceAll(pkgName, ":", "/")
|
||||||
index := strings.LastIndex(name, "/")
|
return parsePkgName(name)
|
||||||
if index != -1 {
|
|
||||||
namespace = name[:index]
|
|
||||||
name = name[index+1:]
|
|
||||||
}
|
|
||||||
return namespace, name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#golang
|
||||||
func parseGolang(pkgName string) (string, string) {
|
func parseGolang(pkgName string) (string, string) {
|
||||||
var namespace string
|
|
||||||
|
|
||||||
name := strings.ToLower(pkgName)
|
name := strings.ToLower(pkgName)
|
||||||
index := strings.LastIndex(name, "/")
|
return parsePkgName(name)
|
||||||
if index != -1 {
|
|
||||||
namespace = name[:index]
|
|
||||||
name = name[index+1:]
|
|
||||||
}
|
|
||||||
return namespace, name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#pypi
|
||||||
func parsePyPI(pkgName string) string {
|
func parsePyPI(pkgName string) string {
|
||||||
|
// PyPi treats - and _ as the same character and is not case-sensitive.
|
||||||
|
// Therefore a Pypi package name must be lowercased and underscore "_" replaced with a dash "-".
|
||||||
return strings.ToLower(strings.ReplaceAll(pkgName, "_", "-"))
|
return strings.ToLower(strings.ReplaceAll(pkgName, "_", "-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#composer
|
||||||
func parseComposer(pkgName string) (string, string) {
|
func parseComposer(pkgName string) (string, string) {
|
||||||
var namespace, name string
|
return parsePkgName(pkgName)
|
||||||
|
|
||||||
index := strings.LastIndex(pkgName, "/")
|
|
||||||
if index != -1 {
|
|
||||||
namespace = pkgName[:index]
|
|
||||||
name = pkgName[index+1:]
|
|
||||||
}
|
|
||||||
return namespace, name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#npm
|
||||||
func parseNpm(pkgName string) (string, string) {
|
func parseNpm(pkgName string) (string, string) {
|
||||||
var namespace string
|
// the name must be lowercased
|
||||||
|
|
||||||
name := strings.ToLower(pkgName)
|
name := strings.ToLower(pkgName)
|
||||||
index := strings.LastIndex(pkgName, "/")
|
return parsePkgName(name)
|
||||||
if index != -1 {
|
|
||||||
namespace = name[:index]
|
|
||||||
name = name[index+1:]
|
|
||||||
}
|
|
||||||
return namespace, name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func purlType(t string) string {
|
func purlType(t string) string {
|
||||||
@@ -210,7 +221,7 @@ func purlType(t string) string {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQualifier(pkg ftypes.Package, distro string) packageurl.Qualifiers {
|
func parseQualifier(pkg ftypes.Package) packageurl.Qualifiers {
|
||||||
qualifiers := packageurl.Qualifiers{}
|
qualifiers := packageurl.Qualifiers{}
|
||||||
if pkg.Arch != "" {
|
if pkg.Arch != "" {
|
||||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
@@ -218,11 +229,16 @@ func parseQualifier(pkg ftypes.Package, distro string) packageurl.Qualifiers {
|
|||||||
Value: pkg.Arch,
|
Value: pkg.Arch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pkg.Epoch != 0 {
|
|
||||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
|
||||||
Key: "epoch",
|
|
||||||
Value: strconv.Itoa(pkg.Epoch),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return qualifiers
|
return qualifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePkgName(name string) (string, string) {
|
||||||
|
var namespace string
|
||||||
|
index := strings.LastIndex(name, "/")
|
||||||
|
if index != -1 {
|
||||||
|
namespace = name[:index]
|
||||||
|
name = name[index+1:]
|
||||||
|
}
|
||||||
|
return namespace, name
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
typ string
|
typ string
|
||||||
pkg ftypes.Package
|
pkg ftypes.Package
|
||||||
metadata types.Metadata
|
metadata types.Metadata
|
||||||
want packageurl.PackageURL
|
want purl.PackageURL
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -32,11 +32,13 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "org.springframework:spring-core",
|
Name: "org.springframework:spring-core",
|
||||||
Version: "5.3.14",
|
Version: "5.3.14",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeMaven,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "org.springframework",
|
Type: packageurl.TypeMaven,
|
||||||
Name: "spring-core",
|
Namespace: "org.springframework",
|
||||||
Version: "5.3.14",
|
Name: "spring-core",
|
||||||
|
Version: "5.3.14",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,11 +48,13 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "@xtuc/ieee754",
|
Name: "@xtuc/ieee754",
|
||||||
Version: "1.2.0",
|
Version: "1.2.0",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeNPM,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "@xtuc",
|
Type: packageurl.TypeNPM,
|
||||||
Name: "ieee754",
|
Namespace: "@xtuc",
|
||||||
Version: "1.2.0",
|
Name: "ieee754",
|
||||||
|
Version: "1.2.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,10 +64,12 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "lodash",
|
Name: "lodash",
|
||||||
Version: "4.17.21",
|
Version: "4.17.21",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeNPM,
|
PackageURL: packageurl.PackageURL{
|
||||||
Name: "lodash",
|
Type: packageurl.TypeNPM,
|
||||||
Version: "4.17.21",
|
Name: "lodash",
|
||||||
|
Version: "4.17.21",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,10 +79,12 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "Django_test",
|
Name: "Django_test",
|
||||||
Version: "1.2.0",
|
Version: "1.2.0",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypePyPi,
|
PackageURL: packageurl.PackageURL{
|
||||||
Name: "django-test",
|
Type: packageurl.TypePyPi,
|
||||||
Version: "1.2.0",
|
Name: "django-test",
|
||||||
|
Version: "1.2.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,11 +94,13 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "symfony/contracts",
|
Name: "symfony/contracts",
|
||||||
Version: "v1.0.2",
|
Version: "v1.0.2",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeComposer,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "symfony",
|
Type: packageurl.TypeComposer,
|
||||||
Name: "contracts",
|
Namespace: "symfony",
|
||||||
Version: "v1.0.2",
|
Name: "contracts",
|
||||||
|
Version: "v1.0.2",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -100,11 +110,13 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "github.com/go-sql-driver/Mysql",
|
Name: "github.com/go-sql-driver/Mysql",
|
||||||
Version: "v1.5.0",
|
Version: "v1.5.0",
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeGolang,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "github.com/go-sql-driver",
|
Type: packageurl.TypeGolang,
|
||||||
Name: "mysql",
|
Namespace: "github.com/go-sql-driver",
|
||||||
Version: "v1.5.0",
|
Name: "mysql",
|
||||||
|
Version: "v1.5.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,19 +141,21 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Name: "8",
|
Name: "8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeRPM,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "redhat",
|
Type: packageurl.TypeRPM,
|
||||||
Name: "acl",
|
Namespace: "redhat",
|
||||||
Version: "2.2.53-1.el8",
|
Name: "acl",
|
||||||
Qualifiers: packageurl.Qualifiers{
|
Version: "2.2.53-1.el8",
|
||||||
{
|
Qualifiers: packageurl.Qualifiers{
|
||||||
Key: "arch",
|
{
|
||||||
Value: "aarch64",
|
Key: "arch",
|
||||||
},
|
Value: "aarch64",
|
||||||
{
|
},
|
||||||
Key: "distro",
|
{
|
||||||
Value: "redhat-8",
|
Key: "distro",
|
||||||
|
Value: "redhat-8",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -161,23 +175,45 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Architecture: "amd64",
|
Architecture: "amd64",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeOCI,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "",
|
Type: packageurl.TypeOCI,
|
||||||
Name: "core",
|
Namespace: "",
|
||||||
Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260",
|
Name: "core",
|
||||||
Qualifiers: packageurl.Qualifiers{
|
Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260",
|
||||||
{
|
Qualifiers: packageurl.Qualifiers{
|
||||||
Key: "repository_url",
|
{
|
||||||
Value: "cblmariner2preview.azurecr.io/base/core",
|
Key: "repository_url",
|
||||||
},
|
Value: "cblmariner2preview.azurecr.io/base/core",
|
||||||
{
|
},
|
||||||
Key: "arch",
|
{
|
||||||
Value: "amd64",
|
Key: "arch",
|
||||||
|
Value: "amd64",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "container local",
|
||||||
|
typ: purl.TypeOCI,
|
||||||
|
metadata: types.Metadata{
|
||||||
|
RepoTags: []string{},
|
||||||
|
RepoDigests: []string{},
|
||||||
|
ImageConfig: v1.ConfigFile{
|
||||||
|
Architecture: "amd64",
|
||||||
|
},
|
||||||
|
ImageID: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260",
|
||||||
|
},
|
||||||
|
want: purl.PackageURL{
|
||||||
|
PackageURL: packageurl.PackageURL{
|
||||||
|
Type: "",
|
||||||
|
Namespace: "",
|
||||||
|
Name: "",
|
||||||
|
Version: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "container with implicit registry",
|
name: "container with implicit registry",
|
||||||
typ: purl.TypeOCI,
|
typ: purl.TypeOCI,
|
||||||
@@ -194,19 +230,21 @@ func TestNewPackageURL(t *testing.T) {
|
|||||||
Architecture: "amd64",
|
Architecture: "amd64",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: packageurl.PackageURL{
|
want: purl.PackageURL{
|
||||||
Type: packageurl.TypeOCI,
|
PackageURL: packageurl.PackageURL{
|
||||||
Namespace: "",
|
Type: packageurl.TypeOCI,
|
||||||
Name: "alpine",
|
Namespace: "",
|
||||||
Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260",
|
Name: "alpine",
|
||||||
Qualifiers: packageurl.Qualifiers{
|
Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260",
|
||||||
{
|
Qualifiers: packageurl.Qualifiers{
|
||||||
Key: "repository_url",
|
{
|
||||||
Value: "index.docker.io/library/alpine",
|
Key: "repository_url",
|
||||||
},
|
Value: "index.docker.io/library/alpine",
|
||||||
{
|
},
|
||||||
Key: "arch",
|
{
|
||||||
Value: "amd64",
|
Key: "arch",
|
||||||
|
Value: "amd64",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
365
pkg/report/cyclonedx/cyclonedx.go
Normal file
365
pkg/report/cyclonedx/cyclonedx.go
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
package cyclonedx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
|
ftypes "github.com/aquasecurity/fanal/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/purl"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Namespace = "aquasecurity:trivy:"
|
||||||
|
|
||||||
|
PropertySchemaVersion = "SchemaVersion"
|
||||||
|
PropertyType = "Type"
|
||||||
|
PropertyClass = "Class"
|
||||||
|
|
||||||
|
// Image properties
|
||||||
|
PropertySize = "Size"
|
||||||
|
PropertyImageID = "ImageID"
|
||||||
|
PropertyRepoDigest = "RepoDigest"
|
||||||
|
PropertyDiffID = "DiffID"
|
||||||
|
PropertyRepoTag = "RepoTag"
|
||||||
|
|
||||||
|
// Package properties
|
||||||
|
PropertySrcName = "SrcName"
|
||||||
|
PropertySrcVersion = "SrcVersion"
|
||||||
|
PropertySrcRelease = "SrcRelease"
|
||||||
|
PropertySrcEpoch = "SrcEpoch"
|
||||||
|
PropertyModularitylabel = "Modularitylabel"
|
||||||
|
PropertyFilePath = "FilePath"
|
||||||
|
PropertyLayerDigest = "LayerDigest"
|
||||||
|
PropertyLayerDiffID = "LayerDiffID"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer implements types.Writer
|
||||||
|
type Writer struct {
|
||||||
|
output io.Writer
|
||||||
|
version string
|
||||||
|
*options
|
||||||
|
}
|
||||||
|
|
||||||
|
type newUUID func() uuid.UUID
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
format cdx.BOMFileFormat
|
||||||
|
clock clock.Clock
|
||||||
|
newUUID newUUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type option func(*options)
|
||||||
|
|
||||||
|
func WithFormat(format cdx.BOMFileFormat) option {
|
||||||
|
return func(opts *options) {
|
||||||
|
opts.format = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClock(clock clock.Clock) option {
|
||||||
|
return func(opts *options) {
|
||||||
|
opts.clock = clock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNewUUID(newUUID newUUID) option {
|
||||||
|
return func(opts *options) {
|
||||||
|
opts.newUUID = newUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWriter(output io.Writer, version string, opts ...option) Writer {
|
||||||
|
o := &options{
|
||||||
|
format: cdx.BOMFileFormatJSON,
|
||||||
|
clock: clock.RealClock{},
|
||||||
|
newUUID: uuid.New,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Writer{
|
||||||
|
output: output,
|
||||||
|
version: version,
|
||||||
|
options: o,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the results in CycloneDX format
|
||||||
|
func (cw Writer) Write(report types.Report) error {
|
||||||
|
bom, err := cw.convertToBom(report, cw.version)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to convert bom: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cdx.NewBOMEncoder(cw.output, cw.format).Encode(bom); err != nil {
|
||||||
|
return xerrors.Errorf("failed to encode bom: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw *Writer) convertToBom(r types.Report, version string) (*cdx.BOM, error) {
|
||||||
|
bom := cdx.NewBOM()
|
||||||
|
bom.SerialNumber = cw.options.newUUID().URN()
|
||||||
|
metadataComponent, err := cw.reportToComponent(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("failed to parse metadata component: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bom.Metadata = &cdx.Metadata{
|
||||||
|
Timestamp: cw.clock.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Name: "trivy",
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: metadataComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
bom.Components, bom.Dependencies, err = cw.parseComponents(r, bom.Metadata.Component.BOMRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("failed to parse components: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw *Writer) parseComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, error) {
|
||||||
|
var components []cdx.Component
|
||||||
|
var dependencies []cdx.Dependency
|
||||||
|
var metadataDependencies []cdx.Dependency
|
||||||
|
libraryUniqMap := map[string]struct{}{}
|
||||||
|
for _, result := range r.Results {
|
||||||
|
var componentDependencies []cdx.Dependency
|
||||||
|
for _, pkg := range result.Packages {
|
||||||
|
pkgComponent, err := cw.pkgToComponent(result.Type, r.Metadata, pkg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, xerrors.Errorf("failed to parse pkg: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When multiple lock files have the same dependency with the same name and version,
|
||||||
|
// "bom-ref" (PURL technically) of Library components may conflict.
|
||||||
|
// In that case, only one Library component will be added and
|
||||||
|
// some Application components will refer to the same component.
|
||||||
|
// e.g.
|
||||||
|
// Application component (/app1/package-lock.json)
|
||||||
|
// |
|
||||||
|
// | Application component (/app2/package-lock.json)
|
||||||
|
// | |
|
||||||
|
// └----┴----> Library component (npm package, express-4.17.3)
|
||||||
|
//
|
||||||
|
if _, ok := libraryUniqMap[pkgComponent.BOMRef]; !ok {
|
||||||
|
libraryUniqMap[pkgComponent.BOMRef] = struct{}{}
|
||||||
|
|
||||||
|
// For components
|
||||||
|
// ref. https://cyclonedx.org/use-cases/#inventory
|
||||||
|
//
|
||||||
|
// TODO: All packages are flattened at the moment. We should construct dependency tree.
|
||||||
|
components = append(components, pkgComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDependencies = append(componentDependencies, cdx.Dependency{Ref: pkgComponent.BOMRef})
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg || result.Type == ftypes.GoBinary ||
|
||||||
|
result.Type == ftypes.GemSpec || result.Type == ftypes.Jar {
|
||||||
|
// If a package is language-specific package that isn't associated with a lock file,
|
||||||
|
// it will be a dependency of a component under "metadata".
|
||||||
|
// e.g.
|
||||||
|
// Container component (alpine:3.15) ----------------------- #1
|
||||||
|
// -> Library component (npm package, express-4.17.3) ---- #2
|
||||||
|
// -> Library component (python package, django-4.0.2) --- #2
|
||||||
|
// -> etc.
|
||||||
|
// ref. https://cyclonedx.org/use-cases/#inventory
|
||||||
|
|
||||||
|
// Dependency graph from #1 to #2
|
||||||
|
metadataDependencies = append(metadataDependencies, componentDependencies...)
|
||||||
|
} else {
|
||||||
|
// If a package is OS package, it will be a dependency of "Operating System" component.
|
||||||
|
// e.g.
|
||||||
|
// Container component (alpine:3.15) --------------------- #1
|
||||||
|
// -> Operating System Component (Alpine Linux 3.15) --- #2
|
||||||
|
// -> Library component (bash-4.12) ------------------ #3
|
||||||
|
// -> Library component (vim-8.2) ------------------ #3
|
||||||
|
// -> etc.
|
||||||
|
//
|
||||||
|
// Else if a package is language-specific package associated with a lock file,
|
||||||
|
// it will be a dependency of "Application" component.
|
||||||
|
// e.g.
|
||||||
|
// Container component (alpine:3.15) ------------------------ #1
|
||||||
|
// -> Application component (/app/package-lock.json) ------ #2
|
||||||
|
// -> Library component (npm package, express-4.17.3) --- #3
|
||||||
|
// -> Library component (npm package, lodash-4.17.21) --- #3
|
||||||
|
// -> etc.
|
||||||
|
|
||||||
|
resultComponent := cw.resultToComponent(result, r.Metadata.OS)
|
||||||
|
components = append(components, resultComponent)
|
||||||
|
|
||||||
|
// Dependency graph from #2 to #3
|
||||||
|
dependencies = append(dependencies,
|
||||||
|
cdx.Dependency{Ref: resultComponent.BOMRef, Dependencies: &componentDependencies},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dependency graph from #1 to #2
|
||||||
|
metadataDependencies = append(metadataDependencies, cdx.Dependency{Ref: resultComponent.BOMRef})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies,
|
||||||
|
cdx.Dependency{Ref: bomRef, Dependencies: &metadataDependencies},
|
||||||
|
)
|
||||||
|
return &components, &dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw *Writer) pkgToComponent(t string, meta types.Metadata, pkg ftypes.Package) (cdx.Component, error) {
|
||||||
|
pu, err := purl.NewPackageURL(t, meta, pkg)
|
||||||
|
if err != nil {
|
||||||
|
return cdx.Component{}, xerrors.Errorf("failed to new package purl: %w", err)
|
||||||
|
}
|
||||||
|
properties := parseProperties(pkg)
|
||||||
|
component := cdx.Component{
|
||||||
|
Type: cdx.ComponentTypeLibrary,
|
||||||
|
Name: pkg.Name,
|
||||||
|
Version: pu.Version,
|
||||||
|
BOMRef: pu.BOMRef(),
|
||||||
|
PackageURL: pu.ToString(),
|
||||||
|
Properties: &properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkg.License != "" {
|
||||||
|
component.Licenses = &cdx.Licenses{
|
||||||
|
cdx.LicenseChoice{Expression: pkg.License},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw *Writer) reportToComponent(r types.Report) (*cdx.Component, error) {
|
||||||
|
component := &cdx.Component{
|
||||||
|
Name: r.ArtifactName,
|
||||||
|
}
|
||||||
|
|
||||||
|
properties := []cdx.Property{
|
||||||
|
property(PropertySchemaVersion, strconv.Itoa(r.SchemaVersion)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Metadata.Size != 0 {
|
||||||
|
properties = appendProperties(properties, PropertySize, strconv.FormatInt(r.Metadata.Size, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.ArtifactType {
|
||||||
|
case ftypes.ArtifactContainerImage:
|
||||||
|
component.Type = cdx.ComponentTypeContainer
|
||||||
|
p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("failed to new package url for oci: %w", err)
|
||||||
|
}
|
||||||
|
properties = appendProperties(properties, PropertyImageID, r.Metadata.ImageID)
|
||||||
|
|
||||||
|
if p.Type == "" {
|
||||||
|
component.BOMRef = cw.newUUID().String()
|
||||||
|
} else {
|
||||||
|
component.BOMRef = p.ToString()
|
||||||
|
component.PackageURL = p.ToString()
|
||||||
|
}
|
||||||
|
case ftypes.ArtifactFilesystem, ftypes.ArtifactRemoteRepository:
|
||||||
|
component.Type = cdx.ComponentTypeApplication
|
||||||
|
component.BOMRef = cw.newUUID().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range r.Metadata.RepoDigests {
|
||||||
|
properties = appendProperties(properties, PropertyRepoDigest, d)
|
||||||
|
}
|
||||||
|
for _, d := range r.Metadata.DiffIDs {
|
||||||
|
properties = appendProperties(properties, PropertyDiffID, d)
|
||||||
|
}
|
||||||
|
for _, t := range r.Metadata.RepoTags {
|
||||||
|
properties = appendProperties(properties, PropertyRepoTag, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Properties = &properties
|
||||||
|
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw Writer) resultToComponent(r types.Result, osFound *ftypes.OS) cdx.Component {
|
||||||
|
component := cdx.Component{
|
||||||
|
Name: r.Target,
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
property(PropertyType, r.Type),
|
||||||
|
property(PropertyClass, string(r.Class)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Class {
|
||||||
|
case types.ClassOSPkg:
|
||||||
|
// UUID needs to be generated since Operating System Component cannot generate PURL.
|
||||||
|
// https://cyclonedx.org/use-cases/#known-vulnerabilities
|
||||||
|
component.BOMRef = cw.newUUID().String()
|
||||||
|
if osFound != nil {
|
||||||
|
component.Name = osFound.Family
|
||||||
|
component.Version = osFound.Name
|
||||||
|
}
|
||||||
|
component.Type = cdx.ComponentTypeOS
|
||||||
|
case types.ClassLangPkg:
|
||||||
|
// UUID needs to be generated since Application Component cannot generate PURL.
|
||||||
|
// https://cyclonedx.org/use-cases/#known-vulnerabilities
|
||||||
|
component.BOMRef = cw.newUUID().String()
|
||||||
|
component.Type = cdx.ComponentTypeApplication
|
||||||
|
case types.ClassConfig:
|
||||||
|
// TODO: Config support
|
||||||
|
component.BOMRef = cw.newUUID().String()
|
||||||
|
component.Type = cdx.ComponentTypeFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProperties(pkg ftypes.Package) []cdx.Property {
|
||||||
|
props := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{PropertyFilePath, pkg.FilePath},
|
||||||
|
{PropertySrcName, pkg.SrcName},
|
||||||
|
{PropertySrcVersion, pkg.SrcVersion},
|
||||||
|
{PropertySrcRelease, pkg.SrcRelease},
|
||||||
|
{PropertySrcEpoch, strconv.Itoa(pkg.SrcEpoch)},
|
||||||
|
{PropertyModularitylabel, pkg.Modularitylabel},
|
||||||
|
{PropertyLayerDigest, pkg.Layer.Digest},
|
||||||
|
{PropertyLayerDiffID, pkg.Layer.DiffID},
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties []cdx.Property
|
||||||
|
for _, prop := range props {
|
||||||
|
properties = appendProperties(properties, prop.name, prop.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendProperties(properties []cdx.Property, key, value string) []cdx.Property {
|
||||||
|
if value == "" || (key == PropertySrcEpoch && value == "0") {
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
return append(properties, property(key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func property(key, value string) cdx.Property {
|
||||||
|
return cdx.Property{
|
||||||
|
Name: Namespace + key,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
672
pkg/report/cyclonedx/cyclonedx_test.go
Normal file
672
pkg/report/cyclonedx/cyclonedx_test.go
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
package cyclonedx_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
fake "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
|
fos "github.com/aquasecurity/fanal/analyzer/os"
|
||||||
|
ftypes "github.com/aquasecurity/fanal/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/report"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/report/cyclonedx"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriter_Write(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputReport types.Report
|
||||||
|
wantSBOM *cdx.BOM
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path for container scan",
|
||||||
|
inputReport: types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "rails:latest",
|
||||||
|
ArtifactType: ftypes.ArtifactContainerImage,
|
||||||
|
Metadata: types.Metadata{
|
||||||
|
Size: 1024,
|
||||||
|
OS: &ftypes.OS{
|
||||||
|
Family: fos.CentOS,
|
||||||
|
Name: "8.3.2011",
|
||||||
|
Eosl: true,
|
||||||
|
},
|
||||||
|
ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
|
||||||
|
RepoTags: []string{"rails:latest"},
|
||||||
|
DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"},
|
||||||
|
RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"},
|
||||||
|
ImageConfig: v1.ConfigFile{
|
||||||
|
Architecture: "arm64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "rails:latest (centos 8.3.2011)",
|
||||||
|
Class: types.ClassOSPkg,
|
||||||
|
Type: fos.CentOS,
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "acl",
|
||||||
|
Version: "2.2.53",
|
||||||
|
Release: "1.el8",
|
||||||
|
Epoch: 0,
|
||||||
|
Arch: "aarch64",
|
||||||
|
SrcName: "acl",
|
||||||
|
SrcVersion: "2.2.53",
|
||||||
|
SrcRelease: "1.el8",
|
||||||
|
SrcEpoch: 0,
|
||||||
|
Modularitylabel: "",
|
||||||
|
License: "GPLv2+",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Target: "app/subproject/Gemfile.lock",
|
||||||
|
Class: types.ClassLangPkg,
|
||||||
|
Type: "bundler",
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "actioncable",
|
||||||
|
Version: "7.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "actioncontroller",
|
||||||
|
Version: "7.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Target: "app/Gemfile.lock",
|
||||||
|
Class: types.ClassLangPkg,
|
||||||
|
Type: ftypes.Bundler,
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "actioncable",
|
||||||
|
Version: "7.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSBOM: &cdx.BOM{
|
||||||
|
BOMFormat: "CycloneDX",
|
||||||
|
SpecVersion: "1.3",
|
||||||
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
|
Version: 1,
|
||||||
|
Metadata: &cdx.Metadata{
|
||||||
|
Timestamp: "2021-08-25T12:20:30.000000005Z",
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Name: "trivy",
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Version: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: &cdx.Component{
|
||||||
|
Type: cdx.ComponentTypeContainer,
|
||||||
|
BOMRef: "pkg:oci/rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?repository_url=index.docker.io%2Flibrary%2Frails&arch=arm64",
|
||||||
|
PackageURL: "pkg:oci/rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?repository_url=index.docker.io%2Flibrary%2Frails&arch=arm64",
|
||||||
|
Name: "rails:latest",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SchemaVersion",
|
||||||
|
Value: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Size",
|
||||||
|
Value: "1024",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:ImageID",
|
||||||
|
Value: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:RepoDigest",
|
||||||
|
Value: "rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:DiffID",
|
||||||
|
Value: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:RepoTag",
|
||||||
|
Value: "rails:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Components: &[]cdx.Component{
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
Type: cdx.ComponentTypeLibrary,
|
||||||
|
Name: "acl",
|
||||||
|
Version: "2.2.53-1.el8",
|
||||||
|
Licenses: &cdx.Licenses{
|
||||||
|
cdx.LicenseChoice{Expression: "GPLv2+"},
|
||||||
|
},
|
||||||
|
PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcName",
|
||||||
|
Value: "acl",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcVersion",
|
||||||
|
Value: "2.2.53",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcRelease",
|
||||||
|
Value: "1.el8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Type: cdx.ComponentTypeOS,
|
||||||
|
Name: "centos",
|
||||||
|
Version: "8.3.2011",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Type",
|
||||||
|
Value: "centos",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Class",
|
||||||
|
Value: "os-pkgs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:gem/actioncable@7.0.0",
|
||||||
|
Type: cdx.ComponentTypeLibrary,
|
||||||
|
Name: "actioncable",
|
||||||
|
Version: "7.0.0",
|
||||||
|
PackageURL: "pkg:gem/actioncable@7.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:gem/actioncontroller@7.0.0",
|
||||||
|
Type: cdx.ComponentTypeLibrary,
|
||||||
|
Name: "actioncontroller",
|
||||||
|
Version: "7.0.0",
|
||||||
|
PackageURL: "pkg:gem/actioncontroller@7.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "app/subproject/Gemfile.lock",
|
||||||
|
Version: "",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Type",
|
||||||
|
Value: "bundler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Class",
|
||||||
|
Value: "lang-pkgs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000004",
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "app/Gemfile.lock",
|
||||||
|
Version: "",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Type",
|
||||||
|
Value: "bundler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Class",
|
||||||
|
Value: "lang-pkgs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:gem/actioncable@7.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "pkg:gem/actioncontroller@7.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000004",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:gem/actioncable@7.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "pkg:oci/rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?repository_url=index.docker.io%2Flibrary%2Frails&arch=arm64",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000004",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path for local container scan",
|
||||||
|
inputReport: types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "centos:latest",
|
||||||
|
ArtifactType: ftypes.ArtifactContainerImage,
|
||||||
|
Metadata: types.Metadata{
|
||||||
|
Size: 1024,
|
||||||
|
OS: &ftypes.OS{
|
||||||
|
Family: "centos",
|
||||||
|
Name: "8.3.2011",
|
||||||
|
Eosl: true,
|
||||||
|
},
|
||||||
|
ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
|
||||||
|
RepoTags: []string{"centos:latest"},
|
||||||
|
RepoDigests: []string{},
|
||||||
|
ImageConfig: v1.ConfigFile{
|
||||||
|
Architecture: "arm64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "centos:latest (centos 8.3.2011)",
|
||||||
|
Class: types.ClassOSPkg,
|
||||||
|
Type: fos.CentOS,
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "acl",
|
||||||
|
Version: "2.2.53",
|
||||||
|
Release: "1.el8",
|
||||||
|
Epoch: 1,
|
||||||
|
Arch: "aarch64",
|
||||||
|
SrcName: "acl",
|
||||||
|
SrcVersion: "2.2.53",
|
||||||
|
SrcRelease: "1.el8",
|
||||||
|
SrcEpoch: 1,
|
||||||
|
Modularitylabel: "",
|
||||||
|
License: "GPLv2+",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSBOM: &cdx.BOM{
|
||||||
|
BOMFormat: "CycloneDX",
|
||||||
|
SpecVersion: "1.3",
|
||||||
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
|
Version: 1,
|
||||||
|
Metadata: &cdx.Metadata{
|
||||||
|
Timestamp: "2021-08-25T12:20:30.000000005Z",
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Name: "trivy",
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Version: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: &cdx.Component{
|
||||||
|
Type: cdx.ComponentTypeContainer,
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
PackageURL: "",
|
||||||
|
Name: "centos:latest",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SchemaVersion",
|
||||||
|
Value: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Size",
|
||||||
|
Value: "1024",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:ImageID",
|
||||||
|
Value: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:RepoTag",
|
||||||
|
Value: "centos:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Components: &[]cdx.Component{
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:rpm/centos/acl@1:2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
Type: cdx.ComponentTypeLibrary,
|
||||||
|
Name: "acl",
|
||||||
|
Version: "1:2.2.53-1.el8",
|
||||||
|
Licenses: &cdx.Licenses{
|
||||||
|
cdx.LicenseChoice{Expression: "GPLv2+"},
|
||||||
|
},
|
||||||
|
PackageURL: "pkg:rpm/centos/acl@1:2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcName",
|
||||||
|
Value: "acl",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcVersion",
|
||||||
|
Value: "2.2.53",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcRelease",
|
||||||
|
Value: "1.el8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SrcEpoch",
|
||||||
|
Value: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Type: cdx.ComponentTypeOS,
|
||||||
|
Name: "centos",
|
||||||
|
Version: "8.3.2011",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Type",
|
||||||
|
Value: "centos",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Class",
|
||||||
|
Value: "os-pkgs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:rpm/centos/acl@1:2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path for fs scan",
|
||||||
|
inputReport: types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "masahiro331/CVE-2021-41098",
|
||||||
|
ArtifactType: ftypes.ArtifactFilesystem,
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "Gemfile.lock",
|
||||||
|
Class: types.ClassLangPkg,
|
||||||
|
Type: ftypes.Bundler,
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "actioncable",
|
||||||
|
Version: "6.1.4.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSBOM: &cdx.BOM{
|
||||||
|
BOMFormat: "CycloneDX",
|
||||||
|
SpecVersion: "1.3",
|
||||||
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
|
Version: 1,
|
||||||
|
Metadata: &cdx.Metadata{
|
||||||
|
Timestamp: "2021-08-25T12:20:30.000000005Z",
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Name: "trivy",
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Version: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: &cdx.Component{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "masahiro331/CVE-2021-41098",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SchemaVersion",
|
||||||
|
Value: "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Components: &[]cdx.Component{
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:gem/actioncable@6.1.4.1",
|
||||||
|
Type: "library",
|
||||||
|
Name: "actioncable",
|
||||||
|
Version: "6.1.4.1",
|
||||||
|
PackageURL: "pkg:gem/actioncable@6.1.4.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "Gemfile.lock",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Type",
|
||||||
|
Value: "bundler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:Class",
|
||||||
|
Value: "lang-pkgs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:gem/actioncable@6.1.4.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path aggregate results",
|
||||||
|
inputReport: types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "test-aggregate",
|
||||||
|
ArtifactType: ftypes.ArtifactRemoteRepository,
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "Node.js",
|
||||||
|
Class: types.ClassLangPkg,
|
||||||
|
Type: ftypes.NodePkg,
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
Name: "ruby-typeprof",
|
||||||
|
Version: "0.20.1",
|
||||||
|
License: "MIT",
|
||||||
|
Layer: ftypes.Layer{
|
||||||
|
DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e",
|
||||||
|
},
|
||||||
|
FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSBOM: &cdx.BOM{
|
||||||
|
BOMFormat: "CycloneDX",
|
||||||
|
SpecVersion: "1.3",
|
||||||
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
|
Version: 1,
|
||||||
|
Metadata: &cdx.Metadata{
|
||||||
|
Timestamp: "2021-08-25T12:20:30.000000005Z",
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Name: "trivy",
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Version: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: &cdx.Component{
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "test-aggregate",
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SchemaVersion",
|
||||||
|
Value: "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Components: &[]cdx.Component{
|
||||||
|
{
|
||||||
|
BOMRef: "pkg:npm/ruby-typeprof@0.20.1?file_path=usr%2Flocal%2Flib%2Fruby%2Fgems%2F3.1.0%2Fgems%2Ftypeprof-0.21.1%2Fvscode%2Fpackage.json",
|
||||||
|
Type: "library",
|
||||||
|
Name: "ruby-typeprof",
|
||||||
|
Version: "0.20.1",
|
||||||
|
PackageURL: "pkg:npm/ruby-typeprof@0.20.1",
|
||||||
|
Licenses: &cdx.Licenses{
|
||||||
|
cdx.LicenseChoice{Expression: "MIT"},
|
||||||
|
},
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:FilePath",
|
||||||
|
Value: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:LayerDiffID",
|
||||||
|
Value: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "pkg:npm/ruby-typeprof@0.20.1?file_path=usr%2Flocal%2Flib%2Fruby%2Fgems%2F3.1.0%2Fgems%2Ftypeprof-0.21.1%2Fvscode%2Fpackage.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path empty",
|
||||||
|
inputReport: types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "empty/path",
|
||||||
|
ArtifactType: ftypes.ArtifactFilesystem,
|
||||||
|
Results: types.Results{},
|
||||||
|
},
|
||||||
|
|
||||||
|
wantSBOM: &cdx.BOM{
|
||||||
|
BOMFormat: "CycloneDX",
|
||||||
|
SpecVersion: "1.3",
|
||||||
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
|
Version: 1,
|
||||||
|
Metadata: &cdx.Metadata{
|
||||||
|
Timestamp: "2021-08-25T12:20:30.000000005Z",
|
||||||
|
Tools: &[]cdx.Tool{
|
||||||
|
{
|
||||||
|
Name: "trivy",
|
||||||
|
Vendor: "aquasecurity",
|
||||||
|
Version: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Component: &cdx.Component{
|
||||||
|
Type: cdx.ComponentTypeApplication,
|
||||||
|
Name: "empty/path",
|
||||||
|
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
Properties: &[]cdx.Property{
|
||||||
|
{
|
||||||
|
Name: "aquasecurity:trivy:SchemaVersion",
|
||||||
|
Value: "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: &[]cdx.Dependency{
|
||||||
|
{
|
||||||
|
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var count int
|
||||||
|
newUUID := func() uuid.UUID {
|
||||||
|
|
||||||
|
count++
|
||||||
|
|
||||||
|
return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
output := bytes.NewBuffer(nil)
|
||||||
|
writer := cyclonedx.NewWriter(output, "dev", cyclonedx.WithClock(clock), cyclonedx.WithNewUUID(newUUID))
|
||||||
|
|
||||||
|
err := writer.Write(tc.inputReport)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var got cdx.BOM
|
||||||
|
err = json.NewDecoder(output).Decode(&got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, *tc.wantSBOM, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/report/cyclonedx"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +46,9 @@ func Write(report types.Report, option Option) error {
|
|||||||
}
|
}
|
||||||
case "json":
|
case "json":
|
||||||
writer = &JSONWriter{Output: option.Output}
|
writer = &JSONWriter{Output: option.Output}
|
||||||
|
case "cyclonedx":
|
||||||
|
// TODO: support xml format option with cyclonedx writer
|
||||||
|
writer = cyclonedx.NewWriter(option.Output, option.AppVersion)
|
||||||
case "template":
|
case "template":
|
||||||
// We keep `sarif.tpl` template working for backward compatibility for a while.
|
// We keep `sarif.tpl` template working for backward compatibility for a while.
|
||||||
if strings.HasPrefix(option.OutputTemplate, "@") && filepath.Base(option.OutputTemplate) == "sarif.tpl" {
|
if strings.HasPrefix(option.OutputTemplate, "@") && filepath.Base(option.OutputTemplate) == "sarif.tpl" {
|
||||||
|
|||||||
Reference in New Issue
Block a user