refactor(internal): export internal packages (#887)

* refactor: export internal packages

* refactor(server): define Server

* refactor: fix lint issues

* test(integration): fix imports
This commit is contained in:
Teppei Fukuda
2021-03-14 17:04:01 +02:00
committed by GitHub
parent 8b3b5d0290
commit c26a3e481f
40 changed files with 104 additions and 89 deletions

View File

@@ -1,545 +0,0 @@
package internal
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/spf13/afero"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/internal/artifact"
"github.com/aquasecurity/trivy/internal/client"
"github.com/aquasecurity/trivy/internal/plugin"
"github.com/aquasecurity/trivy/internal/server"
tdb "github.com/aquasecurity/trivy/pkg/db"
"github.com/aquasecurity/trivy/pkg/utils"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
// 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(types.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"},
}
skipUpdateFlag = cli.BoolFlag{
Name: "skip-update",
Usage: "skip db update",
EnvVars: []string{"TRIVY_SKIP_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: "os,library",
Usage: "comma-separated list of vulnerability types (os,library)",
EnvVars: []string{"TRIVY_VULN_TYPE"},
}
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: vulnerability.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.StringFlag{
Name: "skip-files",
Usage: "specify the file path to skip traversal",
EnvVars: []string{"TRIVY_SKIP_FILES"},
}
skipDirectories = cli.StringFlag{
Name: "skip-dirs",
Usage: "specify the directory where the traversal is skipped",
EnvVars: []string{"TRIVY_SKIP_DIRS"},
}
globalFlags = []cli.Flag{
&quietFlag,
&debugFlag,
&cacheDirFlag,
}
imageFlags = []cli.Flag{
&templateFlag,
&formatFlag,
&inputFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipUpdateFlag,
&downloadDBOnlyFlag,
&resetFlag,
&clearCacheFlag,
&noProgressFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&ignoreFileFlag,
&timeoutFlag,
&lightFlag,
&ignorePolicy,
&listAllPackages,
&skipFiles,
&skipDirectories,
&cacheBackendFlag,
}
// deprecated options
deprecatedFlags = []cli.Flag{
&cli.StringFlag{
Name: "only-update",
Usage: "deprecated",
EnvVars: []string{"TRIVY_ONLY_UPDATE"},
},
&cli.BoolFlag{
Name: "refresh",
Usage: "deprecated",
EnvVars: []string{"TRIVY_REFRESH"},
},
&cli.BoolFlag{
Name: "auto-refresh",
Usage: "deprecated",
EnvVars: []string{"TRIVY_AUTO_REFRESH"},
},
}
)
// 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(deprecatedFlags, true)...)
flags = append(flags, setHidden(imageFlags, true)...)
app.Flags = flags
app.Commands = []*cli.Command{
NewImageCommand(),
NewFilesystemCommand(),
NewRepositoryCommand(),
NewClientCommand(),
NewServerCommand(),
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.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 0:
dbType = "Full"
case 1:
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",
Action: artifact.FilesystemRun,
Flags: []cli.Flag{
&templateFlag,
&formatFlag,
&inputFlag,
&severityFlag,
&outputFlag,
&exitCodeFlag,
&skipUpdateFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&ignoreFileFlag,
&cacheBackendFlag,
&timeoutFlag,
&noProgressFlag,
&ignorePolicy,
&listAllPackages,
&skipFiles,
&skipDirectories,
},
}
}
// 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,
&skipUpdateFlag,
&clearCacheFlag,
&ignoreUnfixedFlag,
&removedPkgsFlag,
&vulnTypeFlag,
&ignoreFileFlag,
&cacheBackendFlag,
&timeoutFlag,
&noProgressFlag,
&ignorePolicy,
&listAllPackages,
&skipFiles,
&skipDirectories,
},
}
}
// 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,
&ignoreFileFlag,
&timeoutFlag,
&ignorePolicy,
// 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{
&skipUpdateFlag,
&downloadDBOnlyFlag,
&resetFlag,
&cacheBackendFlag,
// original flags
&token,
&tokenHeader,
&cli.StringFlag{
Name: "listen",
Value: "localhost:4954",
Usage: "listen address",
EnvVars: []string{"TRIVY_LISTEN"},
},
},
}
}
// NewPluginCommand is the factory method to add plugin command
func NewPluginCommand() *cli.Command {
return &cli.Command{
Name: "plugin",
Aliases: []string{"p"},
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: "run",
Aliases: []string{"r"},
Usage: "run a plugin on the fly",
ArgsUsage: "PLUGIN_NAME [PLUGIN_OPTIONS]",
Action: plugin.Run,
},
},
}
}

View File

