mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -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.
|
||||
- Support multiple targets
|
||||
- container image, local filesystem and remote git repository
|
||||
- Supply chain security (SBOM support)
|
||||
- Support CycloneDX
|
||||
|
||||
# Integrations
|
||||
- [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]
|
||||
- local filesystem and rootfs
|
||||
- remote git repository
|
||||
- SBOM (Software Bill of Materials) support
|
||||
- CycloneDX
|
||||
|
||||
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
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.4.0
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
||||
@@ -28,6 +29,7 @@ require (
|
||||
github.com/goccy/go-yaml v1.8.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2
|
||||
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/hashicorp/go-getter v1.5.11
|
||||
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/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
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/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=
|
||||
@@ -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.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/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/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=
|
||||
|
||||
@@ -5,6 +5,7 @@ package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -32,6 +34,7 @@ type csArgs struct {
|
||||
Input string
|
||||
ClientToken string
|
||||
ClientTokenHeader string
|
||||
ListAllPackages bool
|
||||
}
|
||||
|
||||
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) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -71,6 +71,8 @@ nav:
|
||||
- Overview: advanced/index.md
|
||||
- Plugins: advanced/plugins.md
|
||||
- Air-Gapped Environment: advanced/air-gap.md
|
||||
- SBOM:
|
||||
- CycloneDX: advanced/sbom/cyclonedx.md
|
||||
- Integrations:
|
||||
- Overview: advanced/integrations/index.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",
|
||||
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
@@ -116,7 +116,7 @@ func TestOption_Init(t *testing.T) {
|
||||
name: "invalid option combination: --template and --format json",
|
||||
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
@@ -136,7 +136,7 @@ func TestOption_Init(t *testing.T) {
|
||||
name: "invalid option combination: --format template without --template",
|
||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestConfig_Init(t *testing.T) {
|
||||
name: "invalid option combination: --template enabled without --format",
|
||||
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
@@ -162,7 +162,7 @@ func TestConfig_Init(t *testing.T) {
|
||||
name: "invalid option combination: --template and --format json",
|
||||
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
@@ -183,7 +183,7 @@ func TestConfig_Init(t *testing.T) {
|
||||
name: "invalid option combination: --format template without --template",
|
||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
@@ -203,7 +203,7 @@ func TestConfig_Init(t *testing.T) {
|
||||
name: "invalid option combination: --format template without --template",
|
||||
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
ReportOption: option.ReportOption{
|
||||
|
||||
@@ -7,13 +7,11 @@ import (
|
||||
// ImageOption holds the options for scanning images
|
||||
type ImageOption struct {
|
||||
ScanRemovedPkgs bool
|
||||
ListAllPkgs bool
|
||||
}
|
||||
|
||||
// NewImageOption is the factory method to return ImageOption
|
||||
func NewImageOption(c *cli.Context) ImageOption {
|
||||
return ImageOption{
|
||||
ScanRemovedPkgs: c.Bool("removed-pkgs"),
|
||||
ListAllPkgs: c.Bool("list-all-pkgs"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type ReportOption struct {
|
||||
SecurityChecks []string
|
||||
Output io.Writer
|
||||
Severities []dbTypes.Severity
|
||||
ListAllPkgs bool
|
||||
}
|
||||
|
||||
// NewReportOption is the factory method to return ReportOption
|
||||
@@ -51,31 +52,35 @@ func NewReportOption(c *cli.Context) ReportOption {
|
||||
IgnoreFile: c.String("ignorefile"),
|
||||
IgnoreUnfixed: c.Bool("ignore-unfixed"),
|
||||
ExitCode: c.Int("exit-code"),
|
||||
ListAllPkgs: c.Bool("list-all-pkgs"),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the ReportOption
|
||||
func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
||||
var err error
|
||||
|
||||
if c.Template != "" {
|
||||
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" {
|
||||
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)
|
||||
|
||||
if err = c.populateVulnTypes(); err != nil {
|
||||
if err := c.populateVulnTypes(); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -86,6 +91,7 @@ func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
||||
|
||||
// The output is os.Stdout by default
|
||||
if c.output != "" {
|
||||
var err error
|
||||
if output, err = os.Create(c.output); err != nil {
|
||||
return xerrors.Errorf("failed to create an output file: %w", err)
|
||||
}
|
||||
@@ -124,6 +130,14 @@ func (c *ReportOption) populateSecurityChecks() error {
|
||||
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 {
|
||||
logger.Debugf("Severities: %s", severity)
|
||||
var severities []dbTypes.Severity
|
||||
|
||||
@@ -24,10 +24,12 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
severities string
|
||||
IgnoreFile string
|
||||
IgnoreUnfixed bool
|
||||
listAllPksgs bool
|
||||
ExitCode int
|
||||
VulnType []string
|
||||
Output *os.File
|
||||
Severities []dbTypes.Severity
|
||||
debug bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -70,6 +72,49 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
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",
|
||||
fields: fields{
|
||||
@@ -80,7 +125,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
},
|
||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
Output: os.Stdout,
|
||||
@@ -101,7 +146,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
},
|
||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
Format: "json",
|
||||
@@ -122,7 +167,7 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
},
|
||||
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
|
||||
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{
|
||||
Format: "template",
|
||||
@@ -135,7 +180,12 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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)
|
||||
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
@@ -151,9 +201,9 @@ func TestReportReportConfig_Init(t *testing.T) {
|
||||
IgnoreFile: tt.fields.IgnoreFile,
|
||||
IgnoreUnfixed: tt.fields.IgnoreUnfixed,
|
||||
ExitCode: tt.fields.ExitCode,
|
||||
ListAllPkgs: tt.fields.listAllPksgs,
|
||||
Output: tt.fields.Output,
|
||||
}
|
||||
|
||||
err := c.Init(os.Stdout, logger.Sugar())
|
||||
|
||||
// tests log messages
|
||||
|
||||
112
pkg/purl/purl.go
112
pkg/purl/purl.go
@@ -2,7 +2,6 @@ package purl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cn "github.com/google/go-containerregistry/pkg/name"
|
||||
@@ -20,15 +19,36 @@ const (
|
||||
TypeOCI = "oci"
|
||||
)
|
||||
|
||||
// nolint: gocyclo
|
||||
func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packageurl.PackageURL, error) {
|
||||
ptype := purlType(t)
|
||||
type PackageURL struct {
|
||||
packageurl.PackageURL
|
||||
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
|
||||
if metadata.OS != nil {
|
||||
qualifiers = parseQualifier(pkg, metadata.OS.Name)
|
||||
qualifiers = parseQualifier(pkg)
|
||||
}
|
||||
|
||||
ptype := purlType(t)
|
||||
name := pkg.Name
|
||||
version := utils.FormatVersion(pkg)
|
||||
namespace := ""
|
||||
@@ -41,7 +61,7 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packa
|
||||
case packageurl.TypeDebian:
|
||||
qualifiers = append(qualifiers, parseDeb(metadata.OS)...)
|
||||
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)...)
|
||||
namespace = metadata.OS.Family
|
||||
case packageurl.TypeMaven:
|
||||
@@ -55,15 +75,23 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (packa
|
||||
case packageurl.TypeNPM:
|
||||
namespace, name = parseNpm(name)
|
||||
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) {
|
||||
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])
|
||||
@@ -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 {
|
||||
distro := fmt.Sprintf("%s-%s", fos.Family, fos.Name)
|
||||
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) {
|
||||
// SLES string has whitespace
|
||||
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)
|
||||
|
||||
qualifiers := packageurl.Qualifiers{
|
||||
{
|
||||
Key: "distro",
|
||||
@@ -134,54 +163,36 @@ func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualif
|
||||
return family, qualifiers
|
||||
}
|
||||
|
||||
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#maven
|
||||
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, ":", "/")
|
||||
index := strings.LastIndex(name, "/")
|
||||
if index != -1 {
|
||||
namespace = name[:index]
|
||||
name = name[index+1:]
|
||||
}
|
||||
return namespace, name
|
||||
return parsePkgName(name)
|
||||
}
|
||||
|
||||
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#golang
|
||||
func parseGolang(pkgName string) (string, string) {
|
||||
var namespace string
|
||||
|
||||
name := strings.ToLower(pkgName)
|
||||
index := strings.LastIndex(name, "/")
|
||||
if index != -1 {
|
||||
namespace = name[:index]
|
||||
name = name[index+1:]
|
||||
}
|
||||
return namespace, name
|
||||
return parsePkgName(name)
|
||||
}
|
||||
|
||||
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#pypi
|
||||
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, "_", "-"))
|
||||
}
|
||||
|
||||
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#composer
|
||||
func parseComposer(pkgName string) (string, string) {
|
||||
var namespace, name string
|
||||
|
||||
index := strings.LastIndex(pkgName, "/")
|
||||
if index != -1 {
|
||||
namespace = pkgName[:index]
|
||||
name = pkgName[index+1:]
|
||||
}
|
||||
return namespace, name
|
||||
return parsePkgName(pkgName)
|
||||
}
|
||||
|
||||
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#npm
|
||||
func parseNpm(pkgName string) (string, string) {
|
||||
var namespace string
|
||||
|
||||
// the name must be lowercased
|
||||
name := strings.ToLower(pkgName)
|
||||
index := strings.LastIndex(pkgName, "/")
|
||||
if index != -1 {
|
||||
namespace = name[:index]
|
||||
name = name[index+1:]
|
||||
}
|
||||
return namespace, name
|
||||
return parsePkgName(name)
|
||||
}
|
||||
|
||||
func purlType(t string) string {
|
||||
@@ -210,7 +221,7 @@ func purlType(t string) string {
|
||||
return t
|
||||
}
|
||||
|
||||
func parseQualifier(pkg ftypes.Package, distro string) packageurl.Qualifiers {
|
||||
func parseQualifier(pkg ftypes.Package) packageurl.Qualifiers {
|
||||
qualifiers := packageurl.Qualifiers{}
|
||||
if pkg.Arch != "" {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
@@ -218,11 +229,16 @@ func parseQualifier(pkg ftypes.Package, distro string) packageurl.Qualifiers {
|
||||
Value: pkg.Arch,
|
||||
})
|
||||
}
|
||||
if pkg.Epoch != 0 {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "epoch",
|
||||
Value: strconv.Itoa(pkg.Epoch),
|
||||
})
|
||||
}
|
||||
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
|
||||
pkg ftypes.Package
|
||||
metadata types.Metadata
|
||||
want packageurl.PackageURL
|
||||
want purl.PackageURL
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
@@ -32,13 +32,15 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "org.springframework:spring-core",
|
||||
Version: "5.3.14",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeMaven,
|
||||
Namespace: "org.springframework",
|
||||
Name: "spring-core",
|
||||
Version: "5.3.14",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "yarn package",
|
||||
typ: string(analyzer.TypeYarn),
|
||||
@@ -46,13 +48,15 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "@xtuc/ieee754",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeNPM,
|
||||
Namespace: "@xtuc",
|
||||
Name: "ieee754",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "yarn package with non-namespace",
|
||||
typ: string(analyzer.TypeYarn),
|
||||
@@ -60,12 +64,14 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeNPM,
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pypi package",
|
||||
typ: string(analyzer.TypePip),
|
||||
@@ -73,12 +79,14 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "Django_test",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypePyPi,
|
||||
Name: "django-test",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "composer package",
|
||||
typ: string(analyzer.TypeComposer),
|
||||
@@ -86,13 +94,15 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "symfony/contracts",
|
||||
Version: "v1.0.2",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeComposer,
|
||||
Namespace: "symfony",
|
||||
Name: "contracts",
|
||||
Version: "v1.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "golang package",
|
||||
typ: string(analyzer.TypeGoMod),
|
||||
@@ -100,13 +110,15 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "github.com/go-sql-driver/Mysql",
|
||||
Version: "v1.5.0",
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeGolang,
|
||||
Namespace: "github.com/go-sql-driver",
|
||||
Name: "mysql",
|
||||
Version: "v1.5.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "os package",
|
||||
typ: os.RedHat,
|
||||
@@ -129,7 +141,8 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Name: "8",
|
||||
},
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeRPM,
|
||||
Namespace: "redhat",
|
||||
Name: "acl",
|
||||
@@ -146,6 +159,7 @@ func TestNewPackageURL(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container",
|
||||
typ: purl.TypeOCI,
|
||||
@@ -161,7 +175,8 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeOCI,
|
||||
Namespace: "",
|
||||
Name: "core",
|
||||
@@ -178,6 +193,27 @@ func TestNewPackageURL(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
typ: purl.TypeOCI,
|
||||
@@ -194,7 +230,8 @@ func TestNewPackageURL(t *testing.T) {
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
want: packageurl.PackageURL{
|
||||
want: purl.PackageURL{
|
||||
PackageURL: packageurl.PackageURL{
|
||||
Type: packageurl.TypeOCI,
|
||||
Namespace: "",
|
||||
Name: "alpine",
|
||||
@@ -211,6 +248,7 @@ func TestNewPackageURL(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sad path",
|
||||
typ: purl.TypeOCI,
|
||||
|
||||
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"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/report/cyclonedx"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,9 @@ func Write(report types.Report, option Option) error {
|
||||
}
|
||||
case "json":
|
||||
writer = &JSONWriter{Output: option.Output}
|
||||
case "cyclonedx":
|
||||
// TODO: support xml format option with cyclonedx writer
|
||||
writer = cyclonedx.NewWriter(option.Output, option.AppVersion)
|
||||
case "template":
|
||||
// We keep `sarif.tpl` template working for backward compatibility for a while.
|
||||
if strings.HasPrefix(option.OutputTemplate, "@") && filepath.Base(option.OutputTemplate) == "sarif.tpl" {
|
||||
|
||||
Reference in New Issue
Block a user