mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat(report): output plugin (#4863)
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
@@ -25,7 +25,7 @@ func run() error {
|
||||
if !plugin.IsPredefined(runAsPlugin) {
|
||||
return xerrors.Errorf("unknown plugin: %s", runAsPlugin)
|
||||
}
|
||||
if err := plugin.RunWithArgs(context.Background(), runAsPlugin, os.Args[1:]); err != nil {
|
||||
if err := plugin.RunWithURL(context.Background(), runAsPlugin, plugin.RunOptions{Args: os.Args[1:]}); err != nil {
|
||||
return xerrors.Errorf("plugin error: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -182,8 +182,51 @@ $ trivy myplugin
|
||||
Hello from Trivy demo plugin!
|
||||
```
|
||||
|
||||
## Plugin Types
|
||||
Plugins are typically intended to be used as subcommands of Trivy,
|
||||
but some plugins can be invoked as part of Trivy's built-in commands.
|
||||
Currently, the following type of plugin is experimentally supported:
|
||||
|
||||
- Output plugins
|
||||
|
||||
### Output Plugins
|
||||
|
||||
!!! warning "EXPERIMENTAL"
|
||||
This feature might change without preserving backwards compatibility.
|
||||
|
||||
Trivy supports "output plugins" which process Trivy's output,
|
||||
such as by transforming the output format or sending it elsewhere.
|
||||
For instance, in the case of image scanning, the output plugin can be called as follows:
|
||||
|
||||
```shell
|
||||
$ trivy image --format json --output plugin=<plugin_name> [--output-plugin-arg <plugin_flags>] <image_name>
|
||||
```
|
||||
|
||||
Since scan results are passed to the plugin via standard input, plugins must be capable of handling standard input.
|
||||
|
||||
!!! warning
|
||||
To avoid Trivy hanging, you need to read all data from `Stdin` before the plugin exits successfully or stops with an error.
|
||||
|
||||
While the example passes JSON to the plugin, other formats like SBOM can also be passed (e.g., `--format cyclonedx`).
|
||||
|
||||
If a plugin requires flags or other arguments, they can be passed using `--output-plugin-arg`.
|
||||
This is directly forwarded as arguments to the plugin.
|
||||
For example, `--output plugin=myplugin --output-plugin-arg "--foo --bar=baz"` translates to `myplugin --foo --bar=baz` in execution.
|
||||
|
||||
An example of the output plugin is available [here](https://github.com/aquasecurity/trivy-output-plugin-count).
|
||||
It can be used as below:
|
||||
|
||||
```shell
|
||||
# Install the plugin first
|
||||
$ trivy plugin install github.com/aquasecurity/trivy-output-plugin-count
|
||||
|
||||
# Call the output plugin in image scanning
|
||||
$ trivy image --format json --output plugin=count --output-plugin-arg "--published-after 2023-10-01" debian:12
|
||||
```
|
||||
|
||||
## Example
|
||||
https://github.com/aquasecurity/trivy-plugin-kubectl
|
||||
- https://github.com/aquasecurity/trivy-plugin-kubectl
|
||||
- https://github.com/aquasecurity/trivy-output-plugin-count
|
||||
|
||||
[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/
|
||||
[helm]: https://helm.sh/docs/topics/plugins/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Reporting
|
||||
|
||||
## Supported Formats
|
||||
## Format
|
||||
Trivy supports the following formats:
|
||||
|
||||
- Table
|
||||
@@ -373,6 +373,33 @@ $ trivy image --format template --template "@/usr/local/share/trivy/templates/ht
|
||||
### SBOM
|
||||
See [here](../supply-chain/sbom.md) for details.
|
||||
|
||||
## Output
|
||||
Trivy supports the following output destinations:
|
||||
|
||||
- File
|
||||
- Plugin
|
||||
|
||||
### File
|
||||
By specifying `--output <file_path>`, you can output the results to a file.
|
||||
Here is an example:
|
||||
|
||||
```
|
||||
$ trivy image --format json --output result.json debian:12
|
||||
```
|
||||
|
||||
### Plugin
|
||||
!!! warning "EXPERIMENTAL"
|
||||
This feature might change without preserving backwards compatibility.
|
||||
|
||||
Plugins capable of receiving Trivy's results via standard input, called "output plugin", can be seamlessly invoked using the `--output` flag.
|
||||
|
||||
```
|
||||
$ trivy <target> [--format <format>] --output plugin=<plugin_name> [--output-plugin-arg <plugin_flags>] <target_name>
|
||||
```
|
||||
|
||||
This is useful for cases where you want to convert the output into a custom format, or when you want to send the output somewhere.
|
||||
For more details, please check [here](../advanced/plugins.md#output-plugins).
|
||||
|
||||
## Converting
|
||||
To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand.
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ trivy aws [flags]
|
||||
--max-cache-age duration The maximum age of the cloud cache. Cached data will be requeried from the cloud provider if it is older than this. (default 24h0m0s)
|
||||
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan])
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
--policy-namespaces strings Rego namespaces
|
||||
--region string AWS Region to scan
|
||||
|
||||
@@ -32,6 +32,7 @@ trivy config [flags] DIR
|
||||
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan])
|
||||
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
--policy-namespaces strings Rego namespaces
|
||||
|
||||
@@ -18,19 +18,20 @@ trivy convert [flags] RESULT_JSON
|
||||
### Options
|
||||
|
||||
```
|
||||
--compliance string compliance report to generate
|
||||
--dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages
|
||||
--exit-code int specify exit code when any security issues are found
|
||||
--exit-on-eol int exit with the specified code when the OS reaches end of service/life
|
||||
-f, --format string format (table,json,template,sarif,cyclonedx,spdx,spdx-json,github,cosign-vuln) (default "table")
|
||||
-h, --help help for convert
|
||||
--ignore-policy string specify the Rego file path to evaluate each vulnerability
|
||||
--ignorefile string specify .trivyignore file (default ".trivyignore")
|
||||
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
|
||||
-o, --output string output file name
|
||||
--report string specify a report format for the output (all,summary) (default "all")
|
||||
-s, --severity strings severities of security issues to be displayed (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL])
|
||||
-t, --template string output template
|
||||
--compliance string compliance report to generate
|
||||
--dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages
|
||||
--exit-code int specify exit code when any security issues are found
|
||||
--exit-on-eol int exit with the specified code when the OS reaches end of service/life
|
||||
-f, --format string format (table,json,template,sarif,cyclonedx,spdx,spdx-json,github,cosign-vuln) (default "table")
|
||||
-h, --help help for convert
|
||||
--ignore-policy string specify the Rego file path to evaluate each vulnerability
|
||||
--ignorefile string specify .trivyignore file (default ".trivyignore")
|
||||
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--report string specify a report format for the output (all,summary) (default "all")
|
||||
-s, --severity strings severities of security issues to be displayed (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL])
|
||||
-t, --template string output template
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
@@ -56,6 +56,7 @@ trivy filesystem [flags] PATH
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
|
||||
@@ -74,6 +74,7 @@ trivy image [flags] IMAGE_NAME
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--platform string set platform in the form os/arch if image is multi-platform capable
|
||||
|
||||
@@ -67,6 +67,7 @@ trivy kubernetes [flags] { cluster | all | specific resources like kubectl. eg:
|
||||
--node-collector-namespace string specify the namespace in which the node-collector job should be deployed (default "trivy-temp")
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
|
||||
@@ -56,6 +56,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
|
||||
@@ -58,6 +58,7 @@ trivy rootfs [flags] ROOTDIR
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
|
||||
@@ -42,6 +42,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--redis-ca string redis ca file location, if using redis as cache backend
|
||||
--redis-cert string redis certificate file location, if using redis as cache backend
|
||||
--redis-key string redis key file location, if using redis as cache backend
|
||||
|
||||
@@ -52,6 +52,7 @@ trivy vm [flags] VM_IMAGE
|
||||
--no-progress suppress progress bar
|
||||
--offline-scan do not issue API requests to identify dependencies
|
||||
-o, --output string output file name
|
||||
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
|
||||
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
|
||||
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||
--redis-ca string redis ca file location, if using redis as cache backend
|
||||
|
||||
1
go.mod
1
go.mod
@@ -72,6 +72,7 @@ require (
|
||||
github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08
|
||||
github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd
|
||||
github.com/masahiro331/go-xfs-filesystem v0.0.0-20230608043311-a335f4599b70
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/buildkit v0.11.6
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1326,6 +1326,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
|
||||
@@ -129,7 +129,6 @@ func filterServices(opt *flag.Options) error {
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, opt flag.Options) error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, opt.GlobalOptions.Timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -168,7 +167,7 @@ func Run(ctx context.Context, opt flag.Options) error {
|
||||
}
|
||||
|
||||
r := report.New(cloud.ProviderAWS, opt.Account, opt.Region, res, opt.Services)
|
||||
if err := report.Write(r, opt, cached); err != nil {
|
||||
if err := report.Write(ctx, r, opt, cached); err != nil {
|
||||
return xerrors.Errorf("unable to write results: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ func (r *Report) Failed() bool {
|
||||
}
|
||||
|
||||
// Write writes the results in the give format
|
||||
func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
||||
output, cleanup, err := opt.OutputWriter()
|
||||
func Write(ctx context.Context, rep *Report, opt flag.Options, fromCache bool) error {
|
||||
output, cleanup, err := opt.OutputWriter(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
@@ -72,8 +72,6 @@ func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
||||
|
||||
var filtered []types.Result
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// filter results
|
||||
for _, resultsAtTime := range rep.Results {
|
||||
for _, res := range resultsAtTime.Results {
|
||||
@@ -137,7 +135,7 @@ func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
||||
|
||||
return nil
|
||||
default:
|
||||
return pkgReport.Write(base, opt)
|
||||
return pkgReport.Write(ctx, base, opt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -111,7 +112,7 @@ No problems detected.
|
||||
|
||||
output := bytes.NewBuffer(nil)
|
||||
tt.options.SetOutputWriter(output)
|
||||
require.NoError(t, Write(report, tt.options, tt.fromCache))
|
||||
require.NoError(t, Write(context.Background(), report, tt.options, tt.fromCache))
|
||||
|
||||
assert.Equal(t, "AWS", report.Provider)
|
||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||
|
||||
@@ -2,6 +2,7 @@ package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -70,7 +71,7 @@ See https://avd.aquasec.com/misconfig/avd-aws-9999
|
||||
|
||||
output := bytes.NewBuffer(nil)
|
||||
tt.options.SetOutputWriter(output)
|
||||
require.NoError(t, Write(report, tt.options, tt.fromCache))
|
||||
require.NoError(t, Write(context.Background(), report, tt.options, tt.fromCache))
|
||||
|
||||
assert.Equal(t, "AWS", report.Provider)
|
||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||
|
||||
@@ -2,6 +2,7 @@ package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/aquasecurity/trivy/pkg/clock"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -322,7 +323,7 @@ Scan Overview for AWS Account
|
||||
|
||||
output := bytes.NewBuffer(nil)
|
||||
tt.options.SetOutputWriter(output)
|
||||
require.NoError(t, Write(report, tt.options, tt.fromCache))
|
||||
require.NoError(t, Write(context.Background(), report, tt.options, tt.fromCache))
|
||||
|
||||
assert.Equal(t, "AWS", report.Provider)
|
||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||
|
||||
@@ -124,7 +124,7 @@ func loadPluginCommands() []*cobra.Command {
|
||||
Short: p.Usage,
|
||||
GroupID: groupPlugin,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err = p.Run(cmd.Context(), args); err != nil {
|
||||
if err = p.Run(cmd.Context(), plugin.RunOptions{Args: args}); err != nil {
|
||||
return xerrors.Errorf("plugin error: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -773,7 +773,7 @@ func NewPluginCommand() *cobra.Command {
|
||||
Short: "Run a plugin on the fly",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return plugin.RunWithArgs(cmd.Context(), args[0], args[1:])
|
||||
return plugin.RunWithURL(cmd.Context(), args[0], plugin.RunOptions{Args: args[1:]})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
|
||||
@@ -91,7 +91,7 @@ type Runner interface {
|
||||
// Filter filter a report
|
||||
Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error)
|
||||
// Report a writes a report
|
||||
Report(opts flag.Options, report types.Report) error
|
||||
Report(ctx context.Context, opts flag.Options, report types.Report) error
|
||||
// Close closes runner
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
@@ -280,8 +280,8 @@ func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Rep
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (r *runner) Report(opts flag.Options, report types.Report) error {
|
||||
if err := pkgReport.Write(report, opts); err != nil {
|
||||
func (r *runner) Report(ctx context.Context, opts flag.Options, report types.Report) error {
|
||||
if err := pkgReport.Write(ctx, report, opts); err != nil {
|
||||
return xerrors.Errorf("unable to write results: %w", err)
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
|
||||
return xerrors.Errorf("filter error: %w", err)
|
||||
}
|
||||
|
||||
if err = r.Report(opts, report); err != nil {
|
||||
if err = r.Report(ctx, opts, report); err != nil {
|
||||
return xerrors.Errorf("report error: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ import (
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, opts flag.Options) (err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
|
||||
defer cancel()
|
||||
|
||||
f, err := os.Open(opts.Target)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("file open error: %w", err)
|
||||
@@ -37,7 +40,7 @@ func Run(ctx context.Context, opts flag.Options) (err error) {
|
||||
}
|
||||
|
||||
log.Logger.Debug("Writing report to output...")
|
||||
if err = report.Write(r, opts); err != nil {
|
||||
if err = report.Write(ctx, r, opts); err != nil {
|
||||
return xerrors.Errorf("unable to write results: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/plugin"
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/version"
|
||||
@@ -173,19 +175,46 @@ func (o *Options) SetOutputWriter(w io.Writer) {
|
||||
|
||||
// OutputWriter returns an output writer.
|
||||
// If the output file is not specified, it returns os.Stdout.
|
||||
func (o *Options) OutputWriter() (io.Writer, func(), error) {
|
||||
if o.outputWriter != nil {
|
||||
return o.outputWriter, func() {}, nil
|
||||
func (o *Options) OutputWriter(ctx context.Context) (io.Writer, func() error, error) {
|
||||
cleanup := func() error { return nil }
|
||||
switch {
|
||||
case o.outputWriter != nil:
|
||||
return o.outputWriter, cleanup, nil
|
||||
case o.Output == "":
|
||||
return os.Stdout, cleanup, nil
|
||||
case strings.HasPrefix(o.Output, "plugin="):
|
||||
return o.outputPluginWriter(ctx)
|
||||
}
|
||||
|
||||
if o.Output != "" {
|
||||
f, err := os.Create(o.Output)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
return f, func() { _ = f.Close() }, nil
|
||||
f, err := os.Create(o.Output)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
return os.Stdout, func() {}, nil
|
||||
return f, f.Close, nil
|
||||
}
|
||||
|
||||
func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() error, error) {
|
||||
pluginName := strings.TrimPrefix(o.Output, "plugin=")
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
wait, err := plugin.Start(ctx, pluginName, plugin.RunOptions{
|
||||
Args: o.OutputPluginArgs,
|
||||
Stdin: pr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("plugin start: %w", err)
|
||||
}
|
||||
|
||||
cleanup := func() error {
|
||||
if err = pw.Close(); err != nil {
|
||||
return xerrors.Errorf("failed to close pipe: %w", err)
|
||||
}
|
||||
if err = wait(); err != nil {
|
||||
return xerrors.Errorf("plugin error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return pw, cleanup, nil
|
||||
}
|
||||
|
||||
func addFlag(cmd *cobra.Command, flag *Flag) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package flag
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -86,6 +87,12 @@ var (
|
||||
Default: "",
|
||||
Usage: "output file name",
|
||||
}
|
||||
OutputPluginArgFlag = Flag{
|
||||
Name: "output-plugin-arg",
|
||||
ConfigName: "output-plugin-arg",
|
||||
Default: "",
|
||||
Usage: "[EXPERIMENTAL] output plugin arguments",
|
||||
}
|
||||
SeverityFlag = Flag{
|
||||
Name: "severity",
|
||||
ConfigName: "severity",
|
||||
@@ -105,49 +112,52 @@ var (
|
||||
// ReportFlagGroup composes common printer flag structs
|
||||
// used for commands requiring reporting logic.
|
||||
type ReportFlagGroup struct {
|
||||
Format *Flag
|
||||
ReportFormat *Flag
|
||||
Template *Flag
|
||||
DependencyTree *Flag
|
||||
ListAllPkgs *Flag
|
||||
IgnoreFile *Flag
|
||||
IgnorePolicy *Flag
|
||||
ExitCode *Flag
|
||||
ExitOnEOL *Flag
|
||||
Output *Flag
|
||||
Severity *Flag
|
||||
Compliance *Flag
|
||||
Format *Flag
|
||||
ReportFormat *Flag
|
||||
Template *Flag
|
||||
DependencyTree *Flag
|
||||
ListAllPkgs *Flag
|
||||
IgnoreFile *Flag
|
||||
IgnorePolicy *Flag
|
||||
ExitCode *Flag
|
||||
ExitOnEOL *Flag
|
||||
Output *Flag
|
||||
OutputPluginArg *Flag
|
||||
Severity *Flag
|
||||
Compliance *Flag
|
||||
}
|
||||
|
||||
type ReportOptions struct {
|
||||
Format types.Format
|
||||
ReportFormat string
|
||||
Template string
|
||||
DependencyTree bool
|
||||
ListAllPkgs bool
|
||||
IgnoreFile string
|
||||
ExitCode int
|
||||
ExitOnEOL int
|
||||
IgnorePolicy string
|
||||
Output string
|
||||
Severities []dbTypes.Severity
|
||||
Compliance spec.ComplianceSpec
|
||||
Format types.Format
|
||||
ReportFormat string
|
||||
Template string
|
||||
DependencyTree bool
|
||||
ListAllPkgs bool
|
||||
IgnoreFile string
|
||||
ExitCode int
|
||||
ExitOnEOL int
|
||||
IgnorePolicy string
|
||||
Output string
|
||||
OutputPluginArgs []string
|
||||
Severities []dbTypes.Severity
|
||||
Compliance spec.ComplianceSpec
|
||||
}
|
||||
|
||||
func NewReportFlagGroup() *ReportFlagGroup {
|
||||
return &ReportFlagGroup{
|
||||
Format: &FormatFlag,
|
||||
ReportFormat: &ReportFormatFlag,
|
||||
Template: &TemplateFlag,
|
||||
DependencyTree: &DependencyTreeFlag,
|
||||
ListAllPkgs: &ListAllPkgsFlag,
|
||||
IgnoreFile: &IgnoreFileFlag,
|
||||
IgnorePolicy: &IgnorePolicyFlag,
|
||||
ExitCode: &ExitCodeFlag,
|
||||
ExitOnEOL: &ExitOnEOLFlag,
|
||||
Output: &OutputFlag,
|
||||
Severity: &SeverityFlag,
|
||||
Compliance: &ComplianceFlag,
|
||||
Format: &FormatFlag,
|
||||
ReportFormat: &ReportFormatFlag,
|
||||
Template: &TemplateFlag,
|
||||
DependencyTree: &DependencyTreeFlag,
|
||||
ListAllPkgs: &ListAllPkgsFlag,
|
||||
IgnoreFile: &IgnoreFileFlag,
|
||||
IgnorePolicy: &IgnorePolicyFlag,
|
||||
ExitCode: &ExitCodeFlag,
|
||||
ExitOnEOL: &ExitOnEOLFlag,
|
||||
Output: &OutputFlag,
|
||||
OutputPluginArg: &OutputPluginArgFlag,
|
||||
Severity: &SeverityFlag,
|
||||
Compliance: &ComplianceFlag,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +177,7 @@ func (f *ReportFlagGroup) Flags() []*Flag {
|
||||
f.ExitCode,
|
||||
f.ExitOnEOL,
|
||||
f.Output,
|
||||
f.OutputPluginArg,
|
||||
f.Severity,
|
||||
f.Compliance,
|
||||
}
|
||||
@@ -216,19 +227,28 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
|
||||
return ReportOptions{}, xerrors.Errorf("unable to load compliance spec: %w", err)
|
||||
}
|
||||
|
||||
var outputPluginArgs []string
|
||||
if arg := getString(f.OutputPluginArg); arg != "" {
|
||||
outputPluginArgs, err = shellwords.Parse(arg)
|
||||
if err != nil {
|
||||
return ReportOptions{}, xerrors.Errorf("unable to parse output plugin argument: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ReportOptions{
|
||||
Format: format,
|
||||
ReportFormat: getString(f.ReportFormat),
|
||||
Template: template,
|
||||
DependencyTree: dependencyTree,
|
||||
ListAllPkgs: listAllPkgs,
|
||||
IgnoreFile: getString(f.IgnoreFile),
|
||||
ExitCode: getInt(f.ExitCode),
|
||||
ExitOnEOL: getInt(f.ExitOnEOL),
|
||||
IgnorePolicy: getString(f.IgnorePolicy),
|
||||
Output: getString(f.Output),
|
||||
Severities: toSeverity(getStringSlice(f.Severity)),
|
||||
Compliance: cs,
|
||||
Format: format,
|
||||
ReportFormat: getString(f.ReportFormat),
|
||||
Template: template,
|
||||
DependencyTree: dependencyTree,
|
||||
ListAllPkgs: listAllPkgs,
|
||||
IgnoreFile: getString(f.IgnoreFile),
|
||||
ExitCode: getInt(f.ExitCode),
|
||||
ExitOnEOL: getInt(f.ExitOnEOL),
|
||||
IgnorePolicy: getString(f.IgnorePolicy),
|
||||
Output: getString(f.Output),
|
||||
OutputPluginArgs: outputPluginArgs,
|
||||
Severities: toSeverity(getStringSlice(f.Severity)),
|
||||
Compliance: cs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,20 +18,20 @@ import (
|
||||
|
||||
func TestReportFlagGroup_ToOptions(t *testing.T) {
|
||||
type fields struct {
|
||||
format types.Format
|
||||
template string
|
||||
dependencyTree bool
|
||||
listAllPkgs bool
|
||||
ignoreUnfixed bool
|
||||
ignoreFile string
|
||||
exitCode int
|
||||
exitOnEOSL bool
|
||||
ignorePolicy string
|
||||
output string
|
||||
severities string
|
||||
compliane string
|
||||
|
||||
debug bool
|
||||
format types.Format
|
||||
template string
|
||||
dependencyTree bool
|
||||
listAllPkgs bool
|
||||
ignoreUnfixed bool
|
||||
ignoreFile string
|
||||
exitCode int
|
||||
exitOnEOSL bool
|
||||
ignorePolicy string
|
||||
output string
|
||||
outputPluginArgs string
|
||||
severities string
|
||||
compliance string
|
||||
debug bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -63,8 +63,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
||||
severities: "CRITICAL",
|
||||
format: "cyclonedx",
|
||||
listAllPkgs: false,
|
||||
|
||||
debug: true,
|
||||
debug: true,
|
||||
},
|
||||
wantLogs: []string{
|
||||
`["cyclonedx" "spdx" "spdx-json" "github"] automatically enables '--list-all-pkgs'.`,
|
||||
@@ -138,10 +137,26 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
||||
ListAllPkgs: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with output plugin args",
|
||||
fields: fields{
|
||||
output: "plugin=count",
|
||||
outputPluginArgs: "--publish-after 2023-10-01 --publish-before 2023-10-02",
|
||||
},
|
||||
want: flag.ReportOptions{
|
||||
Output: "plugin=count",
|
||||
OutputPluginArgs: []string{
|
||||
"--publish-after",
|
||||
"2023-10-01",
|
||||
"--publish-before",
|
||||
"2023-10-02",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with compliance",
|
||||
fields: fields{
|
||||
compliane: "@testdata/example-spec.yaml",
|
||||
compliance: "@testdata/example-spec.yaml",
|
||||
severities: dbTypes.SeverityLow.String(),
|
||||
},
|
||||
want: flag.ReportOptions{
|
||||
@@ -187,22 +202,24 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
||||
viper.Set(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode)
|
||||
viper.Set(flag.ExitOnEOLFlag.ConfigName, tt.fields.exitOnEOSL)
|
||||
viper.Set(flag.OutputFlag.ConfigName, tt.fields.output)
|
||||
viper.Set(flag.OutputPluginArgFlag.ConfigName, tt.fields.outputPluginArgs)
|
||||
viper.Set(flag.SeverityFlag.ConfigName, tt.fields.severities)
|
||||
viper.Set(flag.ComplianceFlag.ConfigName, tt.fields.compliane)
|
||||
viper.Set(flag.ComplianceFlag.ConfigName, tt.fields.compliance)
|
||||
|
||||
// Assert options
|
||||
f := &flag.ReportFlagGroup{
|
||||
Format: &flag.FormatFlag,
|
||||
Template: &flag.TemplateFlag,
|
||||
DependencyTree: &flag.DependencyTreeFlag,
|
||||
ListAllPkgs: &flag.ListAllPkgsFlag,
|
||||
IgnoreFile: &flag.IgnoreFileFlag,
|
||||
IgnorePolicy: &flag.IgnorePolicyFlag,
|
||||
ExitCode: &flag.ExitCodeFlag,
|
||||
ExitOnEOL: &flag.ExitOnEOLFlag,
|
||||
Output: &flag.OutputFlag,
|
||||
Severity: &flag.SeverityFlag,
|
||||
Compliance: &flag.ComplianceFlag,
|
||||
Format: &flag.FormatFlag,
|
||||
Template: &flag.TemplateFlag,
|
||||
DependencyTree: &flag.DependencyTreeFlag,
|
||||
ListAllPkgs: &flag.ListAllPkgsFlag,
|
||||
IgnoreFile: &flag.IgnoreFileFlag,
|
||||
IgnorePolicy: &flag.IgnorePolicyFlag,
|
||||
ExitCode: &flag.ExitCodeFlag,
|
||||
ExitOnEOL: &flag.ExitOnEOLFlag,
|
||||
Output: &flag.OutputFlag,
|
||||
OutputPluginArg: &flag.OutputPluginArgFlag,
|
||||
Severity: &flag.SeverityFlag,
|
||||
Compliance: &flag.ComplianceFlag,
|
||||
}
|
||||
|
||||
got, err := f.ToOptions()
|
||||
|
||||
@@ -101,7 +101,7 @@ func (r *runner) run(ctx context.Context, artifacts []*k8sArtifacts.Artifact) er
|
||||
return xerrors.Errorf("k8s scan error: %w", err)
|
||||
}
|
||||
|
||||
output, cleanup, err := r.flagOpts.OutputWriter()
|
||||
output, cleanup, err := r.flagOpts.OutputWriter(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package plugin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -57,21 +58,55 @@ type Selector struct {
|
||||
Arch string
|
||||
}
|
||||
|
||||
// Run runs the plugin
|
||||
func (p Plugin) Run(ctx context.Context, args []string) error {
|
||||
type RunOptions struct {
|
||||
Args []string
|
||||
Stdin io.Reader
|
||||
}
|
||||
|
||||
func (p Plugin) Cmd(ctx context.Context, opts RunOptions) (*exec.Cmd, error) {
|
||||
platform, err := p.selectPlatform()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("platform selection error: %w", err)
|
||||
return nil, xerrors.Errorf("platform selection error: %w", err)
|
||||
}
|
||||
|
||||
execFile := filepath.Join(dir(), p.Name, platform.Bin)
|
||||
|
||||
cmd := exec.CommandContext(ctx, execFile, args...)
|
||||
cmd := exec.CommandContext(ctx, execFile, opts.Args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
if opts.Stdin != nil {
|
||||
cmd.Stdin = opts.Stdin
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
type Wait func() error
|
||||
|
||||
// Start starts the plugin
|
||||
//
|
||||
// After a successful call to Start the Wait method must be called.
|
||||
func (p Plugin) Start(ctx context.Context, opts RunOptions) (Wait, error) {
|
||||
cmd, err := p.Cmd(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("cmd: %w", err)
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return nil, xerrors.Errorf("plugin start: %w", err)
|
||||
}
|
||||
return cmd.Wait, nil
|
||||
}
|
||||
|
||||
// Run runs the plugin
|
||||
func (p Plugin) Run(ctx context.Context, opts RunOptions) error {
|
||||
cmd, err := p.Cmd(ctx, opts)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("cmd: %w", err)
|
||||
}
|
||||
|
||||
// If an error is found during the execution of the plugin, figure
|
||||
// out if the error was from not being able to execute the plugin or
|
||||
// an error set by the plugin itself.
|
||||
@@ -79,10 +114,8 @@ func (p Plugin) Run(ctx context.Context, args []string) error {
|
||||
if _, ok := err.(*exec.ExitError); !ok {
|
||||
return xerrors.Errorf("exit: %w", err)
|
||||
}
|
||||
|
||||
return xerrors.Errorf("plugin exec: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -186,18 +219,9 @@ func Uninstall(name string) error {
|
||||
|
||||
// Information gets the information about an installed plugin
|
||||
func Information(name string) (string, error) {
|
||||
pluginDir := filepath.Join(dir(), name)
|
||||
|
||||
if _, err := os.Stat(pluginDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
|
||||
}
|
||||
return "", xerrors.Errorf("stat error: %w", err)
|
||||
}
|
||||
|
||||
plugin, err := loadMetadata(pluginDir)
|
||||
plugin, err := load(name)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("unable to load metadata: %w", err)
|
||||
return "", xerrors.Errorf("plugin load error: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
@@ -230,19 +254,11 @@ func List() (string, error) {
|
||||
|
||||
// Update updates an existing plugin
|
||||
func Update(name string) error {
|
||||
pluginDir := filepath.Join(dir(), name)
|
||||
|
||||
if _, err := os.Stat(pluginDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return xerrors.Errorf("could not find a plugin called '%s' to update: %w", name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
plugin, err := loadMetadata(pluginDir)
|
||||
plugin, err := load(name)
|
||||
if err != nil {
|
||||
return err
|
||||
return xerrors.Errorf("plugin load error: %w", err)
|
||||
}
|
||||
|
||||
log.Logger.Infof("Updating plugin '%s'", name)
|
||||
updated, err := Install(nil, plugin.Repository, true)
|
||||
if err != nil {
|
||||
@@ -280,15 +296,29 @@ func LoadAll() ([]Plugin, error) {
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// RunWithArgs runs the plugin with arguments
|
||||
func RunWithArgs(ctx context.Context, url string, args []string) error {
|
||||
pl, err := Install(ctx, url, false)
|
||||
// Start starts the plugin
|
||||
func Start(ctx context.Context, name string, opts RunOptions) (Wait, error) {
|
||||
plugin, err := load(name)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("plugin load error: %w", err)
|
||||
}
|
||||
|
||||
wait, err := plugin.Start(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
|
||||
}
|
||||
return wait, nil
|
||||
}
|
||||
|
||||
// RunWithURL runs the plugin with URL
|
||||
func RunWithURL(ctx context.Context, url string, opts RunOptions) error {
|
||||
plugin, err := Install(ctx, url, false)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("plugin install error: %w", err)
|
||||
}
|
||||
|
||||
if err = pl.Run(ctx, args); err != nil {
|
||||
return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err)
|
||||
if err = plugin.Run(ctx, opts); err != nil {
|
||||
return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -298,6 +328,23 @@ func IsPredefined(name string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func load(name string) (Plugin, error) {
|
||||
pluginDir := filepath.Join(dir(), name)
|
||||
if _, err := os.Stat(pluginDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
|
||||
}
|
||||
return Plugin{}, xerrors.Errorf("plugin stat error: %w", err)
|
||||
}
|
||||
|
||||
plugin, err := loadMetadata(pluginDir)
|
||||
if err != nil {
|
||||
return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func loadMetadata(dir string) (Plugin, error) {
|
||||
filePath := filepath.Join(dir, configFile)
|
||||
f, err := os.Open(filePath)
|
||||
|
||||
@@ -29,13 +29,10 @@ func TestPlugin_Run(t *testing.T) {
|
||||
GOOS string
|
||||
GOARCH string
|
||||
}
|
||||
type args struct {
|
||||
args []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
opts plugin.RunOptions
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
@@ -162,7 +159,7 @@ func TestPlugin_Run(t *testing.T) {
|
||||
GOARCH: tt.fields.GOARCH,
|
||||
}
|
||||
|
||||
err := p.Run(context.Background(), tt.args.args)
|
||||
err := p.Run(context.Background(), tt.opts)
|
||||
if tt.wantErr != "" {
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
@@ -338,7 +335,7 @@ description: A simple test plugin`
|
||||
// Get Information for unknown plugin
|
||||
info, err = plugin.Information("unknown")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "could not find a plugin called 'unknown', did you install it?", err.Error())
|
||||
assert.ErrorContains(t, err, "could not find a plugin called 'unknown', did you install it?")
|
||||
}
|
||||
|
||||
func TestLoadAll1(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -24,12 +26,16 @@ const (
|
||||
)
|
||||
|
||||
// Write writes the result to output, format as passed in argument
|
||||
func Write(report types.Report, option flag.Options) error {
|
||||
output, cleanup, err := option.OutputWriter()
|
||||
func Write(ctx context.Context, report types.Report, option flag.Options) (err error) {
|
||||
output, cleanup, err := option.OutputWriter(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to create a file: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
defer func() {
|
||||
if cerr := cleanup(); cerr != nil {
|
||||
err = errors.Join(err, cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Compliance report
|
||||
if option.Compliance.Spec.ID != "" {
|
||||
@@ -91,9 +97,10 @@ func Write(report types.Report, option flag.Options) error {
|
||||
return xerrors.Errorf("unknown format: %v", option.Format)
|
||||
}
|
||||
|
||||
if err := writer.Write(report); err != nil {
|
||||
if err = writer.Write(report); err != nil {
|
||||
return xerrors.Errorf("failed to write results: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user