From 822a133198cc524b053a78d54e94983a34e1521e Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Thu, 9 May 2024 21:29:52 +0400 Subject: [PATCH 01/26] feat: add support for plugin index Signed-off-by: knqyf263 --- .github/workflows/semantic-pr.yaml | 1 + cmd/trivy/main.go | 5 +- docs/community/contribute/pr.md | 1 + pkg/clock/clock.go | 7 +- pkg/commands/app.go | 68 ++++-- pkg/flag/options.go | 2 +- pkg/log/handler.go | 7 + pkg/plugin/index.go | 112 +++++++++ pkg/plugin/index_test.go | 87 +++++++ pkg/plugin/manager.go | 318 ++++++++++++++++++++++++++ pkg/plugin/plugin.go | 290 +++-------------------- pkg/plugin/plugin_test.go | 282 ++++++++++++----------- pkg/plugin/testdata/plugin/index.yaml | 15 ++ pkg/utils/fsutils/fs.go | 9 + 14 files changed, 786 insertions(+), 418 deletions(-) create mode 100644 pkg/plugin/index.go create mode 100644 pkg/plugin/index_test.go create mode 100644 pkg/plugin/manager.go create mode 100644 pkg/plugin/testdata/plugin/index.yaml diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml index f02ef758ae91..ead9c4cccdd8 100644 --- a/.github/workflows/semantic-pr.yaml +++ b/.github/workflows/semantic-pr.yaml @@ -44,6 +44,7 @@ jobs: k8s aws vm + plugin alpine wolfi diff --git a/cmd/trivy/main.go b/cmd/trivy/main.go index dbff5fa54ab1..9e4fe4f5d639 100644 --- a/cmd/trivy/main.go +++ b/cmd/trivy/main.go @@ -28,10 +28,7 @@ func main() { func run() error { // Trivy behaves as the specified plugin. if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" { - if !plugin.IsPredefined(runAsPlugin) { - return xerrors.Errorf("unknown plugin: %s", runAsPlugin) - } - if err := plugin.RunWithURL(context.Background(), runAsPlugin, plugin.RunOptions{Args: os.Args[1:]}); err != nil { + if err := plugin.RunWithURL(context.Background(), runAsPlugin, plugin.Options{Args: os.Args[1:]}); err != nil { return xerrors.Errorf("plugin error: %w", err) } return nil diff --git a/docs/community/contribute/pr.md b/docs/community/contribute/pr.md index 2538cce3327f..072d7358b8c9 100644 --- a/docs/community/contribute/pr.md +++ b/docs/community/contribute/pr.md @@ -114,6 +114,7 @@ mode: - server - aws - vm +- plugin os: diff --git a/pkg/clock/clock.go b/pkg/clock/clock.go index 91f6a5212bd2..38df49259e55 100644 --- a/pkg/clock/clock.go +++ b/pkg/clock/clock.go @@ -8,6 +8,11 @@ import ( clocktesting "k8s.io/utils/clock/testing" ) +type ( + RealClock = clock.RealClock + FakeClock = clocktesting.FakeClock +) + // clockKey is the context key for clock. It is unexported to prevent collisions with context keys defined in // other packages. type clockKey struct{} @@ -27,7 +32,7 @@ func Now(ctx context.Context) time.Time { func Clock(ctx context.Context) clock.Clock { t, ok := ctx.Value(clockKey{}).(clock.Clock) if !ok { - return clock.RealClock{} + return RealClock{} } return t } diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 38ef4c382765..4fdc060ad52d 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -1,6 +1,7 @@ package commands import ( + "context" "encoding/json" "errors" "fmt" @@ -111,10 +112,13 @@ func NewApp() *cobra.Command { } func loadPluginCommands() []*cobra.Command { + ctx := context.Background() + manager := plugin.NewManager() + var commands []*cobra.Command - plugins, err := plugin.LoadAll() + plugins, err := manager.LoadAll(ctx) if err != nil { - log.Debug("No plugins loaded") + log.DebugContext(ctx, "No plugins loaded") return nil } for _, p := range plugins { @@ -124,7 +128,7 @@ func loadPluginCommands() []*cobra.Command { Short: p.Usage, GroupID: groupPlugin, RunE: func(cmd *cobra.Command, args []string) error { - if err = p.Run(cmd.Context(), plugin.RunOptions{Args: args}); err != nil { + if err = p.Run(cmd.Context(), plugin.Options{Args: args}); err != nil { return xerrors.Errorf("plugin error: %w", err) } return nil @@ -716,6 +720,10 @@ func NewPluginCommand() *cobra.Command { Short: "Manage plugins", SilenceErrors: true, SilenceUsage: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + ctx := log.WithContextPrefix(cmd.Context(), "plugin") + cmd.SetContext(ctx) + }, } cmd.AddCommand( &cobra.Command{ @@ -723,10 +731,11 @@ func NewPluginCommand() *cobra.Command { Aliases: []string{"i"}, Short: "Install a plugin", SilenceErrors: true, + SilenceUsage: true, DisableFlagsInUseLine: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if _, err := plugin.Install(cmd.Context(), args[0], true); err != nil { + if _, err := plugin.Install(cmd.Context(), args[0], plugin.Options{}); err != nil { return xerrors.Errorf("plugin install error: %w", err) } return nil @@ -739,8 +748,8 @@ func NewPluginCommand() *cobra.Command { DisableFlagsInUseLine: true, Short: "Uninstall a plugin", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { - if err := plugin.Uninstall(args[0]); err != nil { + RunE: func(cmd *cobra.Command, args []string) error { + if err := plugin.Uninstall(cmd.Context(), args[0]); err != nil { return xerrors.Errorf("plugin uninstall error: %w", err) } return nil @@ -754,13 +763,9 @@ func NewPluginCommand() *cobra.Command { Short: "List installed plugin", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - info, err := plugin.List() - if err != nil { + if err := plugin.List(cmd.Context()); err != nil { return xerrors.Errorf("plugin list display error: %w", err) } - if _, err := fmt.Fprint(os.Stdout, info); err != nil { - return xerrors.Errorf("print error: %w", err) - } return nil }, }, @@ -771,13 +776,9 @@ func NewPluginCommand() *cobra.Command { DisableFlagsInUseLine: true, Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { - info, err := plugin.Information(args[0]) - if err != nil { + if err := plugin.Information(args[0]); err != nil { return xerrors.Errorf("plugin information display error: %w", err) } - if _, err := fmt.Fprint(os.Stdout, info); err != nil { - return xerrors.Errorf("print error: %w", err) - } return nil }, }, @@ -789,22 +790,45 @@ func NewPluginCommand() *cobra.Command { Short: "Run a plugin on the fly", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return plugin.RunWithURL(cmd.Context(), args[0], plugin.RunOptions{Args: args[1:]}) + return plugin.RunWithURL(cmd.Context(), args[0], plugin.Options{Args: args[1:]}) }, }, &cobra.Command{ - Use: "update PLUGIN_NAME", - Short: "Update an existing plugin", + Use: "update", + Short: "Update the local copy of the plugin index", SilenceErrors: true, DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { - if err := plugin.Update(args[0]); err != nil { + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + if err := plugin.Update(cmd.Context()); err != nil { return xerrors.Errorf("plugin update error: %w", err) } return nil }, }, + &cobra.Command{ + Use: "search [KEYWORD]", + SilenceErrors: true, + DisableFlagsInUseLine: true, + Short: "Run a plugin on the fly", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return plugin.Search(cmd.Context(), args) + }, + }, + &cobra.Command{ + Use: "upgrade [PLUGIN_NAMES]", + Short: "Upgrade installed plugins to newer versions", + SilenceErrors: true, + DisableFlagsInUseLine: true, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := plugin.Upgrade(cmd.Context(), args, plugin.Options{}); err != nil { + return xerrors.Errorf("plugin upgrade error: %w", err) + } + return nil + }, + }, ) cmd.SetFlagErrorFunc(flagErrorFunc) return cmd diff --git a/pkg/flag/options.go b/pkg/flag/options.go index 744abbd1ddaa..9f4032fb9711 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -447,7 +447,7 @@ func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() err pluginName := strings.TrimPrefix(o.Output, "plugin=") pr, pw := io.Pipe() - wait, err := plugin.Start(ctx, pluginName, plugin.RunOptions{ + wait, err := plugin.Start(ctx, pluginName, plugin.Options{ Args: o.OutputPluginArgs, Stdin: pr, }) diff --git a/pkg/log/handler.go b/pkg/log/handler.go index 5e07b104715e..b2474cbee3c5 100644 --- a/pkg/log/handler.go +++ b/pkg/log/handler.go @@ -14,6 +14,8 @@ import ( "github.com/fatih/color" "github.com/samber/lo" "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" ) const ( @@ -145,6 +147,11 @@ func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error { freeBuf(bufp) }() + // For tests, use the fake clock's time. + if c, ok := clock.Clock(ctx).(*clock.FakeClock); ok { + r.Time = c.Now() + } + buf = h.handle(ctx, buf, r) h.mu.Lock() diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go new file mode 100644 index 000000000000..a88768a1c233 --- /dev/null +++ b/pkg/plugin/index.go @@ -0,0 +1,112 @@ +package plugin + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml" + +type Index struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Maintainer string `yaml:"maintainer"` + Description string `yaml:"description"` + Repository string `yaml:"repository"` +} + +func (m *Manager) Update(ctx context.Context) error { + m.logger.InfoContext(ctx, "Updating the plugin index...", log.String("url", m.indexURL)) + if err := downloader.Download(ctx, m.indexURL, filepath.Dir(m.indexPath), ""); err != nil { + return xerrors.Errorf("unable to download the plugin index: %w", err) + } + return nil +} + +func (m *Manager) Search(ctx context.Context, args []string) error { + indexes, err := m.loadIndex() + if errors.Is(err, os.ErrNotExist) { + m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") + return xerrors.Errorf("plugin index not found: %w", err) + } else if err != nil { + return xerrors.Errorf("unable to load the plugin index: %w", err) + } + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("%-20s %-10s %-60s %-20s\n", "NAME", "TYPE", "DESCRIPTION", "MAINTAINER")) + for _, index := range indexes { + if len(args) == 0 || strings.Contains(index.Name, args[0]) || strings.Contains(index.Description, args[0]) { + s := fmt.Sprintf("%-20s %-10s %-60s %-20s\n", truncateString(index.Name, 20), index.Type, + truncateString(index.Description, 60), truncateString(index.Maintainer, 20)) + buf.WriteString(s) + } + } + + if _, err = fmt.Fprintf(m.w, buf.String()); err != nil { + return err + } + + return nil +} + +// tryIndex returns the repository URL if the plugin name is found in the index. +// Otherwise, it returns the input name. +func (m *Manager) tryIndex(ctx context.Context, name string) string { + // If the index file does not exist, download it first. + if !fsutils.FileExists(m.indexPath) { + if err := m.Update(ctx); err != nil { + m.logger.ErrorContext(ctx, "Failed to update the plugin index", log.Err(err)) + return name + } + } + + indexes, err := m.loadIndex() + if errors.Is(err, os.ErrNotExist) { + m.logger.WarnContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") + return name + } else if err != nil { + m.logger.ErrorContext(ctx, "Unable to load the plugin index: %w", err) + return name + } + + for _, index := range indexes { + if index.Name == name { + return index.Repository + } + } + return name +} + +func (m *Manager) loadIndex() ([]Index, error) { + f, err := os.Open(m.indexPath) + if err != nil { + return nil, xerrors.Errorf("unable to open the index file: %w", err) + } + defer f.Close() + + var indexes []Index + if err = yaml.NewDecoder(f).Decode(&indexes); err != nil { + return nil, xerrors.Errorf("unable to decode the index file: %w", err) + } + + return indexes, nil +} + +func truncateString(str string, num int) string { + if len(str) <= num { + return str + } + return str[:num-3] + "..." +} diff --git a/pkg/plugin/index_test.go b/pkg/plugin/index_test.go new file mode 100644 index 000000000000..4da20755d25e --- /dev/null +++ b/pkg/plugin/index_test.go @@ -0,0 +1,87 @@ +package plugin_test + +import ( + "bytes" + "context" + "github.com/aquasecurity/trivy/pkg/plugin" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" +) + +func TestManager_Update(t *testing.T) { + tempDir := t.TempDir() + fsutils.SetCacheDir(tempDir) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`this is index`)) + require.NoError(t, err) + })) + t.Cleanup(ts.Close) + + manager := plugin.NewManager(plugin.WithIndexURL(ts.URL + "/index.yaml")) + err := manager.Update(context.Background()) + require.NoError(t, err) + + indexPath := filepath.Join(tempDir, "plugin", "index.yaml") + assert.FileExists(t, indexPath) + + b, err := os.ReadFile(indexPath) + require.NoError(t, err) + assert.Equal(t, "this is index", string(b)) +} + +func TestManager_Search(t *testing.T) { + tests := []struct { + name string + args []string + dir string + want string + wantErr string + }{ + { + name: "all plugins", + args: nil, + dir: "testdata", + want: `NAME TYPE DESCRIPTION MAINTAINER +foo output A foo plugin aquasecurity +bar generic A bar plugin aquasecurity +test generic A test plugin aquasecurity +`, + }, + { + name: "keyword", + args: []string{"bar"}, + dir: "testdata", + want: `NAME TYPE DESCRIPTION MAINTAINER +bar generic A bar plugin aquasecurity +`, + }, + { + name: "no index", + args: nil, + dir: "unknown", + wantErr: "plugin index not found", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsutils.SetCacheDir(tt.dir) + + var got bytes.Buffer + m := plugin.NewManager(plugin.WithWriter(&got)) + err := m.Search(context.Background(), tt.args) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got.String()) + }) + } +} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go new file mode 100644 index 000000000000..4718d74b766b --- /dev/null +++ b/pkg/plugin/manager.go @@ -0,0 +1,318 @@ +package plugin + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +var defaultManager = NewManager() + +type ManagerOption func(indexer *Manager) + +func WithWriter(w io.Writer) ManagerOption { + return func(indexer *Manager) { + indexer.w = w + } +} + +func WithIndexURL(indexURL string) ManagerOption { + return func(indexer *Manager) { + indexer.indexURL = indexURL + } +} + +// Manager manages the plugins +type Manager struct { + w io.Writer + indexURL string + logger *log.Logger + pluginRoot string + indexPath string +} + +func NewManager(opts ...ManagerOption) *Manager { + m := &Manager{ + w: os.Stdout, + indexURL: indexURL, + logger: log.WithPrefix("plugin"), + pluginRoot: filepath.Join(fsutils.HomeDir(), pluginsRelativeDir), + indexPath: filepath.Join(fsutils.CacheDir(), "plugin", "index.yaml"), + } + for _, opt := range opts { + opt(m) + } + return m +} + +func Install(ctx context.Context, name string, opts Options) (Plugin, error) { + return defaultManager.Install(ctx, name, opts) +} +func Upgrade(ctx context.Context, names []string, opts Options) error { + return defaultManager.Upgrade(ctx, names, opts) +} +func Start(ctx context.Context, name string, opts Options) (Wait, error) { + return defaultManager.Start(ctx, name, opts) +} +func RunWithURL(ctx context.Context, name string, opts Options) error { + return defaultManager.RunWithURL(ctx, name, opts) +} +func Uninstall(ctx context.Context, name string) error { return defaultManager.Uninstall(ctx, name) } +func Information(name string) error { return defaultManager.Information(name) } +func List(ctx context.Context) error { return defaultManager.List(ctx) } +func Update(ctx context.Context) error { return defaultManager.Update(ctx) } +func Search(ctx context.Context, args []string) error { return defaultManager.Search(ctx, args) } +func LoadAll(ctx context.Context) ([]Plugin, error) { return defaultManager.LoadAll(ctx) } + +// Install installs a plugin +func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) { + src := m.tryIndex(ctx, name) + + // If the plugin is already installed, it skips installing the plugin. + if p, installed := m.isInstalled(ctx, src); installed { + m.logger.InfoContext(ctx, "The plugin is already installed", log.String("name", p.Name)) + return p, nil + } + + m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", src)) + return m.install(ctx, src, opts) +} + +func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) { + tempDir, err := downloader.DownloadToTempDir(ctx, src) + if err != nil { + return Plugin{}, xerrors.Errorf("download failed: %w", err) + } + defer os.RemoveAll(tempDir) + + m.logger.DebugContext(ctx, "Loading the plugin metadata...") + plugin, err := m.loadMetadata(tempDir) + if err != nil { + return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err) + } + + if err = plugin.install(ctx, plugin.Dir(), tempDir, opts); err != nil { + return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err) + } + + // Copy plugin.yaml into the plugin dir + if _, err = fsutils.CopyFile(filepath.Join(tempDir, configFile), filepath.Join(plugin.Dir(), configFile)); err != nil { + return Plugin{}, xerrors.Errorf("failed to copy plugin.yaml: %w", err) + } + + return plugin, nil +} + +// Uninstall installs the plugin +func (m *Manager) Uninstall(ctx context.Context, name string) error { + pluginDir := filepath.Join(m.pluginRoot, name) + if !fsutils.DirExists(pluginDir) { + m.logger.ErrorContext(ctx, "No such plugin") + return nil + } + return os.RemoveAll(pluginDir) +} + +// Information gets the information about an installed plugin +func (m *Manager) Information(name string) error { + plugin, err := m.load(name) + if err != nil { + return xerrors.Errorf("plugin load error: %w", err) + } + + _, err = fmt.Fprintf(m.w, ` +Plugin: %s + Description: %s + Version: %s + Usage: %s +`, plugin.Name, plugin.Description, plugin.Version, plugin.Usage) + + return err +} + +// List gets a list of all installed plugins +func (m *Manager) List(ctx context.Context) error { + s, err := m.list(ctx) + if err != nil { + return xerrors.Errorf("unable to list plugins: %w", err) + } + _, err = fmt.Fprintf(m.w, "%s\n", s) + return err +} + +func (m *Manager) list(ctx context.Context) (string, error) { + if _, err := os.Stat(m.pluginRoot); err != nil { + if os.IsNotExist(err) { + return "No Installed Plugins", nil + } + return "", xerrors.Errorf("stat error: %w", err) + } + plugins, err := m.LoadAll(ctx) + if err != nil { + return "", xerrors.Errorf("unable to load plugins: %w", err) + } else if len(plugins) == 0 { + return "No Installed Plugins", nil + } + pluginList := []string{"Installed Plugins:"} + for _, plugin := range plugins { + pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version)) + } + + return strings.Join(pluginList, "\n"), nil +} + +// Upgrade upgrades an existing plugins +func (m *Manager) Upgrade(ctx context.Context, names []string, opts Options) error { + if len(names) == 0 { + plugins, err := m.LoadAll(ctx) + if err != nil { + return xerrors.Errorf("unable to load plugins: %w", err) + } else if len(plugins) == 0 { + m.logger.InfoContext(ctx, "No installed plugins") + return nil + } + names = lo.Map(plugins, func(p Plugin, _ int) string { return p.Name }) + } + for _, name := range names { + if err := m.upgrade(ctx, name, opts); err != nil { + return xerrors.Errorf("unable to upgrade '%s' plugin: %w", name, err) + } + } + return nil +} + +func (m *Manager) upgrade(ctx context.Context, name string, opts Options) error { + plugin, err := m.load(name) + if err != nil { + return xerrors.Errorf("plugin load error: %w", err) + } + + logger := log.With("name", name) + logger.InfoContext(ctx, "Upgrading plugin...") + updated, err := m.install(ctx, plugin.Repository, opts) + if err != nil { + return xerrors.Errorf("unable to perform an upgrade installation: %w", err) + } + + if plugin.Version == updated.Version { + logger.InfoContext(ctx, "The plugin is up-to-date", log.String("version", plugin.Version)) + } else { + logger.InfoContext(ctx, "Plugin upgraded", + log.String("from", plugin.Version), log.String("to", updated.Version)) + } + return nil +} + +// LoadAll loads all plugins +func (m *Manager) LoadAll(ctx context.Context) ([]Plugin, error) { + dirs, err := os.ReadDir(m.pluginRoot) + if err != nil { + return nil, xerrors.Errorf("failed to read %s: %w", m.pluginRoot, err) + } + + var plugins []Plugin + for _, d := range dirs { + if !d.IsDir() { + continue + } + plugin, err := m.loadMetadata(filepath.Join(m.pluginRoot, d.Name())) + if err != nil { + m.logger.WarnContext(ctx, "Plugin load error", log.Err(err)) + continue + } + plugins = append(plugins, plugin) + } + return plugins, nil +} + +// Start starts the plugin +func (m *Manager) Start(ctx context.Context, name string, opts Options) (Wait, error) { + plugin, err := m.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 +func (m *Manager) RunWithURL(ctx context.Context, name string, opts Options) error { + plugin, err := m.Install(ctx, name, opts) + if err != nil { + return xerrors.Errorf("plugin install error: %w", err) + } + + if err = plugin.Run(ctx, opts); err != nil { + return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err) + } + return nil +} + +func (m *Manager) load(name string) (Plugin, error) { + pluginDir := filepath.Join(m.pluginRoot, 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 := m.loadMetadata(pluginDir) + if err != nil { + return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err) + } + + return plugin, nil +} + +func (m *Manager) loadMetadata(dir string) (Plugin, error) { + filePath := filepath.Join(dir, configFile) + f, err := os.Open(filePath) + if err != nil { + return Plugin{}, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + var plugin Plugin + if err = yaml.NewDecoder(f).Decode(&plugin); err != nil { + return Plugin{}, xerrors.Errorf("yaml decode error: %w", err) + } + + if plugin.Name == "" { + return Plugin{}, xerrors.Errorf("'name' is empty") + } + + // e.g. ~/.trivy/plugins/kubectl + plugin.dir = filepath.Join(m.pluginRoot, plugin.Name) + + return plugin, nil +} + +func (m *Manager) isInstalled(ctx context.Context, url string) (Plugin, bool) { + installedPlugins, err := m.LoadAll(ctx) + if err != nil { + return Plugin{}, false + } + + for _, plugin := range installedPlugins { + if plugin.Repository == url { + return plugin, true + } + } + return Plugin{}, false +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 11e46a4488a0..ea64bf420ae2 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -3,35 +3,24 @@ package plugin import ( "context" "errors" - "fmt" "io" "os" "os/exec" "path/filepath" "runtime" - "strings" "golang.org/x/xerrors" - "gopkg.in/yaml.v3" "github.com/aquasecurity/trivy/pkg/downloader" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/utils/fsutils" ) -const ( - configFile = "plugin.yaml" -) - -var ( - pluginsRelativeDir = filepath.Join(".trivy", "plugins") +const configFile = "plugin.yaml" - officialPlugins = map[string]string{ - "kubectl": "github.com/aquasecurity/trivy-plugin-kubectl", - "aqua": "github.com/aquasecurity/trivy-plugin-aqua", - } -) +var pluginsRelativeDir = filepath.Join(".trivy", "plugins") // Plugin represents a plugin. type Plugin struct { @@ -42,9 +31,8 @@ type Plugin struct { Description string `yaml:"description"` Platforms []Platform `yaml:"platforms"` - // runtime environment for testability - GOOS string `yaml:"_goos"` - GOARCH string `yaml:"_goarch"` + // dir points to the directory where the plugin is installed + dir string } // Platform represents where the execution file exists per platform. @@ -60,18 +48,19 @@ type Selector struct { Arch string } -type RunOptions struct { - Args []string - Stdin io.Reader +type Options struct { + Args []string + Stdin io.Reader // For output plugin + Platform ftypes.Platform } -func (p Plugin) Cmd(ctx context.Context, opts RunOptions) (*exec.Cmd, error) { - platform, err := p.selectPlatform() +func (p Plugin) Cmd(ctx context.Context, opts Options) (*exec.Cmd, error) { + platform, err := p.selectPlatform(ctx, opts) if err != nil { return nil, xerrors.Errorf("platform selection error: %w", err) } - execFile := filepath.Join(dir(), p.Name, platform.Bin) + execFile := filepath.Join(p.Dir(), platform.Bin) cmd := exec.CommandContext(ctx, execFile, opts.Args...) cmd.Stdin = os.Stdin @@ -90,7 +79,7 @@ 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) { +func (p Plugin) Start(ctx context.Context, opts Options) (Wait, error) { cmd, err := p.Cmd(ctx, opts) if err != nil { return nil, xerrors.Errorf("cmd: %w", err) @@ -103,7 +92,7 @@ func (p Plugin) Start(ctx context.Context, opts RunOptions) (Wait, error) { } // Run runs the plugin -func (p Plugin) Run(ctx context.Context, opts RunOptions) error { +func (p Plugin) Run(ctx context.Context, opts Options) error { cmd, err := p.Cmd(ctx, opts) if err != nil { return xerrors.Errorf("cmd: %w", err) @@ -124,13 +113,15 @@ func (p Plugin) Run(ctx context.Context, opts RunOptions) error { return nil } -func (p Plugin) selectPlatform() (Platform, error) { +func (p Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, error) { // These values are only filled in during unit tests. - if p.GOOS == "" { - p.GOOS = runtime.GOOS + goos := runtime.GOOS + if opts.Platform.Platform != nil && opts.Platform.OS != "" { + goos = opts.Platform.OS } - if p.GOARCH == "" { - p.GOARCH = runtime.GOARCH + goarch := runtime.GOARCH + if opts.Platform.Platform != nil && opts.Platform.Architecture != "" { + goarch = opts.Platform.Architecture } for _, platform := range p.Platforms { @@ -139,9 +130,9 @@ func (p Plugin) selectPlatform() (Platform, error) { } selector := platform.Selector - if (selector.OS == "" || p.GOOS == selector.OS) && - (selector.Arch == "" || p.GOARCH == selector.Arch) { - log.Debug("Platform found", + if (selector.OS == "" || goos == selector.OS) && + (selector.Arch == "" || goarch == selector.Arch) { + log.DebugContext(ctx, "Platform found", log.String("os", selector.OS), log.String("arch", selector.Arch)) return platform, nil } @@ -149,240 +140,23 @@ func (p Plugin) selectPlatform() (Platform, error) { return Platform{}, xerrors.New("platform not found") } -func (p Plugin) install(ctx context.Context, dst, pwd string) error { - log.Debug("Installing the plugin...", log.String("path", dst)) - platform, err := p.selectPlatform() +func (p Plugin) install(ctx context.Context, dst, pwd string, opts Options) error { + log.DebugContext(ctx, "Installing the plugin...", log.String("path", dst)) + platform, err := p.selectPlatform(ctx, opts) if err != nil { return xerrors.Errorf("platform selection error: %w", err) } - log.Debug("Downloading the execution file...", log.String("uri", platform.URI)) + log.DebugContext(ctx, "Downloading the execution file...", log.String("uri", platform.URI)) if err = downloader.Download(ctx, platform.URI, dst, pwd); err != nil { return xerrors.Errorf("unable to download the execution file (%s): %w", platform.URI, err) } return nil } -func (p Plugin) dir() (string, error) { - if p.Name == "" { - return "", xerrors.Errorf("'name' is empty") - } - - // e.g. ~/.trivy/plugins/kubectl - return filepath.Join(dir(), p.Name), nil -} - -// Install installs a plugin -func Install(ctx context.Context, url string, force bool) (Plugin, error) { - // Replace short names with full qualified names - // e.g. kubectl => github.com/aquasecurity/trivy-plugin-kubectl - if v, ok := officialPlugins[url]; ok { - url = v - } - - if !force { - // If the plugin is already installed, it skips installing the plugin. - if p, installed := isInstalled(url); installed { - return p, nil - } - } - - log.Info("Installing the plugin...", log.String("url", url)) - tempDir, err := downloader.DownloadToTempDir(ctx, url) - if err != nil { - return Plugin{}, xerrors.Errorf("download failed: %w", err) - } - defer os.RemoveAll(tempDir) - - log.Info("Loading the plugin metadata...") - plugin, err := loadMetadata(tempDir) - if err != nil { - return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err) - } - - pluginDir, err := plugin.dir() - if err != nil { - return Plugin{}, xerrors.Errorf("failed to determine the plugin dir: %w", err) - } - - if err = plugin.install(ctx, pluginDir, tempDir); err != nil { - return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err) - } - - // Copy plugin.yaml into the plugin dir - if _, err = fsutils.CopyFile(filepath.Join(tempDir, configFile), filepath.Join(pluginDir, configFile)); err != nil { - return Plugin{}, xerrors.Errorf("failed to copy plugin.yaml: %w", err) - } - - return plugin, nil -} - -// Uninstall installs the plugin -func Uninstall(name string) error { - pluginDir := filepath.Join(dir(), name) - return os.RemoveAll(pluginDir) -} - -// Information gets the information about an installed plugin -func Information(name string) (string, error) { - plugin, err := load(name) - if err != nil { - return "", xerrors.Errorf("plugin load error: %w", err) - } - - return fmt.Sprintf(` -Plugin: %s - Description: %s - Version: %s - Usage: %s -`, plugin.Name, plugin.Description, plugin.Version, plugin.Usage), nil -} - -// List gets a list of all installed plugins -func List() (string, error) { - if _, err := os.Stat(dir()); err != nil { - if os.IsNotExist(err) { - return "No Installed Plugins\n", nil - } - return "", xerrors.Errorf("stat error: %w", err) - } - plugins, err := LoadAll() - if err != nil { - return "", xerrors.Errorf("unable to load plugins: %w", err) - } - pluginList := []string{"Installed Plugins:"} - for _, plugin := range plugins { - pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version)) - } - - return strings.Join(pluginList, "\n"), nil -} - -// Update updates an existing plugin -func Update(name string) error { - plugin, err := load(name) - if err != nil { - return xerrors.Errorf("plugin load error: %w", err) - } - - logger := log.With("name", name) - logger.Info("Updating plugin...") - updated, err := Install(nil, plugin.Repository, true) - if err != nil { - return xerrors.Errorf("unable to perform an update installation: %w", err) - } - - if plugin.Version == updated.Version { - logger.Info("The plugin is up-to-date", log.String("version", plugin.Version)) - } else { - logger.Info("Plugin updated", - log.String("from", plugin.Version), log.String("to", updated.Version)) - } - return nil -} - -// LoadAll loads all plugins -func LoadAll() ([]Plugin, error) { - pluginsDir := dir() - dirs, err := os.ReadDir(pluginsDir) - if err != nil { - return nil, xerrors.Errorf("failed to read %s: %w", pluginsDir, err) - } - - var plugins []Plugin - for _, d := range dirs { - if !d.IsDir() { - continue - } - plugin, err := loadMetadata(filepath.Join(pluginsDir, d.Name())) - if err != nil { - log.Warn("Plugin load error", log.Err(err)) - continue - } - plugins = append(plugins, plugin) - } - return plugins, nil -} - -// Start starts the plugin -func Start(ctx context.Context, name string, opts RunOptions) (Wait, error) { - plugin, err := load(name) - if err != nil { - return nil, xerrors.Errorf("plugin load error: %w", err) - } - - wait, err := plugin.Start(ctx, opts) - if err != nil { - return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err) - } - return wait, nil -} - -// RunWithURL runs the plugin with URL -func RunWithURL(ctx context.Context, url string, opts RunOptions) error { - plugin, err := Install(ctx, url, false) - if err != nil { - return xerrors.Errorf("plugin install error: %w", err) - } - - if err = plugin.Run(ctx, opts); err != nil { - return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err) - } - return nil -} - -func IsPredefined(name string) bool { - _, ok := officialPlugins[name] - return ok -} - -func load(name string) (Plugin, error) { - pluginDir := filepath.Join(dir(), name) - if _, err := os.Stat(pluginDir); err != nil { - if os.IsNotExist(err) { - return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name) - } - return Plugin{}, xerrors.Errorf("plugin stat error: %w", err) - } - - plugin, err := loadMetadata(pluginDir) - if err != nil { - return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err) - } - - return plugin, nil -} - -func loadMetadata(dir string) (Plugin, error) { - filePath := filepath.Join(dir, configFile) - f, err := os.Open(filePath) - if err != nil { - return Plugin{}, xerrors.Errorf("file open error: %w", err) - } - defer f.Close() - - var plugin Plugin - if err = yaml.NewDecoder(f).Decode(&plugin); err != nil { - return Plugin{}, xerrors.Errorf("yaml decode error: %w", err) - } - - return plugin, nil -} - -func dir() string { - return filepath.Join(fsutils.HomeDir(), pluginsRelativeDir) -} - -func isInstalled(url string) (Plugin, bool) { - installedPlugins, err := LoadAll() - if err != nil { - return Plugin{}, false - } - - for _, plugin := range installedPlugins { - if plugin.Repository == url { - return plugin, true - } +func (p Plugin) Dir() string { + if p.dir != "" { + return p.dir } - return Plugin{}, false + return filepath.Join(fsutils.HomeDir(), pluginsRelativeDir, p.Name) } diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index d3f5aa1a0fec..95e40790a926 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -1,11 +1,21 @@ package plugin_test import ( + "archive/zip" + "bytes" "context" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + v1 "github.com/google/go-containerregistry/pkg/v1" + "log/slog" + "net/http" + "net/http/httptest" "os" "path/filepath" "runtime" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,7 +24,7 @@ import ( "github.com/aquasecurity/trivy/pkg/plugin" ) -func TestPlugin_Run(t *testing.T) { +func TestManager_Run(t *testing.T) { if runtime.GOOS == "windows" { // the test.sh script can't be run on windows so skipping t.Skip("Test satisfied adequately by Linux tests") @@ -32,7 +42,7 @@ func TestPlugin_Run(t *testing.T) { tests := []struct { name string fields fields - opts plugin.RunOptions + opts plugin.Options wantErr string }{ { @@ -145,8 +155,7 @@ func TestPlugin_Run(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os.Setenv("XDG_DATA_HOME", "testdata") - defer os.Unsetenv("XDG_DATA_HOME") + t.Setenv("XDG_DATA_HOME", "testdata") p := plugin.Plugin{ Name: tt.fields.Name, @@ -155,14 +164,18 @@ func TestPlugin_Run(t *testing.T) { Usage: tt.fields.Usage, Description: tt.fields.Description, Platforms: tt.fields.Platforms, - GOOS: tt.fields.GOOS, - GOARCH: tt.fields.GOARCH, } - err := p.Run(context.Background(), tt.opts) + err := p.Run(context.Background(), plugin.Options{ + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + }) if tt.wantErr != "" { - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr) + require.ErrorContains(t, err, tt.wantErr) return } assert.NoError(t, err) @@ -170,142 +183,139 @@ func TestPlugin_Run(t *testing.T) { } } -func TestInstall(t *testing.T) { +func TestManager_Install(t *testing.T) { if runtime.GOOS == "windows" { // the test.sh script can't be run on windows so skipping t.Skip("Test satisfied adequately by Linux tests") } + wantPlugin := plugin.Plugin{ + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Usage: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "./test.sh", + Bin: "./test.sh", + }, + }, + } + tests := []struct { - name string - url string - want plugin.Plugin - wantFile string - wantErr string + name string + pluginName string + want plugin.Plugin + wantFile string + wantErr string }{ { - name: "happy path", - url: "testdata/test_plugin", - want: plugin.Plugin{ - Name: "test_plugin", - Repository: "github.com/aquasecurity/trivy-plugin-test", - Version: "0.1.0", - Usage: "test", - Description: "test", - Platforms: []plugin.Platform{ - { - Selector: &plugin.Selector{ - OS: "linux", - Arch: "amd64", - }, - URI: "./test.sh", - Bin: "./test.sh", - }, - }, - GOOS: "linux", - GOARCH: "amd64", - }, + name: "http", + want: wantPlugin, wantFile: ".trivy/plugins/test_plugin/test.sh", }, { - name: "plugin not found", - url: "testdata/not_found", - want: plugin.Plugin{ - Name: "test_plugin", - Repository: "github.com/aquasecurity/trivy-plugin-test", - Version: "0.1.0", - Usage: "test", - Description: "test", - Platforms: []plugin.Platform{ - { - Selector: &plugin.Selector{ - OS: "linux", - Arch: "amd64", - }, - URI: "./test.sh", - Bin: "./test.sh", - }, - }, - GOOS: "linux", - GOARCH: "amd64", - }, - wantErr: "no such file or directory", + name: "local path", + pluginName: "testdata/test_plugin", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", }, { - name: "no plugin.yaml", - url: "testdata/no_yaml", - want: plugin.Plugin{ - Name: "no_yaml", - Repository: "github.com/aquasecurity/trivy-plugin-test", - Version: "0.1.0", - Usage: "test", - Description: "test", - Platforms: []plugin.Platform{ - { - Selector: &plugin.Selector{ - OS: "linux", - Arch: "amd64", - }, - URI: "./test.sh", - Bin: "./test.sh", - }, - }, - GOOS: "linux", - GOARCH: "amd64", - }, - wantErr: "file open error", + name: "index", + pluginName: "test", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + }, + { + name: "plugin not found", + pluginName: "testdata/not_found", + wantErr: "no such file or directory", + }, + { + name: "no plugin.yaml", + pluginName: "testdata/no_yaml", + wantErr: "file open error", }, } - log.InitLogger(false, true) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // The test plugin will be installed here dst := t.TempDir() - os.Setenv("XDG_DATA_HOME", dst) + t.Setenv("XDG_DATA_HOME", dst) + + // For plugin index + fsutils.SetCacheDir("testdata") + + if tt.pluginName == "" { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + zr := zip.NewWriter(w) + require.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin"))) + require.NoError(t, zr.Close()) + })) + t.Cleanup(ts.Close) + tt.pluginName = ts.URL + "/test_plugin.zip" + } - got, err := plugin.Install(context.Background(), tt.url, false) + got, err := plugin.NewManager().Install(context.Background(), tt.pluginName, plugin.Options{ + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + }) if tt.wantErr != "" { - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr) + require.ErrorContains(t, err, tt.wantErr) return } assert.NoError(t, err) - assert.Equal(t, tt.want, got) + assert.EqualExportedValues(t, tt.want, got) assert.FileExists(t, filepath.Join(dst, tt.wantFile)) }) } } -func TestUninstall(t *testing.T) { - if runtime.GOOS == "windows" { - // the test.sh script can't be run on windows so skipping - t.Skip("Test satisfied adequately by Linux tests") - } +func TestManager_Uninstall(t *testing.T) { + ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) pluginName := "test_plugin" tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName) - // Create the test plugin directory - err := os.MkdirAll(pluginDir, os.ModePerm) - require.NoError(t, err) - - // Create the test file - err = os.WriteFile(filepath.Join(pluginDir, "test.sh"), []byte(`foo`), os.ModePerm) - require.NoError(t, err) - - // Uninstall the plugin - err = plugin.Uninstall(pluginName) - assert.NoError(t, err) - assert.NoFileExists(t, pluginDir) + t.Run("plugin found", func(t *testing.T) { + // Create the test plugin directory + err := os.MkdirAll(pluginDir, os.ModePerm) + require.NoError(t, err) + + // Create the test file + err = os.WriteFile(filepath.Join(pluginDir, "test.sh"), []byte(`foo`), os.ModePerm) + require.NoError(t, err) + + // Uninstall the plugin + err = plugin.NewManager().Uninstall(ctx, pluginName) + assert.NoError(t, err) + assert.NoDirExists(t, pluginDir) + }) + + t.Run("plugin not found", func(t *testing.T) { + t.Setenv("NO_COLOR", tempDir) + buf := bytes.NewBuffer(nil) + slog.SetDefault(slog.New(log.NewHandler(buf, &log.Options{Level: log.LevelInfo}))) + + err := plugin.NewManager().Uninstall(ctx, pluginName) + assert.NoError(t, err) + assert.Equal(t, "2021-08-25T12:20:30Z\tERROR\t[plugin] No such plugin\n", buf.String()) + }) } -func TestInformation(t *testing.T) { - if runtime.GOOS == "windows" { - // the test.sh script can't be run on windows so skipping - t.Skip("Test satisfied adequately by Linux tests") - } +func TestManager_Information(t *testing.T) { pluginName := "test_plugin" tempDir := t.TempDir() @@ -327,22 +337,22 @@ description: A simple test plugin` err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm) require.NoError(t, err) + var got bytes.Buffer + manager := plugin.NewManager(plugin.WithWriter(&got)) + // Get Information for the plugin - info, err := plugin.Information(pluginName) + err = manager.Information(pluginName) require.NoError(t, err) - assert.Equal(t, "\nPlugin: test_plugin\n Description: A simple test plugin\n Version: 0.1.0\n Usage: test\n", info) + assert.Equal(t, "\nPlugin: test_plugin\n Description: A simple test plugin\n Version: 0.1.0\n Usage: test\n", got.String()) + got.Reset() // Get Information for unknown plugin - info, err = plugin.Information("unknown") + err = manager.Information("unknown") require.Error(t, err) assert.ErrorContains(t, err, "could not find a plugin called 'unknown', did you install it?") } -func TestLoadAll1(t *testing.T) { - if runtime.GOOS == "windows" { - // the test.sh script can't be run on windows so skipping - t.Skip("Test satisfied adequately by Linux tests") - } +func TestManager_LoadAll(t *testing.T) { tests := []struct { name string dir string @@ -369,8 +379,6 @@ func TestLoadAll1(t *testing.T) { Bin: "./test.sh", }, }, - GOOS: "linux", - GOARCH: "amd64", }, }, }, @@ -382,22 +390,23 @@ func TestLoadAll1(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os.Setenv("XDG_DATA_HOME", tt.dir) - defer os.Unsetenv("XDG_DATA_HOME") + t.Setenv("XDG_DATA_HOME", tt.dir) - got, err := plugin.LoadAll() + got, err := plugin.NewManager().LoadAll(context.Background()) if tt.wantErr != "" { - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr) + require.ErrorContains(t, err, tt.wantErr) return } assert.NoError(t, err) - assert.Equal(t, tt.want, got) + require.Len(t, got, len(tt.want)) + for i := range tt.want { + assert.EqualExportedValues(t, tt.want[i], got[i]) + } }) } } -func TestUpdate(t *testing.T) { +func TestManager_Upgrade(t *testing.T) { if runtime.GOOS == "windows" { // the test.sh script can't be run on windows so skipping t.Skip("Test satisfied adequately by Linux tests") @@ -423,19 +432,28 @@ description: A simple test plugin` err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm) require.NoError(t, err) - // verify initial version - verifyVersion(t, pluginName, "0.0.5") + ctx := context.Background() - // Update the existing plugin - err = plugin.Update(pluginName) + // verify initial version + verifyVersion(t, ctx, pluginName, "0.0.5") + + // Upgrade the existing plugin + err = plugin.NewManager().Upgrade(ctx, nil, plugin.Options{ + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + }) require.NoError(t, err) // verify plugin updated - verifyVersion(t, pluginName, "0.1.0") + verifyVersion(t, ctx, pluginName, "0.1.0") } -func verifyVersion(t *testing.T, pluginName, expectedVersion string) { - plugins, err := plugin.LoadAll() +func verifyVersion(t *testing.T, ctx context.Context, pluginName, expectedVersion string) { + plugins, err := plugin.LoadAll(ctx) require.NoError(t, err) for _, plugin := range plugins { if plugin.Name == pluginName { diff --git a/pkg/plugin/testdata/plugin/index.yaml b/pkg/plugin/testdata/plugin/index.yaml new file mode 100644 index 000000000000..651e3a56ae1b --- /dev/null +++ b/pkg/plugin/testdata/plugin/index.yaml @@ -0,0 +1,15 @@ +- name: foo + type: output + maintainer: aquasecurity + description: A foo plugin + repository: github.com/aquasecurity/trivy-plugin-foo +- name: bar + type: generic + maintainer: aquasecurity + description: A bar plugin + repository: github.com/aquasecurity/trivy-plugin-bar +- name: test + type: generic + maintainer: aquasecurity + description: A test plugin + repository: testdata/test_plugin diff --git a/pkg/utils/fsutils/fs.go b/pkg/utils/fsutils/fs.go index 915581f08ad9..6d0502d8cc18 100644 --- a/pkg/utils/fsutils/fs.go +++ b/pkg/utils/fsutils/fs.go @@ -1,6 +1,7 @@ package fsutils import ( + "errors" "fmt" "io" "io/fs" @@ -84,6 +85,14 @@ func DirExists(path string) bool { return true } +func FileExists(filename string) bool { + _, err := os.Stat(filename) + if errors.Is(err, os.ErrNotExist) { + return false + } + return err == nil +} + type WalkDirRequiredFunc func(path string, d fs.DirEntry) bool type WalkDirFunc func(path string, d fs.DirEntry, r io.Reader) error From 486eb4871a0affdff0fe8148cf9a0e15f8447015 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Fri, 10 May 2024 16:20:31 +0400 Subject: [PATCH 02/26] feat(plugin): output mode Signed-off-by: knqyf263 --- pkg/plugin/index.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go index a88768a1c233..05da4c5adba3 100644 --- a/pkg/plugin/index.go +++ b/pkg/plugin/index.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "github.com/samber/lo" "os" "path/filepath" "strings" @@ -21,10 +22,10 @@ const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yam type Index struct { Name string `yaml:"name"` - Type string `yaml:"type"` Maintainer string `yaml:"maintainer"` Description string `yaml:"description"` Repository string `yaml:"repository"` + Output bool `yaml:"output"` } func (m *Manager) Update(ctx context.Context) error { @@ -45,11 +46,12 @@ func (m *Manager) Search(ctx context.Context, args []string) error { } var buf bytes.Buffer - buf.WriteString(fmt.Sprintf("%-20s %-10s %-60s %-20s\n", "NAME", "TYPE", "DESCRIPTION", "MAINTAINER")) + buf.WriteString(fmt.Sprintf("%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT")) for _, index := range indexes { if len(args) == 0 || strings.Contains(index.Name, args[0]) || strings.Contains(index.Description, args[0]) { - s := fmt.Sprintf("%-20s %-10s %-60s %-20s\n", truncateString(index.Name, 20), index.Type, - truncateString(index.Description, 60), truncateString(index.Maintainer, 20)) + s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(index.Name, 20), + truncateString(index.Description, 60), truncateString(index.Maintainer, 20), + lo.Ternary(index.Output, " ✓", "")) buf.WriteString(s) } } From ddbed5094591d37815bc39ae6e606e36854ebefb Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Fri, 10 May 2024 16:20:48 +0400 Subject: [PATCH 03/26] docs: add user-guide and developer-guide Signed-off-by: knqyf263 --- docs/docs/advanced/plugins.md | 236 --------------------------- docs/docs/configuration/reporting.md | 2 +- docs/docs/plugin/developer-guide.md | 124 ++++++++++++++ docs/docs/plugin/index.md | 70 ++++++++ docs/docs/plugin/user-guide.md | 207 +++++++++++++++++++++++ mkdocs.yml | 5 +- 6 files changed, 406 insertions(+), 238 deletions(-) delete mode 100644 docs/docs/advanced/plugins.md create mode 100644 docs/docs/plugin/developer-guide.md create mode 100644 docs/docs/plugin/index.md create mode 100644 docs/docs/plugin/user-guide.md diff --git a/docs/docs/advanced/plugins.md b/docs/docs/advanced/plugins.md deleted file mode 100644 index dfdfb31d8c0d..000000000000 --- a/docs/docs/advanced/plugins.md +++ /dev/null @@ -1,236 +0,0 @@ -# Plugins -Trivy provides a plugin feature to allow others to extend the Trivy CLI without the need to change the Trivycode base. -This plugin system was inspired by the plugin system used in [kubectl][kubectl], [Helm][helm], and [Conftest][conftest]. - -## Overview -Trivy plugins are add-on tools that integrate seamlessly with Trivy. -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. - -- They can be added and removed from a Trivy installation without impacting the core Trivy tool. -- They can be written in any programming language. -- They integrate with Trivy, and will show up in Trivy help and subcommands. - -!!! warning - Trivy plugins available in public are not audited for security. - You should install and run third-party plugins at your own risk, since they are arbitrary programs running on your machine. - - -## Installing a Plugin -A plugin can be installed using the `trivy plugin install` command. -This command takes a url and will download the plugin and install it in the plugin cache. - -Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set. -Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache. -The preference order is as follows: - -- XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir -- ~/.trivy/plugins - -Under the hood Trivy leverages [go-getter][go-getter] to download plugins. -This means the following protocols are supported for downloading plugins: - -- OCI Registries -- Local Files -- Git -- HTTP/HTTPS -- Mercurial -- Amazon S3 -- Google Cloud Storage - -For example, to download the Kubernetes Trivy plugin you can execute the following command: - -```bash -$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl -``` -Also, Trivy plugin can be installed from a local archive: -```bash -$ trivy plugin install myplugin.tar.gz -``` - -## Using Plugins -Once the plugin is installed, Trivy will load all available plugins in the cache on the start of the next Trivy execution. -A plugin will be made in the Trivy CLI based on the plugin name. -To display all plugins, you can list them by `trivy --help` - -```bash -$ trivy --help -NAME: - trivy - A simple and comprehensive vulnerability scanner for containers - -USAGE: - trivy [global options] command [command options] target - -VERSION: - dev - -COMMANDS: - image, i scan an image - filesystem, fs scan local filesystem - repository, repo scan remote repository - client, c client mode - server, s server mode - plugin, p manage plugins - kubectl scan kubectl resources - help, h Shows a list of commands or help for one command -``` - -As shown above, `kubectl` subcommand exists in the `COMMANDS` section. -To call the kubectl plugin and scan existing Kubernetes deployments, you can execute the following command: - -``` -$ trivy kubectl deployment -- --ignore-unfixed --severity CRITICAL -``` - -Internally the kubectl plugin calls the kubectl binary to fetch information about that deployment and passes the using images to Trivy. -You can see the detail [here][trivy-plugin-kubectl]. - -If you want to omit even the subcommand, you can use `TRIVY_RUN_AS_PLUGIN` environment variable. - -```bash -$ TRIVY_RUN_AS_PLUGIN=kubectl trivy job your-job -- --format json -``` - -## Installing and Running Plugins on the fly -`trivy plugin run` installs a plugin and runs it on the fly. -If the plugin is already present in the cache, the installation is skipped. - -```bash -trivy plugin run github.com/aquasecurity/trivy-plugin-kubectl pod your-pod -- --exit-code 1 -``` - -## Uninstalling Plugins -Specify a plugin name with `trivy plugin uninstall` command. - -```bash -$ trivy plugin uninstall kubectl -``` - -## Building Plugins -Each plugin has a top-level directory, and then a plugin.yaml file. - -```bash -your-plugin/ - | - |- plugin.yaml - |- your-plugin.sh -``` - -In the example above, the plugin is contained inside of a directory named `your-plugin`. -It has two files: plugin.yaml (required) and an executable script, your-plugin.sh (optional). - -The core of a plugin is a simple YAML file named plugin.yaml. -Here is an example YAML of trivy-plugin-kubectl plugin that adds support for Kubernetes scanning. - -```yaml -name: "kubectl" -repository: github.com/aquasecurity/trivy-plugin-kubectl -version: "0.1.0" -usage: scan kubectl resources -description: |- - A Trivy plugin that scans the images of a kubernetes resource. - Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME -platforms: - - selector: # optional - os: darwin - arch: amd64 - uri: ./trivy-kubectl # where the execution file is (local file, http, git, etc.) - bin: ./trivy-kubectl # path to the execution file - - selector: # optional - os: linux - arch: amd64 - uri: https://github.com/aquasecurity/trivy-plugin-kubectl/releases/download/v0.1.0/trivy-kubectl.tar.gz - bin: ./trivy-kubectl -``` - -The `plugin.yaml` field should contain the following information: - -- 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) -- version: The version of the plugin. (required) -- usage: A short usage description. (required) -- description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required) -- platforms: (required) - - selector: The OS/Architecture specific variations of a execution file. (optional) - - os: OS information based on GOOS (linux, darwin, etc.) (optional) - - arch: The architecture information based on GOARCH (amd64, arm64, etc.) (optional) - - uri: Where the executable file is. Relative path from the root directory of the plugin or remote URL such as HTTP and S3. (required) - - bin: Which file to call when the plugin is executed. Relative path from the root directory of the plugin. (required) - -The following rules will apply in deciding which platform to select: - -- If both `os` and `arch` under `selector` match the current platform, search will stop and the platform will be used. -- If `selector` is not present, the platform will be used. -- If `os` matches and there is no more specific `arch` match, the platform will be used. -- If no `platform` match is found, Trivy will exit with an error. - -After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache. -When the plugin is called via Trivy CLI, `bin` command will be executed. - -The plugin is responsible for handling flags and arguments. Any arguments are passed to the plugin from the `trivy` command. - -A plugin should be archived `*.tar.gz`. - -```bash -$ tar -czvf myplugin.tar.gz plugin.yaml script.py -plugin.yaml -script.py - -$ trivy plugin install myplugin.tar.gz -2023-03-03T19:04:42.026+0600 INFO Installing the plugin from myplugin.tar.gz... -2023-03-03T19:04:42.026+0600 INFO Loading the plugin metadata... - -$ trivy myplugin -Hello from Trivy demo plugin! -``` - -## Plugin Types -Plugins are typically intended to be used as subcommands of Trivy, -but some plugins can be invoked as part of Trivy's built-in commands. -Currently, the following type of plugin is experimentally supported: - -- Output plugins - -### Output Plugins - -!!! warning "EXPERIMENTAL" - This feature might change without preserving backwards compatibility. - -Trivy supports "output plugins" which process Trivy's output, -such as by transforming the output format or sending it elsewhere. -For instance, in the case of image scanning, the output plugin can be called as follows: - -```shell -$ trivy image --format json --output plugin= [--output-plugin-arg ] -``` - -Since scan results are passed to the plugin via standard input, plugins must be capable of handling standard input. - -!!! warning - To avoid Trivy hanging, you need to read all data from `Stdin` before the plugin exits successfully or stops with an error. - -While the example passes JSON to the plugin, other formats like SBOM can also be passed (e.g., `--format cyclonedx`). - -If a plugin requires flags or other arguments, they can be passed using `--output-plugin-arg`. -This is directly forwarded as arguments to the plugin. -For example, `--output plugin=myplugin --output-plugin-arg "--foo --bar=baz"` translates to `myplugin --foo --bar=baz` in execution. - -An example of the output plugin is available [here](https://github.com/aquasecurity/trivy-output-plugin-count). -It can be used as below: - -```shell -# Install the plugin first -$ trivy plugin install github.com/aquasecurity/trivy-output-plugin-count - -# Call the output plugin in image scanning -$ trivy image --format json --output plugin=count --output-plugin-arg "--published-after 2023-10-01" debian:12 -``` - -## Example -- https://github.com/aquasecurity/trivy-plugin-kubectl -- https://github.com/aquasecurity/trivy-output-plugin-count - -[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/ -[helm]: https://helm.sh/docs/topics/plugins/ -[conftest]: https://www.conftest.dev/plugins/ -[go-getter]: https://github.com/hashicorp/go-getter -[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl - diff --git a/docs/docs/configuration/reporting.md b/docs/docs/configuration/reporting.md index 117db88de866..8671501ad885 100644 --- a/docs/docs/configuration/reporting.md +++ b/docs/docs/configuration/reporting.md @@ -399,7 +399,7 @@ $ trivy [--format ] --output plugin= [--output-plu ``` 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). +For more details, please check [here](../plugin/plugins.md#output-plugins). ## Converting To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand. diff --git a/docs/docs/plugin/developer-guide.md b/docs/docs/plugin/developer-guide.md new file mode 100644 index 000000000000..25613d706d8e --- /dev/null +++ b/docs/docs/plugin/developer-guide.md @@ -0,0 +1,124 @@ +# Developer Guide + +## Introduction +If you are looking to start developing plugins for Trivy, read [the user guide](./user-guide.md) first. + +To summarize the documentation, the procedure is to: + +- Create a repository for your plugin, named `trivy-plugin-`. +- Create an executable binary that can be invoked as `trivy `. +- Place the executable binary in a repository. +- Create a `plugin.yaml` file that describes the plugin. + +After you develop a plugin with a good name following the best practices, you can develop a [trivy-plugin-index][trivy-plugin-index] manifest and submit your plugin. + +## Naming +This section describes guidelines for naming your plugins. + +### Use `trivy-plugin-` prefix +The name of the plugin repository should be prefixed with `trivy-plugin-`. + +### Use lowercase and hyphens +Plugin names must be all lowercase and separate words with hyphens. +Don’t use camelCase, PascalCase, or snake_case; use kebab-case. + +- NO: `trivy OpenSvc` +- YES: `trivy open-svc` + +### Be specific +Plugin names should not be verbs or nouns that are generic, already overloaded, or likely to be used for broader purposes by another plugin. + +- NO: trivy sast (Too broad) +- YES: trivy govulncheck + + +### Be unique +Find a unique name for your plugin that differentiates it from other plugins that perform a similar function. + +- NO: `trivy images` (Unclear how it is different from the builtin “image" command) +- YES: `trivy registry-images` (Unique name). + +### Prefix Vendor Identifiers +Use vendor-specific strings as prefix, separated with a dash. +This makes it easier to search/group plugins that are about a specific vendor. + +- NO: `trivy security-hub-aws (Makes it harder to search or locate in a plugin list) +- YES: `trivy aws-security-hub (Will show up together with other aws-* plugins) + +Each plugin has a top-level directory, and then a plugin.yaml file. + +```bash +your-plugin/ + | + |- plugin.yaml + |- your-plugin.sh +``` + +In the example above, the plugin is contained inside of a directory named `your-plugin`. +It has two files: plugin.yaml (required) and an executable script, your-plugin.sh (optional). + +The core of a plugin is a simple YAML file named plugin.yaml. +Here is an example YAML of trivy-plugin-kubectl plugin that adds support for Kubernetes scanning. + +```yaml +name: "kubectl" +repository: github.com/aquasecurity/trivy-plugin-kubectl +version: "0.1.0" +usage: scan kubectl resources +description: |- + A Trivy plugin that scans the images of a kubernetes resource. + Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME +platforms: + - selector: # optional + os: darwin + arch: amd64 + uri: ./trivy-kubectl # where the execution file is (local file, http, git, etc.) + bin: ./trivy-kubectl # path to the execution file + - selector: # optional + os: linux + arch: amd64 + uri: https://github.com/aquasecurity/trivy-plugin-kubectl/releases/download/v0.1.0/trivy-kubectl.tar.gz + bin: ./trivy-kubectl +``` + +The `plugin.yaml` field should contain the following information: + +- 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) +- version: The version of the plugin. (required) +- usage: A short usage description. (required) +- description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required) +- platforms: (required) + - selector: The OS/Architecture specific variations of a execution file. (optional) + - os: OS information based on GOOS (linux, darwin, etc.) (optional) + - arch: The architecture information based on GOARCH (amd64, arm64, etc.) (optional) + - uri: Where the executable file is. Relative path from the root directory of the plugin or remote URL such as HTTP and S3. (required) + - bin: Which file to call when the plugin is executed. Relative path from the root directory of the plugin. (required) + +The following rules will apply in deciding which platform to select: + +- If both `os` and `arch` under `selector` match the current platform, search will stop and the platform will be used. +- If `selector` is not present, the platform will be used. +- If `os` matches and there is no more specific `arch` match, the platform will be used. +- If no `platform` match is found, Trivy will exit with an error. + +After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache. +When the plugin is called via Trivy CLI, `bin` command will be executed. + +The plugin is responsible for handling flags and arguments. Any arguments are passed to the plugin from the `trivy` command. + +A plugin should be archived `*.tar.gz`. + +```bash +$ tar -czvf myplugin.tar.gz plugin.yaml script.py +plugin.yaml +script.py + +$ trivy plugin install myplugin.tar.gz +2023-03-03T19:04:42.026+0600 INFO Installing the plugin from myplugin.tar.gz... +2023-03-03T19:04:42.026+0600 INFO Loading the plugin metadata... + +$ trivy myplugin +Hello from Trivy demo plugin! +``` + +[trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index \ No newline at end of file diff --git a/docs/docs/plugin/index.md b/docs/docs/plugin/index.md new file mode 100644 index 000000000000..dbf13826a3a8 --- /dev/null +++ b/docs/docs/plugin/index.md @@ -0,0 +1,70 @@ +# Plugins +Trivy provides a plugin feature to allow others to extend the Trivy CLI without the need to change the Trivy code base. +This plugin system was inspired by the plugin system used in [kubectl][kubectl], [Helm][helm], and [Conftest][conftest]. + +## Overview +Trivy plugins are add-on tools that integrate seamlessly with Trivy. +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. + +- They can be added and removed from a Trivy installation without impacting the core Trivy tool. +- They can be written in any programming language. +- They integrate with Trivy, and will show up in Trivy help and subcommands. + +!!! warning + Trivy plugins available in public are not audited for security. + You should install and run third-party plugins at your own risk, since they are arbitrary programs running on your machine. + +## Quickstart +Trivy helps you discover and install plugins on your machine. + +You can install and use a wide variety of Trivy plugins to enhance your experience. + +Let’s get started: + +1. Download the plugin list: + +```bash +$ trivy plugin update +``` + +2. Discover plugins available: + +```bash +$ trivy plugin search +NAME DESCRIPTION MAINTAINER OUTPUT +aqua A plugin for integration with Aqua Security SaaS platform aquasecurity +kubectl A plugin scanning the images of a kubernetes resource aquasecurity +referrer A plugin for OCI referrers aquasecurity ✓ +[...] +``` + +3. Choose a plugin from the list and install it: + +```bash +$ trivy plugin install referrer +``` + +4. Use the installed plugin: + +```bash +$ trivy referrer --help +``` + +5. Keep your plugins up-to-date: + +```bash +$ trivy plugin upgrade +``` + +6. Uninstall a plugin you no longer use: + +```bash +trivy plugin uninstall referrer +``` + +This is practically all you need to know to start using Trivy plugins. + + +[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/ +[helm]: https://helm.sh/docs/topics/plugins/ +[conftest]: https://www.conftest.dev/plugins/ diff --git a/docs/docs/plugin/user-guide.md b/docs/docs/plugin/user-guide.md new file mode 100644 index 000000000000..7668f8df77f5 --- /dev/null +++ b/docs/docs/plugin/user-guide.md @@ -0,0 +1,207 @@ +# User Guide + +## Discovering Plugins +You can find a list of Trivy plugins distributed via trivy-plugin-index [here][trivy-plugin-index]. +However, you can find plugins using the command line as well. + +First, refresh your local copy of the plugin index: + +```bash +$ trivy plugin update +``` + +To list all plugins available, run: + +```bash +$ trivy plugin search +NAME DESCRIPTION MAINTAINER OUTPUT +aqua A plugin for integration with Aqua Security SaaS platform aquasecurity +kubectl A plugin scanning the images of a kubernetes resource aquasecurity +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +You can specify search keywords as arguments: + +```bash +$ trivy plugin search referrer + +NAME DESCRIPTION INSTALLED +NAME DESCRIPTION MAINTAINER OUTPUT +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +## Installing Plugins +Plugins can be installed with the `trivy plugin install` command: + +```bash +$ trivy plugin install referrer +``` + +This command will download the plugin and install it in the plugin cache. + +Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set. +Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache. +The preference order is as follows: + +- XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir +- ~/.trivy/plugins + +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. + +```bash +$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl +``` +` +```bash +$ trivy plugin install myplugin.tar.gz +``` + +Under the hood Trivy leverages [go-getter][go-getter] to download plugins. +This means the following protocols are supported for downloading plugins: + +- OCI Registries +- Local Files +- Git +- HTTP/HTTPS +- Mercurial +- Amazon S3 +- Google Cloud Storage + +## Listing Installed Plugins +To list all plugins installed, run: + +```bash +$ trivy plugin list +``` + +## Using Plugins +Once the plugin is installed, Trivy will load all available plugins in the cache on the start of the next Trivy execution. +A plugin will be made in the Trivy CLI based on the plugin name. +To display all plugins, you can list them by `trivy --help` + +```bash +$ trivy --help +NAME: + trivy - A simple and comprehensive vulnerability scanner for containers + +USAGE: + trivy [global options] command [command options] target + +VERSION: + dev + +Scanning Commands + aws [EXPERIMENTAL] Scan AWS account + config Scan config files for misconfigurations + filesystem Scan local filesystem + image Scan a container image + +... + +Plugin Commands + kubectl scan kubectl resources + referrer Put referrers to OCI registry +``` + +As shown above, `kubectl` subcommand exists in the `Plugin Commands` section. +To call the kubectl plugin and scan existing Kubernetes deployments, you can execute the following command: + +``` +$ trivy kubectl deployment -- --ignore-unfixed --severity CRITICAL +``` + +Internally the kubectl plugin calls the kubectl binary to fetch information about that deployment and passes the using images to Trivy. +You can see the detail [here][trivy-plugin-kubectl]. + +If you want to omit even the subcommand, you can use `TRIVY_RUN_AS_PLUGIN` environment variable. + +```bash +$ TRIVY_RUN_AS_PLUGIN=kubectl trivy job your-job -- --format json +``` + +## Installing and Running Plugins on the fly +`trivy plugin run` installs a plugin and runs it on the fly. +If the plugin is already present in the cache, the installation is skipped. + +```bash +trivy plugin run kubectl pod your-pod -- --exit-code 1 +``` + +## Upgrading Plugins +To upgrade all plugins that you have installed to their latest versions, run: + +```bash +$ trivy plugin upgrade +``` + +To upgrade only certain plugins, you can explicitly specify their names: + +```bash +$ trivy plugin upgrade +``` + +## Uninstalling Plugins +Specify a plugin name with `trivy plugin uninstall` command. + +```bash +$ trivy plugin uninstall kubectl +``` + +Here's the revised English documentation based on your requested changes: + +## Output Mode Support +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. + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +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. +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`. + +```bash +$ trivy plugin search +NAME DESCRIPTION MAINTAINER OUTPUT +aqua A plugin for integration with Aqua Security SaaS platform aquasecurity +kubectl A plugin scanning the images of a kubernetes resource aquasecurity +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +In this case, the `referrer` plugin supports the output mode. + +For instance, in the case of image scanning, a plugin supporting the output mode can be called as follows: + +```bash +$ trivy image --format json --output plugin= [--output-plugin-arg ] +``` + +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 a plugin supporting the output mode is available [here][trivy-plugin-count]. +It can be used as below: + +```bash +# Install the plugin first +$ trivy plugin install count + +# Call the plugin supporting the output mode in image scanning +$ trivy image --format json --output plugin=count --output-plugin-arg "--published-after 2023-10-01" debian:12 +``` + +## Example + +- [kubectl][trivy-plugin-kubectl] +- [count][trivy-plugin-count] + +[trivy-plugin-index]: https://aquasecurity.github.io/trivy-plugin-index/ +[go-getter]: https://github.com/hashicorp/go-getter +[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl +[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count diff --git a/mkdocs.yml b/mkdocs.yml index c3437fecf413..760cbcaf6008 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,9 +128,12 @@ nav: - VEX: docs/supply-chain/vex.md - Compliance: - Reports: docs/compliance/compliance.md + - Plugin: + - Overview: docs/plugin/index.md + - User Guide: docs/plugin/user-guide.md + - Developer Guide: docs/plugin/developer-guide.md - Advanced: - Modules: docs/advanced/modules.md - - Plugins: docs/advanced/plugins.md - Air-Gapped Environment: docs/advanced/air-gap.md - Container Image: - Embed in Dockerfile: docs/advanced/container/embed-in-dockerfile.md From b69a5a819786baee3a2844c8e44fadad3412a63f Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Fri, 10 May 2024 22:10:04 +0400 Subject: [PATCH 04/26] feat: add installed platform Signed-off-by: knqyf263 --- pkg/commands/app.go | 6 +-- pkg/plugin/index.go | 2 +- pkg/plugin/index_test.go | 12 ++--- pkg/plugin/manager.go | 52 +++++++++++++------ .../{plugin_test.go => manager_test.go} | 21 ++++---- pkg/plugin/plugin.go | 29 ++++++----- pkg/plugin/testdata/plugin/index.yaml | 4 +- 7 files changed, 76 insertions(+), 50 deletions(-) rename pkg/plugin/{plugin_test.go => manager_test.go} (98%) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 4fdc060ad52d..8cee93646bb0 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -727,7 +727,7 @@ func NewPluginCommand() *cobra.Command { } cmd.AddCommand( &cobra.Command{ - Use: "install URL | FILE_PATH", + Use: "install NAME | URL | FILE_PATH", Aliases: []string{"i"}, Short: "Install a plugin", SilenceErrors: true, @@ -810,7 +810,7 @@ func NewPluginCommand() *cobra.Command { Use: "search [KEYWORD]", SilenceErrors: true, DisableFlagsInUseLine: true, - Short: "Run a plugin on the fly", + Short: "List available plugins and search among them", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return plugin.Search(cmd.Context(), args) @@ -823,7 +823,7 @@ func NewPluginCommand() *cobra.Command { DisableFlagsInUseLine: true, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if err := plugin.Upgrade(cmd.Context(), args, plugin.Options{}); err != nil { + if err := plugin.Upgrade(cmd.Context(), args); err != nil { return xerrors.Errorf("plugin upgrade error: %w", err) } return nil diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go index 05da4c5adba3..549218cf46ee 100644 --- a/pkg/plugin/index.go +++ b/pkg/plugin/index.go @@ -5,11 +5,11 @@ import ( "context" "errors" "fmt" - "github.com/samber/lo" "os" "path/filepath" "strings" + "github.com/samber/lo" "golang.org/x/xerrors" "gopkg.in/yaml.v3" diff --git a/pkg/plugin/index_test.go b/pkg/plugin/index_test.go index 4da20755d25e..db1411532017 100644 --- a/pkg/plugin/index_test.go +++ b/pkg/plugin/index_test.go @@ -48,18 +48,18 @@ func TestManager_Search(t *testing.T) { name: "all plugins", args: nil, dir: "testdata", - want: `NAME TYPE DESCRIPTION MAINTAINER -foo output A foo plugin aquasecurity -bar generic A bar plugin aquasecurity -test generic A test plugin aquasecurity + want: `NAME DESCRIPTION MAINTAINER OUTPUT +foo A foo plugin aquasecurity ✓ +bar A bar plugin aquasecurity +test A test plugin aquasecurity `, }, { name: "keyword", args: []string{"bar"}, dir: "testdata", - want: `NAME TYPE DESCRIPTION MAINTAINER -bar generic A bar plugin aquasecurity + want: `NAME DESCRIPTION MAINTAINER OUTPUT +bar A bar plugin aquasecurity `, }, { diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 4718d74b766b..6badaaba4b10 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -8,16 +8,24 @@ import ( "path/filepath" "strings" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/samber/lo" "golang.org/x/xerrors" "gopkg.in/yaml.v3" "github.com/aquasecurity/trivy/pkg/downloader" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/utils/fsutils" ) -var defaultManager = NewManager() +const configFile = "plugin.yaml" + +var ( + pluginsRelativeDir = filepath.Join(".trivy", "plugins") + + defaultManager = NewManager() +) type ManagerOption func(indexer *Manager) @@ -59,21 +67,19 @@ func NewManager(opts ...ManagerOption) *Manager { func Install(ctx context.Context, name string, opts Options) (Plugin, error) { return defaultManager.Install(ctx, name, opts) } -func Upgrade(ctx context.Context, names []string, opts Options) error { - return defaultManager.Upgrade(ctx, names, opts) -} func Start(ctx context.Context, name string, opts Options) (Wait, error) { return defaultManager.Start(ctx, name, opts) } func RunWithURL(ctx context.Context, name string, opts Options) error { return defaultManager.RunWithURL(ctx, name, opts) } -func Uninstall(ctx context.Context, name string) error { return defaultManager.Uninstall(ctx, name) } -func Information(name string) error { return defaultManager.Information(name) } -func List(ctx context.Context) error { return defaultManager.List(ctx) } -func Update(ctx context.Context) error { return defaultManager.Update(ctx) } -func Search(ctx context.Context, args []string) error { return defaultManager.Search(ctx, args) } -func LoadAll(ctx context.Context) ([]Plugin, error) { return defaultManager.LoadAll(ctx) } +func Upgrade(ctx context.Context, names []string) error { return defaultManager.Upgrade(ctx, names) } +func Uninstall(ctx context.Context, name string) error { return defaultManager.Uninstall(ctx, name) } +func Information(name string) error { return defaultManager.Information(name) } +func List(ctx context.Context) error { return defaultManager.List(ctx) } +func Update(ctx context.Context) error { return defaultManager.Update(ctx) } +func Search(ctx context.Context, args []string) error { return defaultManager.Search(ctx, args) } +func LoadAll(ctx context.Context) ([]Plugin, error) { return defaultManager.LoadAll(ctx) } // Install installs a plugin func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) { @@ -107,8 +113,14 @@ func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin } // Copy plugin.yaml into the plugin dir - if _, err = fsutils.CopyFile(filepath.Join(tempDir, configFile), filepath.Join(plugin.Dir(), configFile)); err != nil { - return Plugin{}, xerrors.Errorf("failed to copy plugin.yaml: %w", err) + f, err := os.Create(filepath.Join(plugin.Dir(), configFile)) + if err != nil { + return Plugin{}, xerrors.Errorf("failed to create plugin.yaml: %w", err) + } + defer f.Close() + + if err = yaml.NewEncoder(f).Encode(plugin); err != nil { + return Plugin{}, xerrors.Errorf("yaml encode error: %w", err) } return plugin, nil @@ -173,7 +185,7 @@ func (m *Manager) list(ctx context.Context) (string, error) { } // Upgrade upgrades an existing plugins -func (m *Manager) Upgrade(ctx context.Context, names []string, opts Options) error { +func (m *Manager) Upgrade(ctx context.Context, names []string) error { if len(names) == 0 { plugins, err := m.LoadAll(ctx) if err != nil { @@ -185,14 +197,14 @@ func (m *Manager) Upgrade(ctx context.Context, names []string, opts Options) err names = lo.Map(plugins, func(p Plugin, _ int) string { return p.Name }) } for _, name := range names { - if err := m.upgrade(ctx, name, opts); err != nil { + if err := m.upgrade(ctx, name); err != nil { return xerrors.Errorf("unable to upgrade '%s' plugin: %w", name, err) } } return nil } -func (m *Manager) upgrade(ctx context.Context, name string, opts Options) error { +func (m *Manager) upgrade(ctx context.Context, name string) error { plugin, err := m.load(name) if err != nil { return xerrors.Errorf("plugin load error: %w", err) @@ -200,7 +212,15 @@ func (m *Manager) upgrade(ctx context.Context, name string, opts Options) error logger := log.With("name", name) logger.InfoContext(ctx, "Upgrading plugin...") - updated, err := m.install(ctx, plugin.Repository, opts) + updated, err := m.install(ctx, plugin.Repository, Options{ + // Use the current installed platform + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + OS: plugin.Installed.Platform.OS, + Architecture: plugin.Installed.Platform.Arch, + }, + }, + }) if err != nil { return xerrors.Errorf("unable to perform an upgrade installation: %w", err) } diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/manager_test.go similarity index 98% rename from pkg/plugin/plugin_test.go rename to pkg/plugin/manager_test.go index 95e40790a926..4de245cd0d9a 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/manager_test.go @@ -204,6 +204,12 @@ func TestManager_Install(t *testing.T) { Bin: "./test.sh", }, }, + Installed: plugin.Installed{ + Platform: plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + }, } tests := []struct { @@ -427,7 +433,11 @@ func TestManager_Upgrade(t *testing.T) { repository: testdata/test_plugin version: "0.0.5" usage: test -description: A simple test plugin` +description: A simple test plugin +installed: + platform: + os: linux + arch: amd64` err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm) require.NoError(t, err) @@ -438,14 +448,7 @@ description: A simple test plugin` verifyVersion(t, ctx, pluginName, "0.0.5") // Upgrade the existing plugin - err = plugin.NewManager().Upgrade(ctx, nil, plugin.Options{ - Platform: ftypes.Platform{ - Platform: &v1.Platform{ - Architecture: "amd64", - OS: "linux", - }, - }, - }) + err = plugin.NewManager().Upgrade(ctx, nil) require.NoError(t, err) // verify plugin updated diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index ea64bf420ae2..cf246a5ef13b 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" + "github.com/samber/lo" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/downloader" @@ -18,10 +19,6 @@ import ( "github.com/aquasecurity/trivy/pkg/utils/fsutils" ) -const configFile = "plugin.yaml" - -var pluginsRelativeDir = filepath.Join(".trivy", "plugins") - // Plugin represents a plugin. type Plugin struct { Name string `yaml:"name"` @@ -31,10 +28,17 @@ type Plugin struct { Description string `yaml:"description"` Platforms []Platform `yaml:"platforms"` + // Installed holds the metadata about installation + Installed Installed `yaml:"installed"` + // dir points to the directory where the plugin is installed dir string } +type Installed struct { + Platform Selector `yaml:"platform"` +} + // Platform represents where the execution file exists per platform. type Platform struct { Selector *Selector @@ -44,8 +48,8 @@ type Platform struct { // Selector represents the environment. type Selector struct { - OS string - Arch string + OS string `yaml:"os"` + Arch string `yaml:"arch"` } type Options struct { @@ -54,7 +58,7 @@ type Options struct { Platform ftypes.Platform } -func (p Plugin) Cmd(ctx context.Context, opts Options) (*exec.Cmd, error) { +func (p *Plugin) Cmd(ctx context.Context, opts Options) (*exec.Cmd, error) { platform, err := p.selectPlatform(ctx, opts) if err != nil { return nil, xerrors.Errorf("platform selection error: %w", err) @@ -79,7 +83,7 @@ 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 Options) (Wait, error) { +func (p *Plugin) Start(ctx context.Context, opts Options) (Wait, error) { cmd, err := p.Cmd(ctx, opts) if err != nil { return nil, xerrors.Errorf("cmd: %w", err) @@ -92,7 +96,7 @@ func (p Plugin) Start(ctx context.Context, opts Options) (Wait, error) { } // Run runs the plugin -func (p Plugin) Run(ctx context.Context, opts Options) error { +func (p *Plugin) Run(ctx context.Context, opts Options) error { cmd, err := p.Cmd(ctx, opts) if err != nil { return xerrors.Errorf("cmd: %w", err) @@ -113,7 +117,7 @@ func (p Plugin) Run(ctx context.Context, opts Options) error { return nil } -func (p Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, error) { +func (p *Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, error) { // These values are only filled in during unit tests. goos := runtime.GOOS if opts.Platform.Platform != nil && opts.Platform.OS != "" { @@ -140,12 +144,13 @@ func (p Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, err return Platform{}, xerrors.New("platform not found") } -func (p Plugin) install(ctx context.Context, dst, pwd string, opts Options) error { +func (p *Plugin) install(ctx context.Context, dst, pwd string, opts Options) error { log.DebugContext(ctx, "Installing the plugin...", log.String("path", dst)) platform, err := p.selectPlatform(ctx, opts) if err != nil { return xerrors.Errorf("platform selection error: %w", err) } + p.Installed.Platform = lo.FromPtr(platform.Selector) log.DebugContext(ctx, "Downloading the execution file...", log.String("uri", platform.URI)) if err = downloader.Download(ctx, platform.URI, dst, pwd); err != nil { @@ -154,7 +159,7 @@ func (p Plugin) install(ctx context.Context, dst, pwd string, opts Options) erro return nil } -func (p Plugin) Dir() string { +func (p *Plugin) Dir() string { if p.dir != "" { return p.dir } diff --git a/pkg/plugin/testdata/plugin/index.yaml b/pkg/plugin/testdata/plugin/index.yaml index 651e3a56ae1b..0a52db7cd657 100644 --- a/pkg/plugin/testdata/plugin/index.yaml +++ b/pkg/plugin/testdata/plugin/index.yaml @@ -1,15 +1,13 @@ - name: foo - type: output + output: true maintainer: aquasecurity description: A foo plugin repository: github.com/aquasecurity/trivy-plugin-foo - name: bar - type: generic maintainer: aquasecurity description: A bar plugin repository: github.com/aquasecurity/trivy-plugin-bar - name: test - type: generic maintainer: aquasecurity description: A test plugin repository: testdata/test_plugin From bf0847daff0a8ede490cd9f4a239726cec22a2cb Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 09:47:35 +0400 Subject: [PATCH 05/26] test: respect XDG_DATA_HOME Signed-off-by: knqyf263 --- pkg/plugin/manager_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go index 4de245cd0d9a..d472639328ff 100644 --- a/pkg/plugin/manager_test.go +++ b/pkg/plugin/manager_test.go @@ -443,24 +443,25 @@ installed: require.NoError(t, err) ctx := context.Background() + m := plugin.NewManager() // verify initial version - verifyVersion(t, ctx, pluginName, "0.0.5") + verifyVersion(t, ctx, m, pluginName, "0.0.5") // Upgrade the existing plugin - err = plugin.NewManager().Upgrade(ctx, nil) + err = m.Upgrade(ctx, nil) require.NoError(t, err) // verify plugin updated - verifyVersion(t, ctx, pluginName, "0.1.0") + verifyVersion(t, ctx, m, pluginName, "0.1.0") } -func verifyVersion(t *testing.T, ctx context.Context, pluginName, expectedVersion string) { - plugins, err := plugin.LoadAll(ctx) +func verifyVersion(t *testing.T, ctx context.Context, m *plugin.Manager, pluginName, expectedVersion string) { + plugins, err := m.LoadAll(ctx) require.NoError(t, err) - for _, plugin := range plugins { - if plugin.Name == pluginName { - assert.Equal(t, expectedVersion, plugin.Version) + for _, p := range plugins { + if p.Name == pluginName { + assert.Equal(t, expectedVersion, p.Version) } } } From a7c71fa3d13fcc1bae274a39bdccb42b001512e8 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 10:00:45 +0400 Subject: [PATCH 06/26] docs: auto-generate references Signed-off-by: knqyf263 --- .../configuration/cli/trivy_plugin.md | 4 ++- .../configuration/cli/trivy_plugin_install.md | 2 +- .../configuration/cli/trivy_plugin_search.md | 31 +++++++++++++++++++ .../configuration/cli/trivy_plugin_update.md | 4 +-- .../configuration/cli/trivy_plugin_upgrade.md | 31 +++++++++++++++++++ pkg/commands/app.go | 2 +- 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 docs/docs/references/configuration/cli/trivy_plugin_search.md create mode 100644 docs/docs/references/configuration/cli/trivy_plugin_upgrade.md diff --git a/docs/docs/references/configuration/cli/trivy_plugin.md b/docs/docs/references/configuration/cli/trivy_plugin.md index 9f47212d17f5..a3d105d2cd3f 100644 --- a/docs/docs/references/configuration/cli/trivy_plugin.md +++ b/docs/docs/references/configuration/cli/trivy_plugin.md @@ -28,6 +28,8 @@ Manage plugins * [trivy plugin install](trivy_plugin_install.md) - Install a plugin * [trivy plugin list](trivy_plugin_list.md) - List installed plugin * [trivy plugin run](trivy_plugin_run.md) - Run a plugin on the fly +* [trivy plugin search](trivy_plugin_search.md) - List Trivy plugins available on the plugin index and search among them * [trivy plugin uninstall](trivy_plugin_uninstall.md) - Uninstall a plugin -* [trivy plugin update](trivy_plugin_update.md) - Update an existing plugin +* [trivy plugin update](trivy_plugin_update.md) - Update the local copy of the plugin index +* [trivy plugin upgrade](trivy_plugin_upgrade.md) - Upgrade installed plugins to newer versions diff --git a/docs/docs/references/configuration/cli/trivy_plugin_install.md b/docs/docs/references/configuration/cli/trivy_plugin_install.md index f92da9598326..dbd5f21797b8 100644 --- a/docs/docs/references/configuration/cli/trivy_plugin_install.md +++ b/docs/docs/references/configuration/cli/trivy_plugin_install.md @@ -3,7 +3,7 @@ Install a plugin ``` -trivy plugin install URL | FILE_PATH +trivy plugin install NAME | URL | FILE_PATH ``` ### Options diff --git a/docs/docs/references/configuration/cli/trivy_plugin_search.md b/docs/docs/references/configuration/cli/trivy_plugin_search.md new file mode 100644 index 000000000000..931babfd59b8 --- /dev/null +++ b/docs/docs/references/configuration/cli/trivy_plugin_search.md @@ -0,0 +1,31 @@ +## trivy plugin search + +List Trivy plugins available on the plugin index and search among them + +``` +trivy plugin search [KEYWORD] +``` + +### Options + +``` + -h, --help help for search +``` + +### Options inherited from parent commands + +``` + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/docs/references/configuration/cli/trivy_plugin_update.md b/docs/docs/references/configuration/cli/trivy_plugin_update.md index 532add27cfa3..da26290882b1 100644 --- a/docs/docs/references/configuration/cli/trivy_plugin_update.md +++ b/docs/docs/references/configuration/cli/trivy_plugin_update.md @@ -1,9 +1,9 @@ ## trivy plugin update -Update an existing plugin +Update the local copy of the plugin index ``` -trivy plugin update PLUGIN_NAME +trivy plugin update ``` ### Options diff --git a/docs/docs/references/configuration/cli/trivy_plugin_upgrade.md b/docs/docs/references/configuration/cli/trivy_plugin_upgrade.md new file mode 100644 index 000000000000..a3d363d5643a --- /dev/null +++ b/docs/docs/references/configuration/cli/trivy_plugin_upgrade.md @@ -0,0 +1,31 @@ +## trivy plugin upgrade + +Upgrade installed plugins to newer versions + +``` +trivy plugin upgrade [PLUGIN_NAMES] +``` + +### Options + +``` + -h, --help help for upgrade +``` + +### Options inherited from parent commands + +``` + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 8cee93646bb0..f010ac08d58f 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -810,7 +810,7 @@ func NewPluginCommand() *cobra.Command { Use: "search [KEYWORD]", SilenceErrors: true, DisableFlagsInUseLine: true, - Short: "List available plugins and search among them", + Short: "List Trivy plugins available on the plugin index and search among them", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return plugin.Search(cmd.Context(), args) From 855325221afcd18c2513d24ecb07577829defc7a Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 10:37:51 +0400 Subject: [PATCH 07/26] docs: update a developer guide Signed-off-by: knqyf263 --- docs/docs/plugin/developer-guide.md | 75 +++++++++++++++++++++++------ docs/docs/plugin/index.md | 2 +- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/docs/docs/plugin/developer-guide.md b/docs/docs/plugin/developer-guide.md index 25613d706d8e..520e3d9b8b06 100644 --- a/docs/docs/plugin/developer-guide.md +++ b/docs/docs/plugin/developer-guide.md @@ -1,6 +1,11 @@ # Developer Guide -## Introduction +## Developing Trivy plugins +This section will guide you through the process of developing Trivy plugins. +To help you get started quickly, we have published a [plugin template repository][plugin-template]. +You can use this template as a starting point for your plugin development. + +### Introduction If you are looking to start developing plugins for Trivy, read [the user guide](./user-guide.md) first. To summarize the documentation, the procedure is to: @@ -10,42 +15,50 @@ To summarize the documentation, the procedure is to: - Place the executable binary in a repository. - Create a `plugin.yaml` file that describes the plugin. -After you develop a plugin with a good name following the best practices, you can develop a [trivy-plugin-index][trivy-plugin-index] manifest and submit your plugin. +After you develop a plugin with a good name following the best practices, you can develop a [Trivy plugin index][trivy-plugin-index] manifest and submit your plugin. -## Naming +### Naming This section describes guidelines for naming your plugins. -### Use `trivy-plugin-` prefix +#### Use `trivy-plugin-` prefix The name of the plugin repository should be prefixed with `trivy-plugin-`. -### Use lowercase and hyphens +#### Use lowercase and hyphens Plugin names must be all lowercase and separate words with hyphens. Don’t use camelCase, PascalCase, or snake_case; use kebab-case. - NO: `trivy OpenSvc` - YES: `trivy open-svc` -### Be specific +#### Be specific Plugin names should not be verbs or nouns that are generic, already overloaded, or likely to be used for broader purposes by another plugin. - NO: trivy sast (Too broad) - YES: trivy govulncheck -### Be unique +#### Be unique Find a unique name for your plugin that differentiates it from other plugins that perform a similar function. - NO: `trivy images` (Unclear how it is different from the builtin “image" command) - YES: `trivy registry-images` (Unique name). -### Prefix Vendor Identifiers +#### Prefix Vendor Identifiers Use vendor-specific strings as prefix, separated with a dash. This makes it easier to search/group plugins that are about a specific vendor. - NO: `trivy security-hub-aws (Makes it harder to search or locate in a plugin list) - YES: `trivy aws-security-hub (Will show up together with other aws-* plugins) -Each plugin has a top-level directory, and then a plugin.yaml file. +### Choosing a language +Since Trivy plugins are standalone executables, you can write them in any programming language. + +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), +which is the output of Trivy scan. + + +### Writing your plugin +Each plugin has a top-level directory, and then a `plugin.yaml` file. ```bash your-plugin/ @@ -54,16 +67,18 @@ your-plugin/ |- your-plugin.sh ``` -In the example above, the plugin is contained inside of a directory named `your-plugin`. -It has two files: plugin.yaml (required) and an executable script, your-plugin.sh (optional). +In the example above, the plugin is contained inside a directory named `your-plugin`. +It has two files: `plugin.yaml` (required) and an executable script, `your-plugin.sh` (optional). -The core of a plugin is a simple YAML file named plugin.yaml. -Here is an example YAML of trivy-plugin-kubectl plugin that adds support for Kubernetes scanning. +#### Writing a plugin manifest +The plugin manifest is a simple YAML file named `plugin.yaml`. +Here is an example YAML of [trivy-plugin-kubectl][trivy-plugin-kubectl] plugin that adds support for Kubernetes scanning. ```yaml name: "kubectl" repository: github.com/aquasecurity/trivy-plugin-kubectl version: "0.1.0" +output: false usage: scan kubectl resources description: |- A Trivy plugin that scans the images of a kubernetes resource. @@ -81,10 +96,17 @@ platforms: bin: ./trivy-kubectl ``` +We encourage you to copy and adapt plugin manifests of existing plugins. + +- [count][trivy-plugin-count] +- [referrer][trivy-plugin-referrer] + The `plugin.yaml` field should contain the following information: - 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) +- repository: The repository name where the plugin is hosted. (required) - version: The version of the plugin. (required) +- output: Whether the plugin supports [the output mode](./user-guide.md#output-mode-support). (optional) - usage: A short usage description. (required) - description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required) - platforms: (required) @@ -104,9 +126,13 @@ The following rules will apply in deciding which platform to select: After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache. When the plugin is called via Trivy CLI, `bin` command will be executed. -The plugin is responsible for handling flags and arguments. Any arguments are passed to the plugin from the `trivy` command. +#### Plugin arguments/flags +The plugin is responsible for handling flags and arguments. +Any arguments are passed to the plugin from the `trivy` command. +#### Testing plugin installation locally A plugin should be archived `*.tar.gz`. +After you have archived your plugin into a `.tar.gz` file, you can verify that your plugin installs correctly with Trivy. ```bash $ tar -czvf myplugin.tar.gz plugin.yaml script.py @@ -121,4 +147,23 @@ $ trivy myplugin Hello from Trivy demo plugin! ``` -[trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index \ No newline at end of file +## Publishing plugins +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. +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. +Users can then install your plugin with the command, `trivy plugin install github.com/org/repo`. + +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. +You can utilize GitHub Releases to distribute the executable file. +For an example of how to structure your plugin repository, refer to [the plugin template repository][plugin-template]. + +## Distributing plugins on the Trivy plugin index +Trivy can install plugins directly by specifying a repository, so you don't necessarily need to register your plugin in the Trivy Plugin Index. +However, we would recommend it since it makes it easier for other users to find and install your plugin. + +See [the Trivy plugin index repository][trivy-plugin-index] for more information on how to submit your plugin to the plugin index. + +[plugin-template]: https://github.com/aquasecurity/trivy-plugin-template +[trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index +[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl +[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count/blob/main/plugin.yaml +[trivy-plugin-referrer]: https://github.com/aquasecurity/trivy-plugin-referrer/blob/main/plugin.yaml diff --git a/docs/docs/plugin/index.md b/docs/docs/plugin/index.md index dbf13826a3a8..a032f387d6af 100644 --- a/docs/docs/plugin/index.md +++ b/docs/docs/plugin/index.md @@ -27,7 +27,7 @@ Let’s get started: $ trivy plugin update ``` -2. Discover plugins available: +2. Discover Trivy plugins available on the plugin index: ```bash $ trivy plugin search From 739c0e988dfd3824967b32afe51fe365965e9e85 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Mon, 13 May 2024 11:58:27 +0400 Subject: [PATCH 08/26] Use NoArgs Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> --- pkg/commands/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index f010ac08d58f..bdf10d3a03ea 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -798,7 +798,7 @@ func NewPluginCommand() *cobra.Command { Short: "Update the local copy of the plugin index", SilenceErrors: true, DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { if err := plugin.Update(cmd.Context()); err != nil { return xerrors.Errorf("plugin update error: %w", err) From 9051fefc5e7c33ff04090199d7beff9bf66d0dc0 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Mon, 13 May 2024 13:41:07 +0400 Subject: [PATCH 09/26] Update docs/docs/plugin/user-guide.md Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> --- docs/docs/plugin/user-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/docs/plugin/user-guide.md b/docs/docs/plugin/user-guide.md index 7668f8df77f5..31f96094d4f3 100644 --- a/docs/docs/plugin/user-guide.md +++ b/docs/docs/plugin/user-guide.md @@ -25,7 +25,6 @@ You can specify search keywords as arguments: ```bash $ trivy plugin search referrer -NAME DESCRIPTION INSTALLED NAME DESCRIPTION MAINTAINER OUTPUT referrer A plugin for OCI referrers aquasecurity ✓ ``` From d41b59221563150a6087c31163d72995b6ccce1e Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 13:36:21 +0400 Subject: [PATCH 10/26] refactor: remove unneeded PersistentRun Signed-off-by: knqyf263 --- pkg/commands/app.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index bdf10d3a03ea..64342fc7a2a7 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -720,10 +720,6 @@ func NewPluginCommand() *cobra.Command { Short: "Manage plugins", SilenceErrors: true, SilenceUsage: true, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - ctx := log.WithContextPrefix(cmd.Context(), "plugin") - cmd.SetContext(ctx) - }, } cmd.AddCommand( &cobra.Command{ From 9dbecdfa41ecfc5868860d291c38664158cc80ba Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 13:36:49 +0400 Subject: [PATCH 11/26] fix(plugin): delay initilizing plugin manager Signed-off-by: knqyf263 --- pkg/plugin/manager.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 6badaaba4b10..3c9ded361c68 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -24,7 +24,7 @@ const configFile = "plugin.yaml" var ( pluginsRelativeDir = filepath.Join(".trivy", "plugins") - defaultManager = NewManager() + _defaultManager *Manager ) type ManagerOption func(indexer *Manager) @@ -64,22 +64,28 @@ func NewManager(opts ...ManagerOption) *Manager { return m } +func defaultManager() *Manager { + if _defaultManager == nil { + _defaultManager = NewManager() + } + return _defaultManager +} + func Install(ctx context.Context, name string, opts Options) (Plugin, error) { - return defaultManager.Install(ctx, name, opts) + return defaultManager().Install(ctx, name, opts) } func Start(ctx context.Context, name string, opts Options) (Wait, error) { - return defaultManager.Start(ctx, name, opts) + return defaultManager().Start(ctx, name, opts) } func RunWithURL(ctx context.Context, name string, opts Options) error { - return defaultManager.RunWithURL(ctx, name, opts) + return defaultManager().RunWithURL(ctx, name, opts) } -func Upgrade(ctx context.Context, names []string) error { return defaultManager.Upgrade(ctx, names) } -func Uninstall(ctx context.Context, name string) error { return defaultManager.Uninstall(ctx, name) } -func Information(name string) error { return defaultManager.Information(name) } -func List(ctx context.Context) error { return defaultManager.List(ctx) } -func Update(ctx context.Context) error { return defaultManager.Update(ctx) } -func Search(ctx context.Context, args []string) error { return defaultManager.Search(ctx, args) } -func LoadAll(ctx context.Context) ([]Plugin, error) { return defaultManager.LoadAll(ctx) } +func Upgrade(ctx context.Context, names []string) error { return defaultManager().Upgrade(ctx, names) } +func Uninstall(ctx context.Context, name string) error { return defaultManager().Uninstall(ctx, name) } +func Information(name string) error { return defaultManager().Information(name) } +func List(ctx context.Context) error { return defaultManager().List(ctx) } +func Update(ctx context.Context) error { return defaultManager().Update(ctx) } +func Search(ctx context.Context, args []string) error { return defaultManager().Search(ctx, args) } // Install installs a plugin func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) { From 2fcd08ba92d89ad24f6ced49adc8c32f20fff731 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 13:38:20 +0400 Subject: [PATCH 12/26] fix: remove MaximumNArgs Signed-off-by: knqyf263 --- pkg/commands/app.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 64342fc7a2a7..7a34e7b4781e 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -817,7 +817,6 @@ func NewPluginCommand() *cobra.Command { Short: "Upgrade installed plugins to newer versions", SilenceErrors: true, DisableFlagsInUseLine: true, - Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if err := plugin.Upgrade(cmd.Context(), args); err != nil { return xerrors.Errorf("plugin upgrade error: %w", err) From d2f2e963c3bb63993da8cfb922fb1a5c9cf1261b Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 13:41:51 +0400 Subject: [PATCH 13/26] refactor: pass one argument as a keyword Signed-off-by: knqyf263 --- pkg/commands/app.go | 6 +++++- pkg/plugin/index.go | 4 ++-- pkg/plugin/index_test.go | 18 +++++++++--------- pkg/plugin/manager.go | 2 +- pkg/plugin/manager_test.go | 14 +++++++------- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 7a34e7b4781e..96a7e3aa2f38 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -809,7 +809,11 @@ func NewPluginCommand() *cobra.Command { Short: "List Trivy plugins available on the plugin index and search among them", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return plugin.Search(cmd.Context(), args) + var keyword string + if len(args) == 1 { + keyword = args[0] + } + return plugin.Search(cmd.Context(), keyword) }, }, &cobra.Command{ diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go index 549218cf46ee..2036a0adda5a 100644 --- a/pkg/plugin/index.go +++ b/pkg/plugin/index.go @@ -36,7 +36,7 @@ func (m *Manager) Update(ctx context.Context) error { return nil } -func (m *Manager) Search(ctx context.Context, args []string) error { +func (m *Manager) Search(ctx context.Context, keyword string) error { indexes, err := m.loadIndex() if errors.Is(err, os.ErrNotExist) { m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") @@ -48,7 +48,7 @@ func (m *Manager) Search(ctx context.Context, args []string) error { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT")) for _, index := range indexes { - if len(args) == 0 || strings.Contains(index.Name, args[0]) || strings.Contains(index.Description, args[0]) { + if keyword == "" || strings.Contains(index.Name, keyword) || strings.Contains(index.Description, keyword) { s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(index.Name, 20), truncateString(index.Description, 60), truncateString(index.Maintainer, 20), lo.Ternary(index.Output, " ✓", "")) diff --git a/pkg/plugin/index_test.go b/pkg/plugin/index_test.go index db1411532017..d918a9dac7ef 100644 --- a/pkg/plugin/index_test.go +++ b/pkg/plugin/index_test.go @@ -39,15 +39,15 @@ func TestManager_Update(t *testing.T) { func TestManager_Search(t *testing.T) { tests := []struct { name string - args []string + keyword string dir string want string wantErr string }{ { - name: "all plugins", - args: nil, - dir: "testdata", + name: "all plugins", + keyword: "", + dir: "testdata", want: `NAME DESCRIPTION MAINTAINER OUTPUT foo A foo plugin aquasecurity ✓ bar A bar plugin aquasecurity @@ -55,16 +55,16 @@ test A test plugin `, }, { - name: "keyword", - args: []string{"bar"}, - dir: "testdata", + name: "keyword", + keyword: "bar", + dir: "testdata", want: `NAME DESCRIPTION MAINTAINER OUTPUT bar A bar plugin aquasecurity `, }, { name: "no index", - args: nil, + keyword: "", dir: "unknown", wantErr: "plugin index not found", }, @@ -75,7 +75,7 @@ bar A bar plugin var got bytes.Buffer m := plugin.NewManager(plugin.WithWriter(&got)) - err := m.Search(context.Background(), tt.args) + err := m.Search(context.Background(), tt.keyword) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 3c9ded361c68..5d6ce593aab8 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -85,7 +85,7 @@ func Uninstall(ctx context.Context, name string) error { return defaultManager( func Information(name string) error { return defaultManager().Information(name) } func List(ctx context.Context) error { return defaultManager().List(ctx) } func Update(ctx context.Context) error { return defaultManager().Update(ctx) } -func Search(ctx context.Context, args []string) error { return defaultManager().Search(ctx, args) } +func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) } // Install installs a plugin func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) { diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go index d472639328ff..ba1cd1e92d31 100644 --- a/pkg/plugin/manager_test.go +++ b/pkg/plugin/manager_test.go @@ -33,7 +33,7 @@ func TestManager_Run(t *testing.T) { Name string Repository string Version string - Usage string + Summary string Description string Platforms []plugin.Platform GOOS string @@ -51,7 +51,7 @@ func TestManager_Run(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -73,7 +73,7 @@ func TestManager_Run(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -89,7 +89,7 @@ func TestManager_Run(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -112,7 +112,7 @@ func TestManager_Run(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -135,7 +135,7 @@ func TestManager_Run(t *testing.T) { Name: "error_plugin", Repository: "github.com/aquasecurity/trivy-plugin-error", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -161,7 +161,7 @@ func TestManager_Run(t *testing.T) { Name: tt.fields.Name, Repository: tt.fields.Repository, Version: tt.fields.Version, - Usage: tt.fields.Usage, + Summary: tt.fields.Summary, Description: tt.fields.Description, Platforms: tt.fields.Platforms, } From 5a64f2db028edf6c702e50bfdbf7f1e62275cbf8 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:00:20 +0400 Subject: [PATCH 14/26] feat: replace usage with summary Signed-off-by: knqyf263 --- pkg/commands/app.go | 3 ++- pkg/plugin/manager.go | 11 ++++++++--- pkg/plugin/manager_test.go | 11 ++++++++--- pkg/plugin/plugin.go | 3 ++- pkg/plugin/testdata/test_plugin/plugin.yaml | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 96a7e3aa2f38..963956956af4 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -125,7 +125,8 @@ func loadPluginCommands() []*cobra.Command { p := p cmd := &cobra.Command{ Use: fmt.Sprintf("%s [flags]", p.Name), - Short: p.Usage, + Short: p.Summary, + Long: p.Description, GroupID: groupPlugin, RunE: func(cmd *cobra.Command, args []string) error { if err = p.Run(cmd.Context(), plugin.Options{Args: args}); err != nil { diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 5d6ce593aab8..b0c978b956fe 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -151,10 +151,10 @@ func (m *Manager) Information(name string) error { _, err = fmt.Fprintf(m.w, ` Plugin: %s - Description: %s Version: %s - Usage: %s -`, plugin.Name, plugin.Description, plugin.Version, plugin.Usage) + Summary: %s + Description: %s +`, plugin.Name, plugin.Version, plugin.Summary, plugin.Description) return err } @@ -326,6 +326,11 @@ func (m *Manager) loadMetadata(dir string) (Plugin, error) { // e.g. ~/.trivy/plugins/kubectl plugin.dir = filepath.Join(m.pluginRoot, plugin.Name) + if plugin.Summary == "" && plugin.Usage != "" { + plugin.Summary = plugin.Usage // For backward compatibility + plugin.Usage = "" + } + return plugin, nil } diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go index ba1cd1e92d31..71e7bec16919 100644 --- a/pkg/plugin/manager_test.go +++ b/pkg/plugin/manager_test.go @@ -192,7 +192,7 @@ func TestManager_Install(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { @@ -349,7 +349,12 @@ description: A simple test plugin` // Get Information for the plugin err = manager.Information(pluginName) require.NoError(t, err) - assert.Equal(t, "\nPlugin: test_plugin\n Description: A simple test plugin\n Version: 0.1.0\n Usage: test\n", got.String()) + assert.Equal(t, ` +Plugin: test_plugin + Version: 0.1.0 + Summary: test + Description: A simple test plugin +`, got.String()) got.Reset() // Get Information for unknown plugin @@ -373,7 +378,7 @@ func TestManager_LoadAll(t *testing.T) { Name: "test_plugin", Repository: "github.com/aquasecurity/trivy-plugin-test", Version: "0.1.0", - Usage: "test", + Summary: "test", Description: "test", Platforms: []plugin.Platform{ { diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index cf246a5ef13b..68a50ae31780 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -24,7 +24,8 @@ type Plugin struct { Name string `yaml:"name"` Repository string `yaml:"repository"` Version string `yaml:"version"` - Usage string `yaml:"usage"` + Summary string `yaml:"summary"` + Usage string `yaml:"usage"` // Deprecated: Use summary instead Description string `yaml:"description"` Platforms []Platform `yaml:"platforms"` diff --git a/pkg/plugin/testdata/test_plugin/plugin.yaml b/pkg/plugin/testdata/test_plugin/plugin.yaml index 7c2021b29196..272c8d5760a7 100644 --- a/pkg/plugin/testdata/test_plugin/plugin.yaml +++ b/pkg/plugin/testdata/test_plugin/plugin.yaml @@ -1,7 +1,7 @@ name: "test_plugin" repository: github.com/aquasecurity/trivy-plugin-test version: "0.1.0" -usage: test +summary: test description: test platforms: - selector: From 6bcd91bcf2a81c35f24dad06cb68f4585f5da8a9 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:02:21 +0400 Subject: [PATCH 15/26] test: use a common error message Signed-off-by: knqyf263 --- pkg/plugin/manager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go index 71e7bec16919..958a320512d5 100644 --- a/pkg/plugin/manager_test.go +++ b/pkg/plugin/manager_test.go @@ -396,7 +396,7 @@ func TestManager_LoadAll(t *testing.T) { { name: "sad path", dir: "sad", - wantErr: "no such file or directory", + wantErr: "failed to read", }, } for _, tt := range tests { From 39ce40e6b5b6eee60bda488f10585a8aa06fba12 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:07:44 +0400 Subject: [PATCH 16/26] docs: mention the keyword matches name or desc Signed-off-by: knqyf263 --- docs/docs/plugin/user-guide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/plugin/user-guide.md b/docs/docs/plugin/user-guide.md index 31f96094d4f3..24334bc142cd 100644 --- a/docs/docs/plugin/user-guide.md +++ b/docs/docs/plugin/user-guide.md @@ -29,6 +29,8 @@ NAME DESCRIPTION referrer A plugin for OCI referrers aquasecurity ✓ ``` +It lists plugins with the keyword in the name or description. + ## Installing Plugins Plugins can be installed with the `trivy plugin install` command: From 466b9f5ea59b8fdeffe4ab2c2cabb0f4f22aaf79 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:10:21 +0400 Subject: [PATCH 17/26] chore: add info for successful installation Signed-off-by: knqyf263 --- pkg/plugin/manager.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index b0c978b956fe..6046300499bb 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -129,6 +129,8 @@ func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin return Plugin{}, xerrors.Errorf("yaml encode error: %w", err) } + m.logger.InfoContext(ctx, "Plugin successfully installed", log.String("name", plugin.Name)) + return plugin, nil } From 7e6213703e0b33cb2fa3e9858e1912cea3e88d8b Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:13:32 +0400 Subject: [PATCH 18/26] chore: fix usage Signed-off-by: knqyf263 --- docs/docs/references/configuration/cli/trivy_plugin_run.md | 2 +- pkg/commands/app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/references/configuration/cli/trivy_plugin_run.md b/docs/docs/references/configuration/cli/trivy_plugin_run.md index 0dc7087a19d0..5befb58f90ef 100644 --- a/docs/docs/references/configuration/cli/trivy_plugin_run.md +++ b/docs/docs/references/configuration/cli/trivy_plugin_run.md @@ -3,7 +3,7 @@ Run a plugin on the fly ``` -trivy plugin run URL | FILE_PATH +trivy plugin run NAME | URL | FILE_PATH ``` ### Options diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 963956956af4..658eef8c50ef 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -780,7 +780,7 @@ func NewPluginCommand() *cobra.Command { }, }, &cobra.Command{ - Use: "run URL | FILE_PATH", + Use: "run NAME | URL | FILE_PATH", Aliases: []string{"r"}, SilenceErrors: true, DisableFlagsInUseLine: true, From 1a5f094e92d2106996e197d99334ddee23159c28 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 13 May 2024 14:51:35 +0400 Subject: [PATCH 19/26] docs: update the developer guide Signed-off-by: knqyf263 --- docs/docs/plugin/developer-guide.md | 54 +++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/docs/docs/plugin/developer-guide.md b/docs/docs/plugin/developer-guide.md index 520e3d9b8b06..5080bab9ede9 100644 --- a/docs/docs/plugin/developer-guide.md +++ b/docs/docs/plugin/developer-guide.md @@ -8,14 +8,15 @@ You can use this template as a starting point for your plugin development. ### Introduction If you are looking to start developing plugins for Trivy, read [the user guide](./user-guide.md) first. -To summarize the documentation, the procedure is to: +The development process involves the following steps: - Create a repository for your plugin, named `trivy-plugin-`. - Create an executable binary that can be invoked as `trivy `. - Place the executable binary in a repository. - Create a `plugin.yaml` file that describes the plugin. +- (Submit your plugin to the [Trivy plugin index][trivy-plugin-index].) -After you develop a plugin with a good name following the best practices, you can develop a [Trivy plugin index][trivy-plugin-index] manifest and submit your plugin. +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]. ### Naming This section describes guidelines for naming your plugins. @@ -76,10 +77,11 @@ Here is an example YAML of [trivy-plugin-kubectl][trivy-plugin-kubectl] plugin t ```yaml name: "kubectl" -repository: github.com/aquasecurity/trivy-plugin-kubectl version: "0.1.0" +repository: github.com/aquasecurity/trivy-plugin-kubectl +maintainer: aquasecurity output: false -usage: scan kubectl resources +summary: Scan kubectl resources description: |- A Trivy plugin that scans the images of a kubernetes resource. Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME @@ -104,10 +106,12 @@ We encourage you to copy and adapt plugin manifests of existing plugins. The `plugin.yaml` field should contain the following information: - 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) +- version: The version of the plugin. [Semantic Versioning][semver] should be used. (required) - repository: The repository name where the plugin is hosted. (required) -- version: The version of the plugin. (required) +- maintainer: The name of the maintainer of the plugin. (required) - output: Whether the plugin supports [the output mode](./user-guide.md#output-mode-support). (optional) -- usage: A short usage description. (required) +- usage: Deprecated: use summary instead. (optional) +- summary: A short usage description. (required) - description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required) - platforms: (required) - selector: The OS/Architecture specific variations of a execution file. (optional) @@ -156,13 +160,43 @@ While the `uri` specified in the plugin.yaml file doesn't necessarily need to po You can utilize GitHub Releases to distribute the executable file. For an example of how to structure your plugin repository, refer to [the plugin template repository][plugin-template]. -## Distributing plugins on the Trivy plugin index -Trivy can install plugins directly by specifying a repository, so you don't necessarily need to register your plugin in the Trivy Plugin Index. -However, we would recommend it since it makes it easier for other users to find and install your plugin. +## Distributing plugins via the Trivy plugin index +Trivy can install plugins directly by specifying a repository, like `trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`, +so you don't necessarily need to register your plugin in the Trivy plugin index. +However, we would recommend distributing your plugin via the Trivy plugin index +since it makes it easier for other users to find (`trivy plugin search`) and install your plugin (e.g. `trivy plugin install kubectl`). + +### Pre-submit checklist +- Review [the plugin naming guide](#naming). +- Ensure the `plugin.yaml` file has all the required fields. +- Tag a git release with a semantic version (e.g. v1.0.0). +- [Test your plugin installation locally](#testing-plugin-installation-locally). + +### Submitting plugins +Submitting your plugin to the plugin index is a straightforward process. +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]. + +Once you've done that, create a pull request (PR) and have it reviewed by the maintainers. +Once your PR is merged, the index will be updated, and your plugin will be available for installation. +[The plugin index page][plugin-list] will also be automatically updated to list your newly added plugin. + +The content of the YAML file is very simple. +You only need to specify the name of your plugin and the repository where it is distributed. + +```yaml +name: referrer +repository: github.com/aquasecurity/trivy-plugin-referrer +``` + +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]. +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. +Once [the index.yaml][index] has been updated, running `trivy plugin update` will download the updated index to your local machine. -See [the Trivy plugin index repository][trivy-plugin-index] for more information on how to submit your plugin to the plugin index. [plugin-template]: https://github.com/aquasecurity/trivy-plugin-template +[plugin-list]: https://aquasecurity.github.io/trivy-plugin-index/ +[index]: https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml +[semver]: https://semver.org/ [trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index [trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl [trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count/blob/main/plugin.yaml From 99ab575cc29d0cae3e8dff194ca74218f7f33ff9 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Tue, 14 May 2024 09:55:59 +0400 Subject: [PATCH 20/26] Update docs/docs/plugin/user-guide.md Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> --- docs/docs/plugin/user-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/docs/plugin/user-guide.md b/docs/docs/plugin/user-guide.md index 24334bc142cd..b216f3d63c85 100644 --- a/docs/docs/plugin/user-guide.md +++ b/docs/docs/plugin/user-guide.md @@ -52,7 +52,6 @@ Furthermore, it is possible to download plugins that are not registered in the i ```bash $ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl ``` -` ```bash $ trivy plugin install myplugin.tar.gz ``` From c622be1c6d84e0a276105d11f449e717422a69fd Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Tue, 14 May 2024 09:58:24 +0400 Subject: [PATCH 21/26] Update docs/docs/plugin/index.md Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> --- docs/docs/plugin/index.md | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/docs/plugin/index.md b/docs/docs/plugin/index.md index a032f387d6af..b640dce47b90 100644 --- a/docs/docs/plugin/index.md +++ b/docs/docs/plugin/index.md @@ -23,44 +23,44 @@ Let’s get started: 1. Download the plugin list: -```bash -$ trivy plugin update -``` + ```bash + $ trivy plugin update + ``` 2. Discover Trivy plugins available on the plugin index: -```bash -$ trivy plugin search -NAME DESCRIPTION MAINTAINER OUTPUT -aqua A plugin for integration with Aqua Security SaaS platform aquasecurity -kubectl A plugin scanning the images of a kubernetes resource aquasecurity -referrer A plugin for OCI referrers aquasecurity ✓ -[...] -``` + ```bash + $ trivy plugin search + NAME DESCRIPTION MAINTAINER OUTPUT + aqua A plugin for integration with Aqua Security SaaS platform aquasecurity + kubectl A plugin scanning the images of a kubernetes resource aquasecurity + referrer A plugin for OCI referrers aquasecurity ✓ + [...] + ``` 3. Choose a plugin from the list and install it: -```bash -$ trivy plugin install referrer -``` + ```bash + $ trivy plugin install referrer + ``` 4. Use the installed plugin: -```bash -$ trivy referrer --help -``` + ```bash + $ trivy referrer --help + ``` 5. Keep your plugins up-to-date: -```bash -$ trivy plugin upgrade -``` + ```bash + $ trivy plugin upgrade + ``` 6. Uninstall a plugin you no longer use: -```bash -trivy plugin uninstall referrer -``` + ```bash + trivy plugin uninstall referrer + ``` This is practically all you need to know to start using Trivy plugins. From a240e5268c8b8040be13dd5624c9acdb8dbad2f1 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Tue, 14 May 2024 10:10:43 +0400 Subject: [PATCH 22/26] docs: update mkdocs Signed-off-by: knqyf263 --- mkdocs.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 760cbcaf6008..9fb769a7c921 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -155,16 +155,20 @@ nav: - Filesystem: docs/references/configuration/cli/trivy_filesystem.md - Image: docs/references/configuration/cli/trivy_image.md - Kubernetes: docs/references/configuration/cli/trivy_kubernetes.md - - Module: docs/references/configuration/cli/trivy_module.md - - Module Install: docs/references/configuration/cli/trivy_module_install.md - - Module Uninstall: docs/references/configuration/cli/trivy_module_uninstall.md - - Plugin: docs/references/configuration/cli/trivy_plugin.md - - Plugin Info: docs/references/configuration/cli/trivy_plugin_info.md - - Plugin Install: docs/references/configuration/cli/trivy_plugin_install.md - - Plugin List: docs/references/configuration/cli/trivy_plugin_list.md - - Plugin Run: docs/references/configuration/cli/trivy_plugin_run.md - - Plugin Uninstall: docs/references/configuration/cli/trivy_plugin_uninstall.md - - Plugin Update: docs/references/configuration/cli/trivy_plugin_update.md + - Module: + - Module: docs/references/configuration/cli/trivy_module.md + - Module Install: docs/references/configuration/cli/trivy_module_install.md + - Module Uninstall: docs/references/configuration/cli/trivy_module_uninstall.md + - Plugin: + - Plugin: docs/references/configuration/cli/trivy_plugin.md + - Plugin Info: docs/references/configuration/cli/trivy_plugin_info.md + - Plugin Install: docs/references/configuration/cli/trivy_plugin_install.md + - Plugin List: docs/references/configuration/cli/trivy_plugin_list.md + - Plugin Run: docs/references/configuration/cli/trivy_plugin_run.md + - Plugin Uninstall: docs/references/configuration/cli/trivy_plugin_uninstall.md + - Plugin Update: docs/references/configuration/cli/trivy_plugin_update.md + - Plugin Upgrade: docs/references/configuration/cli/trivy_plugin_upgrade.md + - Plugin Search: docs/references/configuration/cli/trivy_plugin_search.md - Repository: docs/references/configuration/cli/trivy_repository.md - Rootfs: docs/references/configuration/cli/trivy_rootfs.md - SBOM: docs/references/configuration/cli/trivy_sbom.md From 60f4848f546f7e9de9ccd58f96762faebac4b8b2 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Tue, 14 May 2024 10:11:24 +0400 Subject: [PATCH 23/26] fix: use a local logger Signed-off-by: knqyf263 --- pkg/plugin/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 6046300499bb..7c9082591e68 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -218,7 +218,7 @@ func (m *Manager) upgrade(ctx context.Context, name string) error { return xerrors.Errorf("plugin load error: %w", err) } - logger := log.With("name", name) + logger := m.logger.With("name", name) logger.InfoContext(ctx, "Upgrading plugin...") updated, err := m.install(ctx, plugin.Repository, Options{ // Use the current installed platform From 1835268ff46527a693fb3635c2fb65067e5f4725 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Tue, 14 May 2024 10:12:32 +0400 Subject: [PATCH 24/26] fix: add SilenceUsage Signed-off-by: knqyf263 --- pkg/commands/app.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 658eef8c50ef..6ae687276f2a 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -741,9 +741,10 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "uninstall PLUGIN_NAME", Aliases: []string{"u"}, - SilenceErrors: true, DisableFlagsInUseLine: true, Short: "Uninstall a plugin", + SilenceErrors: true, + SilenceUsage: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if err := plugin.Uninstall(cmd.Context(), args[0]); err != nil { @@ -755,8 +756,9 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "list", Aliases: []string{"l"}, - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, Short: "List installed plugin", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -769,8 +771,9 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "info PLUGIN_NAME", Short: "Show information about the specified plugin", - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { if err := plugin.Information(args[0]); err != nil { @@ -782,8 +785,9 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "run NAME | URL | FILE_PATH", Aliases: []string{"r"}, - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, Short: "Run a plugin on the fly", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -793,8 +797,9 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "update", Short: "Update the local copy of the plugin index", - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { if err := plugin.Update(cmd.Context()); err != nil { @@ -805,8 +810,9 @@ func NewPluginCommand() *cobra.Command { }, &cobra.Command{ Use: "search [KEYWORD]", - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, Short: "List Trivy plugins available on the plugin index and search among them", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -820,8 +826,9 @@ func NewPluginCommand() *cobra.Command { &cobra.Command{ Use: "upgrade [PLUGIN_NAMES]", Short: "Upgrade installed plugins to newer versions", - SilenceErrors: true, DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if err := plugin.Upgrade(cmd.Context(), args); err != nil { return xerrors.Errorf("plugin upgrade error: %w", err) From 2fe4f21ad27392e63b0115413a8d83484737a19d Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Tue, 14 May 2024 10:14:30 +0400 Subject: [PATCH 25/26] chore: add a message on uninstallation Signed-off-by: knqyf263 --- pkg/plugin/manager.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 7c9082591e68..8f79d744bfb0 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -141,7 +141,11 @@ func (m *Manager) Uninstall(ctx context.Context, name string) error { m.logger.ErrorContext(ctx, "No such plugin") return nil } - return os.RemoveAll(pluginDir) + if err := os.RemoveAll(pluginDir); err != nil { + return xerrors.Errorf("failed to uninstall the plugin: %w", err) + } + m.logger.InfoContext(ctx, "Plugin successfully uninstalled", log.String("name", name)) + return nil } // Information gets the information about an installed plugin From 7fae791ae70a7d89e0d91ec7bfc61c875b2f70da Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Tue, 14 May 2024 10:59:08 +0400 Subject: [PATCH 26/26] feat: add version to the index Signed-off-by: knqyf263 --- pkg/plugin/index.go | 41 ++++++++++++++------------- pkg/plugin/testdata/plugin/index.yaml | 28 +++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go index 2036a0adda5a..c825c16e67af 100644 --- a/pkg/plugin/index.go +++ b/pkg/plugin/index.go @@ -21,11 +21,14 @@ import ( const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml" type Index struct { - Name string `yaml:"name"` - Maintainer string `yaml:"maintainer"` - Description string `yaml:"description"` - Repository string `yaml:"repository"` - Output bool `yaml:"output"` + Version int `yaml:"version"` + Plugins []struct { + Name string `yaml:"name"` + Maintainer string `yaml:"maintainer"` + Summary string `yaml:"summary"` + Repository string `yaml:"repository"` + Output bool `yaml:"output"` + } `yaml:"plugins"` } func (m *Manager) Update(ctx context.Context) error { @@ -37,7 +40,7 @@ func (m *Manager) Update(ctx context.Context) error { } func (m *Manager) Search(ctx context.Context, keyword string) error { - indexes, err := m.loadIndex() + index, err := m.loadIndex() if errors.Is(err, os.ErrNotExist) { m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") return xerrors.Errorf("plugin index not found: %w", err) @@ -47,11 +50,11 @@ func (m *Manager) Search(ctx context.Context, keyword string) error { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT")) - for _, index := range indexes { - if keyword == "" || strings.Contains(index.Name, keyword) || strings.Contains(index.Description, keyword) { - s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(index.Name, 20), - truncateString(index.Description, 60), truncateString(index.Maintainer, 20), - lo.Ternary(index.Output, " ✓", "")) + for _, p := range index.Plugins { + if keyword == "" || strings.Contains(p.Name, keyword) || strings.Contains(p.Summary, keyword) { + s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(p.Name, 20), + truncateString(p.Summary, 60), truncateString(p.Maintainer, 20), + lo.Ternary(p.Output, " ✓", "")) buf.WriteString(s) } } @@ -74,7 +77,7 @@ func (m *Manager) tryIndex(ctx context.Context, name string) string { } } - indexes, err := m.loadIndex() + index, err := m.loadIndex() if errors.Is(err, os.ErrNotExist) { m.logger.WarnContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") return name @@ -83,27 +86,27 @@ func (m *Manager) tryIndex(ctx context.Context, name string) string { return name } - for _, index := range indexes { - if index.Name == name { - return index.Repository + for _, p := range index.Plugins { + if p.Name == name { + return p.Repository } } return name } -func (m *Manager) loadIndex() ([]Index, error) { +func (m *Manager) loadIndex() (*Index, error) { f, err := os.Open(m.indexPath) if err != nil { return nil, xerrors.Errorf("unable to open the index file: %w", err) } defer f.Close() - var indexes []Index - if err = yaml.NewDecoder(f).Decode(&indexes); err != nil { + var index Index + if err = yaml.NewDecoder(f).Decode(&index); err != nil { return nil, xerrors.Errorf("unable to decode the index file: %w", err) } - return indexes, nil + return &index, nil } func truncateString(str string, num int) string { diff --git a/pkg/plugin/testdata/plugin/index.yaml b/pkg/plugin/testdata/plugin/index.yaml index 0a52db7cd657..17fbb1987939 100644 --- a/pkg/plugin/testdata/plugin/index.yaml +++ b/pkg/plugin/testdata/plugin/index.yaml @@ -1,13 +1,15 @@ -- name: foo - output: true - maintainer: aquasecurity - description: A foo plugin - repository: github.com/aquasecurity/trivy-plugin-foo -- name: bar - maintainer: aquasecurity - description: A bar plugin - repository: github.com/aquasecurity/trivy-plugin-bar -- name: test - maintainer: aquasecurity - description: A test plugin - repository: testdata/test_plugin +version: 1 +plugins: + - name: foo + output: true + maintainer: aquasecurity + summary: A foo plugin + repository: github.com/aquasecurity/trivy-plugin-foo + - name: bar + maintainer: aquasecurity + summary: A bar plugin + repository: github.com/aquasecurity/trivy-plugin-bar + - name: test + maintainer: aquasecurity + summary: A test plugin + repository: testdata/test_plugin \ No newline at end of file