Skip to content

Commit

Permalink
feat: cyclonedx kbom support
Browse files Browse the repository at this point in the history
Signed-off-by: chenk <hen.keinan@gmail.com>
  • Loading branch information
chen-keinan committed Jun 6, 2023
1 parent e1a3812 commit 5c9453f
Show file tree
Hide file tree
Showing 24 changed files with 1,408 additions and 390 deletions.
2 changes: 1 addition & 1 deletion docs/docs/references/configuration/cli/trivy_kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ trivy kubernetes [flags] { cluster | all | specific resources like kubectl. eg:
--exclude-nodes strings indicate the node labels that the node-collector job should exclude from scanning (example: kubernetes.io/arch:arm64,team:dev)
--exit-code int specify exit code when any security issues are found
--file-patterns strings specify config file patterns
-f, --format string format (table, json, template, sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln) (default "table")
-f, --format string format (table, json, cyclonedx) (default "table")
--helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)
--helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
Expand Down
8 changes: 7 additions & 1 deletion docs/docs/supply-chain/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Trivy can generate the following SBOM formats.
- [SPDX](#spdx)

### CLI commands
To generate SBOM, you can use the `--format` option for each subcommand such as `image`, `fs` and `vm`.
To generate SBOM, you can use the `--format` option for each subcommand such as `image`, `fs`, `vm` and `k8s`(support cyclonedx format only).

```
$ trivy image --format spdx-json --output result.json alpine:3.15
Expand All @@ -19,6 +19,12 @@ $ trivy image --format spdx-json --output result.json alpine:3.15
$ trivy fs --format cyclonedx --output result.json /app/myproject
```

#### Kubernetes sbom refer as kbom , produce the k8s core components (Control Plane Components, Node Components and Addons) bill of material

```
trivy k8s cluster --format cyclonedx --output result.json
```

<details>
<summary>Result</summary>

Expand Down
9 changes: 9 additions & 0 deletions docs/docs/target/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,12 @@ Trivy has a native [Kubernetes Operator][operator] which continuously scans your
[operator]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
[crd]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
[trivy-operator]: https://aquasecurity.github.io/trivy-operator/latest

## SBOM

Trivy supports the generation of Kubernetes Bill of Materials (KBOM) for kubernetes cluster control plane components, node components and addons.

### Generation

Trivy can generate KBOM in cyclonedx format for kubernetes cluster core components .
See [here](../supply-chain/sbom.md) for the detail.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/BurntSushi/toml v1.2.1
github.com/CycloneDX/cyclonedx-go v0.7.0
github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/NYTimes/gziphandler v1.1.1
github.com/alicebob/miniredis/v2 v2.30.3
Expand Down Expand Up @@ -67,6 +68,7 @@ require (
github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd
github.com/masahiro331/go-xfs-filesystem v0.0.0-20221225060805-c02764233454
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.11.5
github.com/open-policy-agent/opa v0.45.0
github.com/opencontainers/go-digest v1.0.0
Expand Down Expand Up @@ -125,7 +127,6 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/squirrel v1.5.3 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
Expand Down Expand Up @@ -294,7 +295,6 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.5.0 // indirect
Expand Down
138 changes: 90 additions & 48 deletions integration/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,63 +8,105 @@ import (
"path/filepath"
"testing"

cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/aquasecurity/trivy/pkg/k8s/report"
"github.com/aquasecurity/trivy/pkg/types"

"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aquasecurity/trivy/pkg/k8s/report"
"github.com/aquasecurity/trivy/pkg/types"
)

// Note: the test required k8s (kind) cluster installed.
// "mage test:k8s" will run this test.

func TestK8s(t *testing.T) {
// Set up the output file
outputFile := filepath.Join(t.TempDir(), "output.json")

osArgs := []string{
"k8s",
"cluster",
"--report",
"summary",
"-q",
"--timeout",
"5m0s",
"--format",
"json",
"--components",
"workload",
"--context",
"kind-kind-test",
"--output",
outputFile,
}

// Run Trivy
err := execute(osArgs)
require.NoError(t, err)

var got report.ConsolidatedReport
f, err := os.Open(outputFile)
require.NoError(t, err)
defer f.Close()

err = json.NewDecoder(f).Decode(&got)
require.NoError(t, err)

// Flatten findings
results := lo.FlatMap(got.Findings, func(resource report.Resource, _ int) []types.Result {
return resource.Results
t.Run("misconfig and vulnerability scan", func(t *testing.T) {
// Set up the output file
outputFile := filepath.Join(t.TempDir(), "output.json")

osArgs := []string{
"k8s",
"cluster",
"--report",
"summary",
"-q",
"--timeout",
"5m0s",
"--format",
"json",
"--components",
"workload",
"--context",
"kind-kind-test",
"--output",
outputFile,
}

// Run Trivy
err := execute(osArgs)
require.NoError(t, err)

var got report.ConsolidatedReport
f, err := os.Open(outputFile)
require.NoError(t, err)
defer f.Close()

err = json.NewDecoder(f).Decode(&got)
require.NoError(t, err)

// Flatten findings
results := lo.FlatMap(got.Findings, func(resource report.Resource, _ int) []types.Result {
return resource.Results
})

// Has vulnerabilities
assert.True(t, lo.SomeBy(results, func(r types.Result) bool {
return len(r.Vulnerabilities) > 0
}))

// Has misconfigurations
assert.True(t, lo.SomeBy(results, func(r types.Result) bool {
return len(r.Misconfigurations) > 0
}))
})
t.Run("kbom cycloneDx", func(t *testing.T) {
// Set up the output file
outputFile := filepath.Join(t.TempDir(), "output.json")
osArgs := []string{
"k8s",
"cluster",
"--format",
"cyclonedx",
"-q",
"--context",
"kind-kind-test",
"--output",
outputFile,
}

// Run Trivy
err := execute(osArgs)
require.NoError(t, err)

var got *cdx.BOM
f, err := os.Open(outputFile)
require.NoError(t, err)
defer f.Close()

// Has vulnerabilities
assert.True(t, lo.SomeBy(results, func(r types.Result) bool {
return len(r.Vulnerabilities) > 0
}))
err = json.NewDecoder(f).Decode(&got)
require.NoError(t, err)

// Has misconfigurations
assert.True(t, lo.SomeBy(results, func(r types.Result) bool {
return len(r.Misconfigurations) > 0
}))
}
assert.Equal(t, got.Metadata.Component.Name, "kind-kind-test")
assert.Equal(t, got.Metadata.Component.Type, cdx.ComponentType("container"))

// Has components
assert.True(t, len(*got.Components) > 0)

// Has dependecies
assert.True(t, lo.SomeBy(*got.Dependencies, func(r cdx.Dependency) bool {
return len(*r.Dependencies) > 0
}))

})
}
5 changes: 5 additions & 0 deletions pkg/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/aquasecurity/trivy/pkg/module"
"github.com/aquasecurity/trivy/pkg/plugin"
"github.com/aquasecurity/trivy/pkg/policy"
r "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/types"
)

Expand Down Expand Up @@ -896,6 +897,10 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
reportFlagGroup.Compliance = &compliance // override usage as the accepted values differ for each subcommand.
reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'

formatFlag := flag.FormatFlag
formatFlag.Usage = "format (" + strings.Join([]string{r.FormatTable, r.FormatJSON, r.FormatCycloneDX}, ", ") + ")"
reportFlagGroup.Format = &formatFlag

k8sFlags := &flag.Flags{
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/types/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ const (
ArtifactSPDX ArtifactType = "spdx"
ArtifactAWSAccount ArtifactType = "aws_account"
ArtifactVM ArtifactType = "vm"
KubernetesPod ArtifactType = "k8s_pod"
)

// ArtifactReference represents a reference of container image, local filesystem and repository
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/types/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
Cocoapods = "cocoapods"
Pub = "pub"
Hex = "hex"
Oci = "oci"

// Config files
YAML = "yaml"
Expand Down
2 changes: 1 addition & 1 deletion pkg/flag/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (o *Options) Align() {
}

// Vulnerability scanning is disabled by default for CycloneDX.
if o.Format == report.FormatCycloneDX && !viper.IsSet(ScannersFlag.ConfigName) {
if o.Format == report.FormatCycloneDX && !viper.IsSet(ScannersFlag.ConfigName) && len(o.K8sOptions.Components) == 0 { // remove K8sOptions.Components validation check when vuln scan is supported for k8s report with cycloneDX
log.Logger.Info(`"--format cyclonedx" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the CycloneDX report.`)
o.Scanners = nil
}
Expand Down
23 changes: 17 additions & 6 deletions pkg/k8s/commands/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/types"
)

Expand All @@ -21,16 +22,26 @@ func clusterRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) err
}
var artifacts []*artifacts.Artifact
var err error
if opts.Scanners.AnyEnabled(types.MisconfigScanner) && slices.Contains(opts.Components, "infra") {
artifacts, err = trivyk8s.New(cluster, log.Logger).ListArtifactAndNodeInfo(ctx, opts.NodeCollectorNamespace, opts.ExcludeNodes, opts.Tolerations...)
switch opts.Format {
case report.FormatCycloneDX:
artifacts, err = trivyk8s.New(cluster, log.Logger).ListBomInfo(ctx)
if err != nil {
return xerrors.Errorf("get k8s artifacts with node info error: %w", err)
}
} else {
artifacts, err = trivyk8s.New(cluster, log.Logger).ListArtifacts(ctx)
if err != nil {
return xerrors.Errorf("get k8s artifacts error: %w", err)
case report.FormatJSON, report.FormatTable:
if opts.Scanners.AnyEnabled(types.MisconfigScanner) && slices.Contains(opts.Components, "infra") {
artifacts, err = trivyk8s.New(cluster, log.Logger).ListArtifactAndNodeInfo(ctx, opts.NodeCollectorNamespace, opts.ExcludeNodes, opts.Tolerations...)
if err != nil {
return xerrors.Errorf("get k8s artifacts with node info error: %w", err)
}
} else {
artifacts, err = trivyk8s.New(cluster, log.Logger).ListArtifacts(ctx)
if err != nil {
return xerrors.Errorf("get k8s artifacts error: %w", err)
}
}
default:
return xerrors.Errorf(`unknown format %q. Use "json" or "table" or "cyclonedx"`, opts.Format)
}

runner := newRunner(opts, cluster.GetCurrentContext())
Expand Down
8 changes: 5 additions & 3 deletions pkg/k8s/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/aquasecurity/trivy/pkg/commands/operation"
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/flag"
k8sRep "github.com/aquasecurity/trivy/pkg/k8s"
"github.com/aquasecurity/trivy/pkg/k8s/report"
"github.com/aquasecurity/trivy/pkg/k8s/scanner"
"github.com/aquasecurity/trivy/pkg/log"
Expand Down Expand Up @@ -88,8 +89,8 @@ func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error
}
r.flagOpts.ScanOptions.Scanners = scanners
}

rpt, err := s.Scan(ctx, artifacts)
var rpt report.Report
rpt, err = s.Scan(ctx, artifacts)
if err != nil {
return xerrors.Errorf("k8s scan error: %w", err)
}
Expand All @@ -110,13 +111,14 @@ func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error
})
}