@@ -1,122 +0,0 @@
package internal
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/spf13/afero"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/stretchr/testify/assert"
)
func Test_showVersion(t *testing.T) {
type args struct {
cacheDir string
outputFormat string
version string
}
tests := []struct {
name string
args args
createDB bool
expectedOutput string
}{
{
name: "happy path, table output",
args: args{
outputFormat: "table",
version: "v1.2.3",
},
expectedOutput: `Version: v1.2.3
Vulnerability DB:
Type: Light
Version: 42
UpdatedAt: 2020-03-16 23:40:20 +0000 UTC
NextUpdate: 2020-03-16 23:57:00 +0000 UTC
DownloadedAt: 2020-03-16 23:40:20 +0000 UTC
`,
createDB: true,
},
{
name: "happy path, JSON output",
args: args{
outputFormat: "json",
version: "1.2.3",
},
expectedOutput: `{"Version":"1.2.3","VulnerabilityDB":{"Version":42,"Type":1,"NextUpdate":"2020-03-16T23:57:00Z","UpdatedAt":"2020-03-16T23:40:20Z","DownloadedAt":"2020-03-16T23:40:20Z"}}
`,
createDB: true,
},
{
name: "sad path, no DB is available",
args: args{
outputFormat: "json",
version: "1.2.3",
},
expectedOutput: `{"Version":"1.2.3"}
`,
},
{
name: "sad path, bogus cache dir",
args: args{
outputFormat: "json",
version: "1.2.3",
cacheDir: "/foo/bar/bogus",
},
expectedOutput: `{"Version":"1.2.3"}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cacheDir string
switch {
case tt.args.cacheDir != "":
cacheDir = tt.args.cacheDir
default:
cacheDir, _ = ioutil.TempDir("", "Test_showVersion-*")
defer os.RemoveAll(cacheDir)
}
if tt.createDB {
fs := afero.NewOsFs()
err := os.MkdirAll(filepath.Join(cacheDir, "db"), os.ModePerm)
require.NoError(t, err)
metadataFile := filepath.Join(cacheDir, "db", "metadata.json")
b, err := json.Marshal(db.Metadata{
Version: 42,
Type: 1,
NextUpdate: time.Unix(1584403020, 0),
UpdatedAt: time.Unix(1584402020, 0),
DownloadedAt: time.Unix(1584402020, 0),
})
require.NoError(t, err)
err = afero.WriteFile(fs, metadataFile, b, 0600)
require.NoError(t, err)
}
fw := new(bytes.Buffer)
showVersion(cacheDir, tt.args.outputFormat, tt.args.version, fw)
assert.Equal(t, tt.expectedOutput, fw.String(), tt.name)
})
}
}
func TestNewCommands(t *testing.T) {
NewApp("test")
NewClientCommand()
NewFilesystemCommand()
NewImageCommand()
NewRepositoryCommand()
NewServerCommand()
}

View File

@@ -1,88 +0,0 @@
package config
import (
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/internal/config"
)
// Config holds the artifact config
type Config struct {
config.GlobalConfig
config.ArtifactConfig
config.DBConfig
config.ImageConfig
config.ReportConfig
config.CacheConfig
// deprecated
onlyUpdate string
// deprecated
refresh bool
// deprecated
autoRefresh bool
}
// New is the factory method to return config
func New(c *cli.Context) (Config, error) {
gc, err := config.NewGlobalConfig(c)
if err != nil {
return Config{}, xerrors.Errorf("failed to initialize global options: %w", err)
}
return Config{
GlobalConfig: gc,
ArtifactConfig: config.NewArtifactConfig(c),
DBConfig: config.NewDBConfig(c),
ImageConfig: config.NewImageConfig(c),
ReportConfig: config.NewReportConfig(c),
CacheConfig: config.NewCacheConfig(c),
onlyUpdate: c.String("only-update"),
refresh: c.Bool("refresh"),
autoRefresh: c.Bool("auto-refresh"),
}, nil
}
// Init initializes the artifact config
func (c *Config) Init() error {
if c.onlyUpdate != "" || c.refresh || c.autoRefresh {
c.Logger.Warn("--only-update, --refresh and --auto-refresh are unnecessary and ignored now. These commands will be removed in the next version.")
}
if err := c.initPreScanConfigs(); err != nil {
return err
}
// --clear-cache, --download-db-only and --reset don't conduct the scan
if c.skipScan() {
return nil
}
if err := c.ArtifactConfig.Init(c.Context, c.Logger); err != nil {
return err
}
return nil
}
func (c *Config) initPreScanConfigs() error {
if err := c.ReportConfig.Init(c.Logger); err != nil {
return err
}
if err := c.DBConfig.Init(); err != nil {
return err
}
if err := c.CacheConfig.Init(); err != nil {
return err
}
return nil
}
func (c *Config) skipScan() bool {
if c.ClearCache || c.DownloadDBOnly || c.Reset {
return true
}
return false
}

View File

@@ -1,218 +0,0 @@
package config
import (
"flag"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/internal/config"
)
func TestConfig_Init(t *testing.T) {
tests := []struct {
name string
globalConfig config.GlobalConfig
dbConfig config.DBConfig
imageConfig config.ImageConfig
reportConfig config.ReportConfig
args []string
logs []string
want Config
wantErr string
}{
{
name: "happy path",
reportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{"os"},
},
args: []string{"--severity", "CRITICAL", "--vuln-type", "os", "--quiet", "alpine:3.10"},
want: Config{
GlobalConfig: config.GlobalConfig{
Quiet: true,
},
ArtifactConfig: config.ArtifactConfig{
Target: "alpine:3.10",
},
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{"os"},
Output: os.Stdout,
},
},
},
{
name: "happy path: reset",
args: []string{"--reset"},
want: Config{
DBConfig: config.DBConfig{
Reset: true,
},
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
},
},
{
name: "happy path with an unknown severity",
args: []string{"--severity", "CRITICAL,INVALID", "centos:7"},
logs: []string{
"unknown severity option: unknown severity: INVALID",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "centos:7",
},
},
},
{
name: "deprecated options",
args: []string{"--only-update", "alpine", "--severity", "LOW", "debian:buster"},
logs: []string{
"--only-update, --refresh and --auto-refresh are unnecessary and ignored now. These commands will be removed in the next version.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityLow},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "debian:buster",
},
onlyUpdate: "alpine",
},
},
{
name: "invalid option combination: --template enabled without --format",
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Template: "@contrib/gitlab.tpl",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
},
},
{
name: "invalid option combination: --template and --format json",
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format json is specified. Use --template option with --format template option.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Template: "@contrib/gitlab.tpl",
Format: "json",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
},
},
{
name: "invalid option combination: --format template without --template",
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Format: "template",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
},
},
{
name: "sad: skip and download db",
args: []string{"--skip-update", "--download-db-only", "alpine:3.10"},
wantErr: "--skip-update and --download-db-only options can not be specified both",
},
{
name: "sad: multiple image names",
args: []string{"centos:7", "alpine:3.10"},
logs: []string{
"multiple targets cannot be specified",
},
wantErr: "arguments error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
core, obs := observer.New(zap.InfoLevel)
logger := zap.New(core)
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
set.Bool("quiet", false, "")
set.Bool("no-progress", false, "")
set.Bool("reset", false, "")
set.Bool("skip-update", false, "")
set.Bool("download-db-only", false, "")
set.Bool("auto-refresh", false, "")
set.String("severity", "CRITICAL", "")
set.String("vuln-type", "os,library", "")
set.String("only-update", "", "")
set.String("template", "", "")
set.String("format", "", "")
ctx := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
c, err := New(ctx)
require.NoError(t, err, err)
c.GlobalConfig.Logger = logger.Sugar()
err = c.Init()
// tests log messages
var gotMessages []string
for _, entry := range obs.AllUntimed() {
gotMessages = append(gotMessages, entry.Message)
}
assert.Equal(t, tt.logs, gotMessages, tt.name)
// test the error
switch {
case tt.wantErr != "":
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
tt.want.GlobalConfig.Context = ctx
tt.want.GlobalConfig.Logger = logger.Sugar()
assert.Equal(t, tt.want, c, tt.name)
})
}
}

View File

@@ -1,38 +0,0 @@
package artifact
import (
"context"
"time"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/internal/artifact/config"
"github.com/aquasecurity/trivy/pkg/scanner"
)
func filesystemScanner(ctx context.Context, dir string, ac cache.ArtifactCache, lac cache.LocalArtifactCache,
_ time.Duration, disabled []analyzer.Type) (scanner.Scanner, func(), error) {
s, cleanup, err := initializeFilesystemScanner(ctx, dir, ac, lac, disabled)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err)
}
return s, cleanup, nil
}
// FilesystemRun runs scan on filesystem
func FilesystemRun(cliCtx *cli.Context) error {
c, err := config.New(cliCtx)
if err != nil {
return err
}
// initialize config
if err = c.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
return run(c, filesystemScanner)
}

View File

@@ -1,53 +0,0 @@
package artifact
import (
"context"
"time"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/internal/artifact/config"
"github.com/aquasecurity/trivy/pkg/scanner"
)
func archiveScanner(ctx context.Context, input string, ac cache.ArtifactCache, lac cache.LocalArtifactCache,
timeout time.Duration, disabled []analyzer.Type) (scanner.Scanner, func(), error) {
s, err := initializeArchiveScanner(ctx, input, ac, lac, timeout, disabled)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize the archive scanner: %w", err)
}
return s, func() {}, nil
}
func dockerScanner(ctx context.Context, imageName string, ac cache.ArtifactCache, lac cache.LocalArtifactCache,
timeout time.Duration, disabled []analyzer.Type) (
scanner.Scanner, func(), error) {
s, cleanup, err := initializeDockerScanner(ctx, imageName, ac, lac, timeout, disabled)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err)
}
return s, cleanup, nil
}
// ImageRun runs scan on docker image
func ImageRun(cliCtx *cli.Context) error {
c, err := config.New(cliCtx)
if err != nil {
return err
}
// initialize config
if err := c.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
if c.Input != "" {
// scan tar file
return run(c, archiveScanner)
}
return run(c, dockerScanner)
}

View File

@@ -1,46 +0,0 @@
// +build wireinject
package artifact
import (
"context"
"time"
"github.com/google/wire"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, timeout time.Duration, disableAnalyzers []analyzer.Type) (
scanner.Scanner, func(), error) {
wire.Build(scanner.StandaloneDockerSet)
return scanner.Scanner{}, nil, nil
}
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, timeout time.Duration, disableAnalyzers []analyzer.Type) (
scanner.Scanner, error) {
wire.Build(scanner.StandaloneArchiveSet)
return scanner.Scanner{}, nil
}
func initializeFilesystemScanner(ctx context.Context, dir string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, disableAnalyzers []analyzer.Type) (scanner.Scanner, func(), error) {
wire.Build(scanner.StandaloneFilesystemSet)
return scanner.Scanner{}, nil, nil
}
func initializeRepositoryScanner(ctx context.Context, url string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, disableAnalyzers []analyzer.Type) (scanner.Scanner, func(), error) {
wire.Build(scanner.StandaloneRepositorySet)
return scanner.Scanner{}, nil, nil
}
func initializeVulnerabilityClient() vulnerability.Client {
wire.Build(vulnerability.SuperSet)
return vulnerability.Client{}
}

View File

@@ -1,39 +0,0 @@
package artifact
import (
"context"
"time"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/internal/artifact/config"
"github.com/aquasecurity/trivy/pkg/scanner"
)
func repositoryScanner(ctx context.Context, dir string, ac cache.ArtifactCache, lac cache.LocalArtifactCache,
_ time.Duration, disabled []analyzer.Type) (
scanner.Scanner, func(), error) {
s, cleanup, err := initializeRepositoryScanner(ctx, dir, ac, lac, disabled)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err)
}
return s, cleanup, nil
}
// RepositoryRun runs scan on repository
func RepositoryRun(cliCtx *cli.Context) error {
c, err := config.New(cliCtx)
if err != nil {
return err
}
// initialize config
if err = c.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
return run(c, repositoryScanner)
}

View File

@@ -1,177 +0,0 @@
package artifact
import (
"context"
"errors"
l "log"
"os"
"time"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/internal/artifact/config"
"github.com/aquasecurity/trivy/internal/operation"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils"
)
var errSkipScan = errors.New("skip subsequent processes")
// InitializeScanner type to define initialize function signature
type InitializeScanner func(context.Context, string, cache.ArtifactCache, cache.LocalArtifactCache, time.Duration,
[]analyzer.Type) (scanner.Scanner, func(), error)
func run(conf config.Config, initializeScanner InitializeScanner) error {
ctx, cancel := context.WithTimeout(context.Background(), conf.Timeout)
defer cancel()
return runWithContext(ctx, conf, initializeScanner)
}
func runWithContext(ctx context.Context, conf config.Config, initializeScanner InitializeScanner) error {
if err := log.InitLogger(conf.Debug, conf.Quiet); err != nil {
l.Fatal(err)
}
cacheClient, err := initCache(conf)
if err != nil {
if errors.Is(err, errSkipScan) {
return nil
}
return xerrors.Errorf("cache error: %w", err)
}
defer cacheClient.Close()
if err = initDB(conf); err != nil {
if errors.Is(err, errSkipScan) {
return nil
}
return xerrors.Errorf("DB error: %w", err)
}
defer db.Close()
results, err := scan(ctx, conf, initializeScanner, cacheClient)
if err != nil {
return xerrors.Errorf("scan error: %w", err)
}
results, err = filter(ctx, conf, results)
if err != nil {
return xerrors.Errorf("filter error: %w", err)
}
if err = report.WriteResults(conf.Format, conf.Output, conf.Severities, results, conf.Template, conf.Light); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
exit(conf, results)
return nil
}
func initCache(c config.Config) (operation.Cache, error) {
utils.SetCacheDir(c.CacheDir)
cache, err := operation.NewCache(c.CacheBackend)
if err != nil {
return operation.Cache{}, xerrors.Errorf("unable to initialize the cache: %w", err)
}
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
if c.Reset {
defer cache.Close()
if err = cache.Reset(); err != nil {
return operation.Cache{}, xerrors.Errorf("cache reset error: %w", err)
}
return operation.Cache{}, errSkipScan
}
if c.ClearCache {
defer cache.Close()
if err = cache.ClearImages(); err != nil {
return operation.Cache{}, xerrors.Errorf("cache clear error: %w", err)
}
return operation.Cache{}, errSkipScan
}
return cache, nil
}
func initDB(c config.Config) error {
// download the database file
noProgress := c.Quiet || c.NoProgress
if err := operation.DownloadDB(c.AppVersion, c.CacheDir, noProgress, c.Light, c.SkipUpdate); err != nil {
return err
}
if c.DownloadDBOnly {
return errSkipScan
}
if err := db.Init(c.CacheDir); err != nil {
return xerrors.Errorf("error in vulnerability DB initialize: %w", err)
}
return nil
}
func scan(ctx context.Context, conf config.Config, initializeScanner InitializeScanner, cacheClient cache.Cache) (
report.Results, error) {
target := conf.Target
if conf.Input != "" {
target = conf.Input
}
scanOptions := types.ScanOptions{
VulnType: conf.VulnType,
ScanRemovedPackages: conf.ScanRemovedPkgs, // this is valid only for image subcommand
ListAllPackages: conf.ListAllPkgs,
SkipFiles: conf.SkipFiles,
SkipDirectories: conf.SkipDirectories,
}
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
// It doesn't analyze apk commands by default.
disabledAnalyzers := []analyzer.Type{analyzer.TypeApkCommand}
if conf.ScanRemovedPkgs {
disabledAnalyzers = []analyzer.Type{}
}
s, cleanup, err := initializeScanner(ctx, target, cacheClient, cacheClient, conf.Timeout, disabledAnalyzers)
if err != nil {
return nil, xerrors.Errorf("unable to initialize a scanner: %w", err)
}
defer cleanup()
results, err := s.ScanArtifact(ctx, scanOptions)
if err != nil {
return nil, xerrors.Errorf("image scan failed: %w", err)
}
return results, nil
}
func filter(ctx context.Context, conf config.Config, results report.Results) (report.Results, error) {
vulnClient := initializeVulnerabilityClient()
for i := range results {
vulnClient.FillInfo(results[i].Vulnerabilities, results[i].Type)
vulns, err := vulnClient.Filter(ctx, results[i].Vulnerabilities,
conf.Severities, conf.IgnoreUnfixed, conf.IgnoreFile, conf.IgnorePolicy)
if err != nil {
return nil, xerrors.Errorf("unable to filter vulnerabilities: %w", err)
}
results[i].Vulnerabilities = vulns
}
return results, nil
}
func exit(c config.Config, results report.Results) {
if c.ExitCode != 0 {
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(c.ExitCode)
}
}
}
}

View File

@@ -1,88 +0,0 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package artifact
import (
"context"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/applier"
image2 "github.com/aquasecurity/fanal/artifact/image"
local2 "github.com/aquasecurity/fanal/artifact/local"
"github.com/aquasecurity/fanal/artifact/remote"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/image"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/scanner/local"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/vulnerability"
"time"
)
// Injectors from inject.go:
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, timeout time.Duration, disableAnalyzers []analyzer.Type) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
dockerOption, err := types.GetDockerOption(timeout)
if err != nil {
return scanner.Scanner{}, nil, err
}
imageImage, cleanup, err := image.NewDockerImage(ctx, imageName, dockerOption)
if err != nil {
return scanner.Scanner{}, nil, err
}
artifact := image2.NewArtifact(imageImage, artifactCache, disableAnalyzers)
scannerScanner := scanner.NewScanner(localScanner, artifact)
return scannerScanner, func() {
cleanup()
}, nil
}
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, timeout time.Duration, disableAnalyzers []analyzer.Type) (scanner.Scanner, error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
imageImage, err := image.NewArchiveImage(filePath)
if err != nil {
return scanner.Scanner{}, err
}
artifact := image2.NewArtifact(imageImage, artifactCache, disableAnalyzers)
scannerScanner := scanner.NewScanner(localScanner, artifact)
return scannerScanner, nil
}
func initializeFilesystemScanner(ctx context.Context, dir string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, disableAnalyzers []analyzer.Type) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
artifact := local2.NewArtifact(dir, artifactCache, disableAnalyzers)
scannerScanner := scanner.NewScanner(localScanner, artifact)
return scannerScanner, func() {
}, nil
}
func initializeRepositoryScanner(ctx context.Context, url string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, disableAnalyzers []analyzer.Type) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
artifact, cleanup, err := remote.NewArtifact(url, artifactCache, disableAnalyzers)
if err != nil {
return scanner.Scanner{}, nil, err
}
scannerScanner := scanner.NewScanner(localScanner, artifact)
return scannerScanner, func() {
cleanup()
}, nil
}
func initializeVulnerabilityClient() vulnerability.Client {
config := db.Config{}
client := vulnerability.NewClient(config)
return client
}

View File

@@ -1,84 +0,0 @@
package config
import (
"net/http"
"strings"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/internal/config"
)
// Config holds the Trivy client config
type Config struct {
config.GlobalConfig
config.ArtifactConfig
config.ImageConfig
config.ReportConfig
RemoteAddr string
token string
tokenHeader string
customHeaders []string
// this field is populated in Init()
CustomHeaders http.Header
}
// New is the factory method for Config
func New(c *cli.Context) (Config, error) {
gc, err := config.NewGlobalConfig(c)
if err != nil {
return Config{}, xerrors.Errorf("failed to initialize global options: %w", err)
}
return Config{
GlobalConfig: gc,
ArtifactConfig: config.NewArtifactConfig(c),
ImageConfig: config.NewImageConfig(c),
ReportConfig: config.NewReportConfig(c),
RemoteAddr: c.String("remote"),
token: c.String("token"),
tokenHeader: c.String("token-header"),
customHeaders: c.StringSlice("custom-headers"),
}, nil
}
// Init initializes the config
func (c *Config) Init() (err error) {
// --clear-cache doesn't conduct the scan
if c.ClearCache {
return nil
}
c.CustomHeaders = splitCustomHeaders(c.customHeaders)
// add token to custom headers
if c.token != "" {
c.CustomHeaders.Set(c.tokenHeader, c.token)
}
if err := c.ReportConfig.Init(c.Logger); err != nil {
return err
}
if err := c.ArtifactConfig.Init(c.Context, c.Logger); err != nil {
return err
}
return nil
}
func splitCustomHeaders(headers []string) http.Header {
result := make(http.Header)
for _, header := range headers {
// e.g. x-api-token:XXX
s := strings.SplitN(header, ":", 2)
if len(s) != 2 {
continue
}
result.Set(s[0], s[1])
}
return result
}

View File

@@ -1,287 +0,0 @@
package config
import (
"flag"
"net/http"
"os"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/internal/config"
)
func TestConfig_Init(t *testing.T) {
tests := []struct {
name string
globalConfig config.GlobalConfig
imageConfig config.ImageConfig
reportConfig config.ReportConfig
args []string
logs []string
want Config
wantErr string
}{
{
name: "happy path",
reportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{"os"},
},
args: []string{"--severity", "CRITICAL", "--vuln-type", "os", "--quiet", "alpine:3.10"},
want: Config{
GlobalConfig: config.GlobalConfig{
Quiet: true,
},
ArtifactConfig: config.ArtifactConfig{
Target: "alpine:3.10",
},
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{"os"},
Output: os.Stdout,
},
CustomHeaders: http.Header{},
},
},
{
name: "happy path with token and token header",
args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "alpine:3.11",
},
token: "secret",
tokenHeader: "X-Trivy-Token",
CustomHeaders: http.Header{
"X-Trivy-Token": []string{"secret"},
},
},
},
{
name: "happy path with good custom headers",
args: []string{"--custom-headers", "foo:bar", "alpine:3.11"},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "alpine:3.11",
},
customHeaders: []string{"foo:bar"},
CustomHeaders: http.Header{
"Foo": []string{"bar"},
},
},
},
{
name: "happy path with bad custom headers",
args: []string{"--custom-headers", "foobaz", "alpine:3.11"},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "alpine:3.11",
},
customHeaders: []string{"foobaz"},
CustomHeaders: http.Header{},
},
},
{
name: "happy path with an unknown severity",
args: []string{"--severity", "CRITICAL,INVALID", "centos:7"},
logs: []string{
"unknown severity option: unknown severity: INVALID",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown},
Output: os.Stdout,
VulnType: []string{"os", "library"},
},
ArtifactConfig: config.ArtifactConfig{
Target: "centos:7",
},
CustomHeaders: http.Header{},
},
},
{
name: "invalid option combination: --template enabled without --format",
args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Template: "@contrib/gitlab.tpl",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
CustomHeaders: http.Header{},
},
},
{
name: "invalid option combination: --template and --format json",
args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format json is specified. Use --template option with --format template option.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Template: "@contrib/gitlab.tpl",
Format: "json",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
CustomHeaders: http.Header{},
},
},
{
name: "invalid option combination: --format template without --template",
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Format: "template",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
CustomHeaders: http.Header{},
},
},
{
name: "invalid option combination: --format template without --template",
args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
},
want: Config{
ReportConfig: config.ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
Output: os.Stdout,
VulnType: []string{"os", "library"},
Format: "template",
},
ArtifactConfig: config.ArtifactConfig{
Target: "gitlab/gitlab-ce:12.7.2-ce.0",
},
CustomHeaders: http.Header{},
},
},
{
name: "sad: multiple image names",
args: []string{"centos:7", "alpine:3.10"},
logs: []string{
"multiple targets cannot be specified",
},
wantErr: "arguments error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
core, obs := observer.New(zap.InfoLevel)
logger := zap.New(core)
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
set.Bool("quiet", false, "")
set.Bool("no-progress", false, "")
set.Bool("clear-cache", false, "")
set.String("severity", "CRITICAL", "")
set.String("vuln-type", "os,library", "")
set.String("template", "", "")
set.String("format", "", "")
set.String("token", "", "")
set.String("token-header", "", "")
set.Var(&cli.StringSlice{}, "custom-headers", "")
ctx := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
c, err := New(ctx)
require.NoError(t, err, err)
c.GlobalConfig.Logger = logger.Sugar()
err = c.Init()
// tests log messages
var gotMessages []string
for _, entry := range obs.AllUntimed() {
gotMessages = append(gotMessages, entry.Message)
}
assert.Equal(t, tt.logs, gotMessages, tt.name)
// test the error
switch {
case tt.wantErr != "":
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
tt.want.GlobalConfig.Context = ctx
tt.want.GlobalConfig.Logger = logger.Sugar()
assert.Equal(t, tt.want, c, tt.name)
})
}
}
func Test_splitCustomHeaders(t *testing.T) {
type args struct {
headers []string
}
tests := []struct {
name string
args args
want http.Header
}{
{
name: "happy path",
args: args{
headers: []string{"x-api-token:foo bar", "Authorization:user:password"},
},
want: http.Header{
"X-Api-Token": []string{"foo bar"},
"Authorization": []string{"user:password"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := splitCustomHeaders(tt.args.headers); !reflect.DeepEqual(got, tt.want) {
t.Errorf("splitCustomHeaders() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,33 +0,0 @@
// +build wireinject
package client
import (
"context"
"time"
"github.com/google/wire"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders,
url client.RemoteURL, timeout time.Duration, disabled []analyzer.Type) (scanner.Scanner, func(), error) {
wire.Build(scanner.RemoteDockerSet)
return scanner.Scanner{}, nil, nil
}
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders,
url client.RemoteURL, timeout time.Duration, disabled []analyzer.Type) (scanner.Scanner, error) {
wire.Build(scanner.RemoteArchiveSet)
return scanner.Scanner{}, nil
}
func initializeVulnerabilityClient() vulnerability.Client {
wire.Build(vulnerability.SuperSet)
return vulnerability.Client{}
}

View File

@@ -1,138 +0,0 @@
package client
import (
"context"
"os"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/trivy/internal/client/config"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils"
)
// Run runs the scan
func Run(cliCtx *cli.Context) error {
c, err := config.New(cliCtx)
if err != nil {
return err
}
return run(c)
}
func run(conf config.Config) error {
ctx, cancel := context.WithTimeout(context.Background(), conf.Timeout)
defer cancel()
return runWithContext(ctx, conf)
}
func runWithContext(ctx context.Context, conf config.Config) error {
if err := initialize(&conf); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
if conf.ClearCache {
log.Logger.Warn("A client doesn't have image cache")
return nil
}
s, cleanup, err := initializeScanner(ctx, conf)
if err != nil {
return xerrors.Errorf("scanner initialize error: %w", err)
}
defer cleanup()
scanOptions := types.ScanOptions{
VulnType: conf.VulnType,
ScanRemovedPackages: conf.ScanRemovedPkgs,
}
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
results, err := s.ScanArtifact(ctx, scanOptions)
if err != nil {
return xerrors.Errorf("error in image scan: %w", err)
}
vulnClient := initializeVulnerabilityClient()
for i := range results {
vulns, err := vulnClient.Filter(ctx, results[i].Vulnerabilities,
conf.Severities, conf.IgnoreUnfixed, conf.IgnoreFile, conf.IgnorePolicy)
if err != nil {
return err
}
results[i].Vulnerabilities = vulns
}
if err = report.WriteResults(conf.Format, conf.Output, conf.Severities, results, conf.Template, false); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
exit(conf, results)
return nil
}
func initialize(conf *config.Config) error {
// Initialize logger
if err := log.InitLogger(conf.Debug, conf.Quiet); err != nil {
return xerrors.Errorf("failed to initialize a logger: %w", err)
}
// Initialize config
if err := conf.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
// configure cache dir
utils.SetCacheDir(conf.CacheDir)
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
return nil
}
func initializeScanner(ctx context.Context, conf config.Config) (scanner.Scanner, func(), error) {
remoteCache := cache.NewRemoteCache(cache.RemoteURL(conf.RemoteAddr), conf.CustomHeaders)
// By default, apk commands are not analyzed.
disabledAnalyzers := []analyzer.Type{analyzer.TypeApkCommand}
if conf.ScanRemovedPkgs {
disabledAnalyzers = []analyzer.Type{}
}
if conf.Input != "" {
// Scan tar file
s, err := initializeArchiveScanner(ctx, conf.Input, remoteCache,
client.CustomHeaders(conf.CustomHeaders), client.RemoteURL(conf.RemoteAddr), conf.Timeout, disabledAnalyzers)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the archive scanner: %w", err)
}
return s, func() {}, nil
}
// Scan an image in Docker Engine or Docker Registry
s, cleanup, err := initializeDockerScanner(ctx, conf.Target, remoteCache,
client.CustomHeaders(conf.CustomHeaders), client.RemoteURL(conf.RemoteAddr), conf.Timeout, disabledAnalyzers)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the docker scanner: %w", err)
}
return s, cleanup, nil
}
func exit(c config.Config, results report.Results) {
if c.ExitCode != 0 {
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(c.ExitCode)
}
}
}
}

View File

@@ -1,58 +0,0 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package client
import (
"context"
"github.com/aquasecurity/fanal/analyzer"
image2 "github.com/aquasecurity/fanal/artifact/image"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/image"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/vulnerability"
"time"
)
// Injectors from inject.go:
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders, url client.RemoteURL, timeout time.Duration, disabled []analyzer.Type) (scanner.Scanner, func(), error) {
scannerScanner := client.NewProtobufClient(url)
clientScanner := client.NewScanner(customHeaders, scannerScanner)
dockerOption, err := types.GetDockerOption(timeout)
if err != nil {
return scanner.Scanner{}, nil, err
}
imageImage, cleanup, err := image.NewDockerImage(ctx, imageName, dockerOption)
if err != nil {
return scanner.Scanner{}, nil, err
}
artifact := image2.NewArtifact(imageImage, artifactCache, disabled)
scanner2 := scanner.NewScanner(clientScanner, artifact)
return scanner2, func() {
cleanup()
}, nil
}
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders, url client.RemoteURL, timeout time.Duration, disabled []analyzer.Type) (scanner.Scanner, error) {
scannerScanner := client.NewProtobufClient(url)
clientScanner := client.NewScanner(customHeaders, scannerScanner)
imageImage, err := image.NewArchiveImage(filePath)
if err != nil {
return scanner.Scanner{}, err
}
artifact := image2.NewArtifact(imageImage, artifactCache, disabled)
scanner2 := scanner.NewScanner(clientScanner, artifact)
return scanner2, nil
}
func initializeVulnerabilityClient() vulnerability.Client {
config := db.Config{}
vulnerabilityClient := vulnerability.NewClient(config)
return vulnerabilityClient
}

View File

@@ -1,63 +0,0 @@
package config
import (
"os"
"strings"
"time"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"golang.org/x/xerrors"
)
// ArtifactConfig holds the config for a artifact scanning
type ArtifactConfig struct {
Input string
Timeout time.Duration
ClearCache bool
skipDirectories string
SkipDirectories []string
skipFiles string
SkipFiles []string
// this field is populated in Init()
Target string
}
// NewArtifactConfig is the factory method to return artifact config
func NewArtifactConfig(c *cli.Context) ArtifactConfig {
return ArtifactConfig{
Input: c.String("input"),
Timeout: c.Duration("timeout"),
ClearCache: c.Bool("clear-cache"),
skipFiles: c.String("skip-files"),
skipDirectories: c.String("skip-dirs"),
}
}
// Init initialize the CLI context for artifact scanning
func (c *ArtifactConfig) Init(ctx *cli.Context, logger *zap.SugaredLogger) (err error) {
if c.Input == "" && ctx.Args().Len() == 0 {
logger.Debug(`trivy requires at least 1 argument or --input option`)
_ = cli.ShowSubcommandHelp(ctx) // nolint: errcheck
os.Exit(0)
} else if ctx.Args().Len() > 1 {
logger.Error(`multiple targets cannot be specified`)
return xerrors.New("arguments error")
}
if c.Input == "" {
c.Target = ctx.Args().First()
}
if c.skipDirectories != "" {
c.SkipDirectories = strings.Split(c.skipDirectories, ",")
}
if c.skipFiles != "" {
c.SkipFiles = strings.Split(c.skipFiles, ",")
}
return nil
}

View File

@@ -1,73 +0,0 @@
package config_test
import (
"flag"
"testing"
"github.com/aquasecurity/trivy/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func TestArtifactConfig_Init(t *testing.T) {
tests := []struct {
name string
args []string
logs []string
want config.ArtifactConfig
wantErr string
}{
{
name: "happy path",
args: []string{"alpine:3.10"},
want: config.ArtifactConfig{
Target: "alpine:3.10",
},
},
{
name: "sad: multiple image names",
args: []string{"centos:7", "alpine:3.10"},
logs: []string{
"multiple targets cannot be specified",
},
wantErr: "arguments error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
core, obs := observer.New(zap.DebugLevel)
logger := zap.New(core)
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ctx := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
c := config.NewArtifactConfig(ctx)
err := c.Init(ctx, logger.Sugar())
// tests log messages
var gotMessages []string
for _, entry := range obs.AllUntimed() {
gotMessages = append(gotMessages, entry.Message)
}
assert.Equal(t, tt.logs, gotMessages, tt.name)
// test the error
switch {
case tt.wantErr != "":
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
assert.Equal(t, tt.want, c, tt.name)
})
}
}

View File

@@ -1,31 +0,0 @@
package config
import (
"strings"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
)
// CacheConfig holds the config for cache
type CacheConfig struct {
CacheBackend string
}
// NewCacheConfig returns an instance of CacheConfig
func NewCacheConfig(c *cli.Context) CacheConfig {
return CacheConfig{
CacheBackend: c.String("cache-backend"),
}
}
// Init initialize the CacheConfig
func (c *CacheConfig) Init() error {
// "redis://" or "fs" are allowed for now
// An empty value is also allowed for testability
if !strings.HasPrefix(c.CacheBackend, "redis://") &&
c.CacheBackend != "fs" && c.CacheBackend != "" {
return xerrors.Errorf("unsupported cache backend: %s", c.CacheBackend)
}
return nil
}

View File

@@ -1,92 +0,0 @@
package config_test
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy/internal/config"
)
func TestNewCacheConfig(t *testing.T) {
tests := []struct {
name string
args []string
want config.CacheConfig
}{
{
name: "happy path",
args: []string{"--cache-backend", "redis://localhost:6379"},
want: config.CacheConfig{
CacheBackend: "redis://localhost:6379",
},
},
{
name: "default",
args: []string{},
want: config.CacheConfig{
CacheBackend: "fs",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("cache-backend", "fs", "")
c := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
got := config.NewCacheConfig(c)
assert.Equal(t, tt.want, got, tt.name)
})
}
}
func TestCacheConfig_Init(t *testing.T) {
type fields struct {
backend string
}
tests := []struct {
name string
fields fields
wantErr string
}{
{
name: "fs",
fields: fields{
backend: "fs",
},
},
{
name: "redis",
fields: fields{
backend: "redis://localhost:6379",
},
},
{
name: "sad path",
fields: fields{
backend: "unknown://",
},
wantErr: "unsupported cache backend: unknown://",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &config.CacheConfig{
CacheBackend: tt.fields.backend,
}
err := c.Init()
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -1,34 +0,0 @@
package config
import (
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
)
// DBConfig holds the config for trivy DB
type DBConfig struct {
Reset bool
DownloadDBOnly bool
SkipUpdate bool
Light bool
NoProgress bool
}
// NewDBConfig is the factory method to return the DBConfig
func NewDBConfig(c *cli.Context) DBConfig {
return DBConfig{
Reset: c.Bool("reset"),
DownloadDBOnly: c.Bool("download-db-only"),
SkipUpdate: c.Bool("skip-update"),
Light: c.Bool("light"),
NoProgress: c.Bool("no-progress"),
}
}
// Init initialize the DBConfig
func (c *DBConfig) Init() (err error) {
if c.SkipUpdate && c.DownloadDBOnly {
return xerrors.New("--skip-update and --download-db-only options can not be specified both")
}
return nil
}

View File

@@ -1,88 +0,0 @@
package config_test
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy/internal/config"
)
func TestNewDBConfig(t *testing.T) {
tests := []struct {
name string
args []string
want config.DBConfig
}{
{
name: "happy path",
args: []string{"--reset", "--skip-update"},
want: config.DBConfig{
Reset: true,
SkipUpdate: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
set.Bool("reset", false, "")
set.Bool("skip-update", false, "")
c := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
got := config.NewDBConfig(c)
assert.Equal(t, tt.want, got, tt.name)
})
}
}
func TestDBConfig_Init(t *testing.T) {
type fields struct {
Reset bool
DownloadDBOnly bool
SkipUpdate bool
Light bool
}
tests := []struct {
name string
fields fields
wantErr string
}{
{
name: "happy path",
fields: fields{
Light: true,
},
},
{
name: "sad path",
fields: fields{
DownloadDBOnly: true,
SkipUpdate: true,
},
wantErr: "--skip-update and --download-db-only options can not be specified both",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &config.DBConfig{
Reset: tt.fields.Reset,
DownloadDBOnly: tt.fields.DownloadDBOnly,
SkipUpdate: tt.fields.SkipUpdate,
Light: tt.fields.Light,
}
err := c.Init()
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -1,40 +0,0 @@
package config
import (
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/log"
)
// GlobalConfig holds the global config for trivy
type GlobalConfig struct {
Context *cli.Context
Logger *zap.SugaredLogger
AppVersion string
Quiet bool
Debug bool
CacheDir string
}
// NewGlobalConfig is the factory method to return GlobalConfig
func NewGlobalConfig(c *cli.Context) (GlobalConfig, error) {
quiet := c.Bool("quiet")
debug := c.Bool("debug")
logger, err := log.NewLogger(debug, quiet)
if err != nil {
return GlobalConfig{}, xerrors.New("failed to create a logger")
}
return GlobalConfig{
Context: c,
Logger: logger,
AppVersion: c.App.Version,
Quiet: quiet,
Debug: debug,
CacheDir: c.String("cache-dir"),
}, nil
}

View File

@@ -1,46 +0,0 @@
package config_test
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy/internal/config"
)
func TestNewGlobalConfig(t *testing.T) {
tests := []struct {
name string
args []string
want config.GlobalConfig
}{
{
name: "happy path",
args: []string{"--quiet", "--debug"},
want: config.GlobalConfig{
Quiet: true,
Debug: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
set.Bool("debug", false, "")
set.Bool("quiet", false, "")
c := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
got, err := config.NewGlobalConfig(c)
require.NoError(t, err, err)
assert.Equal(t, tt.want.Quiet, got.Quiet, tt.name)
assert.Equal(t, tt.want.Debug, got.Debug, tt.name)
assert.Equal(t, tt.want.CacheDir, got.CacheDir, tt.name)
})
}
}

View File

@@ -1,19 +0,0 @@
package config
import (
"github.com/urfave/cli/v2"
)
// ImageConfig holds the config for scanning images
type ImageConfig struct {
ScanRemovedPkgs bool
ListAllPkgs bool
}
// NewImageConfig is the factory method to return imageConfig
func NewImageConfig(c *cli.Context) ImageConfig {
return ImageConfig{
ScanRemovedPkgs: c.Bool("removed-pkgs"),
ListAllPkgs: c.Bool("list-all-pkgs"),
}
}

View File

@@ -1,92 +0,0 @@
package config
import (
"os"
"strings"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"golang.org/x/xerrors"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
)
// ReportConfig holds the config for reporting scan results
type ReportConfig struct {
Format string
Template string
IgnoreFile string
IgnoreUnfixed bool
ExitCode int
IgnorePolicy string
// these variables are not exported
vulnType string
output string
severities string
// these variables are populated by Init()
VulnType []string
Output *os.File
Severities []dbTypes.Severity
}
// NewReportConfig is the factory method to return ReportConfig
func NewReportConfig(c *cli.Context) ReportConfig {
return ReportConfig{
output: c.String("output"),
Format: c.String("format"),
Template: c.String("template"),
IgnorePolicy: c.String("ignore-policy"),
vulnType: c.String("vuln-type"),
severities: c.String("severity"),
IgnoreFile: c.String("ignorefile"),
IgnoreUnfixed: c.Bool("ignore-unfixed"),
ExitCode: c.Int("exit-code"),
}
}
// Init initializes the ReportConfig
func (c *ReportConfig) Init(logger *zap.SugaredLogger) (err error) {
if c.Template != "" {
if c.Format == "" {
logger.Warn("--template is ignored because --format template is not specified. Use --template option with --format template option.")
} else if c.Format != "template" {
logger.Warnf("--template is ignored because --format %s is specified. Use --template option with --format template option.", c.Format)
}
}
if c.Format == "template" && c.Template == "" {
logger.Warn("--format template is ignored because --template not is specified. Specify --template option when you use --format template.")
}
c.Severities = c.splitSeverity(logger, c.severities)
c.VulnType = strings.Split(c.vulnType, ",")
// for testability
c.severities = ""
c.vulnType = ""
c.Output = os.Stdout
if c.output != "" {
if c.Output, err = os.Create(c.output); err != nil {
return xerrors.Errorf("failed to create an output file: %w", err)
}
}
return nil
}
func (c *ReportConfig) splitSeverity(logger *zap.SugaredLogger, severity string) []dbTypes.Severity {
logger.Debugf("Severities: %s", severity)
var severities []dbTypes.Severity
for _, s := range strings.Split(severity, ",") {
severity, err := dbTypes.NewSeverity(s)
if err != nil {
logger.Warnf("unknown severity option: %s", err)
}
severities = append(severities, severity)
}
return severities
}

View File

@@ -1,162 +0,0 @@
package config
import (
"flag"
"os"
"testing"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func TestReportReportConfig_Init(t *testing.T) {
type fields struct {
output string
Format string
Template string
vulnType string
severities string
IgnoreFile string
IgnoreUnfixed bool
ExitCode int
VulnType []string
Output *os.File
Severities []dbTypes.Severity
}
tests := []struct {
name string
fields fields
args []string
logs []string
want ReportConfig
wantErr string
}{
{
name: "happy path",
fields: fields{
severities: "CRITICAL",
vulnType: "os",
},
args: []string{"alpine:3.10"},
want: ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{"os"},
Output: os.Stdout,
},
},
{
name: "happy path with an unknown severity",
fields: fields{
severities: "CRITICAL,INVALID",
vulnType: "os,library",
},
args: []string{"centos:7"},
logs: []string{
"unknown severity option: unknown severity: INVALID",
},
want: ReportConfig{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown},
VulnType: []string{"os", "library"},
Output: os.Stdout,
},
},
{
name: "invalid option combination: --template enabled without --format",
fields: fields{
Template: "@contrib/gitlab.tpl",
severities: "LOW",
},
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format template is not specified. Use --template option with --format template option.",
},
want: ReportConfig{
Output: os.Stdout,
Severities: []dbTypes.Severity{dbTypes.SeverityLow},
Template: "@contrib/gitlab.tpl",
VulnType: []string{""},
},
},
{
name: "invalid option combination: --template and --format json",
fields: fields{
Format: "json",
Template: "@contrib/gitlab.tpl",
severities: "LOW",
},
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--template is ignored because --format json is specified. Use --template option with --format template option.",
},
want: ReportConfig{
Format: "json",
Output: os.Stdout,
Severities: []dbTypes.Severity{dbTypes.SeverityLow},
Template: "@contrib/gitlab.tpl",
VulnType: []string{""},
},
},
{
name: "invalid option combination: --format template without --template",
fields: fields{
Format: "template",
severities: "LOW",
},
args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"},
logs: []string{
"--format template is ignored because --template not is specified. Specify --template option when you use --format template.",
},
want: ReportConfig{
Format: "template",
Output: os.Stdout,
Severities: []dbTypes.Severity{dbTypes.SeverityLow},
VulnType: []string{""},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
core, obs := observer.New(zap.InfoLevel)
logger := zap.New(core)
set := flag.NewFlagSet("test", 0)
_ = set.Parse(tt.args)
c := &ReportConfig{
output: tt.fields.output,
Format: tt.fields.Format,
Template: tt.fields.Template,
vulnType: tt.fields.vulnType,
severities: tt.fields.severities,
IgnoreFile: tt.fields.IgnoreFile,
IgnoreUnfixed: tt.fields.IgnoreUnfixed,
ExitCode: tt.fields.ExitCode,
Output: tt.fields.Output,
}
err := c.Init(logger.Sugar())
// tests log messages
var gotMessages []string
for _, entry := range obs.AllUntimed() {
gotMessages = append(gotMessages, entry.Message)
}
assert.Equal(t, tt.logs, gotMessages, tt.name)
// test the error
switch {
case tt.wantErr != "":
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
assert.Equal(t, &tt.want, c, tt.name)
})
}
}

View File

@@ -1,13 +0,0 @@
// +build wireinject
package operation
import (
"github.com/aquasecurity/trivy/pkg/db"
"github.com/google/wire"
)
func initializeDBClient(cacheDir string, quiet bool) db.Client {
wire.Build(db.SuperSet)
return db.Client{}
}

View File

@@ -1,114 +0,0 @@
package operation
import (
"context"
"os"
"strings"
"github.com/go-redis/redis/v8"
"github.com/google/wire"
"github.com/spf13/afero"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/db"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/utils"
)
// SuperSet binds cache dependencies
var SuperSet = wire.NewSet(
cache.NewFSCache,
wire.Bind(new(cache.LocalArtifactCache), new(cache.FSCache)),
NewCache,
)
// Cache implements the local cache
type Cache struct {
cache.Cache
}
// NewCache is the factory method for Cache
func NewCache(backend string) (Cache, error) {
if strings.HasPrefix(backend, "redis://") {
log.Logger.Infof("Redis cache: %s", backend)
options, err := redis.ParseURL(backend)
if err != nil {
return Cache{}, err
}
redisCache := cache.NewRedisCache(options)
return Cache{Cache: redisCache}, nil
}
fsCache, err := cache.NewFSCache(utils.CacheDir())
if err != nil {
return Cache{}, xerrors.Errorf("unable to initialize fs cache: %w", err)
}
return Cache{Cache: fsCache}, nil
}
// Reset resets the cache
func (c Cache) Reset() (err error) {
if err := c.ClearDB(); err != nil {
return xerrors.Errorf("failed to clear the database: %w", err)
}
if err := c.ClearImages(); err != nil {
return xerrors.Errorf("failed to clear the image cache: %w", err)
}
return nil
}
// ClearDB clears the DB cache
func (c Cache) ClearDB() (err error) {
log.Logger.Info("Removing DB file...")
if err = os.RemoveAll(utils.CacheDir()); err != nil {
return xerrors.Errorf("failed to remove the directory (%s) : %w", utils.CacheDir(), err)
}
return nil
}
// ClearImages clears the cache images
func (c Cache) ClearImages() error {
log.Logger.Info("Removing image caches...")
if err := c.Clear(); err != nil {
return xerrors.Errorf("failed to remove the cache: %w", err)
}
return nil
}
// DownloadDB downloads the DB
func DownloadDB(appVersion, cacheDir string, quiet, light, skipUpdate bool) error {
client := initializeDBClient(cacheDir, quiet)
ctx := context.Background()
needsUpdate, err := client.NeedsUpdate(appVersion, light, skipUpdate)
if err != nil {
return xerrors.Errorf("database error: %w", err)
}
if needsUpdate {
log.Logger.Info("Need to update DB")
log.Logger.Info("Downloading DB...")
if err = client.Download(ctx, cacheDir, light); err != nil {
return xerrors.Errorf("failed to download vulnerability DB: %w", err)
}
if err = client.UpdateMetadata(cacheDir); err != nil {
return xerrors.Errorf("unable to update database metadata: %w", err)
}
}
// for debug
if err := showDBInfo(cacheDir); err != nil {
return xerrors.Errorf("failed to show database info: %w", err)
}
return nil
}
func showDBInfo(cacheDir string) error {
m := db.NewMetadata(afero.NewOsFs(), cacheDir)
metadata, err := m.Get()
if err != nil {
return xerrors.Errorf("something wrong with DB: %w", err)
}
log.Logger.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s, DownloadedAt: %s",
metadata.Version, metadata.Type, metadata.UpdatedAt, metadata.NextUpdate, metadata.DownloadedAt)
return nil
}

View File

@@ -1,28 +0,0 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package operation
import (
db2 "github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/db"
"github.com/aquasecurity/trivy/pkg/github"
"github.com/aquasecurity/trivy/pkg/indicator"
"github.com/spf13/afero"
"k8s.io/utils/clock"
)
// Injectors from inject.go:
func initializeDBClient(cacheDir string, quiet bool) db.Client {
config := db2.Config{}
client := github.NewClient()
progressBar := indicator.NewProgressBar(quiet)
realClock := clock.RealClock{}
fs := afero.NewOsFs()
metadata := db.NewMetadata(fs, cacheDir)
dbClient := db.NewClient(config, client, progressBar, realClock, metadata)
return dbClient
}

View File

@@ -1,117 +0,0 @@
package plugin
import (
"context"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/internal/config"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/plugin"
)
// Install installs a plugin
func Install(c *cli.Context) error {
if c.NArg() != 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
if err := initLogger(c); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
url := c.Args().First()
if _, err := plugin.Install(c.Context, url, true); err != nil {
return xerrors.Errorf("plugin install error: %w", err)
}
return nil
}
// Uninstall uninstalls the plugin
func Uninstall(c *cli.Context) error {
if c.NArg() != 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
if err := initLogger(c); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
pluginName := c.Args().First()
if err := plugin.Uninstall(pluginName); err != nil {
return xerrors.Errorf("plugin uninstall error: %w", err)
}
return nil
}
// Run runs the plugin
func Run(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
if err := initLogger(c); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
url := c.Args().First()
args := c.Args().Tail()
return RunWithArgs(c.Context, url, args)
}
// RunWithArgs runs the plugin with arguments
func RunWithArgs(ctx context.Context, url string, args []string) error {
pl, err := plugin.Install(ctx, url, false)
if err != nil {
return xerrors.Errorf("plugin install error: %w", err)
}
if err = pl.Run(ctx, args); err != nil {
return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err)
}
return nil
}
// LoadCommands loads plugins as subcommands
func LoadCommands() cli.Commands {
var commands cli.Commands
plugins, err := plugin.LoadAll()
if err != nil {
log.Logger.Debugf("no plugins were loaded")
return nil
}
for _, p := range plugins {
cmd := &cli.Command{
Name: p.Name,
Usage: p.Usage,
Action: func(c *cli.Context) error {
if err := initLogger(c); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
if err := p.Run(c.Context, c.Args().Slice()); err != nil {
return xerrors.Errorf("plugin error: %w", err)
}
return nil
},
SkipFlagParsing: true,
}
commands = append(commands, cmd)
}
return commands
}
func initLogger(ctx *cli.Context) error {
conf, err := config.NewGlobalConfig(ctx)
if err != nil {
return xerrors.Errorf("config error: %w", err)
}
if err = log.InitLogger(conf.Debug, conf.Quiet); err != nil {
return xerrors.Errorf("failed to initialize a logger: %w", err)
}
return nil
}

View File

@@ -1,45 +0,0 @@
package config
import (
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy/internal/config"
)
// Config holds the Trivy config
type Config struct {
config.GlobalConfig
config.DBConfig
config.CacheConfig
Listen string
Token string
TokenHeader string
}
// New is the factory method to return config
func New(c *cli.Context) Config {
// the error is ignored because logger is unnecessary
gc, _ := config.NewGlobalConfig(c) // nolint: errcheck
return Config{
GlobalConfig: gc,
DBConfig: config.NewDBConfig(c),
CacheConfig: config.NewCacheConfig(c),
Listen: c.String("listen"),
Token: c.String("token"),
TokenHeader: c.String("token-header"),
}
}
// Init initializes the config
func (c *Config) Init() (err error) {
if err := c.DBConfig.Init(); err != nil {
return err
}
if err := c.CacheConfig.Init(); err != nil {
return err
}
return nil
}

View File

@@ -1,108 +0,0 @@
package config_test
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"github.com/aquasecurity/trivy/internal/config"
c "github.com/aquasecurity/trivy/internal/server/config"
)
func TestNew(t *testing.T) {
tests := []struct {
name string
args []string
want c.Config
}{
{
name: "happy path",
args: []string{"-quiet", "--no-progress", "--reset", "--skip-update", "--listen", "localhost:8080"},
want: c.Config{
GlobalConfig: config.GlobalConfig{
Quiet: true,
},
DBConfig: config.DBConfig{
Reset: true,
SkipUpdate: true,
NoProgress: true,
},
Listen: "localhost:8080",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
set.Bool("quiet", false, "")
set.Bool("no-progress", false, "")
set.Bool("reset", false, "")
set.Bool("skip-update", false, "")
set.String("listen", "", "")
ctx := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args)
tt.want.GlobalConfig.Context = ctx
got := c.New(ctx)
assert.Equal(t, tt.want.GlobalConfig.Quiet, got.Quiet, tt.name)
assert.Equal(t, tt.want.DBConfig, got.DBConfig, tt.name)
assert.Equal(t, tt.want.Listen, got.Listen, tt.name)
})
}
}
func TestConfig_Init(t *testing.T) {
tests := []struct {
name string
globalConfig config.GlobalConfig
dbConfig config.DBConfig
args []string
wantErr string
}{
{
name: "happy path",
args: []string{"alpine:3.10"},
},
{
name: "happy path: reset",
dbConfig: config.DBConfig{
Reset: true,
},
args: []string{"alpine:3.10"},
},
{
name: "sad: skip and download db",
dbConfig: config.DBConfig{
SkipUpdate: true,
DownloadDBOnly: true,
},
args: []string{"alpine:3.10"},
wantErr: "--skip-update and --download-db-only options can not be specified both",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &c.Config{
DBConfig: tt.dbConfig,
}
err := c.Init()
// test the error
switch {
case tt.wantErr != "":
require.NotNil(t, err, tt.name)
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
})
}
}

View File

@@ -1,57 +0,0 @@
package server
import (
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/internal/operation"
"github.com/aquasecurity/trivy/internal/server/config"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/rpc/server"
"github.com/aquasecurity/trivy/pkg/utils"
)
// Run runs the scan
func Run(ctx *cli.Context) error {
return run(config.New(ctx))
}
func run(c config.Config) (err error) {
if err = log.InitLogger(c.Debug, c.Quiet); err != nil {
return xerrors.Errorf("failed to initialize a logger: %w", err)
}
// initialize config
if err = c.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
// configure cache dir
utils.SetCacheDir(c.CacheDir)
cache, err := operation.NewCache(c.CacheBackend)
if err != nil {
return xerrors.Errorf("server cache error: %w", err)
}
defer cache.Close()
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
if c.Reset {
return cache.ClearDB()
}
// download the database file
if err = operation.DownloadDB(c.AppVersion, c.CacheDir, true, false, c.SkipUpdate); err != nil {
return err
}
if c.DownloadDBOnly {
return nil
}
if err = db.Init(c.CacheDir); err != nil {
return xerrors.Errorf("error in vulnerability DB initialize: %w", err)
}
return server.ListenAndServe(c, cache)
}