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 }