Skip to content

Commit 26faf8f

Browse files
knqyf263DmitriyLewen
andauthoredMay 14, 2024··
feat: add support for plugin index (#6674)
Signed-off-by: knqyf263 <[email protected]> Co-authored-by: DmitriyLewen <[email protected]>
1 parent 150a773 commit 26faf8f

File tree

27 files changed

+1444
-697
lines changed

27 files changed

+1444
-697
lines changed
 

‎.github/workflows/semantic-pr.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
k8s
4545
aws
4646
vm
47+
plugin
4748
4849
alpine
4950
wolfi

‎cmd/trivy/main.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ func main() {
2828
func run() error {
2929
// Trivy behaves as the specified plugin.
3030
if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" {
31-
if !plugin.IsPredefined(runAsPlugin) {
32-
return xerrors.Errorf("unknown plugin: %s", runAsPlugin)
33-
}
34-
if err := plugin.RunWithURL(context.Background(), runAsPlugin, plugin.RunOptions{Args: os.Args[1:]}); err != nil {
31+
if err := plugin.RunWithURL(context.Background(), runAsPlugin, plugin.Options{Args: os.Args[1:]}); err != nil {
3532
return xerrors.Errorf("plugin error: %w", err)
3633
}
3734
return nil

‎docs/community/contribute/pr.md

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ mode:
114114
- server
115115
- aws
116116
- vm
117+
- plugin
117118

118119
os:
119120

‎docs/docs/advanced/plugins.md

-236
This file was deleted.

‎docs/docs/configuration/reporting.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ $ trivy <target> [--format <format>] --output plugin=<plugin_name> [--output-plu
399399
```
400400

401401
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.
402-
For more details, please check [here](../advanced/plugins.md#output-plugins).
402+
For more details, please check [here](../plugin/plugins.md#output-plugins).
403403

404404
## Converting
405405
To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand.

‎docs/docs/plugin/developer-guide.md

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Developer Guide
2+
3+
## Developing Trivy plugins
4+
This section will guide you through the process of developing Trivy plugins.
5+
To help you get started quickly, we have published a [plugin template repository][plugin-template].
6+
You can use this template as a starting point for your plugin development.
7+
8+
### Introduction
9+
If you are looking to start developing plugins for Trivy, read [the user guide](./user-guide.md) first.
10+
11+
The development process involves the following steps:
12+
13+
- Create a repository for your plugin, named `trivy-plugin-<name>`.
14+
- Create an executable binary that can be invoked as `trivy <name>`.
15+
- Place the executable binary in a repository.
16+
- Create a `plugin.yaml` file that describes the plugin.
17+
- (Submit your plugin to the [Trivy plugin index][trivy-plugin-index].)
18+
19+
After you develop a plugin with a good name following the best practices and publish it, you can submit your plugin to the [Trivy plugin index][trivy-plugin-index].
20+
21+
### Naming
22+
This section describes guidelines for naming your plugins.
23+
24+
#### Use `trivy-plugin-` prefix
25+
The name of the plugin repository should be prefixed with `trivy-plugin-`.
26+
27+
#### Use lowercase and hyphens
28+
Plugin names must be all lowercase and separate words with hyphens.
29+
Don’t use camelCase, PascalCase, or snake_case; use kebab-case.
30+
31+
- NO: `trivy OpenSvc`
32+
- YES: `trivy open-svc`
33+
34+
#### Be specific
35+
Plugin names should not be verbs or nouns that are generic, already overloaded, or likely to be used for broader purposes by another plugin.
36+
37+
- NO: trivy sast (Too broad)
38+
- YES: trivy govulncheck
39+
40+
41+
#### Be unique
42+
Find a unique name for your plugin that differentiates it from other plugins that perform a similar function.
43+
44+
- NO: `trivy images` (Unclear how it is different from the builtin “image" command)
45+
- YES: `trivy registry-images` (Unique name).
46+
47+
#### Prefix Vendor Identifiers
48+
Use vendor-specific strings as prefix, separated with a dash.
49+
This makes it easier to search/group plugins that are about a specific vendor.
50+
51+
- NO: `trivy security-hub-aws (Makes it harder to search or locate in a plugin list)
52+
- YES: `trivy aws-security-hub (Will show up together with other aws-* plugins)
53+
54+
### Choosing a language
55+
Since Trivy plugins are standalone executables, you can write them in any programming language.
56+
57+
If you are planning to write a plugin with Go, check out [the Report struct](https://github.com/aquasecurity/trivy/blob/787b466e069e2d04e73b3eddbda621e5eec8543b/pkg/types/report.go#L13-L24),
58+
which is the output of Trivy scan.
59+
60+
61+
### Writing your plugin
62+
Each plugin has a top-level directory, and then a `plugin.yaml` file.
63+
64+
```bash
65+
your-plugin/
66+
|
67+
|- plugin.yaml
68+
|- your-plugin.sh
69+
```
70+
71+
In the example above, the plugin is contained inside a directory named `your-plugin`.
72+
It has two files: `plugin.yaml` (required) and an executable script, `your-plugin.sh` (optional).
73+
74+
#### Writing a plugin manifest
75+
The plugin manifest is a simple YAML file named `plugin.yaml`.
76+
Here is an example YAML of [trivy-plugin-kubectl][trivy-plugin-kubectl] plugin that adds support for Kubernetes scanning.
77+
78+
```yaml
79+
name: "kubectl"
80+
version: "0.1.0"
81+
repository: github.com/aquasecurity/trivy-plugin-kubectl
82+
maintainer: aquasecurity
83+
output: false
84+
summary: Scan kubectl resources
85+
description: |-
86+
A Trivy plugin that scans the images of a kubernetes resource.
87+
Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME
88+
platforms:
89+
- selector: # optional
90+
os: darwin
91+
arch: amd64
92+
uri: ./trivy-kubectl # where the execution file is (local file, http, git, etc.)
93+
bin: ./trivy-kubectl # path to the execution file
94+
- selector: # optional
95+
os: linux
96+
arch: amd64
97+
uri: https://github.com/aquasecurity/trivy-plugin-kubectl/releases/download/v0.1.0/trivy-kubectl.tar.gz
98+
bin: ./trivy-kubectl
99+
```
100+
101+
We encourage you to copy and adapt plugin manifests of existing plugins.
102+
103+
- [count][trivy-plugin-count]
104+
- [referrer][trivy-plugin-referrer]
105+
106+
The `plugin.yaml` field should contain the following information:
107+
108+
- name: The name of the plugin. This also determines how the plugin will be made available in the Trivy CLI. For example, if the plugin is named kubectl, you can call the plugin with `trivy kubectl`. (required)
109+
- version: The version of the plugin. [Semantic Versioning][semver] should be used. (required)
110+
- repository: The repository name where the plugin is hosted. (required)
111+
- maintainer: The name of the maintainer of the plugin. (required)
112+
- output: Whether the plugin supports [the output mode](./user-guide.md#output-mode-support). (optional)
113+
- usage: Deprecated: use summary instead. (optional)
114+
- summary: A short usage description. (required)
115+
- description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required)
116+
- platforms: (required)
117+
- selector: The OS/Architecture specific variations of a execution file. (optional)
118+
- os: OS information based on GOOS (linux, darwin, etc.) (optional)
119+
- arch: The architecture information based on GOARCH (amd64, arm64, etc.) (optional)
120+
- uri: Where the executable file is. Relative path from the root directory of the plugin or remote URL such as HTTP and S3. (required)
121+
- bin: Which file to call when the plugin is executed. Relative path from the root directory of the plugin. (required)
122+
123+
The following rules will apply in deciding which platform to select:
124+
125+
- If both `os` and `arch` under `selector` match the current platform, search will stop and the platform will be used.
126+
- If `selector` is not present, the platform will be used.
127+
- If `os` matches and there is no more specific `arch` match, the platform will be used.
128+
- If no `platform` match is found, Trivy will exit with an error.
129+
130+
After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache.
131+
When the plugin is called via Trivy CLI, `bin` command will be executed.
132+
133+
#### Plugin arguments/flags
134+
The plugin is responsible for handling flags and arguments.
135+
Any arguments are passed to the plugin from the `trivy` command.
136+
137+
#### Testing plugin installation locally
138+
A plugin should be archived `*.tar.gz`.
139+
After you have archived your plugin into a `.tar.gz` file, you can verify that your plugin installs correctly with Trivy.
140+
141+
```bash
142+
$ tar -czvf myplugin.tar.gz plugin.yaml script.py
143+
plugin.yaml
144+
script.py
145+
146+
$ trivy plugin install myplugin.tar.gz
147+
2023-03-03T19:04:42.026+0600 INFO Installing the plugin from myplugin.tar.gz...
148+
2023-03-03T19:04:42.026+0600 INFO Loading the plugin metadata...
149+
150+
$ trivy myplugin
151+
Hello from Trivy demo plugin!
152+
```
153+
154+
## Publishing plugins
155+
The [plugin.yaml](#writing-a-plugin-manifest) file is the core of your plugin, so as long as it is published somewhere, your plugin can be installed.
156+
If you choose to publish your plugin on GitHub, you can make it installable by placing the plugin.yaml file in the root directory of your repository.
157+
Users can then install your plugin with the command, `trivy plugin install github.com/org/repo`.
158+
159+
While the `uri` specified in the plugin.yaml file doesn't necessarily need to point to the same repository, it's a good practice to host the executable file within the same repository when using GitHub.
160+
You can utilize GitHub Releases to distribute the executable file.
161+
For an example of how to structure your plugin repository, refer to [the plugin template repository][plugin-template].
162+
163+
## Distributing plugins via the Trivy plugin index
164+
Trivy can install plugins directly by specifying a repository, like `trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`,
165+
so you don't necessarily need to register your plugin in the Trivy plugin index.
166+
However, we would recommend distributing your plugin via the Trivy plugin index
167+
since it makes it easier for other users to find (`trivy plugin search`) and install your plugin (e.g. `trivy plugin install kubectl`).
168+
169+
### Pre-submit checklist
170+
- Review [the plugin naming guide](#naming).
171+
- Ensure the `plugin.yaml` file has all the required fields.
172+
- Tag a git release with a semantic version (e.g. v1.0.0).
173+
- [Test your plugin installation locally](#testing-plugin-installation-locally).
174+
175+
### Submitting plugins
176+
Submitting your plugin to the plugin index is a straightforward process.
177+
All you need to do is create a YAML file for your plugin and place it in the [plugins/](https://github.com/aquasecurity/trivy-plugin-index/tree/main/plugins) directory of [the index repository][trivy-plugin-index].
178+
179+
Once you've done that, create a pull request (PR) and have it reviewed by the maintainers.
180+
Once your PR is merged, the index will be updated, and your plugin will be available for installation.
181+
[The plugin index page][plugin-list] will also be automatically updated to list your newly added plugin.
182+
183+
The content of the YAML file is very simple.
184+
You only need to specify the name of your plugin and the repository where it is distributed.
185+
186+
```yaml
187+
name: referrer
188+
repository: github.com/aquasecurity/trivy-plugin-referrer
189+
```
190+
191+
After your PR is merged, the CI system will automatically retrieve the `plugin.yaml` file from your repository and update [the index.yaml file][index].
192+
If any required fields are missing from your `plugin.yaml`, the CI will fail, so make sure your `plugin.yaml` has all the required fields before creating a PR.
193+
Once [the index.yaml][index] has been updated, running `trivy plugin update` will download the updated index to your local machine.
194+
195+
196+
[plugin-template]: https://github.com/aquasecurity/trivy-plugin-template
197+
[plugin-list]: https://aquasecurity.github.io/trivy-plugin-index/
198+
[index]: https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml
199+
[semver]: https://semver.org/
200+
[trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index
201+
[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl
202+
[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count/blob/main/plugin.yaml
203+
[trivy-plugin-referrer]: https://github.com/aquasecurity/trivy-plugin-referrer/blob/main/plugin.yaml

‎docs/docs/plugin/index.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Plugins
2+
Trivy provides a plugin feature to allow others to extend the Trivy CLI without the need to change the Trivy code base.
3+
This plugin system was inspired by the plugin system used in [kubectl][kubectl], [Helm][helm], and [Conftest][conftest].
4+
5+
## Overview
6+
Trivy plugins are add-on tools that integrate seamlessly with Trivy.
7+
They provide a way to extend the core feature set of Trivy, but without requiring every new feature to be written in Go and added to the core tool.
8+
9+
- They can be added and removed from a Trivy installation without impacting the core Trivy tool.
10+
- They can be written in any programming language.
11+
- They integrate with Trivy, and will show up in Trivy help and subcommands.
12+
13+
!!! warning
14+
Trivy plugins available in public are not audited for security.
15+
You should install and run third-party plugins at your own risk, since they are arbitrary programs running on your machine.
16+
17+
## Quickstart
18+
Trivy helps you discover and install plugins on your machine.
19+
20+
You can install and use a wide variety of Trivy plugins to enhance your experience.
21+
22+
Let’s get started:
23+
24+
1. Download the plugin list:
25+
26+
```bash
27+
$ trivy plugin update
28+
```
29+
30+
2. Discover Trivy plugins available on the plugin index:
31+
32+
```bash
33+
$ trivy plugin search
34+
NAME DESCRIPTION MAINTAINER OUTPUT
35+
aqua A plugin for integration with Aqua Security SaaS platform aquasecurity
36+
kubectl A plugin scanning the images of a kubernetes resource aquasecurity
37+
referrer A plugin for OCI referrers aquasecurity ✓
38+
[...]
39+
```
40+
41+
3. Choose a plugin from the list and install it:
42+
43+
```bash
44+
$ trivy plugin install referrer
45+
```
46+
47+
4. Use the installed plugin:
48+
49+
```bash
50+
$ trivy referrer --help
51+
```
52+
53+
5. Keep your plugins up-to-date:
54+
55+
```bash
56+
$ trivy plugin upgrade
57+
```
58+
59+
6. Uninstall a plugin you no longer use:
60+
61+
```bash
62+
trivy plugin uninstall referrer
63+
```
64+
65+
This is practically all you need to know to start using Trivy plugins.
66+
67+
68+
[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/
69+
[helm]: https://helm.sh/docs/topics/plugins/
70+
[conftest]: https://www.conftest.dev/plugins/

‎docs/docs/plugin/user-guide.md

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# User Guide
2+
3+
## Discovering Plugins
4+
You can find a list of Trivy plugins distributed via trivy-plugin-index [here][trivy-plugin-index].
5+
However, you can find plugins using the command line as well.
6+
7+
First, refresh your local copy of the plugin index:
8+
9+
```bash
10+
$ trivy plugin update
11+
```
12+
13+
To list all plugins available, run:
14+
15+
```bash
16+
$ trivy plugin search
17+
NAME DESCRIPTION MAINTAINER OUTPUT
18+
aqua A plugin for integration with Aqua Security SaaS platform aquasecurity
19+
kubectl A plugin scanning the images of a kubernetes resource aquasecurity
20+
referrer A plugin for OCI referrers aquasecurity ✓
21+
```
22+
23+
You can specify search keywords as arguments:
24+
25+
```bash
26+
$ trivy plugin search referrer
27+
28+
NAME DESCRIPTION MAINTAINER OUTPUT
29+
referrer A plugin for OCI referrers aquasecurity ✓
30+
```
31+
32+
It lists plugins with the keyword in the name or description.
33+
34+
## Installing Plugins
35+
Plugins can be installed with the `trivy plugin install` command:
36+
37+
```bash
38+
$ trivy plugin install referrer
39+
```
40+
41+
This command will download the plugin and install it in the plugin cache.
42+
43+
Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set.
44+
Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache.
45+
The preference order is as follows:
46+
47+
- XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir
48+
- ~/.trivy/plugins
49+
50+
Furthermore, it is possible to download plugins that are not registered in the index by specifying the URL directly or by specifying the file path.
51+
52+
```bash
53+
$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl
54+
```
55+
```bash
56+
$ trivy plugin install myplugin.tar.gz
57+
```
58+
59+
Under the hood Trivy leverages [go-getter][go-getter] to download plugins.
60+
This means the following protocols are supported for downloading plugins:
61+
62+
- OCI Registries
63+
- Local Files
64+
- Git
65+
- HTTP/HTTPS
66+
- Mercurial
67+
- Amazon S3
68+
- Google Cloud Storage
69+
70+
## Listing Installed Plugins
71+
To list all plugins installed, run:
72+
73+
```bash
74+
$ trivy plugin list
75+
```
76+
77+
## Using Plugins
78+
Once the plugin is installed, Trivy will load all available plugins in the cache on the start of the next Trivy execution.
79+
A plugin will be made in the Trivy CLI based on the plugin name.
80+
To display all plugins, you can list them by `trivy --help`
81+
82+
```bash
83+
$ trivy --help
84+
NAME:
85+
trivy - A simple and comprehensive vulnerability scanner for containers
86+
87+
USAGE:
88+
trivy [global options] command [command options] target
89+
90+
VERSION:
91+
dev
92+
93+
Scanning Commands
94+
aws [EXPERIMENTAL] Scan AWS account
95+
config Scan config files for misconfigurations
96+
filesystem Scan local filesystem
97+
image Scan a container image
98+
99+
...
100+
101+
Plugin Commands
102+
kubectl scan kubectl resources
103+
referrer Put referrers to OCI registry
104+
```
105+
106+
As shown above, `kubectl` subcommand exists in the `Plugin Commands` section.
107+
To call the kubectl plugin and scan existing Kubernetes deployments, you can execute the following command:
108+
109+
```
110+
$ trivy kubectl deployment <deployment-id> -- --ignore-unfixed --severity CRITICAL
111+
```
112+
113+
Internally the kubectl plugin calls the kubectl binary to fetch information about that deployment and passes the using images to Trivy.
114+
You can see the detail [here][trivy-plugin-kubectl].
115+
116+
If you want to omit even the subcommand, you can use `TRIVY_RUN_AS_PLUGIN` environment variable.
117+
118+
```bash
119+
$ TRIVY_RUN_AS_PLUGIN=kubectl trivy job your-job -- --format json
120+
```
121+
122+
## Installing and Running Plugins on the fly
123+
`trivy plugin run` installs a plugin and runs it on the fly.
124+
If the plugin is already present in the cache, the installation is skipped.
125+
126+
```bash
127+
trivy plugin run kubectl pod your-pod -- --exit-code 1
128+
```
129+
130+
## Upgrading Plugins
131+
To upgrade all plugins that you have installed to their latest versions, run:
132+
133+
```bash
134+
$ trivy plugin upgrade
135+
```
136+
137+
To upgrade only certain plugins, you can explicitly specify their names:
138+
139+
```bash
140+
$ trivy plugin upgrade <PLUGIN1> <PLUGIN2>
141+
```
142+
143+
## Uninstalling Plugins
144+
Specify a plugin name with `trivy plugin uninstall` command.
145+
146+
```bash
147+
$ trivy plugin uninstall kubectl
148+
```
149+
150+
Here's the revised English documentation based on your requested changes:
151+
152+
## Output Mode Support
153+
While plugins are typically intended to be used as subcommands of Trivy, plugins supporting the output mode can be invoked as part of Trivy's built-in commands.
154+
155+
!!! warning "EXPERIMENTAL"
156+
This feature might change without preserving backwards compatibility.
157+
158+
Trivy supports plugins that are compatible with the output mode, which process Trivy's output, such as by transforming the output format or sending it elsewhere.
159+
You can determine whether a plugin supports the output mode by checking the `OUTPUT` column in the output of `trivy plugin search` or `trivy plugin list`.
160+
161+
```bash
162+
$ trivy plugin search
163+
NAME DESCRIPTION MAINTAINER OUTPUT
164+
aqua A plugin for integration with Aqua Security SaaS platform aquasecurity
165+
kubectl A plugin scanning the images of a kubernetes resource aquasecurity
166+
referrer A plugin for OCI referrers aquasecurity ✓
167+
```
168+
169+
In this case, the `referrer` plugin supports the output mode.
170+
171+
For instance, in the case of image scanning, a plugin supporting the output mode can be called as follows:
172+
173+
```bash
174+
$ trivy image --format json --output plugin=<plugin_name> [--output-plugin-arg <plugin_flags>] <image_name>
175+
```
176+
177+
Since scan results are passed to the plugin via standard input, plugins must be capable of handling standard input.
178+
179+
!!! warning
180+
To avoid Trivy hanging, you need to read all data from `Stdin` before the plugin exits successfully or stops with an error.
181+
182+
While the example passes JSON to the plugin, other formats like SBOM can also be passed (e.g., `--format cyclonedx`).
183+
184+
If a plugin requires flags or other arguments, they can be passed using `--output-plugin-arg`.
185+
This is directly forwarded as arguments to the plugin.
186+
For example, `--output plugin=myplugin --output-plugin-arg "--foo --bar=baz"` translates to `myplugin --foo --bar=baz` in execution.
187+
188+
An example of a plugin supporting the output mode is available [here][trivy-plugin-count].
189+
It can be used as below:
190+
191+
```bash
192+
# Install the plugin first
193+
$ trivy plugin install count
194+
195+
# Call the plugin supporting the output mode in image scanning
196+
$ trivy image --format json --output plugin=count --output-plugin-arg "--published-after 2023-10-01" debian:12
197+
```
198+
199+
## Example
200+
201+
- [kubectl][trivy-plugin-kubectl]
202+
- [count][trivy-plugin-count]
203+
204+
[trivy-plugin-index]: https://aquasecurity.github.io/trivy-plugin-index/
205+
[go-getter]: https://github.com/hashicorp/go-getter
206+
[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl
207+
[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count

‎docs/docs/references/configuration/cli/trivy_plugin.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Manage plugins
2828
* [trivy plugin install](trivy_plugin_install.md) - Install a plugin
2929
* [trivy plugin list](trivy_plugin_list.md) - List installed plugin
3030
* [trivy plugin run](trivy_plugin_run.md) - Run a plugin on the fly
31+
* [trivy plugin search](trivy_plugin_search.md) - List Trivy plugins available on the plugin index and search among them
3132
* [trivy plugin uninstall](trivy_plugin_uninstall.md) - Uninstall a plugin
32-
* [trivy plugin update](trivy_plugin_update.md) - Update an existing plugin
33+
* [trivy plugin update](trivy_plugin_update.md) - Update the local copy of the plugin index
34+
* [trivy plugin upgrade](trivy_plugin_upgrade.md) - Upgrade installed plugins to newer versions
3335

‎docs/docs/references/configuration/cli/trivy_plugin_install.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Install a plugin
44

55
```
6-
trivy plugin install URL | FILE_PATH
6+
trivy plugin install NAME | URL | FILE_PATH
77
```
88

99
### Options

‎docs/docs/references/configuration/cli/trivy_plugin_run.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Run a plugin on the fly
44

55
```
6-
trivy plugin run URL | FILE_PATH
6+
trivy plugin run NAME | URL | FILE_PATH
77
```
88

99
### Options
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## trivy plugin search
2+
3+
List Trivy plugins available on the plugin index and search among them
4+
5+
```
6+
trivy plugin search [KEYWORD]
7+
```
8+
9+
### Options
10+
11+
```
12+
-h, --help help for search
13+
```
14+
15+
### Options inherited from parent commands
16+
17+
```
18+
--cache-dir string cache directory (default "/path/to/cache")
19+
-c, --config string config path (default "trivy.yaml")
20+
-d, --debug debug mode
21+
--generate-default-config write the default config to trivy-default.yaml
22+
--insecure allow insecure server connections
23+
-q, --quiet suppress progress bar and log output
24+
--timeout duration timeout (default 5m0s)
25+
-v, --version show version
26+
```
27+
28+
### SEE ALSO
29+
30+
* [trivy plugin](trivy_plugin.md) - Manage plugins
31+

‎docs/docs/references/configuration/cli/trivy_plugin_update.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
## trivy plugin update
22

3-
Update an existing plugin
3+
Update the local copy of the plugin index
44

55
```
6-
trivy plugin update PLUGIN_NAME
6+
trivy plugin update
77
```
88

99
### Options
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## trivy plugin upgrade
2+
3+
Upgrade installed plugins to newer versions
4+
5+
```
6+
trivy plugin upgrade [PLUGIN_NAMES]
7+
```
8+
9+
### Options
10+
11+
```
12+
-h, --help help for upgrade
13+
```
14+
15+
### Options inherited from parent commands
16+
17+
```
18+
--cache-dir string cache directory (default "/path/to/cache")
19+
-c, --config string config path (default "trivy.yaml")
20+
-d, --debug debug mode
21+
--generate-default-config write the default config to trivy-default.yaml
22+
--insecure allow insecure server connections
23+
-q, --quiet suppress progress bar and log output
24+
--timeout duration timeout (default 5m0s)
25+
-v, --version show version
26+
```
27+
28+
### SEE ALSO
29+
30+
* [trivy plugin](trivy_plugin.md) - Manage plugins
31+

‎mkdocs.yml

+18-11
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,12 @@ nav:
128128
- VEX: docs/supply-chain/vex.md
129129
- Compliance:
130130
- Reports: docs/compliance/compliance.md
131+
- Plugin:
132+
- Overview: docs/plugin/index.md
133+
- User Guide: docs/plugin/user-guide.md
134+
- Developer Guide: docs/plugin/developer-guide.md
131135
- Advanced:
132136
- Modules: docs/advanced/modules.md
133-
- Plugins: docs/advanced/plugins.md
134137
- Air-Gapped Environment: docs/advanced/air-gap.md
135138
- Container Image:
136139
- Embed in Dockerfile: docs/advanced/container/embed-in-dockerfile.md
@@ -152,16 +155,20 @@ nav:
152155
- Filesystem: docs/references/configuration/cli/trivy_filesystem.md
153156
- Image: docs/references/configuration/cli/trivy_image.md
154157
- Kubernetes: docs/references/configuration/cli/trivy_kubernetes.md
155-
- Module: docs/references/configuration/cli/trivy_module.md
156-
- Module Install: docs/references/configuration/cli/trivy_module_install.md
157-
- Module Uninstall: docs/references/configuration/cli/trivy_module_uninstall.md
158-
- Plugin: docs/references/configuration/cli/trivy_plugin.md
159-
- Plugin Info: docs/references/configuration/cli/trivy_plugin_info.md
160-
- Plugin Install: docs/references/configuration/cli/trivy_plugin_install.md
161-
- Plugin List: docs/references/configuration/cli/trivy_plugin_list.md
162-
- Plugin Run: docs/references/configuration/cli/trivy_plugin_run.md
163-
- Plugin Uninstall: docs/references/configuration/cli/trivy_plugin_uninstall.md
164-
- Plugin Update: docs/references/configuration/cli/trivy_plugin_update.md
158+
- Module:
159+
- Module: docs/references/configuration/cli/trivy_module.md
160+
- Module Install: docs/references/configuration/cli/trivy_module_install.md
161+
- Module Uninstall: docs/references/configuration/cli/trivy_module_uninstall.md
162+
- Plugin:
163+
- Plugin: docs/references/configuration/cli/trivy_plugin.md
164+
- Plugin Info: docs/references/configuration/cli/trivy_plugin_info.md
165+
- Plugin Install: docs/references/configuration/cli/trivy_plugin_install.md
166+
- Plugin List: docs/references/configuration/cli/trivy_plugin_list.md
167+
- Plugin Run: docs/references/configuration/cli/trivy_plugin_run.md
168+
- Plugin Uninstall: docs/references/configuration/cli/trivy_plugin_uninstall.md
169+
- Plugin Update: docs/references/configuration/cli/trivy_plugin_update.md
170+
- Plugin Upgrade: docs/references/configuration/cli/trivy_plugin_upgrade.md
171+
- Plugin Search: docs/references/configuration/cli/trivy_plugin_search.md
165172
- Repository: docs/references/configuration/cli/trivy_repository.md
166173
- Rootfs: docs/references/configuration/cli/trivy_rootfs.md
167174
- SBOM: docs/references/configuration/cli/trivy_sbom.md

‎pkg/clock/clock.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import (
88
clocktesting "k8s.io/utils/clock/testing"
99
)
1010

11+
type (
12+
RealClock = clock.RealClock
13+
FakeClock = clocktesting.FakeClock
14+
)
15+
1116
// clockKey is the context key for clock. It is unexported to prevent collisions with context keys defined in
1217
// other packages.
1318
type clockKey struct{}
@@ -27,7 +32,7 @@ func Now(ctx context.Context) time.Time {
2732
func Clock(ctx context.Context) clock.Clock {
2833
t, ok := ctx.Value(clockKey{}).(clock.Clock)
2934
if !ok {
30-
return clock.RealClock{}
35+
return RealClock{}
3136
}
3237
return t
3338
}

‎pkg/commands/app.go

+61-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -111,20 +112,24 @@ func NewApp() *cobra.Command {
111112
}
112113

113114
func loadPluginCommands() []*cobra.Command {
115+
ctx := context.Background()
116+
manager := plugin.NewManager()
117+
114118
var commands []*cobra.Command
115-
plugins, err := plugin.LoadAll()
119+
plugins, err := manager.LoadAll(ctx)
116120
if err != nil {
117-
log.Debug("No plugins loaded")
121+
log.DebugContext(ctx, "No plugins loaded")
118122
return nil
119123
}
120124
for _, p := range plugins {
121125
p := p
122126
cmd := &cobra.Command{
123127
Use: fmt.Sprintf("%s [flags]", p.Name),
124-
Short: p.Usage,
128+
Short: p.Summary,
129+
Long: p.Description,
125130
GroupID: groupPlugin,
126131
RunE: func(cmd *cobra.Command, args []string) error {
127-
if err = p.Run(cmd.Context(), plugin.RunOptions{Args: args}); err != nil {
132+
if err = p.Run(cmd.Context(), plugin.Options{Args: args}); err != nil {
128133
return xerrors.Errorf("plugin error: %w", err)
129134
}
130135
return nil
@@ -719,14 +724,15 @@ func NewPluginCommand() *cobra.Command {
719724
}
720725
cmd.AddCommand(
721726
&cobra.Command{
722-
Use: "install URL | FILE_PATH",
727+
Use: "install NAME | URL | FILE_PATH",
723728
Aliases: []string{"i"},
724729
Short: "Install a plugin",
725730
SilenceErrors: true,
731+
SilenceUsage: true,
726732
DisableFlagsInUseLine: true,
727733
Args: cobra.ExactArgs(1),
728734
RunE: func(cmd *cobra.Command, args []string) error {
729-
if _, err := plugin.Install(cmd.Context(), args[0], true); err != nil {
735+
if _, err := plugin.Install(cmd.Context(), args[0], plugin.Options{}); err != nil {
730736
return xerrors.Errorf("plugin install error: %w", err)
731737
}
732738
return nil
@@ -735,12 +741,13 @@ func NewPluginCommand() *cobra.Command {
735741
&cobra.Command{
736742
Use: "uninstall PLUGIN_NAME",
737743
Aliases: []string{"u"},
738-
SilenceErrors: true,
739744
DisableFlagsInUseLine: true,
740745
Short: "Uninstall a plugin",
746+
SilenceErrors: true,
747+
SilenceUsage: true,
741748
Args: cobra.ExactArgs(1),
742-
RunE: func(_ *cobra.Command, args []string) error {
743-
if err := plugin.Uninstall(args[0]); err != nil {
749+
RunE: func(cmd *cobra.Command, args []string) error {
750+
if err := plugin.Uninstall(cmd.Context(), args[0]); err != nil {
744751
return xerrors.Errorf("plugin uninstall error: %w", err)
745752
}
746753
return nil
@@ -749,62 +756,86 @@ func NewPluginCommand() *cobra.Command {
749756
&cobra.Command{
750757
Use: "list",
751758
Aliases: []string{"l"},
752-
SilenceErrors: true,
753759
DisableFlagsInUseLine: true,
760+
SilenceErrors: true,
761+
SilenceUsage: true,
754762
Short: "List installed plugin",
755763
Args: cobra.NoArgs,
756764
RunE: func(cmd *cobra.Command, args []string) error {
757-
info, err := plugin.List()
758-
if err != nil {
765+
if err := plugin.List(cmd.Context()); err != nil {
759766
return xerrors.Errorf("plugin list display error: %w", err)
760767
}
761-
if _, err := fmt.Fprint(os.Stdout, info); err != nil {
762-
return xerrors.Errorf("print error: %w", err)
763-
}
764768
return nil
765769
},
766770
},
767771
&cobra.Command{
768772
Use: "info PLUGIN_NAME",
769773
Short: "Show information about the specified plugin",
770-
SilenceErrors: true,
771774
DisableFlagsInUseLine: true,
775+
SilenceErrors: true,
776+
SilenceUsage: true,
772777
Args: cobra.ExactArgs(1),
773778
RunE: func(_ *cobra.Command, args []string) error {
774-
info, err := plugin.Information(args[0])
775-
if err != nil {
779+
if err := plugin.Information(args[0]); err != nil {
776780
return xerrors.Errorf("plugin information display error: %w", err)
777781
}
778-
if _, err := fmt.Fprint(os.Stdout, info); err != nil {
779-
return xerrors.Errorf("print error: %w", err)
780-
}
781782
return nil
782783
},
783784
},
784785
&cobra.Command{
785-
Use: "run URL | FILE_PATH",
786+
Use: "run NAME | URL | FILE_PATH",
786787
Aliases: []string{"r"},
787-
SilenceErrors: true,
788788
DisableFlagsInUseLine: true,
789+
SilenceErrors: true,
790+
SilenceUsage: true,
789791
Short: "Run a plugin on the fly",
790792
Args: cobra.MinimumNArgs(1),
791793
RunE: func(cmd *cobra.Command, args []string) error {
792-
return plugin.RunWithURL(cmd.Context(), args[0], plugin.RunOptions{Args: args[1:]})
794+
return plugin.RunWithURL(cmd.Context(), args[0], plugin.Options{Args: args[1:]})
793795
},
794796
},
795797
&cobra.Command{
796-
Use: "update PLUGIN_NAME",
797-
Short: "Update an existing plugin",
798-
SilenceErrors: true,
798+
Use: "update",
799+
Short: "Update the local copy of the plugin index",
799800
DisableFlagsInUseLine: true,
800-
Args: cobra.ExactArgs(1),
801-
RunE: func(_ *cobra.Command, args []string) error {
802-
if err := plugin.Update(args[0]); err != nil {
801+
SilenceErrors: true,
802+
SilenceUsage: true,
803+
Args: cobra.NoArgs,
804+
RunE: func(cmd *cobra.Command, _ []string) error {
805+
if err := plugin.Update(cmd.Context()); err != nil {
803806
return xerrors.Errorf("plugin update error: %w", err)
804807
}
805808
return nil
806809
},
807810
},
811+
&cobra.Command{
812+
Use: "search [KEYWORD]",
813+
DisableFlagsInUseLine: true,
814+
SilenceErrors: true,
815+
SilenceUsage: true,
816+
Short: "List Trivy plugins available on the plugin index and search among them",
817+
Args: cobra.MaximumNArgs(1),
818+
RunE: func(cmd *cobra.Command, args []string) error {
819+
var keyword string
820+
if len(args) == 1 {
821+
keyword = args[0]
822+
}
823+
return plugin.Search(cmd.Context(), keyword)
824+
},
825+
},
826+
&cobra.Command{
827+
Use: "upgrade [PLUGIN_NAMES]",
828+
Short: "Upgrade installed plugins to newer versions",
829+
DisableFlagsInUseLine: true,
830+
SilenceErrors: true,
831+
SilenceUsage: true,
832+
RunE: func(cmd *cobra.Command, args []string) error {
833+
if err := plugin.Upgrade(cmd.Context(), args); err != nil {
834+
return xerrors.Errorf("plugin upgrade error: %w", err)
835+
}
836+
return nil
837+
},
838+
},
808839
)
809840
cmd.SetFlagErrorFunc(flagErrorFunc)
810841
return cmd

‎pkg/flag/options.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() err
447447
pluginName := strings.TrimPrefix(o.Output, "plugin=")
448448

449449
pr, pw := io.Pipe()
450-
wait, err := plugin.Start(ctx, pluginName, plugin.RunOptions{
450+
wait, err := plugin.Start(ctx, pluginName, plugin.Options{
451451
Args: o.OutputPluginArgs,
452452
Stdin: pr,
453453
})

‎pkg/log/handler.go

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/fatih/color"
1515
"github.com/samber/lo"
1616
"golang.org/x/xerrors"
17+
18+
"github.com/aquasecurity/trivy/pkg/clock"
1719
)
1820

1921
const (
@@ -145,6 +147,11 @@ func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error {
145147
freeBuf(bufp)
146148
}()
147149

150+
// For tests, use the fake clock's time.
151+
if c, ok := clock.Clock(ctx).(*clock.FakeClock); ok {
152+
r.Time = c.Now()
153+
}
154+
148155
buf = h.handle(ctx, buf, r)
149156

150157
h.mu.Lock()

‎pkg/plugin/index.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package plugin
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/samber/lo"
13+
"golang.org/x/xerrors"
14+
"gopkg.in/yaml.v3"
15+
16+
"github.com/aquasecurity/trivy/pkg/downloader"
17+
"github.com/aquasecurity/trivy/pkg/log"
18+
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
19+
)
20+
21+
const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml"
22+
23+
type Index struct {
24+
Version int `yaml:"version"`
25+
Plugins []struct {
26+
Name string `yaml:"name"`
27+
Maintainer string `yaml:"maintainer"`
28+
Summary string `yaml:"summary"`
29+
Repository string `yaml:"repository"`
30+
Output bool `yaml:"output"`
31+
} `yaml:"plugins"`
32+
}
33+
34+
func (m *Manager) Update(ctx context.Context) error {
35+
m.logger.InfoContext(ctx, "Updating the plugin index...", log.String("url", m.indexURL))
36+
if err := downloader.Download(ctx, m.indexURL, filepath.Dir(m.indexPath), ""); err != nil {
37+
return xerrors.Errorf("unable to download the plugin index: %w", err)
38+
}
39+
return nil
40+
}
41+
42+
func (m *Manager) Search(ctx context.Context, keyword string) error {
43+
index, err := m.loadIndex()
44+
if errors.Is(err, os.ErrNotExist) {
45+
m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.")
46+
return xerrors.Errorf("plugin index not found: %w", err)
47+
} else if err != nil {
48+
return xerrors.Errorf("unable to load the plugin index: %w", err)
49+
}
50+
51+
var buf bytes.Buffer
52+
buf.WriteString(fmt.Sprintf("%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT"))
53+
for _, p := range index.Plugins {
54+
if keyword == "" || strings.Contains(p.Name, keyword) || strings.Contains(p.Summary, keyword) {
55+
s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(p.Name, 20),
56+
truncateString(p.Summary, 60), truncateString(p.Maintainer, 20),
57+
lo.Ternary(p.Output, " ✓", ""))
58+
buf.WriteString(s)
59+
}
60+
}
61+
62+
if _, err = fmt.Fprintf(m.w, buf.String()); err != nil {
63+
return err
64+
}
65+
66+
return nil
67+
}
68+
69+
// tryIndex returns the repository URL if the plugin name is found in the index.
70+
// Otherwise, it returns the input name.
71+
func (m *Manager) tryIndex(ctx context.Context, name string) string {
72+
// If the index file does not exist, download it first.
73+
if !fsutils.FileExists(m.indexPath) {
74+
if err := m.Update(ctx); err != nil {
75+
m.logger.ErrorContext(ctx, "Failed to update the plugin index", log.Err(err))
76+
return name
77+
}
78+
}
79+
80+
index, err := m.loadIndex()
81+
if errors.Is(err, os.ErrNotExist) {
82+
m.logger.WarnContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.")
83+
return name
84+
} else if err != nil {
85+
m.logger.ErrorContext(ctx, "Unable to load the plugin index: %w", err)
86+
return name
87+
}
88+
89+
for _, p := range index.Plugins {
90+
if p.Name == name {
91+
return p.Repository
92+
}
93+
}
94+
return name
95+
}
96+
97+
func (m *Manager) loadIndex() (*Index, error) {
98+
f, err := os.Open(m.indexPath)
99+
if err != nil {
100+
return nil, xerrors.Errorf("unable to open the index file: %w", err)
101+
}
102+
defer f.Close()
103+
104+
var index Index
105+
if err = yaml.NewDecoder(f).Decode(&index); err != nil {
106+
return nil, xerrors.Errorf("unable to decode the index file: %w", err)
107+
}
108+
109+
return &index, nil
110+
}
111+
112+
func truncateString(str string, num int) string {
113+
if len(str) <= num {
114+
return str
115+
}
116+
return str[:num-3] + "..."
117+
}

‎pkg/plugin/index_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package plugin_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"github.com/aquasecurity/trivy/pkg/plugin"
7+
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"net/http"
11+
"net/http/httptest"
12+
"os"
13+
"path/filepath"
14+
"testing"
15+
)
16+
17+
func TestManager_Update(t *testing.T) {
18+
tempDir := t.TempDir()
19+
fsutils.SetCacheDir(tempDir)
20+
21+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22+
_, err := w.Write([]byte(`this is index`))
23+
require.NoError(t, err)
24+
}))
25+
t.Cleanup(ts.Close)
26+
27+
manager := plugin.NewManager(plugin.WithIndexURL(ts.URL + "/index.yaml"))
28+
err := manager.Update(context.Background())
29+
require.NoError(t, err)
30+
31+
indexPath := filepath.Join(tempDir, "plugin", "index.yaml")
32+
assert.FileExists(t, indexPath)
33+
34+
b, err := os.ReadFile(indexPath)
35+
require.NoError(t, err)
36+
assert.Equal(t, "this is index", string(b))
37+
}
38+
39+
func TestManager_Search(t *testing.T) {
40+
tests := []struct {
41+
name string
42+
keyword string
43+
dir string
44+
want string
45+
wantErr string
46+
}{
47+
{
48+
name: "all plugins",
49+
keyword: "",
50+
dir: "testdata",
51+
want: `NAME DESCRIPTION MAINTAINER OUTPUT
52+
foo A foo plugin aquasecurity ✓
53+
bar A bar plugin aquasecurity
54+
test A test plugin aquasecurity
55+
`,
56+
},
57+
{
58+
name: "keyword",
59+
keyword: "bar",
60+
dir: "testdata",
61+
want: `NAME DESCRIPTION MAINTAINER OUTPUT
62+
bar A bar plugin aquasecurity
63+
`,
64+
},
65+
{
66+
name: "no index",
67+
keyword: "",
68+
dir: "unknown",
69+
wantErr: "plugin index not found",
70+
},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
fsutils.SetCacheDir(tt.dir)
75+
76+
var got bytes.Buffer
77+
m := plugin.NewManager(plugin.WithWriter(&got))
78+
err := m.Search(context.Background(), tt.keyword)
79+
if tt.wantErr != "" {
80+
require.ErrorContains(t, err, tt.wantErr)
81+
return
82+
}
83+
require.NoError(t, err)
84+
assert.Equal(t, tt.want, got.String())
85+
})
86+
}
87+
}

‎pkg/plugin/manager.go

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
"github.com/samber/lo"
13+
"golang.org/x/xerrors"
14+
"gopkg.in/yaml.v3"
15+
16+
"github.com/aquasecurity/trivy/pkg/downloader"
17+
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
18+
"github.com/aquasecurity/trivy/pkg/log"
19+
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
20+
)
21+
22+
const configFile = "plugin.yaml"
23+
24+
var (
25+
pluginsRelativeDir = filepath.Join(".trivy", "plugins")
26+
27+
_defaultManager *Manager
28+
)
29+
30+
type ManagerOption func(indexer *Manager)
31+
32+
func WithWriter(w io.Writer) ManagerOption {
33+
return func(indexer *Manager) {
34+
indexer.w = w
35+
}
36+
}
37+
38+
func WithIndexURL(indexURL string) ManagerOption {
39+
return func(indexer *Manager) {
40+
indexer.indexURL = indexURL
41+
}
42+
}
43+
44+
// Manager manages the plugins
45+
type Manager struct {
46+
w io.Writer
47+
indexURL string
48+
logger *log.Logger
49+
pluginRoot string
50+
indexPath string
51+
}
52+
53+
func NewManager(opts ...ManagerOption) *Manager {
54+
m := &Manager{
55+
w: os.Stdout,
56+
indexURL: indexURL,
57+
logger: log.WithPrefix("plugin"),
58+
pluginRoot: filepath.Join(fsutils.HomeDir(), pluginsRelativeDir),
59+
indexPath: filepath.Join(fsutils.CacheDir(), "plugin", "index.yaml"),
60+
}
61+
for _, opt := range opts {
62+
opt(m)
63+
}
64+
return m
65+
}
66+
67+
func defaultManager() *Manager {
68+
if _defaultManager == nil {
69+
_defaultManager = NewManager()
70+
}
71+
return _defaultManager
72+
}
73+
74+
func Install(ctx context.Context, name string, opts Options) (Plugin, error) {
75+
return defaultManager().Install(ctx, name, opts)
76+
}
77+
func Start(ctx context.Context, name string, opts Options) (Wait, error) {
78+
return defaultManager().Start(ctx, name, opts)
79+
}
80+
func RunWithURL(ctx context.Context, name string, opts Options) error {
81+
return defaultManager().RunWithURL(ctx, name, opts)
82+
}
83+
func Upgrade(ctx context.Context, names []string) error { return defaultManager().Upgrade(ctx, names) }
84+
func Uninstall(ctx context.Context, name string) error { return defaultManager().Uninstall(ctx, name) }
85+
func Information(name string) error { return defaultManager().Information(name) }
86+
func List(ctx context.Context) error { return defaultManager().List(ctx) }
87+
func Update(ctx context.Context) error { return defaultManager().Update(ctx) }
88+
func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) }
89+
90+
// Install installs a plugin
91+
func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) {
92+
src := m.tryIndex(ctx, name)
93+
94+
// If the plugin is already installed, it skips installing the plugin.
95+
if p, installed := m.isInstalled(ctx, src); installed {
96+
m.logger.InfoContext(ctx, "The plugin is already installed", log.String("name", p.Name))
97+
return p, nil
98+
}
99+
100+
m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", src))
101+
return m.install(ctx, src, opts)
102+
}
103+
104+
func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) {
105+
tempDir, err := downloader.DownloadToTempDir(ctx, src)
106+
if err != nil {
107+
return Plugin{}, xerrors.Errorf("download failed: %w", err)
108+
}
109+
defer os.RemoveAll(tempDir)
110+
111+
m.logger.DebugContext(ctx, "Loading the plugin metadata...")
112+
plugin, err := m.loadMetadata(tempDir)
113+
if err != nil {
114+
return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err)
115+
}
116+
117+
if err = plugin.install(ctx, plugin.Dir(), tempDir, opts); err != nil {
118+
return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err)
119+
}
120+
121+
// Copy plugin.yaml into the plugin dir
122+
f, err := os.Create(filepath.Join(plugin.Dir(), configFile))
123+
if err != nil {
124+
return Plugin{}, xerrors.Errorf("failed to create plugin.yaml: %w", err)
125+
}
126+
defer f.Close()
127+
128+
if err = yaml.NewEncoder(f).Encode(plugin); err != nil {
129+
return Plugin{}, xerrors.Errorf("yaml encode error: %w", err)
130+
}
131+
132+
m.logger.InfoContext(ctx, "Plugin successfully installed", log.String("name", plugin.Name))
133+
134+
return plugin, nil
135+
}
136+
137+
// Uninstall installs the plugin
138+
func (m *Manager) Uninstall(ctx context.Context, name string) error {
139+
pluginDir := filepath.Join(m.pluginRoot, name)
140+
if !fsutils.DirExists(pluginDir) {
141+
m.logger.ErrorContext(ctx, "No such plugin")
142+
return nil
143+
}
144+
if err := os.RemoveAll(pluginDir); err != nil {
145+
return xerrors.Errorf("failed to uninstall the plugin: %w", err)
146+
}
147+
m.logger.InfoContext(ctx, "Plugin successfully uninstalled", log.String("name", name))
148+
return nil
149+
}
150+
151+
// Information gets the information about an installed plugin
152+
func (m *Manager) Information(name string) error {
153+
plugin, err := m.load(name)
154+
if err != nil {
155+
return xerrors.Errorf("plugin load error: %w", err)
156+
}
157+
158+
_, err = fmt.Fprintf(m.w, `
159+
Plugin: %s
160+
Version: %s
161+
Summary: %s
162+
Description: %s
163+
`, plugin.Name, plugin.Version, plugin.Summary, plugin.Description)
164+
165+
return err
166+
}
167+
168+
// List gets a list of all installed plugins
169+
func (m *Manager) List(ctx context.Context) error {
170+
s, err := m.list(ctx)
171+
if err != nil {
172+
return xerrors.Errorf("unable to list plugins: %w", err)
173+
}
174+
_, err = fmt.Fprintf(m.w, "%s\n", s)
175+
return err
176+
}
177+
178+
func (m *Manager) list(ctx context.Context) (string, error) {
179+
if _, err := os.Stat(m.pluginRoot); err != nil {
180+
if os.IsNotExist(err) {
181+
return "No Installed Plugins", nil
182+
}
183+
return "", xerrors.Errorf("stat error: %w", err)
184+
}
185+
plugins, err := m.LoadAll(ctx)
186+
if err != nil {
187+
return "", xerrors.Errorf("unable to load plugins: %w", err)
188+
} else if len(plugins) == 0 {
189+
return "No Installed Plugins", nil
190+
}
191+
pluginList := []string{"Installed Plugins:"}
192+
for _, plugin := range plugins {
193+
pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version))
194+
}
195+
196+
return strings.Join(pluginList, "\n"), nil
197+
}
198+
199+
// Upgrade upgrades an existing plugins
200+
func (m *Manager) Upgrade(ctx context.Context, names []string) error {
201+
if len(names) == 0 {
202+
plugins, err := m.LoadAll(ctx)
203+
if err != nil {
204+
return xerrors.Errorf("unable to load plugins: %w", err)
205+
} else if len(plugins) == 0 {
206+
m.logger.InfoContext(ctx, "No installed plugins")
207+
return nil
208+
}
209+
names = lo.Map(plugins, func(p Plugin, _ int) string { return p.Name })
210+
}
211+
for _, name := range names {
212+
if err := m.upgrade(ctx, name); err != nil {
213+
return xerrors.Errorf("unable to upgrade '%s' plugin: %w", name, err)
214+
}
215+
}
216+
return nil
217+
}
218+
219+
func (m *Manager) upgrade(ctx context.Context, name string) error {
220+
plugin, err := m.load(name)
221+
if err != nil {
222+
return xerrors.Errorf("plugin load error: %w", err)
223+
}
224+
225+
logger := m.logger.With("name", name)
226+
logger.InfoContext(ctx, "Upgrading plugin...")
227+
updated, err := m.install(ctx, plugin.Repository, Options{
228+
// Use the current installed platform
229+
Platform: ftypes.Platform{
230+
Platform: &v1.Platform{
231+
OS: plugin.Installed.Platform.OS,
232+
Architecture: plugin.Installed.Platform.Arch,
233+
},
234+
},
235+
})
236+
if err != nil {
237+
return xerrors.Errorf("unable to perform an upgrade installation: %w", err)
238+
}
239+
240+
if plugin.Version == updated.Version {
241+
logger.InfoContext(ctx, "The plugin is up-to-date", log.String("version", plugin.Version))
242+
} else {
243+
logger.InfoContext(ctx, "Plugin upgraded",
244+
log.String("from", plugin.Version), log.String("to", updated.Version))
245+
}
246+
return nil
247+
}
248+
249+
// LoadAll loads all plugins
250+
func (m *Manager) LoadAll(ctx context.Context) ([]Plugin, error) {
251+
dirs, err := os.ReadDir(m.pluginRoot)
252+
if err != nil {
253+
return nil, xerrors.Errorf("failed to read %s: %w", m.pluginRoot, err)
254+
}
255+
256+
var plugins []Plugin
257+
for _, d := range dirs {
258+
if !d.IsDir() {
259+
continue
260+
}
261+
plugin, err := m.loadMetadata(filepath.Join(m.pluginRoot, d.Name()))
262+
if err != nil {
263+
m.logger.WarnContext(ctx, "Plugin load error", log.Err(err))
264+
continue
265+
}
266+
plugins = append(plugins, plugin)
267+
}
268+
return plugins, nil
269+
}
270+
271+
// Start starts the plugin
272+
func (m *Manager) Start(ctx context.Context, name string, opts Options) (Wait, error) {
273+
plugin, err := m.load(name)
274+
if err != nil {
275+
return nil, xerrors.Errorf("plugin load error: %w", err)
276+
}
277+
278+
wait, err := plugin.Start(ctx, opts)
279+
if err != nil {
280+
return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
281+
}
282+
return wait, nil
283+
}
284+
285+
// RunWithURL runs the plugin
286+
func (m *Manager) RunWithURL(ctx context.Context, name string, opts Options) error {
287+
plugin, err := m.Install(ctx, name, opts)
288+
if err != nil {
289+
return xerrors.Errorf("plugin install error: %w", err)
290+
}
291+
292+
if err = plugin.Run(ctx, opts); err != nil {
293+
return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
294+
}
295+
return nil
296+
}
297+
298+
func (m *Manager) load(name string) (Plugin, error) {
299+
pluginDir := filepath.Join(m.pluginRoot, name)
300+
if _, err := os.Stat(pluginDir); err != nil {
301+
if os.IsNotExist(err) {
302+
return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
303+
}
304+
return Plugin{}, xerrors.Errorf("plugin stat error: %w", err)
305+
}
306+
307+
plugin, err := m.loadMetadata(pluginDir)
308+
if err != nil {
309+
return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err)
310+
}
311+
312+
return plugin, nil
313+
}
314+
315+
func (m *Manager) loadMetadata(dir string) (Plugin, error) {
316+
filePath := filepath.Join(dir, configFile)
317+
f, err := os.Open(filePath)
318+
if err != nil {
319+
return Plugin{}, xerrors.Errorf("file open error: %w", err)
320+
}
321+
defer f.Close()
322+
323+
var plugin Plugin
324+
if err = yaml.NewDecoder(f).Decode(&plugin); err != nil {
325+
return Plugin{}, xerrors.Errorf("yaml decode error: %w", err)
326+
}
327+
328+
if plugin.Name == "" {
329+
return Plugin{}, xerrors.Errorf("'name' is empty")
330+
}
331+
332+
// e.g. ~/.trivy/plugins/kubectl
333+
plugin.dir = filepath.Join(m.pluginRoot, plugin.Name)
334+
335+
if plugin.Summary == "" && plugin.Usage != "" {
336+
plugin.Summary = plugin.Usage // For backward compatibility
337+
plugin.Usage = ""
338+
}
339+
340+
return plugin, nil
341+
}
342+
343+
func (m *Manager) isInstalled(ctx context.Context, url string) (Plugin, bool) {
344+
installedPlugins, err := m.LoadAll(ctx)
345+
if err != nil {
346+
return Plugin{}, false
347+
}
348+
349+
for _, plugin := range installedPlugins {
350+
if plugin.Repository == url {
351+
return plugin, true
352+
}
353+
}
354+
return Plugin{}, false
355+
}

‎pkg/plugin/plugin_test.go renamed to ‎pkg/plugin/manager_test.go

+171-144
Large diffs are not rendered by default.

‎pkg/plugin/plugin.go

+43-263
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,41 @@ package plugin
33
import (
44
"context"
55
"errors"
6-
"fmt"
76
"io"
87
"os"
98
"os/exec"
109
"path/filepath"
1110
"runtime"
12-
"strings"
1311

12+
"github.com/samber/lo"
1413
"golang.org/x/xerrors"
15-
"gopkg.in/yaml.v3"
1614

1715
"github.com/aquasecurity/trivy/pkg/downloader"
16+
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
1817
"github.com/aquasecurity/trivy/pkg/log"
1918
"github.com/aquasecurity/trivy/pkg/types"
2019
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
2120
)
2221

23-
const (
24-
configFile = "plugin.yaml"
25-
)
26-
27-
var (
28-
pluginsRelativeDir = filepath.Join(".trivy", "plugins")
29-
30-
officialPlugins = map[string]string{
31-
"kubectl": "github.com/aquasecurity/trivy-plugin-kubectl",
32-
"aqua": "github.com/aquasecurity/trivy-plugin-aqua",
33-
}
34-
)
35-
3622
// Plugin represents a plugin.
3723
type Plugin struct {
3824
Name string `yaml:"name"`
3925
Repository string `yaml:"repository"`
4026
Version string `yaml:"version"`
41-
Usage string `yaml:"usage"`
27+
Summary string `yaml:"summary"`
28+
Usage string `yaml:"usage"` // Deprecated: Use summary instead
4229
Description string `yaml:"description"`
4330
Platforms []Platform `yaml:"platforms"`
4431

45-
// runtime environment for testability
46-
GOOS string `yaml:"_goos"`
47-
GOARCH string `yaml:"_goarch"`
32+
// Installed holds the metadata about installation
33+
Installed Installed `yaml:"installed"`
34+
35+
// dir points to the directory where the plugin is installed
36+
dir string
37+
}
38+
39+
type Installed struct {
40+
Platform Selector `yaml:"platform"`
4841
}
4942

5043
// Platform represents where the execution file exists per platform.
@@ -56,22 +49,23 @@ type Platform struct {
5649

5750
// Selector represents the environment.
5851
type Selector struct {
59-
OS string
60-
Arch string
52+
OS string `yaml:"os"`
53+
Arch string `yaml:"arch"`
6154
}
6255

63-
type RunOptions struct {
64-
Args []string
65-
Stdin io.Reader
56+
type Options struct {
57+
Args []string
58+
Stdin io.Reader // For output plugin
59+
Platform ftypes.Platform
6660
}
6761

68-
func (p Plugin) Cmd(ctx context.Context, opts RunOptions) (*exec.Cmd, error) {
69-
platform, err := p.selectPlatform()
62+
func (p *Plugin) Cmd(ctx context.Context, opts Options) (*exec.Cmd, error) {
63+
platform, err := p.selectPlatform(ctx, opts)
7064
if err != nil {
7165
return nil, xerrors.Errorf("platform selection error: %w", err)
7266
}
7367

74-
execFile := filepath.Join(dir(), p.Name, platform.Bin)
68+
execFile := filepath.Join(p.Dir(), platform.Bin)
7569

7670
cmd := exec.CommandContext(ctx, execFile, opts.Args...)
7771
cmd.Stdin = os.Stdin
@@ -90,7 +84,7 @@ type Wait func() error
9084
// Start starts the plugin
9185
//
9286
// After a successful call to Start the Wait method must be called.
93-
func (p Plugin) Start(ctx context.Context, opts RunOptions) (Wait, error) {
87+
func (p *Plugin) Start(ctx context.Context, opts Options) (Wait, error) {
9488
cmd, err := p.Cmd(ctx, opts)
9589
if err != nil {
9690
return nil, xerrors.Errorf("cmd: %w", err)
@@ -103,7 +97,7 @@ func (p Plugin) Start(ctx context.Context, opts RunOptions) (Wait, error) {
10397
}
10498

10599
// Run runs the plugin
106-
func (p Plugin) Run(ctx context.Context, opts RunOptions) error {
100+
func (p *Plugin) Run(ctx context.Context, opts Options) error {
107101
cmd, err := p.Cmd(ctx, opts)
108102
if err != nil {
109103
return xerrors.Errorf("cmd: %w", err)
@@ -124,13 +118,15 @@ func (p Plugin) Run(ctx context.Context, opts RunOptions) error {
124118
return nil
125119
}
126120

127-
func (p Plugin) selectPlatform() (Platform, error) {
121+
func (p *Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, error) {
128122
// These values are only filled in during unit tests.
129-
if p.GOOS == "" {
130-
p.GOOS = runtime.GOOS
123+
goos := runtime.GOOS
124+
if opts.Platform.Platform != nil && opts.Platform.OS != "" {
125+
goos = opts.Platform.OS
131126
}
132-
if p.GOARCH == "" {
133-
p.GOARCH = runtime.GOARCH
127+
goarch := runtime.GOARCH
128+
if opts.Platform.Platform != nil && opts.Platform.Architecture != "" {
129+
goarch = opts.Platform.Architecture
134130
}
135131

136132
for _, platform := range p.Platforms {
@@ -139,250 +135,34 @@ func (p Plugin) selectPlatform() (Platform, error) {
139135
}
140136

141137
selector := platform.Selector
142-
if (selector.OS == "" || p.GOOS == selector.OS) &&
143-
(selector.Arch == "" || p.GOARCH == selector.Arch) {
144-
log.Debug("Platform found",
138+
if (selector.OS == "" || goos == selector.OS) &&
139+
(selector.Arch == "" || goarch == selector.Arch) {
140+
log.DebugContext(ctx, "Platform found",
145141
log.String("os", selector.OS), log.String("arch", selector.Arch))
146142
return platform, nil
147143
}
148144
}
149145
return Platform{}, xerrors.New("platform not found")
150146
}
151147

152-
func (p Plugin) install(ctx context.Context, dst, pwd string) error {
153-
log.Debug("Installing the plugin...", log.String("path", dst))
154-
platform, err := p.selectPlatform()
148+
func (p *Plugin) install(ctx context.Context, dst, pwd string, opts Options) error {
149+
log.DebugContext(ctx, "Installing the plugin...", log.String("path", dst))
150+
platform, err := p.selectPlatform(ctx, opts)
155151
if err != nil {
156152
return xerrors.Errorf("platform selection error: %w", err)
157153
}
154+
p.Installed.Platform = lo.FromPtr(platform.Selector)
158155

159-
log.Debug("Downloading the execution file...", log.String("uri", platform.URI))
156+
log.DebugContext(ctx, "Downloading the execution file...", log.String("uri", platform.URI))
160157
if err = downloader.Download(ctx, platform.URI, dst, pwd); err != nil {
161158
return xerrors.Errorf("unable to download the execution file (%s): %w", platform.URI, err)
162159
}
163160
return nil
164161
}
165162

166-
func (p Plugin) dir() (string, error) {
167-
if p.Name == "" {
168-
return "", xerrors.Errorf("'name' is empty")
169-
}
170-
171-
// e.g. ~/.trivy/plugins/kubectl
172-
return filepath.Join(dir(), p.Name), nil
173-
}
174-
175-
// Install installs a plugin
176-
func Install(ctx context.Context, url string, force bool) (Plugin, error) {
177-
// Replace short names with full qualified names
178-
// e.g. kubectl => github.com/aquasecurity/trivy-plugin-kubectl
179-
if v, ok := officialPlugins[url]; ok {
180-
url = v
181-
}
182-
183-
if !force {
184-
// If the plugin is already installed, it skips installing the plugin.
185-
if p, installed := isInstalled(url); installed {
186-
return p, nil
187-
}
188-
}
189-
190-
log.Info("Installing the plugin...", log.String("url", url))
191-
tempDir, err := downloader.DownloadToTempDir(ctx, url)
192-
if err != nil {
193-
return Plugin{}, xerrors.Errorf("download failed: %w", err)
194-
}
195-
defer os.RemoveAll(tempDir)
196-
197-
log.Info("Loading the plugin metadata...")
198-
plugin, err := loadMetadata(tempDir)
199-
if err != nil {
200-
return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err)
201-
}
202-
203-
pluginDir, err := plugin.dir()
204-
if err != nil {
205-
return Plugin{}, xerrors.Errorf("failed to determine the plugin dir: %w", err)
206-
}
207-
208-
if err = plugin.install(ctx, pluginDir, tempDir); err != nil {
209-
return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err)
210-
}
211-
212-
// Copy plugin.yaml into the plugin dir
213-
if _, err = fsutils.CopyFile(filepath.Join(tempDir, configFile), filepath.Join(pluginDir, configFile)); err != nil {
214-
return Plugin{}, xerrors.Errorf("failed to copy plugin.yaml: %w", err)
215-
}
216-
217-
return plugin, nil
218-
}
219-
220-
// Uninstall installs the plugin
221-
func Uninstall(name string) error {
222-
pluginDir := filepath.Join(dir(), name)
223-
return os.RemoveAll(pluginDir)
224-
}
225-
226-
// Information gets the information about an installed plugin
227-
func Information(name string) (string, error) {
228-
plugin, err := load(name)
229-
if err != nil {
230-
return "", xerrors.Errorf("plugin load error: %w", err)
231-
}
232-
233-
return fmt.Sprintf(`
234-
Plugin: %s
235-
Description: %s
236-
Version: %s
237-
Usage: %s
238-
`, plugin.Name, plugin.Description, plugin.Version, plugin.Usage), nil
239-
}
240-
241-
// List gets a list of all installed plugins
242-
func List() (string, error) {
243-
if _, err := os.Stat(dir()); err != nil {
244-
if os.IsNotExist(err) {
245-
return "No Installed Plugins\n", nil
246-
}
247-
return "", xerrors.Errorf("stat error: %w", err)
248-
}
249-
plugins, err := LoadAll()
250-
if err != nil {
251-
return "", xerrors.Errorf("unable to load plugins: %w", err)
252-
}
253-
pluginList := []string{"Installed Plugins:"}
254-
for _, plugin := range plugins {
255-
pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version))
256-
}
257-
258-
return strings.Join(pluginList, "\n"), nil
259-
}
260-
261-
// Update updates an existing plugin
262-
func Update(name string) error {
263-
plugin, err := load(name)
264-
if err != nil {
265-
return xerrors.Errorf("plugin load error: %w", err)
266-
}
267-
268-
logger := log.With("name", name)
269-
logger.Info("Updating plugin...")
270-
updated, err := Install(nil, plugin.Repository, true)
271-
if err != nil {
272-
return xerrors.Errorf("unable to perform an update installation: %w", err)
273-
}
274-
275-
if plugin.Version == updated.Version {
276-
logger.Info("The plugin is up-to-date", log.String("version", plugin.Version))
277-
} else {
278-
logger.Info("Plugin updated",
279-
log.String("from", plugin.Version), log.String("to", updated.Version))
280-
}
281-
return nil
282-
}
283-
284-
// LoadAll loads all plugins
285-
func LoadAll() ([]Plugin, error) {
286-
pluginsDir := dir()
287-
dirs, err := os.ReadDir(pluginsDir)
288-
if err != nil {
289-
return nil, xerrors.Errorf("failed to read %s: %w", pluginsDir, err)
290-
}
291-
292-
var plugins []Plugin
293-
for _, d := range dirs {
294-
if !d.IsDir() {
295-
continue
296-
}
297-
plugin, err := loadMetadata(filepath.Join(pluginsDir, d.Name()))
298-
if err != nil {
299-
log.Warn("Plugin load error", log.Err(err))
300-
continue
301-
}
302-
plugins = append(plugins, plugin)
303-
}
304-
return plugins, nil
305-
}
306-
307-
// Start starts the plugin
308-
func Start(ctx context.Context, name string, opts RunOptions) (Wait, error) {
309-
plugin, err := load(name)
310-
if err != nil {
311-
return nil, xerrors.Errorf("plugin load error: %w", err)
312-
}
313-
314-
wait, err := plugin.Start(ctx, opts)
315-
if err != nil {
316-
return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
317-
}
318-
return wait, nil
319-
}
320-
321-
// RunWithURL runs the plugin with URL
322-
func RunWithURL(ctx context.Context, url string, opts RunOptions) error {
323-
plugin, err := Install(ctx, url, false)
324-
if err != nil {
325-
return xerrors.Errorf("plugin install error: %w", err)
326-
}
327-
328-
if err = plugin.Run(ctx, opts); err != nil {
329-
return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
330-
}
331-
return nil
332-
}
333-
334-
func IsPredefined(name string) bool {
335-
_, ok := officialPlugins[name]
336-
return ok
337-
}
338-
339-
func load(name string) (Plugin, error) {
340-
pluginDir := filepath.Join(dir(), name)
341-
if _, err := os.Stat(pluginDir); err != nil {
342-
if os.IsNotExist(err) {
343-
return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
344-
}
345-
return Plugin{}, xerrors.Errorf("plugin stat error: %w", err)
346-
}
347-
348-
plugin, err := loadMetadata(pluginDir)
349-
if err != nil {
350-
return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err)
351-
}
352-
353-
return plugin, nil
354-
}
355-
356-
func loadMetadata(dir string) (Plugin, error) {
357-
filePath := filepath.Join(dir, configFile)
358-
f, err := os.Open(filePath)
359-
if err != nil {
360-
return Plugin{}, xerrors.Errorf("file open error: %w", err)
361-
}
362-
defer f.Close()
363-
364-
var plugin Plugin
365-
if err = yaml.NewDecoder(f).Decode(&plugin); err != nil {
366-
return Plugin{}, xerrors.Errorf("yaml decode error: %w", err)
367-
}
368-
369-
return plugin, nil
370-
}
371-
372-
func dir() string {
373-
return filepath.Join(fsutils.HomeDir(), pluginsRelativeDir)
374-
}
375-
376-
func isInstalled(url string) (Plugin, bool) {
377-
installedPlugins, err := LoadAll()
378-
if err != nil {
379-
return Plugin{}, false
380-
}
381-
382-
for _, plugin := range installedPlugins {
383-
if plugin.Repository == url {
384-
return plugin, true
385-
}
163+
func (p *Plugin) Dir() string {
164+
if p.dir != "" {
165+
return p.dir
386166
}
387-
return Plugin{}, false
167+
return filepath.Join(fsutils.HomeDir(), pluginsRelativeDir, p.Name)
388168
}

‎pkg/plugin/testdata/plugin/index.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: 1
2+
plugins:
3+
- name: foo
4+
output: true
5+
maintainer: aquasecurity
6+
summary: A foo plugin
7+
repository: github.com/aquasecurity/trivy-plugin-foo
8+
- name: bar
9+
maintainer: aquasecurity
10+
summary: A bar plugin
11+
repository: github.com/aquasecurity/trivy-plugin-bar
12+
- name: test
13+
maintainer: aquasecurity
14+
summary: A test plugin
15+
repository: testdata/test_plugin

‎pkg/plugin/testdata/test_plugin/plugin.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: "test_plugin"
22
repository: github.com/aquasecurity/trivy-plugin-test
33
version: "0.1.0"
4-
usage: test
4+
summary: test
55
description: test
66
platforms:
77
- selector:

‎pkg/utils/fsutils/fs.go

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fsutils
22

33
import (
4+
"errors"
45
"fmt"
56
"io"
67
"io/fs"
@@ -84,6 +85,14 @@ func DirExists(path string) bool {
8485
return true
8586
}
8687

88+
func FileExists(filename string) bool {
89+
_, err := os.Stat(filename)
90+
if errors.Is(err, os.ErrNotExist) {
91+
return false
92+
}
93+
return err == nil
94+
}
95+
8796
type WalkDirRequiredFunc func(path string, d fs.DirEntry) bool
8897

8998
type WalkDirFunc func(path string, d fs.DirEntry, r io.Reader) error

0 commit comments

Comments
 (0)
Please sign in to comment.