diff --git a/docs/advanced/sbom/cyclonedx.md b/docs/advanced/sbom/cyclonedx.md index 4fb27b8838..ddf575b8b0 100644 --- a/docs/advanced/sbom/cyclonedx.md +++ b/docs/advanced/sbom/cyclonedx.md @@ -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 . } ``` + [cyclonedx]: https://cyclonedx.org/ \ No newline at end of file diff --git a/docs/advanced/sbom/index.md b/docs/advanced/sbom/index.md new file mode 100644 index 0000000000..22a77b5d20 --- /dev/null +++ b/docs/advanced/sbom/index.md @@ -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 +``` + +
+Result + +``` +{ + "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" + ] + } + ] +} + +``` + +
+ +`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 \ No newline at end of file diff --git a/docs/getting-started/cli/sbom.md b/docs/getting-started/cli/sbom.md new file mode 100644 index 0000000000..b03f6da1c4 --- /dev/null +++ b/docs/getting-started/cli/sbom.md @@ -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) +``` diff --git a/mkdocs.yml b/mkdocs.yml index 0a527d69f3..6748b72f45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/pkg/commands/app.go b/pkg/commands/app.go index dead7a0ed4..e470f80ef1 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -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{ diff --git a/pkg/commands/artifact/option.go b/pkg/commands/artifact/option.go index 1671dac598..1967d306d1 100644 --- a/pkg/commands/artifact/option.go +++ b/pkg/commands/artifact/option.go @@ -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 } diff --git a/pkg/commands/artifact/option_test.go b/pkg/commands/artifact/option_test.go index d8fed26b39..fbb6ce4215 100644 --- a/pkg/commands/artifact/option_test.go +++ b/pkg/commands/artifact/option_test.go @@ -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{ diff --git a/pkg/commands/artifact/sbom.go b/pkg/commands/artifact/sbom.go new file mode 100644 index 0000000000..231555355e --- /dev/null +++ b/pkg/commands/artifact/sbom.go @@ -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) +} diff --git a/pkg/commands/option/remote.go b/pkg/commands/option/remote.go index 656cf449d9..134fb376b5 100644 --- a/pkg/commands/option/remote.go +++ b/pkg/commands/option/remote.go @@ -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 } diff --git a/pkg/commands/option/sbom.go b/pkg/commands/option/sbom.go new file mode 100644 index 0000000000..519505fe92 --- /dev/null +++ b/pkg/commands/option/sbom.go @@ -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 +}