feat(sbom): add a dedicated sbom command (#1799)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Tamir Kiviti
2022-03-23 23:42:22 +02:00
committed by GitHub
parent 7a148089ec
commit f36d9b6f90
10 changed files with 381 additions and 5 deletions

View File

@@ -1,9 +1,9 @@
# CycloneDX # CycloneDX
Trivy generates JSON reports in the [CycloneDX][cyclonedx] format. Trivy generates JSON reports in the [CycloneDX][cyclonedx] format.
Note that XML format is not supported at the moment. Note that XML format is not supported at the moment.
You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `cyclonedx` with the `--format` option.
You can specify `cyclonedx` with the `--format` option.
``` ```
$ trivy image --format cyclonedx --output result.json alpine:3.15 $ trivy image --format cyclonedx --output result.json alpine:3.15
@@ -227,6 +227,7 @@ $ cat result.json | jq .
} }
``` ```
</details> </details>
[cyclonedx]: https://cyclonedx.org/ [cyclonedx]: https://cyclonedx.org/

191
docs/advanced/sbom/index.md Normal file
View 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

View 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)
```

View File

@@ -25,6 +25,7 @@ nav:
- Repository: getting-started/cli/repo.md - Repository: getting-started/cli/repo.md
- Client: getting-started/cli/client.md - Client: getting-started/cli/client.md
- Server: getting-started/cli/server.md - Server: getting-started/cli/server.md
- SBOM: getting-started/cli/sbom.md
- Vulnerability: - Vulnerability:
- Scanning: - Scanning:
- Overview: vulnerability/scanning/index.md - Overview: vulnerability/scanning/index.md
@@ -72,6 +73,7 @@ nav:
- Plugins: advanced/plugins.md - Plugins: advanced/plugins.md
- Air-Gapped Environment: advanced/air-gap.md - Air-Gapped Environment: advanced/air-gap.md
- SBOM: - SBOM:
- Overview: advanced/sbom/index.md
- CycloneDX: advanced/sbom/cyclonedx.md - CycloneDX: advanced/sbom/cyclonedx.md
- Integrations: - Integrations:
- Overview: advanced/integrations/index.md - Overview: advanced/integrations/index.md

View File

@@ -365,6 +365,7 @@ func NewApp(version string) *cli.App {
NewImageCommand(), NewImageCommand(),
NewFilesystemCommand(), NewFilesystemCommand(),
NewRootfsCommand(), NewRootfsCommand(),
NewSbomCommand(),
NewRepositoryCommand(), NewRepositoryCommand(),
NewClientCommand(), NewClientCommand(),
NewServerCommand(), 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 // NewVersionCommand adds version command
func NewVersionCommand() *cli.Command { func NewVersionCommand() *cli.Command {
return &cli.Command{ return &cli.Command{

View File

@@ -18,6 +18,7 @@ type Option struct {
option.CacheOption option.CacheOption
option.ConfigOption option.ConfigOption
option.RemoteOption option.RemoteOption
option.SbomOption
// We don't want to allow disabled analyzers to be passed by users, // We don't want to allow disabled analyzers to be passed by users,
// but it differs depending on scanning modes. // but it differs depending on scanning modes.
@@ -40,6 +41,7 @@ func NewOption(c *cli.Context) (Option, error) {
CacheOption: option.NewCacheOption(c), CacheOption: option.NewCacheOption(c),
ConfigOption: option.NewConfigOption(c), ConfigOption: option.NewConfigOption(c),
RemoteOption: option.NewRemoteOption(c), RemoteOption: option.NewRemoteOption(c),
SbomOption: option.NewSbomOption(c),
}, nil }, nil
} }
@@ -70,6 +72,9 @@ func (c *Option) initPreScanOptions() error {
if err := c.CacheOption.Init(); err != nil { if err := c.CacheOption.Init(); err != nil {
return err return err
} }
if err := c.SbomOption.Init(c.Context, c.Logger); err != nil {
return err
}
c.RemoteOption.Init(c.Logger) c.RemoteOption.Init(c.Logger)
return nil return nil
} }

View File

@@ -86,7 +86,7 @@ func TestOption_Init(t *testing.T) {
name: "invalid option combination: token and token header without server", name: "invalid option combination: token and token header without server",
args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"}, args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"},
logs: []string{ logs: []string{
"'--token', '--token-header' and 'custom-header' can be used only with '--server'", `"--token" can be used only with "--server"`,
}, },
want: Option{ want: Option{
ReportOption: option.ReportOption{ ReportOption: option.ReportOption{

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

View File

@@ -50,8 +50,13 @@ func (c *RemoteOption) Init(logger *zap.SugaredLogger) {
} }
if c.RemoteAddr == "" { if c.RemoteAddr == "" {
if len(c.customHeaders) > 0 || c.token != "" || c.tokenHeader != DefaultTokenHeader { switch {
logger.Warn(`'--token', '--token-header' and 'custom-header' can be used only with '--server'`) 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 return
} }

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