mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
feat(sbom): add a dedicated sbom command (#1799)
Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# 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.
|
||||
You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `cyclonedx` with the `--format` option.
|
||||
|
||||
```
|
||||
$ trivy image --format cyclonedx --output result.json alpine:3.15
|
||||
@@ -227,6 +227,7 @@ $ cat result.json | jq .
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
[cyclonedx]: https://cyclonedx.org/
|
||||
191
docs/advanced/sbom/index.md
Normal file
191
docs/advanced/sbom/index.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# SBOM
|
||||
Trivy currently supports the following SBOM formats.
|
||||
|
||||
- [CycloneDX][cyclonedx]
|
||||
|
||||
To generate SBOM, you can use the `--format` option for each subcommand such as `image` and `fs`.
|
||||
|
||||
```
|
||||
$ trivy image --format cyclonedx --output result.json alpine:3.15
|
||||
```
|
||||
|
||||
In addition, you can use the `trivy sbom` subcommand.
|
||||
|
||||
```
|
||||
$ trivy sbom alpine:3.15
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Result</summary>
|
||||
|
||||
```
|
||||
{
|
||||
"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>
|
||||
|
||||
`fs`, `repo` and `archive` also work with `sbom` subcommand.
|
||||
|
||||
```
|
||||
# filesystem
|
||||
$ trivy sbom --artifact-type fs /path/to/project
|
||||
|
||||
# repository
|
||||
$ trivy sbom --artifact-type repo github.com/aquasecurity/trivy-ci-test
|
||||
|
||||
# container image archive
|
||||
$ trivy sbom --artifact-type archive alpine.tar
|
||||
```
|
||||
|
||||
[cyclonedx]: cyclonedx.md
|
||||
19
docs/getting-started/cli/sbom.md
Normal file
19
docs/getting-started/cli/sbom.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# SBOM
|
||||
|
||||
```bash
|
||||
NAME:
|
||||
trivy sbom - generate SBOM for an artifact
|
||||
|
||||
USAGE:
|
||||
trivy sbom [command options] ARTIFACT
|
||||
|
||||
OPTIONS:
|
||||
--output value, -o value output file name [$TRIVY_OUTPUT]
|
||||
--clear-cache, -c clear image caches without scanning (default: false) [$TRIVY_CLEAR_CACHE]
|
||||
--ignorefile value specify .trivyignore file (default: ".trivyignore") [$TRIVY_IGNOREFILE]
|
||||
--timeout value timeout (default: 5m0s) [$TRIVY_TIMEOUT]
|
||||
--severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL") [$TRIVY_SEVERITY]
|
||||
--artifact-type value, --type value input artifact type (image, fs, repo, archive) (default: "image") [$TRIVY_ARTIFACT_TYPE]
|
||||
--sbom-format value, --format value SBOM format (cyclonedx) (default: "cyclonedx") [$TRIVY_SBOM_FORMAT]
|
||||
--help, -h show help (default: false)
|
||||
```
|
||||
@@ -25,6 +25,7 @@ nav:
|
||||
- Repository: getting-started/cli/repo.md
|
||||
- Client: getting-started/cli/client.md
|
||||
- Server: getting-started/cli/server.md
|
||||
- SBOM: getting-started/cli/sbom.md
|
||||
- Vulnerability:
|
||||
- Scanning:
|
||||
- Overview: vulnerability/scanning/index.md
|
||||
@@ -72,6 +73,7 @@ nav:
|
||||
- Plugins: advanced/plugins.md
|
||||
- Air-Gapped Environment: advanced/air-gap.md
|
||||
- SBOM:
|
||||
- Overview: advanced/sbom/index.md
|
||||
- CycloneDX: advanced/sbom/cyclonedx.md
|
||||
- Integrations:
|
||||
- Overview: advanced/integrations/index.md
|
||||
|
||||
@@ -365,6 +365,7 @@ func NewApp(version string) *cli.App {
|
||||
NewImageCommand(),
|
||||
NewFilesystemCommand(),
|
||||
NewRootfsCommand(),
|
||||
NewSbomCommand(),
|
||||
NewRepositoryCommand(),
|
||||
NewClientCommand(),
|
||||
NewServerCommand(),
|
||||
@@ -735,6 +736,57 @@ func NewPluginCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// NewSbomCommand is the factory method to add sbom command
|
||||
func NewSbomCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "sbom",
|
||||
ArgsUsage: "ARTIFACT",
|
||||
Usage: "generate SBOM for an artifact",
|
||||
Description: `ARTIFACT can be a container image, file path/directory, git repository or container image archive. See examples.`,
|
||||
CustomHelpTemplate: cli.CommandHelpTemplate + `EXAMPLES:
|
||||
- image scanning:
|
||||
$ trivy sbom alpine:3.15
|
||||
|
||||
- filesystem scanning:
|
||||
$ trivy sbom --artifact-type fs /path/to/myapp
|
||||
|
||||
- git repository scanning:
|
||||
$ trivy sbom --artifact-type repo github.com/aquasecurity/trivy-ci-test
|
||||
|
||||
- image archive scanning:
|
||||
$ trivy sbom --artifact-type archive ./alpine.tar
|
||||
|
||||
`,
|
||||
Action: artifact.SbomRun,
|
||||
Flags: []cli.Flag{
|
||||
&outputFlag,
|
||||
&clearCacheFlag,
|
||||
&ignoreFileFlag,
|
||||
&timeoutFlag,
|
||||
&severityFlag,
|
||||
&offlineScan,
|
||||
stringSliceFlag(skipFiles),
|
||||
stringSliceFlag(skipDirs),
|
||||
|
||||
// dedicated options
|
||||
&cli.StringFlag{
|
||||
Name: "artifact-type",
|
||||
Aliases: []string{"type"},
|
||||
Value: "image",
|
||||
Usage: "input artifact type (image, fs, repo, archive)",
|
||||
EnvVars: []string{"TRIVY_ARTIFACT_TYPE"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sbom-format",
|
||||
Aliases: []string{"format"},
|
||||
Value: "cyclonedx",
|
||||
Usage: "SBOM format (cyclonedx)",
|
||||
EnvVars: []string{"TRIVY_SBOM_FORMAT"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewVersionCommand adds version command
|
||||
func NewVersionCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
|
||||
@@ -18,6 +18,7 @@ type Option struct {
|
||||
option.CacheOption
|
||||
option.ConfigOption
|
||||
option.RemoteOption
|
||||
option.SbomOption
|
||||
|
||||
// We don't want to allow disabled analyzers to be passed by users,
|
||||
// but it differs depending on scanning modes.
|
||||
@@ -40,6 +41,7 @@ func NewOption(c *cli.Context) (Option, error) {
|
||||
CacheOption: option.NewCacheOption(c),
|
||||
ConfigOption: option.NewConfigOption(c),
|
||||
RemoteOption: option.NewRemoteOption(c),
|
||||
SbomOption: option.NewSbomOption(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -70,6 +72,9 @@ func (c *Option) initPreScanOptions() error {
|
||||
if err := c.CacheOption.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.SbomOption.Init(c.Context, c.Logger); err != nil {
|
||||
return err
|
||||
}
|
||||
c.RemoteOption.Init(c.Logger)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestOption_Init(t *testing.T) {
|
||||
name: "invalid option combination: token and token header without server",
|
||||
args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"},
|
||||
logs: []string{
|
||||
"'--token', '--token-header' and 'custom-header' can be used only with '--server'",
|
||||
`"--token" can be used only with "--server"`,
|
||||
},
|
||||
want: Option{
|
||||
ReportOption: option.ReportOption{
|
||||
|
||||
62
pkg/commands/artifact/sbom.go
Normal file
62
pkg/commands/artifact/sbom.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/fanal/analyzer"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
type ArtifactType string
|
||||
|
||||
const (
|
||||
containerImageArtifact ArtifactType = "image"
|
||||
filesystemArtifact ArtifactType = "fs"
|
||||
repositoryArtifact ArtifactType = "repo"
|
||||
imageArchiveArtifact ArtifactType = "archive"
|
||||
)
|
||||
|
||||
var artifactTypes = map[ArtifactType]struct {
|
||||
initializer InitializeScanner
|
||||
disableAnalyzers []analyzer.Type
|
||||
}{
|
||||
containerImageArtifact: {
|
||||
initializer: imageScanner,
|
||||
disableAnalyzers: analyzer.TypeLockfiles,
|
||||
},
|
||||
filesystemArtifact: {
|
||||
initializer: filesystemStandaloneScanner,
|
||||
disableAnalyzers: analyzer.TypeIndividualPkgs,
|
||||
},
|
||||
repositoryArtifact: {
|
||||
initializer: repositoryScanner,
|
||||
disableAnalyzers: analyzer.TypeIndividualPkgs,
|
||||
},
|
||||
imageArchiveArtifact: {
|
||||
initializer: archiveScanner,
|
||||
disableAnalyzers: analyzer.TypeLockfiles,
|
||||
},
|
||||
}
|
||||
|
||||
// SbomRun runs generates sbom for image and package artifacts
|
||||
func SbomRun(ctx *cli.Context) error {
|
||||
opt, err := initOption(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("option error: %w", err)
|
||||
}
|
||||
|
||||
artifactType := opt.SbomOption.ArtifactType
|
||||
s, ok := artifactTypes[ArtifactType(artifactType)]
|
||||
if !ok {
|
||||
return xerrors.Errorf(`"--artifact-type" must be %q`, maps.Keys(artifactTypes))
|
||||
}
|
||||
|
||||
// Scan the relevant dependencies
|
||||
opt.DisabledAnalyzers = s.disableAnalyzers
|
||||
opt.ReportOption.VulnType = []string{types.VulnTypeOS, types.VulnTypeLibrary}
|
||||
opt.ReportOption.SecurityChecks = []string{types.SecurityCheckVulnerability}
|
||||
|
||||
return Run(ctx.Context, opt, s.initializer, initCache)
|
||||
}
|
||||
@@ -50,8 +50,13 @@ func (c *RemoteOption) Init(logger *zap.SugaredLogger) {
|
||||
}
|
||||
|
||||
if c.RemoteAddr == "" {
|
||||
if len(c.customHeaders) > 0 || c.token != "" || c.tokenHeader != DefaultTokenHeader {
|
||||
logger.Warn(`'--token', '--token-header' and 'custom-header' can be used only with '--server'`)
|
||||
switch {
|
||||
case len(c.customHeaders) > 0:
|
||||
logger.Warn(`"--custom-header"" can be used only with "--server"`)
|
||||
case c.token != "":
|
||||
logger.Warn(`"--token" can be used only with "--server"`)
|
||||
case c.tokenHeader != "" && c.tokenHeader != DefaultTokenHeader:
|
||||
logger.Warn(`'--token-header' can be used only with "--server"`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
39
pkg/commands/option/sbom.go
Normal file
39
pkg/commands/option/sbom.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var supportedSbomFormats = []string{"cyclonedx"}
|
||||
|
||||
// SbomOption holds the options for SBOM generation
|
||||
type SbomOption struct {
|
||||
ArtifactType string
|
||||
SbomFormat string
|
||||
}
|
||||
|
||||
// NewSbomOption is the factory method to return SBOM options
|
||||
func NewSbomOption(c *cli.Context) SbomOption {
|
||||
return SbomOption{
|
||||
ArtifactType: c.String("artifact-type"),
|
||||
SbomFormat: c.String("sbom-format"),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initialize the CLI context for SBOM generation
|
||||
func (c *SbomOption) Init(ctx *cli.Context, logger *zap.SugaredLogger) error {
|
||||
if ctx.Command.Name != "sbom" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !slices.Contains(supportedSbomFormats, c.SbomFormat) {
|
||||
logger.Errorf(`"--format" must be %q`, supportedSbomFormats)
|
||||
return xerrors.Errorf(`"--format" must be %q`, supportedSbomFormats)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user