if err := report.Write(rpt, report.Option{
if err := k8sRep.Write(rpt, report.Option{
Format: r.flagOpts.Format,
Report: r.flagOpts.ReportFormat,
Output: r.flagOpts.Output,
Severities: r.flagOpts.Severities,
Components: r.flagOpts.Components,
Scanners: r.flagOpts.ScanOptions.Scanners,
APIVersion: r.flagOpts.AppVersion,
}); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
Expand Down
15 changes: 8 additions & 7 deletions pkg/k8s/report/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ func (jw JSONWriter) Write(report Report) error {
var err error

switch jw.Report {
case allReport:
case AllReport:
output, err = json.MarshalIndent(report, "", " ")
case summaryReport:
if err != nil {
return xerrors.Errorf("failed to write json: %w", err)
}
case SummaryReport:
output, err = json.MarshalIndent(report.consolidate(), "", " ")
if err != nil {
return xerrors.Errorf("failed to write json: %w", err)
}
default:
return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, jw.Report)
}

if err != nil {
return xerrors.Errorf("failed to marshal json: %w", err)
}

if _, err = fmt.Fprintln(jw.Output, string(output)); err != nil {
return xerrors.Errorf("failed to write json: %w", err)
}
Expand Down
Loading

0 comments on commit 5c9453f

Please sign in to comment.