mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -08:00
246 lines
6.0 KiB
Go
246 lines
6.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"golang.org/x/xerrors"
|
|
yaml "gopkg.in/yaml.v3"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/downloader"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/utils"
|
|
)
|
|
|
|
const (
|
|
configFile = "plugin.yaml"
|
|
xdgDataHome = "XDG_DATA_HOME"
|
|
)
|
|
|
|
var (
|
|
pluginsRelativeDir = filepath.Join(".trivy", "plugins")
|
|
|
|
officialPlugins = map[string]string{
|
|
"kubectl": "github.com/aquasecurity/trivy-plugin-kubectl",
|
|
}
|
|
)
|
|
|
|
// Plugin represents a plugin.
|
|
type Plugin struct {
|
|
Name string `yaml:"name"`
|
|
Repository string `yaml:"repository"`
|
|
Version string `yaml:"version"`
|
|
Usage string `yaml:"usage"`
|
|
Description string `yaml:"description"`
|
|
Platforms []Platform `yaml:"platforms"`
|
|
|
|
// runtime environment for testability
|
|
GOOS string `yaml:"_goos"`
|
|
GOARCH string `yaml:"_goarch"`
|
|
}
|
|
|
|
// Platform represents where the execution file exists per platform.
|
|
type Platform struct {
|
|
Selector *Selector
|
|
URI string
|
|
Bin string
|
|
}
|
|
|
|
// Selector represents the environment.
|
|
type Selector struct {
|
|
OS string
|
|
Arch string
|
|
}
|
|
|
|
// Run runs the plugin
|
|
func (p Plugin) Run(ctx context.Context, args []string) error {
|
|
platform, err := p.selectPlatform()
|
|
if err != nil {
|
|
return xerrors.Errorf("platform selection error: %w", err)
|
|
}
|
|
|
|
execFile := filepath.Join(dir(), p.Name, platform.Bin)
|
|
|
|
cmd := exec.CommandContext(ctx, execFile, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Env = os.Environ()
|
|
|
|
// If an error is found during the execution of the plugin, figure
|
|
// out if the error was from not being able to execute the plugin or
|
|
// an error set by the plugin itself.
|
|
if err = cmd.Run(); err != nil {
|
|
if _, ok := err.(*exec.ExitError); !ok {
|
|
return xerrors.Errorf("exit: %w", err)
|
|
}
|
|
|
|
return xerrors.Errorf("plugin exec: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Plugin) selectPlatform() (Platform, error) {
|
|
// These values are only filled in during unit tests.
|
|
if p.GOOS == "" {
|
|
p.GOOS = runtime.GOOS
|
|
}
|
|
if p.GOARCH == "" {
|
|
p.GOARCH = runtime.GOARCH
|
|
}
|
|
|
|
for _, platform := range p.Platforms {
|
|
if platform.Selector == nil {
|
|
return platform, nil
|
|
}
|
|
|
|
selector := platform.Selector
|
|
if (selector.OS == "" || p.GOOS == selector.OS) &&
|
|
(selector.Arch == "" || p.GOARCH == selector.Arch) {
|
|
log.Logger.Debugf("Platform found, os: %s, arch: %s", selector.OS, selector.Arch)
|
|
return platform, nil
|
|
}
|
|
}
|
|
return Platform{}, xerrors.New("platform not found")
|
|
}
|
|
|
|
func (p Plugin) install(ctx context.Context, dst, pwd string) error {
|
|
log.Logger.Debugf("Installing the plugin to %s...", dst)
|
|
platform, err := p.selectPlatform()
|
|
if err != nil {
|
|
return xerrors.Errorf("platform selection error: %w", err)
|
|
}
|
|
|
|
log.Logger.Debugf("Downloading the execution file from %s...", 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.Logger.Infof("Installing the plugin from %s...", url)
|
|
tempDir, err := downloader.DownloadToTempDir(ctx, url)
|
|
if err != nil {
|
|
return Plugin{}, xerrors.Errorf("download failed: %w", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
log.Logger.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 = utils.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)
|
|
}
|
|
|
|
// 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.Logger.Warnf("plugin load error: %s", err)
|
|
continue
|
|
}
|
|
plugins = append(plugins, plugin)
|
|
}
|
|
return plugins, 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)
|
|
}
|
|
|
|
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 {
|
|
dataHome := os.Getenv(xdgDataHome)
|
|
if dataHome != "" {
|
|
return filepath.Join(dataHome, pluginsRelativeDir)
|
|
}
|
|
|
|
homeDir, _ := os.UserHomeDir()
|
|
return filepath.Join(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
|
|
}
|
|
}
|
|
return Plugin{}, false
|
|
}
|