github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/plugin/plugin.go (about)

     1  package plugin
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"golang.org/x/xerrors"
    13  	"gopkg.in/yaml.v3"
    14  
    15  	"github.com/devseccon/trivy/pkg/downloader"
    16  	"github.com/devseccon/trivy/pkg/log"
    17  	"github.com/devseccon/trivy/pkg/utils/fsutils"
    18  )
    19  
    20  const (
    21  	configFile = "plugin.yaml"
    22  )
    23  
    24  var (
    25  	pluginsRelativeDir = filepath.Join(".trivy", "plugins")
    26  
    27  	officialPlugins = map[string]string{
    28  		"kubectl": "github.com/devseccon/trivy-plugin-kubectl",
    29  		"aqua":    "github.com/devseccon/trivy-plugin-aqua",
    30  	}
    31  )
    32  
    33  // Plugin represents a plugin.
    34  type Plugin struct {
    35  	Name        string     `yaml:"name"`
    36  	Repository  string     `yaml:"repository"`
    37  	Version     string     `yaml:"version"`
    38  	Usage       string     `yaml:"usage"`
    39  	Description string     `yaml:"description"`
    40  	Platforms   []Platform `yaml:"platforms"`
    41  
    42  	// runtime environment for testability
    43  	GOOS   string `yaml:"_goos"`
    44  	GOARCH string `yaml:"_goarch"`
    45  }
    46  
    47  // Platform represents where the execution file exists per platform.
    48  type Platform struct {
    49  	Selector *Selector
    50  	URI      string
    51  	Bin      string
    52  }
    53  
    54  // Selector represents the environment.
    55  type Selector struct {
    56  	OS   string
    57  	Arch string
    58  }
    59  
    60  // Run runs the plugin
    61  func (p Plugin) Run(ctx context.Context, args []string) error {
    62  	platform, err := p.selectPlatform()
    63  	if err != nil {
    64  		return xerrors.Errorf("platform selection error: %w", err)
    65  	}
    66  
    67  	execFile := filepath.Join(dir(), p.Name, platform.Bin)
    68  
    69  	cmd := exec.CommandContext(ctx, execFile, args...)
    70  	cmd.Stdin = os.Stdin
    71  	cmd.Stdout = os.Stdout
    72  	cmd.Stderr = os.Stderr
    73  	cmd.Env = os.Environ()
    74  
    75  	// If an error is found during the execution of the plugin, figure
    76  	// out if the error was from not being able to execute the plugin or
    77  	// an error set by the plugin itself.
    78  	if err = cmd.Run(); err != nil {
    79  		if _, ok := err.(*exec.ExitError); !ok {
    80  			return xerrors.Errorf("exit: %w", err)
    81  		}
    82  
    83  		return xerrors.Errorf("plugin exec: %w", err)
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  func (p Plugin) selectPlatform() (Platform, error) {
    90  	// These values are only filled in during unit tests.
    91  	if p.GOOS == "" {
    92  		p.GOOS = runtime.GOOS
    93  	}
    94  	if p.GOARCH == "" {
    95  		p.GOARCH = runtime.GOARCH
    96  	}
    97  
    98  	for _, platform := range p.Platforms {
    99  		if platform.Selector == nil {
   100  			return platform, nil
   101  		}
   102  
   103  		selector := platform.Selector
   104  		if (selector.OS == "" || p.GOOS == selector.OS) &&
   105  			(selector.Arch == "" || p.GOARCH == selector.Arch) {
   106  			log.Logger.Debugf("Platform found, os: %s, arch: %s", selector.OS, selector.Arch)
   107  			return platform, nil
   108  		}
   109  	}
   110  	return Platform{}, xerrors.New("platform not found")
   111  }
   112  
   113  func (p Plugin) install(ctx context.Context, dst, pwd string) error {
   114  	log.Logger.Debugf("Installing the plugin to %s...", dst)
   115  	platform, err := p.selectPlatform()
   116  	if err != nil {
   117  		return xerrors.Errorf("platform selection error: %w", err)
   118  	}
   119  
   120  	log.Logger.Debugf("Downloading the execution file from %s...", platform.URI)
   121  	if err = downloader.Download(ctx, platform.URI, dst, pwd); err != nil {
   122  		return xerrors.Errorf("unable to download the execution file (%s): %w", platform.URI, err)
   123  	}
   124  	return nil
   125  }
   126  
   127  func (p Plugin) dir() (string, error) {
   128  	if p.Name == "" {
   129  		return "", xerrors.Errorf("'name' is empty")
   130  	}
   131  
   132  	// e.g. ~/.trivy/plugins/kubectl
   133  	return filepath.Join(dir(), p.Name), nil
   134  }
   135  
   136  // Install installs a plugin
   137  func Install(ctx context.Context, url string, force bool) (Plugin, error) {
   138  	// Replace short names with full qualified names
   139  	// e.g. kubectl => github.com/devseccon/trivy-plugin-kubectl
   140  	if v, ok := officialPlugins[url]; ok {
   141  		url = v
   142  	}
   143  
   144  	if !force {
   145  		// If the plugin is already installed, it skips installing the plugin.
   146  		if p, installed := isInstalled(url); installed {
   147  			return p, nil
   148  		}
   149  	}
   150  
   151  	log.Logger.Infof("Installing the plugin from %s...", url)
   152  	tempDir, err := downloader.DownloadToTempDir(ctx, url)
   153  	if err != nil {
   154  		return Plugin{}, xerrors.Errorf("download failed: %w", err)
   155  	}
   156  	defer os.RemoveAll(tempDir)
   157  
   158  	log.Logger.Info("Loading the plugin metadata...")
   159  	plugin, err := loadMetadata(tempDir)
   160  	if err != nil {
   161  		return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err)
   162  	}
   163  
   164  	pluginDir, err := plugin.dir()
   165  	if err != nil {
   166  		return Plugin{}, xerrors.Errorf("failed to determine the plugin dir: %w", err)
   167  	}
   168  
   169  	if err = plugin.install(ctx, pluginDir, tempDir); err != nil {
   170  		return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err)
   171  	}
   172  
   173  	// Copy plugin.yaml into the plugin dir
   174  	if _, err = fsutils.CopyFile(filepath.Join(tempDir, configFile), filepath.Join(pluginDir, configFile)); err != nil {
   175  		return Plugin{}, xerrors.Errorf("failed to copy plugin.yaml: %w", err)
   176  	}
   177  
   178  	return plugin, nil
   179  }
   180  
   181  // Uninstall installs the plugin
   182  func Uninstall(name string) error {
   183  	pluginDir := filepath.Join(dir(), name)
   184  	return os.RemoveAll(pluginDir)
   185  }
   186  
   187  // Information gets the information about an installed plugin
   188  func Information(name string) (string, error) {
   189  	pluginDir := filepath.Join(dir(), name)
   190  
   191  	if _, err := os.Stat(pluginDir); err != nil {
   192  		if os.IsNotExist(err) {
   193  			return "", xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
   194  		}
   195  		return "", xerrors.Errorf("stat error: %w", err)
   196  	}
   197  
   198  	plugin, err := loadMetadata(pluginDir)
   199  	if err != nil {
   200  		return "", xerrors.Errorf("unable to load metadata: %w", err)
   201  	}
   202  
   203  	return fmt.Sprintf(`
   204  Plugin: %s
   205    Description: %s
   206    Version:     %s
   207    Usage:       %s
   208  `, plugin.Name, plugin.Description, plugin.Version, plugin.Usage), nil
   209  }
   210  
   211  // List gets a list of all installed plugins
   212  func List() (string, error) {
   213  	if _, err := os.Stat(dir()); err != nil {
   214  		if os.IsNotExist(err) {
   215  			return "No Installed Plugins\n", nil
   216  		}
   217  		return "", xerrors.Errorf("stat error: %w", err)
   218  	}
   219  	plugins, err := LoadAll()
   220  	if err != nil {
   221  		return "", xerrors.Errorf("unable to load plugins: %w", err)
   222  	}
   223  	pluginList := []string{"Installed Plugins:"}
   224  	for _, plugin := range plugins {
   225  		pluginList = append(pluginList, fmt.Sprintf("  Name:    %s\n  Version: %s\n", plugin.Name, plugin.Version))
   226  	}
   227  
   228  	return strings.Join(pluginList, "\n"), nil
   229  }
   230  
   231  // Update updates an existing plugin
   232  func Update(name string) error {
   233  	pluginDir := filepath.Join(dir(), name)
   234  
   235  	if _, err := os.Stat(pluginDir); err != nil {
   236  		if os.IsNotExist(err) {
   237  			return xerrors.Errorf("could not find a plugin called '%s' to update: %w", name, err)
   238  		}
   239  		return err
   240  	}
   241  
   242  	plugin, err := loadMetadata(pluginDir)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	log.Logger.Infof("Updating plugin '%s'", name)
   247  	updated, err := Install(nil, plugin.Repository, true)
   248  	if err != nil {
   249  		return xerrors.Errorf("unable to perform an update installation: %w", err)
   250  	}
   251  
   252  	if plugin.Version == updated.Version {
   253  		log.Logger.Infof("The %s plugin is the latest version. [%s]", name, plugin.Version)
   254  	} else {
   255  		log.Logger.Infof("Updated '%s' from %s to %s", name, plugin.Version, updated.Version)
   256  	}
   257  	return nil
   258  }
   259  
   260  // LoadAll loads all plugins
   261  func LoadAll() ([]Plugin, error) {
   262  	pluginsDir := dir()
   263  	dirs, err := os.ReadDir(pluginsDir)
   264  	if err != nil {
   265  		return nil, xerrors.Errorf("failed to read %s: %w", pluginsDir, err)
   266  	}
   267  
   268  	var plugins []Plugin
   269  	for _, d := range dirs {
   270  		if !d.IsDir() {
   271  			continue
   272  		}
   273  		plugin, err := loadMetadata(filepath.Join(pluginsDir, d.Name()))
   274  		if err != nil {
   275  			log.Logger.Warnf("plugin load error: %s", err)
   276  			continue
   277  		}
   278  		plugins = append(plugins, plugin)
   279  	}
   280  	return plugins, nil
   281  }
   282  
   283  // RunWithArgs runs the plugin with arguments
   284  func RunWithArgs(ctx context.Context, url string, args []string) error {
   285  	pl, err := Install(ctx, url, false)
   286  	if err != nil {
   287  		return xerrors.Errorf("plugin install error: %w", err)
   288  	}
   289  
   290  	if err = pl.Run(ctx, args); err != nil {
   291  		return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err)
   292  	}
   293  	return nil
   294  }
   295  
   296  func IsPredefined(name string) bool {
   297  	_, ok := officialPlugins[name]
   298  	return ok
   299  }
   300  
   301  func loadMetadata(dir string) (Plugin, error) {
   302  	filePath := filepath.Join(dir, configFile)
   303  	f, err := os.Open(filePath)
   304  	if err != nil {
   305  		return Plugin{}, xerrors.Errorf("file open error: %w", err)
   306  	}
   307  
   308  	var plugin Plugin
   309  	if err = yaml.NewDecoder(f).Decode(&plugin); err != nil {
   310  		return Plugin{}, xerrors.Errorf("yaml decode error: %w", err)
   311  	}
   312  
   313  	return plugin, nil
   314  }
   315  
   316  func dir() string {
   317  	return filepath.Join(fsutils.HomeDir(), pluginsRelativeDir)
   318  }
   319  
   320  func isInstalled(url string) (Plugin, bool) {
   321  	installedPlugins, err := LoadAll()
   322  	if err != nil {
   323  		return Plugin{}, false
   324  	}
   325  
   326  	for _, plugin := range installedPlugins {
   327  		if plugin.Repository == url {
   328  			return plugin, true
   329  		}
   330  	}
   331  	return Plugin{}, false
   332  }