Files
trivy/pkg/commands/app.go
Owen Rumney 8bfbc84a41 feat(plugin): Add option to update plugin (#1462)
* Add option to update plugin

- add plugin update [pluginName] to update
- add supporting test

* refactor: wrap errors
2021-12-16 13:30:19 +02:00

705 lines
17 KiB
Go

package commands
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/spf13/afero"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/client"
"github.com/aquasecurity/trivy/pkg/commands/plugin"
"github.com/aquasecurity/trivy/pkg/commands/server"
tdb "github.com/aquasecurity/trivy/pkg/db"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils"
)
// VersionInfo holds the trivy DB version Info
type VersionInfo struct {
Version string `json:",omitempty"`
VulnerabilityDB *db.Metadata `json:",omitempty"`
}
var (
templateFlag = cli.StringFlag{
Name: "template",
Aliases: []string{"t"},
Value: "",
Usage: "output template",
EnvVars: []string{"TRIVY_TEMPLATE"},
}
formatFlag = cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Value: "table",
Usage: "format (table, json, template)",
EnvVars: []string{"TRIVY_FORMAT"},
}
inputFlag = cli.StringFlag{
Name: "input",
Aliases: []string{"i"},
Value: "",
Usage: "input file path instead of image name",
EnvVars: []string{"TRIVY_INPUT"},
}
severityFlag = cli.StringFlag{
Name: "severity",
Aliases: []string{"s"},
Value: strings.Join(dbTypes.SeverityNames, ","),
Usage: "severities of vulnerabilities to be displayed (comma separated)",
EnvVars: []string{"TRIVY_SEVERITY"},
}
outputFlag = cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "output file name",
EnvVars: []string{"TRIVY_OUTPUT"},
}
exitCodeFlag = cli.IntFlag{
Name: "exit-code",
Usage: "Exit code when vulnerabilities were found",
Value: 0,
EnvVars: []string{"TRIVY_EXIT_CODE"},
}
skipDBUpdateFlag = cli.BoolFlag{
Name: "skip-db-update",
Aliases: []string{"skip-update"},
Usage: "skip updating vulnerability database",
EnvVars: []string{"TRIVY_SKIP_UPDATE", "TRIVY_SKIP_DB_UPDATE"},
}
skipPolicyUpdateFlag = cli.BoolFlag{
Name: "skip-policy-update",
Usage: "skip updating built-in policies",
EnvVars: []string{"TRIVY_SKIP_POLICY_UPDATE"},
}
downloadDBOnlyFlag = cli.BoolFlag{
Name: "download-db-only",
Usage: "download/update vulnerability database but don't run a scan",
EnvVars: []string{"TRIVY_DOWNLOAD_DB_ONLY"},
}
resetFlag = cli.BoolFlag{
Name: "reset",
Usage: "remove all caches and database",
EnvVars: []string{"TRIVY_RESET"},
}
clearCacheFlag = cli.BoolFlag{
Name: "clear-cache",
Aliases: []string{"c"},
Usage: "clear image caches without scanning",
EnvVars: []string{"TRIVY_CLEAR_CACHE"},
}
quietFlag = cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "suppress progress bar and log output",
EnvVars: []string{"TRIVY_QUIET"},
}
noProgressFlag = cli.BoolFlag{
Name: "no-progress",
Usage: "suppress progress bar",
EnvVars: []string{"TRIVY_NO_PROGRESS"},
}
ignoreUnfixedFlag = cli.BoolFlag{
Name: "ignore-unfixed",
Usage: "display only fixed vulnerabilities",
EnvVars: []string{"TRIVY_IGNORE_UNFIXED"},
}
debugFlag = cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "debug mode",
EnvVars: []string{"TRIVY_DEBUG"},
}
removedPkgsFlag = cli.BoolFlag{
Name: "removed-pkgs",
Usage: "detect vulnerabilities of removed packages (only for Alpine)",
EnvVars: []string{"TRIVY_REMOVED_PKGS"},
}
vulnTypeFlag = cli.StringFlag{
Name: "vuln-type",
Value: strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","),
Usage: "comma-separated list of vulnerability types (os,library)",
EnvVars: []string{"TRIVY_VULN_TYPE"},
}
securityChecksFlag = cli.StringFlag{
Name: "security-checks",
Value: types.SecurityCheckVulnerability,
Usage: "comma-separated list of what security issues to detect (vuln,config)",
EnvVars: []string{"TRIVY_SECURITY_CHECKS"},
}
cacheDirFlag = cli.StringFlag{
Name: "cache-dir",
Value: utils.DefaultCacheDir(),
Usage: "cache directory",
EnvVars: []string{"TRIVY_CACHE_DIR"},
}
cacheBackendFlag = cli.StringFlag{
Name: "cache-backend",
Value: "fs",
Usage: "cache backend (e.g. redis://localhost:6379)",
EnvVars: []string{"TRIVY_CACHE_BACKEND"},
}
ignoreFileFlag = cli.StringFlag{
Name: "ignorefile",
Value: result.DefaultIgnoreFile,
Usage: "specify .trivyignore file",
EnvVars: []string{"TRIVY_IGNOREFILE"},
}
timeoutFlag = cli.DurationFlag{
Name: "timeout",
Value: time.Second * 300,
Usage: "timeout",
EnvVars: []string{"TRIVY_TIMEOUT"},
}
lightFlag = cli.BoolFlag{
Name: "light",
Usage: "light mode: it's faster, but vulnerability descriptions and references are not displayed",
EnvVars: []string{"TRIVY_LIGHT"},
}
token = cli.StringFlag{
Name: "token",
Usage: "for authentication",
EnvVars: []string{"TRIVY_TOKEN"},
}
tokenHeader = cli.StringFlag{
Name: "token-header",
Value: "Trivy-Token",
Usage: "specify a header name for token",
EnvVars: []string{"TRIVY_TOKEN_HEADER"},
}
ignorePolicy = cli.StringFlag{
Name: "ignore-policy",
Usage: "specify the Rego file to evaluate each vulnerability",
EnvVars: []string{"TRIVY_IGNORE_POLICY"},
}
listAllPackages = cli.BoolFlag{
Name: "list-all-pkgs",
Usage: "enabling the option will output all packages regardless of vulnerability",
EnvVars: []string{"TRIVY_LIST_ALL_PKGS"},
}
skipFiles = cli.StringSliceFlag{
Name: "skip-files",
Usage: "specify the file paths to skip traversal",
EnvVars: []string{"TRIVY_SKIP_FILES"},
}
skipDirs = cli.StringSliceFlag{
Name: "skip-dirs",
Usage: "specify the directories where the traversal is skipped",
EnvVars: []string{"TRIVY_SKIP_DIRS"},
}
// For misconfigurations
configPolicy = cli.StringSliceFlag{
Name: "config-policy",
Usage: "specify paths to the Rego policy files directory, applying config files",
EnvVars: []string{"TRIVY_CONFIG_POLICY"},
}
configPolicyAlias = cli.StringSliceFlag{
Name: "policy",
Aliases: []string{"config-policy"},
Usage: "specify paths to the Rego policy files directory, applying config files",
EnvVars: []string{"TRIVY_POLICY"},
}
configData = cli.StringSliceFlag{
Name: "config-data",
Usage: "specify paths from which data for the Rego policies will be recursively loaded",
EnvVars: []string{"TRIVY_CONFIG_DATA"},
}
configDataAlias = cli.StringSliceFlag{
Name: "data",
Aliases: []string{"config-data"},
Usage: "specify paths from which data for the Rego policies will be recursively loaded",
EnvVars: []string{"TRIVY_DATA"},
}
filePatterns = cli.StringSliceFlag{
Name: "file-patterns",
Usage: "specify file patterns",
EnvVars: []string{"TRIVY_FILE_PATTERNS"},
}
policyNamespaces = cli.StringSliceFlag{
Name: "policy-namespaces",
Aliases: []string{"namespaces"},
Usage: "Rego namespaces",
Value: cli.NewStringSlice("users"),
EnvVars: []string{"TRIVY_POLICY_NAMESPACES"},
}
includeNonFailures = cli.BoolFlag{
Name: "include-non-failures",
Usage: "include successes and exceptions",
Value: false,
EnvVars: []string{"TRIVY_INCLUDE_NON_FAILURES"},
}
traceFlag = cli.BoolFlag{
Name: "trace",
Usage: "enable more verbose trace output for custom queries",
Value: false,
EnvVars: []string{"TRIVY_TRACE"},
}
// Global flags
globalFlags = []cli.Flag{
&quietFlag,
&debugFlag,
&cacheDirFlag,
}
imageFlags = []cli.Flag{
&templateFlag,
&formatFlag,
&inputFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipDBUpdateFlag,
&downloadDBOnlyFlag,
&resetFlag,
&clearCacheFlag,
&noProgressFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&securityChecksFlag,
&ignoreFileFlag,
&timeoutFlag,
&lightFlag,
&ignorePolicy,
&listAllPackages,
&cacheBackendFlag,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
}
)
// NewApp is the factory method to return Trivy CLI
func NewApp(version string) *cli.App {
cli.VersionPrinter = func(c *cli.Context) {
showVersion(c.String("cache-dir"), c.String("format"), c.App.Version, c.App.Writer)
}
app := cli.NewApp()
app.Name = "trivy"
app.Version = version
app.ArgsUsage = "target"
app.Usage = "A simple and comprehensive vulnerability scanner for containers"
app.EnableBashCompletion = true
flags := append(globalFlags, setHidden(imageFlags, true)...)
app.Flags = flags
app.Commands = []*cli.Command{
NewImageCommand(),
NewFilesystemCommand(),
NewRootfsCommand(),
NewRepositoryCommand(),
NewClientCommand(),
NewServerCommand(),
NewConfigCommand(),
NewPluginCommand(),
}
app.Commands = append(app.Commands, plugin.LoadCommands()...)
runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN")
if runAsPlugin == "" {
app.Action = artifact.ImageRun
} else {
app.Action = func(ctx *cli.Context) error {
return plugin.RunWithArgs(ctx.Context, runAsPlugin, ctx.Args().Slice())
}
}
return app
}
func setHidden(flags []cli.Flag, hidden bool) []cli.Flag {
var newFlags []cli.Flag
for _, flag := range flags {
var f cli.Flag
switch pf := flag.(type) {
case *cli.StringFlag:
stringFlag := *pf
stringFlag.Hidden = hidden
f = &stringFlag
case *cli.StringSliceFlag:
stringSliceFlag := *pf
stringSliceFlag.Hidden = hidden
f = &stringSliceFlag
case *cli.BoolFlag:
boolFlag := *pf
boolFlag.Hidden = hidden
f = &boolFlag
case *cli.IntFlag:
intFlag := *pf
intFlag.Hidden = hidden
f = &intFlag
case *cli.DurationFlag:
durationFlag := *pf
durationFlag.Hidden = hidden
f = &durationFlag
}
newFlags = append(newFlags, f)
}
return newFlags
}
func showVersion(cacheDir, outputFormat, version string, outputWriter io.Writer) {
var dbMeta *db.Metadata
metadata, _ := tdb.NewMetadata(afero.NewOsFs(), cacheDir).Get() // nolint: errcheck
if !metadata.UpdatedAt.IsZero() && !metadata.NextUpdate.IsZero() && metadata.Version != 0 {
dbMeta = &db.Metadata{
Version: metadata.Version,
Type: metadata.Type,
NextUpdate: metadata.NextUpdate.UTC(),
UpdatedAt: metadata.UpdatedAt.UTC(),
DownloadedAt: metadata.DownloadedAt.UTC(),
}
}
switch outputFormat {
case "json":
b, _ := json.Marshal(VersionInfo{ // nolint: errcheck
Version: version,
VulnerabilityDB: dbMeta,
})
fmt.Fprintln(outputWriter, string(b))
default:
output := fmt.Sprintf("Version: %s\n", version)
if dbMeta != nil {
var dbType string
switch dbMeta.Type {
case db.TypeFull:
dbType = "Full"
case db.TypeLight:
dbType = "Light"
}
output += fmt.Sprintf(`Vulnerability DB:
Type: %s
Version: %d
UpdatedAt: %s
NextUpdate: %s
DownloadedAt: %s
`, dbType, dbMeta.Version, dbMeta.UpdatedAt.UTC(), dbMeta.NextUpdate.UTC(), dbMeta.DownloadedAt.UTC())
}
fmt.Fprintf(outputWriter, output)
}
}
// NewImageCommand is the factory method to add image command
func NewImageCommand() *cli.Command {
return &cli.Command{
Name: "image",
Aliases: []string{"i"},
ArgsUsage: "image_name",
Usage: "scan an image",
Action: artifact.ImageRun,
Flags: imageFlags,
}
}
// NewFilesystemCommand is the factory method to add filesystem command
func NewFilesystemCommand() *cli.Command {
return &cli.Command{
Name: "filesystem",
Aliases: []string{"fs"},
ArgsUsage: "dir",
Usage: "scan local filesystem for language-specific dependencies and config files",
Action: artifact.FilesystemRun,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipDBUpdateFlag,
&skipPolicyUpdateFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&vulnTypeFlag,
&securityChecksFlag,
&ignoreFileFlag,
&cacheBackendFlag,
&timeoutFlag,
&noProgressFlag,
&ignorePolicy,
&listAllPackages,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
stringSliceFlag(configPolicy),
stringSliceFlag(configData),
stringSliceFlag(policyNamespaces),
},
}
}
// NewRootfsCommand is the factory method to add filesystem command
func NewRootfsCommand() *cli.Command {
return &cli.Command{
Name: "rootfs",
ArgsUsage: "dir",
Usage: "scan rootfs",
Action: artifact.RootfsRun,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipDBUpdateFlag,
&skipPolicyUpdateFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&vulnTypeFlag,
&securityChecksFlag,
&ignoreFileFlag,
&cacheBackendFlag,
&timeoutFlag,
&noProgressFlag,
&ignorePolicy,
&listAllPackages,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
stringSliceFlag(configPolicy),
stringSliceFlag(configData),
stringSliceFlag(policyNamespaces),
},
}
}
// NewRepositoryCommand is the factory method to add repository command
func NewRepositoryCommand() *cli.Command {
return &cli.Command{
Name: "repository",
Aliases: []string{"repo"},
ArgsUsage: "repo_url",
Usage: "scan remote repository",
Action: artifact.RepositoryRun,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&inputFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipDBUpdateFlag,
&skipPolicyUpdateFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&securityChecksFlag,
&ignoreFileFlag,
&cacheBackendFlag,
&timeoutFlag,
&noProgressFlag,
&ignorePolicy,
&listAllPackages,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
},
}
}
// NewClientCommand is the factory method to add client command
func NewClientCommand() *cli.Command {
return &cli.Command{
Name: "client",
Aliases: []string{"c"},
ArgsUsage: "image_name",
Usage: "client mode",
Action: client.Run,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&inputFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&securityChecksFlag,
&ignoreFileFlag,
&timeoutFlag,
&ignorePolicy,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
stringSliceFlag(configPolicy),
&listAllPackages,
// original flags
&token,
&tokenHeader,
&cli.StringFlag{
Name: "remote",
Value: "http://localhost:4954",
Usage: "server address",
EnvVars: []string{"TRIVY_REMOTE"},
},
&cli.StringSliceFlag{
Name: "custom-headers",
Usage: "custom headers",
EnvVars: []string{"TRIVY_CUSTOM_HEADERS"},
},
},
}
}
// NewServerCommand is the factory method to add server command
func NewServerCommand() *cli.Command {
return &cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "server mode",
Action: server.Run,
Flags: []cli.Flag{
&skipDBUpdateFlag,
&downloadDBOnlyFlag,
&resetFlag,
&cacheBackendFlag,
// original flags
&token,
&tokenHeader,
&cli.StringFlag{
Name: "listen",
Value: "localhost:4954",
Usage: "listen address",
EnvVars: []string{"TRIVY_LISTEN"},
},
},
}
}
// NewConfigCommand adds config command
func NewConfigCommand() *cli.Command {
return &cli.Command{
Name: "config",
Aliases: []string{"conf"},
ArgsUsage: "dir",
Usage: "scan config files",
Action: artifact.ConfigRun,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipPolicyUpdateFlag,
&resetFlag,
&clearCacheFlag,
&ignoreFileFlag,
&timeoutFlag,
stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs),
stringSliceFlag(configPolicyAlias),
stringSliceFlag(configDataAlias),
stringSliceFlag(policyNamespaces),
stringSliceFlag(filePatterns),
&includeNonFailures,
&traceFlag,
},
}
}
// NewPluginCommand is the factory method to add plugin command
func NewPluginCommand() *cli.Command {
return &cli.Command{
Name: "plugin",
Aliases: []string{"p"},
ArgsUsage: "plugin_uri",
Usage: "manage plugins",
Subcommands: cli.Commands{
{
Name: "install",
Aliases: []string{"i"},
Usage: "install a plugin",
ArgsUsage: "URL | FILE_PATH",
Action: plugin.Install,
},
{
Name: "uninstall",
Aliases: []string{"u"},
Usage: "uninstall a plugin",
ArgsUsage: "PLUGIN_NAME",
Action: plugin.Uninstall,
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "list installed plugin",
Action: plugin.List,
},
{
Name: "info",
Usage: "information about a plugin",
ArgsUsage: "PLUGIN_NAME",
Action: plugin.Information,
},
{
Name: "run",
Aliases: []string{"r"},
Usage: "run a plugin on the fly",
ArgsUsage: "PLUGIN_NAME [PLUGIN_OPTIONS]",
Action: plugin.Run,
},
{
Name: "update",
Usage: "update an existing plugin",
ArgsUsage: "PLUGIN_NAME",
Action: plugin.Update,
},
},
}
}
// StringSliceFlag is defined globally. When the app runs multiple times,
// the previous value will be retained and it causes unexpected results.
// The flag value is copied through this function to prevent the issue.
func stringSliceFlag(f cli.StringSliceFlag) *cli.StringSliceFlag {
return &f
}