mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 07:29:00 -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) {
|
if !plugin.IsPredefined(runAsPlugin) {
|
||||||
return xerrors.Errorf("unknown plugin: %s", 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 xerrors.Errorf("plugin error: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -182,8 +182,51 @@ $ trivy myplugin
|
|||||||
Hello from Trivy demo plugin!
|
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
|
## 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/
|
[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/
|
||||||
[helm]: https://helm.sh/docs/topics/plugins/
|
[helm]: https://helm.sh/docs/topics/plugins/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Reporting
|
# Reporting
|
||||||
|
|
||||||
## Supported Formats
|
## Format
|
||||||
Trivy supports the following formats:
|
Trivy supports the following formats:
|
||||||
|
|
||||||
- Table
|
- Table
|
||||||
@@ -373,6 +373,33 @@ $ trivy image --format template --template "@/usr/local/share/trivy/templates/ht
|
|||||||
### SBOM
|
### SBOM
|
||||||
See [here](../supply-chain/sbom.md) for details.
|
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
|
## Converting
|
||||||
To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand.
|
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)
|
--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])
|
--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
|
-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-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||||
--policy-namespaces strings Rego namespaces
|
--policy-namespaces strings Rego namespaces
|
||||||
--region string AWS Region to scan
|
--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])
|
--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")
|
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
|
||||||
-o, --output string output file name
|
-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.
|
--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-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
|
||||||
--policy-namespaces strings Rego namespaces
|
--policy-namespaces strings Rego namespaces
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ trivy convert [flags] RESULT_JSON
|
|||||||
--ignorefile string specify .trivyignore file (default ".trivyignore")
|
--ignorefile string specify .trivyignore file (default ".trivyignore")
|
||||||
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
|
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
|
||||||
-o, --output string output file name
|
-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")
|
--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])
|
-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
|
-t, --template string output template
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ trivy filesystem [flags] PATH
|
|||||||
--no-progress suppress progress bar
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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.
|
--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-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
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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.
|
--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
|
--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")
|
--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
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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.
|
--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-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
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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.
|
--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-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
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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.
|
--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-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
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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-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-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
|
--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
|
--no-progress suppress progress bar
|
||||||
--offline-scan do not issue API requests to identify dependencies
|
--offline-scan do not issue API requests to identify dependencies
|
||||||
-o, --output string output file name
|
-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)
|
--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")
|
--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
|
--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-mvn-version v0.0.0-20210429150710-d3157d602a08
|
||||||
github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd
|
github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd
|
||||||
github.com/masahiro331/go-xfs-filesystem v0.0.0-20230608043311-a335f4599b70
|
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/hashstructure/v2 v2.0.2
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/moby/buildkit v0.11.6
|
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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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.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.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 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
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 {
|
func Run(ctx context.Context, opt flag.Options) error {
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, opt.GlobalOptions.Timeout)
|
ctx, cancel := context.WithTimeout(ctx, opt.GlobalOptions.Timeout)
|
||||||
defer cancel()
|
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)
|
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)
|
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
|
// Write writes the results in the give format
|
||||||
func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
func Write(ctx context.Context, rep *Report, opt flag.Options, fromCache bool) error {
|
||||||
output, cleanup, err := opt.OutputWriter()
|
output, cleanup, err := opt.OutputWriter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("failed to create output file: %w", err)
|
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
|
var filtered []types.Result
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// filter results
|
// filter results
|
||||||
for _, resultsAtTime := range rep.Results {
|
for _, resultsAtTime := range rep.Results {
|
||||||
for _, res := range resultsAtTime.Results {
|
for _, res := range resultsAtTime.Results {
|
||||||
@@ -137,7 +135,7 @@ func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return pkgReport.Write(base, opt)
|
return pkgReport.Write(ctx, base, opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package report
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -111,7 +112,7 @@ No problems detected.
|
|||||||
|
|
||||||
output := bytes.NewBuffer(nil)
|
output := bytes.NewBuffer(nil)
|
||||||
tt.options.SetOutputWriter(output)
|
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, "AWS", report.Provider)
|
||||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package report
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ See https://avd.aquasec.com/misconfig/avd-aws-9999
|
|||||||
|
|
||||||
output := bytes.NewBuffer(nil)
|
output := bytes.NewBuffer(nil)
|
||||||
tt.options.SetOutputWriter(output)
|
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, "AWS", report.Provider)
|
||||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package report
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"github.com/aquasecurity/trivy/pkg/clock"
|
"github.com/aquasecurity/trivy/pkg/clock"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -322,7 +323,7 @@ Scan Overview for AWS Account
|
|||||||
|
|
||||||
output := bytes.NewBuffer(nil)
|
output := bytes.NewBuffer(nil)
|
||||||
tt.options.SetOutputWriter(output)
|
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, "AWS", report.Provider)
|
||||||
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func loadPluginCommands() []*cobra.Command {
|
|||||||
Short: p.Usage,
|
Short: p.Usage,
|
||||||
GroupID: groupPlugin,
|
GroupID: groupPlugin,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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 xerrors.Errorf("plugin error: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -773,7 +773,7 @@ func NewPluginCommand() *cobra.Command {
|
|||||||
Short: "Run a plugin on the fly",
|
Short: "Run a plugin on the fly",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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{
|
&cobra.Command{
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ type Runner interface {
|
|||||||
// Filter filter a report
|
// Filter filter a report
|
||||||
Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error)
|
Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error)
|
||||||
// Report a writes a report
|
// 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 closes runner
|
||||||
Close(ctx context.Context) error
|
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
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *runner) Report(opts flag.Options, report types.Report) error {
|
func (r *runner) Report(ctx context.Context, opts flag.Options, report types.Report) error {
|
||||||
if err := pkgReport.Write(report, opts); err != nil {
|
if err := pkgReport.Write(ctx, report, opts); err != nil {
|
||||||
return xerrors.Errorf("unable to write results: %w", err)
|
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)
|
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)
|
return xerrors.Errorf("report error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Run(ctx context.Context, opts flag.Options) (err error) {
|
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)
|
f, err := os.Open(opts.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("file open error: %w", err)
|
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...")
|
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)
|
return xerrors.Errorf("unable to write results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package flag
|
package flag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/plugin"
|
||||||
"github.com/aquasecurity/trivy/pkg/result"
|
"github.com/aquasecurity/trivy/pkg/result"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/version"
|
"github.com/aquasecurity/trivy/pkg/version"
|
||||||
@@ -173,19 +175,46 @@ func (o *Options) SetOutputWriter(w io.Writer) {
|
|||||||
|
|
||||||
// OutputWriter returns an output writer.
|
// OutputWriter returns an output writer.
|
||||||
// If the output file is not specified, it returns os.Stdout.
|
// If the output file is not specified, it returns os.Stdout.
|
||||||
func (o *Options) OutputWriter() (io.Writer, func(), error) {
|
func (o *Options) OutputWriter(ctx context.Context) (io.Writer, func() error, error) {
|
||||||
if o.outputWriter != nil {
|
cleanup := func() error { return nil }
|
||||||
return o.outputWriter, func() {}, 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)
|
f, err := os.Create(o.Output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, xerrors.Errorf("failed to create output file: %w", err)
|
return nil, nil, xerrors.Errorf("failed to create output file: %w", err)
|
||||||
}
|
}
|
||||||
return f, func() { _ = f.Close() }, 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)
|
||||||
}
|
}
|
||||||
return os.Stdout, func() {}, nil
|
|
||||||
|
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) {
|
func addFlag(cmd *cobra.Command, flag *Flag) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package flag
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
@@ -86,6 +87,12 @@ var (
|
|||||||
Default: "",
|
Default: "",
|
||||||
Usage: "output file name",
|
Usage: "output file name",
|
||||||
}
|
}
|
||||||
|
OutputPluginArgFlag = Flag{
|
||||||
|
Name: "output-plugin-arg",
|
||||||
|
ConfigName: "output-plugin-arg",
|
||||||
|
Default: "",
|
||||||
|
Usage: "[EXPERIMENTAL] output plugin arguments",
|
||||||
|
}
|
||||||
SeverityFlag = Flag{
|
SeverityFlag = Flag{
|
||||||
Name: "severity",
|
Name: "severity",
|
||||||
ConfigName: "severity",
|
ConfigName: "severity",
|
||||||
@@ -115,6 +122,7 @@ type ReportFlagGroup struct {
|
|||||||
ExitCode *Flag
|
ExitCode *Flag
|
||||||
ExitOnEOL *Flag
|
ExitOnEOL *Flag
|
||||||
Output *Flag
|
Output *Flag
|
||||||
|
OutputPluginArg *Flag
|
||||||
Severity *Flag
|
Severity *Flag
|
||||||
Compliance *Flag
|
Compliance *Flag
|
||||||
}
|
}
|
||||||
@@ -130,6 +138,7 @@ type ReportOptions struct {
|
|||||||
ExitOnEOL int
|
ExitOnEOL int
|
||||||
IgnorePolicy string
|
IgnorePolicy string
|
||||||
Output string
|
Output string
|
||||||
|
OutputPluginArgs []string
|
||||||
Severities []dbTypes.Severity
|
Severities []dbTypes.Severity
|
||||||
Compliance spec.ComplianceSpec
|
Compliance spec.ComplianceSpec
|
||||||
}
|
}
|
||||||
@@ -146,6 +155,7 @@ func NewReportFlagGroup() *ReportFlagGroup {
|
|||||||
ExitCode: &ExitCodeFlag,
|
ExitCode: &ExitCodeFlag,
|
||||||
ExitOnEOL: &ExitOnEOLFlag,
|
ExitOnEOL: &ExitOnEOLFlag,
|
||||||
Output: &OutputFlag,
|
Output: &OutputFlag,
|
||||||
|
OutputPluginArg: &OutputPluginArgFlag,
|
||||||
Severity: &SeverityFlag,
|
Severity: &SeverityFlag,
|
||||||
Compliance: &ComplianceFlag,
|
Compliance: &ComplianceFlag,
|
||||||
}
|
}
|
||||||
@@ -167,6 +177,7 @@ func (f *ReportFlagGroup) Flags() []*Flag {
|
|||||||
f.ExitCode,
|
f.ExitCode,
|
||||||
f.ExitOnEOL,
|
f.ExitOnEOL,
|
||||||
f.Output,
|
f.Output,
|
||||||
|
f.OutputPluginArg,
|
||||||
f.Severity,
|
f.Severity,
|
||||||
f.Compliance,
|
f.Compliance,
|
||||||
}
|
}
|
||||||
@@ -216,6 +227,14 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
|
|||||||
return ReportOptions{}, xerrors.Errorf("unable to load compliance spec: %w", err)
|
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{
|
return ReportOptions{
|
||||||
Format: format,
|
Format: format,
|
||||||
ReportFormat: getString(f.ReportFormat),
|
ReportFormat: getString(f.ReportFormat),
|
||||||
@@ -227,6 +246,7 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
|
|||||||
ExitOnEOL: getInt(f.ExitOnEOL),
|
ExitOnEOL: getInt(f.ExitOnEOL),
|
||||||
IgnorePolicy: getString(f.IgnorePolicy),
|
IgnorePolicy: getString(f.IgnorePolicy),
|
||||||
Output: getString(f.Output),
|
Output: getString(f.Output),
|
||||||
|
OutputPluginArgs: outputPluginArgs,
|
||||||
Severities: toSeverity(getStringSlice(f.Severity)),
|
Severities: toSeverity(getStringSlice(f.Severity)),
|
||||||
Compliance: cs,
|
Compliance: cs,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
|||||||
exitOnEOSL bool
|
exitOnEOSL bool
|
||||||
ignorePolicy string
|
ignorePolicy string
|
||||||
output string
|
output string
|
||||||
|
outputPluginArgs string
|
||||||
severities string
|
severities string
|
||||||
compliane string
|
compliance string
|
||||||
|
|
||||||
debug bool
|
debug bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -63,7 +63,6 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
|||||||
severities: "CRITICAL",
|
severities: "CRITICAL",
|
||||||
format: "cyclonedx",
|
format: "cyclonedx",
|
||||||
listAllPkgs: false,
|
listAllPkgs: false,
|
||||||
|
|
||||||
debug: true,
|
debug: true,
|
||||||
},
|
},
|
||||||
wantLogs: []string{
|
wantLogs: []string{
|
||||||
@@ -138,10 +137,26 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
|||||||
ListAllPkgs: true,
|
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",
|
name: "happy path with compliance",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
compliane: "@testdata/example-spec.yaml",
|
compliance: "@testdata/example-spec.yaml",
|
||||||
severities: dbTypes.SeverityLow.String(),
|
severities: dbTypes.SeverityLow.String(),
|
||||||
},
|
},
|
||||||
want: flag.ReportOptions{
|
want: flag.ReportOptions{
|
||||||
@@ -187,8 +202,9 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
|||||||
viper.Set(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode)
|
viper.Set(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode)
|
||||||
viper.Set(flag.ExitOnEOLFlag.ConfigName, tt.fields.exitOnEOSL)
|
viper.Set(flag.ExitOnEOLFlag.ConfigName, tt.fields.exitOnEOSL)
|
||||||
viper.Set(flag.OutputFlag.ConfigName, tt.fields.output)
|
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.SeverityFlag.ConfigName, tt.fields.severities)
|
||||||
viper.Set(flag.ComplianceFlag.ConfigName, tt.fields.compliane)
|
viper.Set(flag.ComplianceFlag.ConfigName, tt.fields.compliance)
|
||||||
|
|
||||||
// Assert options
|
// Assert options
|
||||||
f := &flag.ReportFlagGroup{
|
f := &flag.ReportFlagGroup{
|
||||||
@@ -201,6 +217,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
|
|||||||
ExitCode: &flag.ExitCodeFlag,
|
ExitCode: &flag.ExitCodeFlag,
|
||||||
ExitOnEOL: &flag.ExitOnEOLFlag,
|
ExitOnEOL: &flag.ExitOnEOLFlag,
|
||||||
Output: &flag.OutputFlag,
|
Output: &flag.OutputFlag,
|
||||||
|
OutputPluginArg: &flag.OutputPluginArgFlag,
|
||||||
Severity: &flag.SeverityFlag,
|
Severity: &flag.SeverityFlag,
|
||||||
Compliance: &flag.ComplianceFlag,
|
Compliance: &flag.ComplianceFlag,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (r *runner) run(ctx context.Context, artifacts []*k8sArtifacts.Artifact) er
|
|||||||
return xerrors.Errorf("k8s scan error: %w", err)
|
return xerrors.Errorf("k8s scan error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, cleanup, err := r.flagOpts.OutputWriter()
|
output, cleanup, err := r.flagOpts.OutputWriter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("failed to create output file: %w", err)
|
return xerrors.Errorf("failed to create output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -57,21 +58,55 @@ type Selector struct {
|
|||||||
Arch string
|
Arch string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run runs the plugin
|
type RunOptions struct {
|
||||||
func (p Plugin) Run(ctx context.Context, args []string) error {
|
Args []string
|
||||||
|
Stdin io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Plugin) Cmd(ctx context.Context, opts RunOptions) (*exec.Cmd, error) {
|
||||||
platform, err := p.selectPlatform()
|
platform, err := p.selectPlatform()
|
||||||
if err != nil {
|
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)
|
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
|
cmd.Stdin = os.Stdin
|
||||||
|
if opts.Stdin != nil {
|
||||||
|
cmd.Stdin = opts.Stdin
|
||||||
|
}
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = os.Environ()
|
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
|
// 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
|
// out if the error was from not being able to execute the plugin or
|
||||||
// an error set by the plugin itself.
|
// 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 {
|
if _, ok := err.(*exec.ExitError); !ok {
|
||||||
return xerrors.Errorf("exit: %w", err)
|
return xerrors.Errorf("exit: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return xerrors.Errorf("plugin exec: %w", err)
|
return xerrors.Errorf("plugin exec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,18 +219,9 @@ func Uninstall(name string) error {
|
|||||||
|
|
||||||
// Information gets the information about an installed plugin
|
// Information gets the information about an installed plugin
|
||||||
func Information(name string) (string, error) {
|
func Information(name string) (string, error) {
|
||||||
pluginDir := filepath.Join(dir(), name)
|
plugin, err := load(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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", xerrors.Errorf("unable to load metadata: %w", err)
|
return "", xerrors.Errorf("plugin load error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
@@ -230,19 +254,11 @@ func List() (string, error) {
|
|||||||
|
|
||||||
// Update updates an existing plugin
|
// Update updates an existing plugin
|
||||||
func Update(name string) error {
|
func Update(name string) error {
|
||||||
pluginDir := filepath.Join(dir(), name)
|
plugin, err := load(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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return xerrors.Errorf("plugin load error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Logger.Infof("Updating plugin '%s'", name)
|
log.Logger.Infof("Updating plugin '%s'", name)
|
||||||
updated, err := Install(nil, plugin.Repository, true)
|
updated, err := Install(nil, plugin.Repository, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,15 +296,29 @@ func LoadAll() ([]Plugin, error) {
|
|||||||
return plugins, nil
|
return plugins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunWithArgs runs the plugin with arguments
|
// Start starts the plugin
|
||||||
func RunWithArgs(ctx context.Context, url string, args []string) error {
|
func Start(ctx context.Context, name string, opts RunOptions) (Wait, error) {
|
||||||
pl, err := Install(ctx, url, false)
|
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 {
|
if err != nil {
|
||||||
return xerrors.Errorf("plugin install error: %w", err)
|
return xerrors.Errorf("plugin install error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = pl.Run(ctx, args); err != nil {
|
if err = plugin.Run(ctx, opts); err != nil {
|
||||||
return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err)
|
return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -298,6 +328,23 @@ func IsPredefined(name string) bool {
|
|||||||
return ok
|
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) {
|
func loadMetadata(dir string) (Plugin, error) {
|
||||||
filePath := filepath.Join(dir, configFile)
|
filePath := filepath.Join(dir, configFile)
|
||||||
f, err := os.Open(filePath)
|
f, err := os.Open(filePath)
|
||||||
|
|||||||
@@ -29,13 +29,10 @@ func TestPlugin_Run(t *testing.T) {
|
|||||||
GOOS string
|
GOOS string
|
||||||
GOARCH string
|
GOARCH string
|
||||||
}
|
}
|
||||||
type args struct {
|
|
||||||
args []string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
opts plugin.RunOptions
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -162,7 +159,7 @@ func TestPlugin_Run(t *testing.T) {
|
|||||||
GOARCH: tt.fields.GOARCH,
|
GOARCH: tt.fields.GOARCH,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.Run(context.Background(), tt.args.args)
|
err := p.Run(context.Background(), tt.opts)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.wantErr)
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
@@ -338,7 +335,7 @@ description: A simple test plugin`
|
|||||||
// Get Information for unknown plugin
|
// Get Information for unknown plugin
|
||||||
info, err = plugin.Information("unknown")
|
info, err = plugin.Information("unknown")
|
||||||
require.Error(t, err)
|
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) {
|
func TestLoadAll1(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package report
|
package report
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -24,12 +26,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Write writes the result to output, format as passed in argument
|
// Write writes the result to output, format as passed in argument
|
||||||
func Write(report types.Report, option flag.Options) error {
|
func Write(ctx context.Context, report types.Report, option flag.Options) (err error) {
|
||||||
output, cleanup, err := option.OutputWriter()
|
output, cleanup, err := option.OutputWriter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("failed to create a file: %w", err)
|
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
|
// Compliance report
|
||||||
if option.Compliance.Spec.ID != "" {
|
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)
|
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 xerrors.Errorf("failed to write results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user