From 32acd293fd9e39a36d1017393ded843d99b7f368 Mon Sep 17 00:00:00 2001 From: Jack Lin Date: Wed, 15 Feb 2023 16:51:15 +0800 Subject: [PATCH] feat(flag): add exit-on-eosl option (#3423) Co-authored-by: knqyf263 --- docs/docs/references/cli/client.md | 1 + docs/docs/references/cli/image.md | 1 + .../references/customization/config-file.md | 4 ++ docs/docs/vulnerability/examples/others.md | 39 +++++++++++++++++++ pkg/commands/app.go | 5 +++ pkg/commands/artifact/run.go | 7 ++++ pkg/flag/report_flags.go | 19 ++++++++- pkg/flag/report_flags_test.go | 17 ++++++++ 8 files changed, 92 insertions(+), 1 deletion(-) diff --git a/docs/docs/references/cli/client.md b/docs/docs/references/cli/client.md index 3eeb40042c1..222aeee26e0 100644 --- a/docs/docs/references/cli/client.md +++ b/docs/docs/references/cli/client.md @@ -16,6 +16,7 @@ Scan Flags Report Flags --dependency-tree show dependency origin tree (EXPERIMENTAL) --exit-code int specify exit code when any security issues are found + --exit-on-eosl exit with the specified code when the os of image ends of service/life -f, --format string format (table, json, sarif, template, cyclonedx, spdx, spdx-json, github, cosign-vuln) (default "table") --ignore-policy string specify the Rego file path to evaluate each vulnerability --ignorefile string specify .trivyignore file (default ".trivyignore") diff --git a/docs/docs/references/cli/image.md b/docs/docs/references/cli/image.md index a6aab885fb1..7d5824031bf 100644 --- a/docs/docs/references/cli/image.md +++ b/docs/docs/references/cli/image.md @@ -39,6 +39,7 @@ Scan Flags Report Flags --exit-code int specify exit code when any security issues are found + --exit-on-eosl exit with the specified code when the os of image ends of service/life -f, --format string format (table, json, sarif, template, cyclonedx, spdx, spdx-json, github, cosign-vuln) (default "table") --ignore-policy string specify the Rego file path to evaluate each vulnerability --ignorefile string specify .trivyignore file (default ".trivyignore") diff --git a/docs/docs/references/customization/config-file.md b/docs/docs/references/customization/config-file.md index 9d6f6b6ac5b..f76433f044a 100644 --- a/docs/docs/references/customization/config-file.md +++ b/docs/docs/references/customization/config-file.md @@ -63,6 +63,10 @@ ignore-policy: # Default is 0 exit-code: 0 +# Same as '--exit-on-eosl' +# Default is false +exit-on-eosl: false + # Same as '--output' # Default is empty (stdout) output: diff --git a/docs/docs/vulnerability/examples/others.md b/docs/docs/vulnerability/examples/others.md index d63bced8b0f..49f365817e0 100644 --- a/docs/docs/vulnerability/examples/others.md +++ b/docs/docs/vulnerability/examples/others.md @@ -68,6 +68,45 @@ $ trivy image --exit-code 0 --severity MEDIUM,HIGH ruby:2.4.0 $ trivy image --exit-code 1 --severity CRITICAL ruby:2.4.0 ``` +## Exit on EOSL +Sometimes you may surprisingly get 0 vulnerabilities in an old image: + +- Enabling `--ignore-unfixed` option while all packages have no fixed versions. +- Scanning a rather outdated OS (e.g. Ubuntu 10.04). + +An OS at the end of service/life (EOSL) usually gets into this situation, which is definitely full of vulnerabilities. +Use the `exit-on-eosl` option accompanied by `exit-code` option to exit with a non-zero code. +This flag is available with the following targets. + +- Container images (`trivy image`) +- Virtual machine images (`trivy vm`) +- SBOM (`trivy sbom`) +- Root filesystem (`trivy rootfs`) + +``` +$ trivy image --exit-code 1 --exit-on-eosl testbeds/ubuntu:10.04 +``` + +
+Result + +``` +2023-01-19T22:05:54.358+0800 WARN This OS version is no longer supported by the distribution: ubuntu 10.04 +2023-01-19T22:05:54.358+0800 WARN The vulnerability detection may be insufficient because security updates are not provided + +testbeds/ubuntu:10.04 (ubuntu 10.04) +==================================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) +``` + +
+ +This option is useful for CI/CD. The following example will fail when a critical vulnerability is found or the OS is EOSL: + +``` +$ ./trivy image --exit-code 1 --exit-on-eosl --severity CRITICAL alpine:3.16.3 +``` + ## Reset The `--reset` option removes all caches and database. After this, it takes a long time as the vulnerability database needs to be rebuilt locally. diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 02b23fe21d6..0b1b44032c8 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -293,6 +293,7 @@ func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { reportFlagGroup := flag.NewReportFlagGroup() reportFlagGroup.ReportFormat = nil // TODO: support --report summary reportFlagGroup.Compliance = nil // disable '--compliance' + reportFlagGroup.ExitOnEOSL = nil // disable '--exit-on-eosl' fsFlags := &flag.Flags{ CacheFlagGroup: flag.NewCacheFlagGroup(), @@ -402,6 +403,7 @@ func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { reportFlagGroup := flag.NewReportFlagGroup() reportFlagGroup.ReportFormat = nil // TODO: support --report summary reportFlagGroup.Compliance = nil // disable '--compliance' + reportFlagGroup.ExitOnEOSL = nil // disable '--exit-on-eosl' repoFlags := &flag.Flags{ CacheFlagGroup: flag.NewCacheFlagGroup(), @@ -549,6 +551,7 @@ func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { reportFlagGroup.ListAllPkgs = nil // disable '--list-all-pkgs' reportFlagGroup.ReportFormat = nil // TODO: support --report summary reportFlagGroup.Compliance = nil // disable '--compliance' + reportFlagGroup.ExitOnEOSL = nil // disable '--exit-on-eosl' scanFlags := &flag.ScanFlagGroup{ // Enable only '--skip-dirs' and '--skip-files' and disable other flags @@ -764,6 +767,7 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { compliance := flag.ComplianceFlag compliance.Usage += fmt.Sprintf(" (%s,%s)", types.ComplianceK8sNsa, types.ComplianceK8sCIS) reportFlagGroup.Compliance = &compliance // override usage as the accepted values differ for each subcommand. + reportFlagGroup.ExitOnEOSL = nil // disable '--exit-on-eosl' k8sFlags := &flag.Flags{ CacheFlagGroup: flag.NewCacheFlagGroup(), @@ -826,6 +830,7 @@ func NewAWSCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { compliance := flag.ComplianceFlag compliance.Usage += fmt.Sprintf(" (%s, %s)", types.ComplianceAWSCIS12, types.ComplianceAWSCIS14) reportFlagGroup.Compliance = &compliance // override usage as the accepted values differ for each subcommand. + reportFlagGroup.ExitOnEOSL = nil // disable '--exit-on-eosl' awsFlags := &flag.Flags{ AWSFlagGroup: flag.NewAWSFlagGroup(), diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 2ec34a90d4e..355f7b9310e 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -454,6 +454,7 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err return xerrors.Errorf("report error: %w", err) } + exitOnEosl(opts, report.Metadata) Exit(opts, report.Results.Failed()) return nil @@ -663,6 +664,12 @@ func Exit(opts flag.Options, failedResults bool) { } } +func exitOnEosl(opts flag.Options, m types.Metadata) { + if opts.ReportOptions.ExitOnEOSL && m.OS != nil && m.OS.Eosl { + Exit(opts, true) + } +} + func canonicalVersion(ver string) string { if ver == devVersion { return ver diff --git a/pkg/flag/report_flags.go b/pkg/flag/report_flags.go index 3e71c5551e9..e083e204605 100644 --- a/pkg/flag/report_flags.go +++ b/pkg/flag/report_flags.go @@ -72,6 +72,12 @@ var ( Value: 0, Usage: "specify exit code when any security issues are found", } + ExitOnEOSLFlag = Flag{ + Name: "exit-on-eosl", + ConfigName: "exit-on-eosl", + Value: false, + Usage: "exit with the specified code when the os of image ends of service/life", + } OutputFlag = Flag{ Name: "output", ConfigName: "output", @@ -105,6 +111,7 @@ type ReportFlagGroup struct { IgnoreFile *Flag IgnorePolicy *Flag ExitCode *Flag + ExitOnEOSL *Flag Output *Flag Severity *Flag Compliance *Flag @@ -118,6 +125,7 @@ type ReportOptions struct { ListAllPkgs bool IgnoreFile string ExitCode int + ExitOnEOSL bool IgnorePolicy string Output io.Writer Severities []dbTypes.Severity @@ -134,6 +142,7 @@ func NewReportFlagGroup() *ReportFlagGroup { IgnoreFile: &IgnoreFileFlag, IgnorePolicy: &IgnorePolicyFlag, ExitCode: &ExitCodeFlag, + ExitOnEOSL: &ExitOnEOSLFlag, Output: &OutputFlag, Severity: &SeverityFlag, Compliance: &ComplianceFlag, @@ -154,6 +163,7 @@ func (f *ReportFlagGroup) Flags() []*Flag { f.IgnoreFile, f.IgnorePolicy, f.ExitCode, + f.ExitOnEOSL, f.Output, f.Severity, f.Compliance, @@ -161,12 +171,18 @@ func (f *ReportFlagGroup) Flags() []*Flag { } func (f *ReportFlagGroup) ToOptions(out io.Writer) (ReportOptions, error) { + exitCode := getInt(f.ExitCode) + exitOnEOSL := getBool(f.ExitOnEOSL) format := getString(f.Format) template := getString(f.Template) dependencyTree := getBool(f.DependencyTree) listAllPkgs := getBool(f.ListAllPkgs) output := getString(f.Output) + if exitOnEOSL && exitCode == 0 { + log.Logger.Warn("'--exit-on-eosl' is ignored because '--exit-code' is 0 or not specified. Use '--exit-on-eosl' option with non-zero '--exit-code' option.") + } + if format != "" && !slices.Contains(report.SupportedFormats, format) { return ReportOptions{}, xerrors.Errorf("unknown format: %v", format) } @@ -223,7 +239,8 @@ func (f *ReportFlagGroup) ToOptions(out io.Writer) (ReportOptions, error) { DependencyTree: dependencyTree, ListAllPkgs: listAllPkgs, IgnoreFile: getString(f.IgnoreFile), - ExitCode: getInt(f.ExitCode), + ExitCode: exitCode, + ExitOnEOSL: exitOnEOSL, IgnorePolicy: getString(f.IgnorePolicy), Output: out, Severities: splitSeverity(getStringSlice(f.Severity)), diff --git a/pkg/flag/report_flags_test.go b/pkg/flag/report_flags_test.go index 7d888053fdf..a8257015b9b 100644 --- a/pkg/flag/report_flags_test.go +++ b/pkg/flag/report_flags_test.go @@ -25,6 +25,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) { ignoreUnfixed bool ignoreFile string exitCode int + exitOnEOSL bool ignorePolicy string output string severities string @@ -202,6 +203,20 @@ func TestReportFlagGroup_ToOptions(t *testing.T) { Severities: []dbTypes.Severity{dbTypes.SeverityLow}, }, }, + { + name: "invalid option combination: --exit-code 0 with --exit-on-eosl", + fields: fields{ + exitCode: 0, + exitOnEOSL: true, + }, + wantLogs: []string{ + "'--exit-on-eosl' is ignored because '--exit-code' is 0 or not specified. Use '--exit-on-eosl' option with non-zero '--exit-code' option.", + }, + want: flag.ReportOptions{ + Output: os.Stdout, + ExitOnEOSL: true, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -220,6 +235,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) { viper.Set(flag.IgnoreUnfixedFlag.ConfigName, tt.fields.ignoreUnfixed) viper.Set(flag.IgnorePolicyFlag.ConfigName, tt.fields.ignorePolicy) viper.Set(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode) + viper.Set(flag.ExitOnEOSLFlag.ConfigName, tt.fields.exitOnEOSL) viper.Set(flag.OutputFlag.ConfigName, tt.fields.output) viper.Set(flag.SeverityFlag.ConfigName, tt.fields.severities) viper.Set(flag.ComplianceFlag.ConfigName, tt.fields.compliane) @@ -233,6 +249,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) { IgnoreFile: &flag.IgnoreFileFlag, IgnorePolicy: &flag.IgnorePolicyFlag, ExitCode: &flag.ExitCodeFlag, + ExitOnEOSL: &flag.ExitOnEOSLFlag, Output: &flag.OutputFlag, Severity: &flag.SeverityFlag, Compliance: &flag.ComplianceFlag,