feat(filesystem): scan in client/server mode (#1829)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
afdesk
2022-03-21 19:51:18 +06:00
committed by GitHub
parent 12d0317a67
commit d6418cf0de
49 changed files with 763 additions and 1014 deletions

View File

@@ -31,5 +31,9 @@ OPTIONS:
--config-policy value specify paths to the Rego policy files directory, applying config files [$TRIVY_CONFIG_POLICY] --config-policy value specify paths to the Rego policy files directory, applying config files [$TRIVY_CONFIG_POLICY]
--config-data value specify paths from which data for the Rego policies will be recursively loaded [$TRIVY_CONFIG_DATA] --config-data value specify paths from which data for the Rego policies will be recursively loaded [$TRIVY_CONFIG_DATA]
--policy-namespaces value, --namespaces value Rego namespaces (default: "users") [$TRIVY_POLICY_NAMESPACES] --policy-namespaces value, --namespaces value Rego namespaces (default: "users") [$TRIVY_POLICY_NAMESPACES]
--server value server address [$TRIVY_SERVER]
--token value for authentication [$TRIVY_TOKEN]
--token-header value specify a header name for token (default: "Trivy-Token") [$TRIVY_TOKEN_HEADER]
--custom-headers value custom headers [$TRIVY_CUSTOM_HEADERS]
--help, -h show help (default: false) --help, -h show help (default: false)
``` ```

View File

@@ -6,7 +6,8 @@ Scan a local project including language-specific files.
$ trivy fs /path/to/project $ trivy fs /path/to/project
``` ```
## Local Project ## Standalone mode
### Local Project
Trivy will look for vulnerabilities based on lock files such as Gemfile.lock and package-lock.json. Trivy will look for vulnerabilities based on lock files such as Gemfile.lock and package-lock.json.
``` ```
@@ -54,3 +55,49 @@ It's also possible to scan a single file.
``` ```
$ trivy fs ~/src/github.com/aquasecurity/trivy-ci-test/Pipfile.lock $ trivy fs ~/src/github.com/aquasecurity/trivy-ci-test/Pipfile.lock
``` ```
## Client/Server mode
You must launch Trivy server in advance.
```sh
$ trivy server
```
Then, Trivy works as a client if you specify the `--server` option.
```sh
$ trivy fs --server http://localhost:4954 --severity CRITICAL ./integration/testdata/fixtures/fs/pom/
```
<details>
<summary>Result</summary>
```
pom.xml (pom)
=============
Total: 4 (CRITICAL: 4)
+---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+
| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE |
+---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+
| com.fasterxml.jackson.core:jackson-databind | CVE-2017-17485 | CRITICAL | 2.9.1 | 2.8.11, 2.9.4 | jackson-databind: Unsafe |
| | | | | | deserialization due to |
| | | | | | incomplete black list (incomplete |
| | | | | | fix for CVE-2017-15095)... |
| | | | | | -->avd.aquasec.com/nvd/cve-2017-17485 |
+ +------------------+ + +--------------------------------+---------------------------------------+
| | CVE-2020-9546 | | | 2.7.9.7, 2.8.11.6, 2.9.10.4 | jackson-databind: Serialization |
| | | | | | gadgets in shaded-hikari-config |
| | | | | | -->avd.aquasec.com/nvd/cve-2020-9546 |
+ +------------------+ + + +---------------------------------------+
| | CVE-2020-9547 | | | | jackson-databind: Serialization |
| | | | | | gadgets in ibatis-sqlmap |
| | | | | | -->avd.aquasec.com/nvd/cve-2020-9547 |
+ +------------------+ + + +---------------------------------------+
| | CVE-2020-9548 | | | | jackson-databind: Serialization |
| | | | | | gadgets in anteros-core |
| | | | | | -->avd.aquasec.com/nvd/cve-2020-9548 |
+---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+
```
</details>

View File

@@ -26,6 +26,8 @@ import (
) )
type csArgs struct { type csArgs struct {
Command string
RemoteAddrOption string
Format string Format string
TemplatePath string TemplatePath string
IgnoreUnfixed bool IgnoreUnfixed bool
@@ -35,6 +37,7 @@ type csArgs struct {
ClientToken string ClientToken string
ClientTokenHeader string ClientTokenHeader string
ListAllPackages bool ListAllPackages bool
Target string
} }
func TestClientServer(t *testing.T) { func TestClientServer(t *testing.T) {
@@ -220,6 +223,15 @@ func TestClientServer(t *testing.T) {
}, },
golden: "testdata/busybox-with-lockfile.json.golden", golden: "testdata/busybox-with-lockfile.json.golden",
}, },
{
name: "scan pox.xml with fs command in client/server mode",
args: csArgs{
Command: "fs",
RemoteAddrOption: "--server",
Target: "testdata/fixtures/fs/pom/",
},
golden: "testdata/pom.json.golden",
},
} }
app, addr, cacheDir := setup(t, setupOptions{}) app, addr, cacheDir := setup(t, setupOptions{})
@@ -525,8 +537,14 @@ func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []stri
} }
func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden string) ([]string, string) { func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden string) ([]string, string) {
if c.Command == "" {
c.Command = "client"
}
if c.RemoteAddrOption == "" {
c.RemoteAddrOption = "--remote"
}
t.Helper() t.Helper()
osArgs := []string{"trivy", "--cache-dir", cacheDir, "client", "--remote", "http://" + addr} osArgs := []string{"trivy", "--cache-dir", cacheDir, c.Command, c.RemoteAddrOption, "http://" + addr}
if c.Format != "" { if c.Format != "" {
osArgs = append(osArgs, "--format", c.Format) osArgs = append(osArgs, "--format", c.Format)
@@ -567,6 +585,10 @@ func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden st
osArgs = append(osArgs, "--output", outputFile) osArgs = append(osArgs, "--output", outputFile)
if c.Target != "" {
osArgs = append(osArgs, c.Target)
}
return osArgs, outputFile return osArgs, outputFile
} }

16
pkg/cache/nop.go vendored Normal file
View File

@@ -0,0 +1,16 @@
package cache
import "github.com/aquasecurity/fanal/cache"
func NopCache(ac cache.ArtifactCache) cache.Cache {
return nopCache{ArtifactCache: ac}
}
type nopCache struct {
cache.ArtifactCache
cache.LocalArtifactCache
}
func (nopCache) Close() error {
return nil
}

1
pkg/cache/remote.go vendored
View File

@@ -31,7 +31,6 @@ func NewRemoteCache(url string, customHeaders http.Header, insecure bool) cache.
}, },
}, },
} }
c := rpcCache.NewCacheProtobufClient(url, httpClient) c := rpcCache.NewCacheProtobufClient(url, httpClient)
return &RemoteCache{ctx: ctx, client: c} return &RemoteCache{ctx: ctx, client: c}
} }

View File

@@ -13,7 +13,6 @@ import (
"github.com/aquasecurity/trivy-db/pkg/metadata" "github.com/aquasecurity/trivy-db/pkg/metadata"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types" dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/commands/artifact" "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/plugin"
"github.com/aquasecurity/trivy/pkg/commands/server" "github.com/aquasecurity/trivy/pkg/commands/server"
"github.com/aquasecurity/trivy/pkg/result" "github.com/aquasecurity/trivy/pkg/result"
@@ -210,14 +209,14 @@ var (
token = cli.StringFlag{ token = cli.StringFlag{
Name: "token", Name: "token",
Usage: "for authentication", Usage: "for authentication in client/server mode",
EnvVars: []string{"TRIVY_TOKEN"}, EnvVars: []string{"TRIVY_TOKEN"},
} }
tokenHeader = cli.StringFlag{ tokenHeader = cli.StringFlag{
Name: "token-header", Name: "token-header",
Value: "Trivy-Token", Value: "Trivy-Token",
Usage: "specify a header name for token", Usage: "specify a header name for token in client/server mode",
EnvVars: []string{"TRIVY_TOKEN_HEADER"}, EnvVars: []string{"TRIVY_TOKEN_HEADER"},
} }
@@ -313,6 +312,18 @@ var (
EnvVars: []string{"TRIVY_INSECURE"}, EnvVars: []string{"TRIVY_INSECURE"},
} }
remoteServer = cli.StringFlag{
Name: "server",
Usage: "server address",
EnvVars: []string{"TRIVY_SERVER"},
}
customHeaders = cli.StringSliceFlag{
Name: "custom-headers",
Usage: "custom headers in client/server mode",
EnvVars: []string{"TRIVY_CUSTOM_HEADERS"},
}
// Global flags // Global flags
globalFlags = []cli.Flag{ globalFlags = []cli.Flag{
&quietFlag, &quietFlag,
@@ -473,9 +484,17 @@ func NewFilesystemCommand() *cli.Command {
&offlineScan, &offlineScan,
stringSliceFlag(skipFiles), stringSliceFlag(skipFiles),
stringSliceFlag(skipDirs), stringSliceFlag(skipDirs),
// for misconfiguration
stringSliceFlag(configPolicy), stringSliceFlag(configPolicy),
stringSliceFlag(configData), stringSliceFlag(configData),
stringSliceFlag(policyNamespaces), stringSliceFlag(policyNamespaces),
// for client/server
&remoteServer,
&token,
&tokenHeader,
&customHeaders,
}, },
} }
} }
@@ -565,7 +584,7 @@ func NewClientCommand() *cli.Command {
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "image_name", ArgsUsage: "image_name",
Usage: "client mode", Usage: "client mode",
Action: client.Run, Action: artifact.ImageRun,
Flags: []cli.Flag{ Flags: []cli.Flag{
&templateFlag, &templateFlag,
&formatFlag, &formatFlag,
@@ -589,20 +608,17 @@ func NewClientCommand() *cli.Command {
&offlineScan, &offlineScan,
&insecureFlag, &insecureFlag,
// original flags
&token, &token,
&tokenHeader, &tokenHeader,
&customHeaders,
// original flags
&cli.StringFlag{ &cli.StringFlag{
Name: "remote", Name: "remote",
Value: "http://localhost:4954", Value: "http://localhost:4954",
Usage: "server address", Usage: "server address",
EnvVars: []string{"TRIVY_REMOTE"}, EnvVars: []string{"TRIVY_REMOTE"},
}, },
&cli.StringSliceFlag{
Name: "custom-headers",
Usage: "custom headers",
EnvVars: []string{"TRIVY_CUSTOM_HEADERS"},
},
}, },
} }
} }

View File

@@ -26,5 +26,5 @@ func ConfigRun(ctx *cli.Context) error {
opt.SkipDBUpdate = true opt.SkipDBUpdate = true
// Run filesystem command internally // Run filesystem command internally
return Run(ctx.Context, opt, filesystemScanner, initFSCache) return Run(ctx.Context, opt, filesystemStandaloneScanner, initCache)
} }

View File

@@ -7,15 +7,23 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer" "github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
) )
func filesystemScanner(ctx context.Context, path string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, // filesystemStandaloneScanner initializes a filesystem scanner in standalone mode
_ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { func filesystemStandaloneScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
s, cleanup, err := initializeFilesystemScanner(ctx, path, ac, lac, artifactOpt, scannerOpt) s, cleanup, err := initializeFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
conf.ArtifactOption, conf.MisconfOption)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err)
}
return s, cleanup, nil
}
// filesystemRemoteScanner initializes a filesystem scanner in client/server mode
func filesystemRemoteScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
s, cleanup, err := initializeRemoteFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption,
conf.ArtifactOption, conf.MisconfOption)
if err != nil { if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err)
} }
@@ -32,7 +40,13 @@ func FilesystemRun(ctx *cli.Context) error {
// Disable the individual package scanning // Disable the individual package scanning
opt.DisabledAnalyzers = analyzer.TypeIndividualPkgs opt.DisabledAnalyzers = analyzer.TypeIndividualPkgs
return Run(ctx.Context, opt, filesystemScanner, initFSCache) // client/server mode
if opt.RemoteAddr != "" {
return Run(ctx.Context, opt, filesystemRemoteScanner, initCache)
}
// standalone mode
return Run(ctx.Context, opt, filesystemStandaloneScanner, initCache)
} }
// RootfsRun runs scan on rootfs. // RootfsRun runs scan on rootfs.
@@ -45,5 +59,11 @@ func RootfsRun(ctx *cli.Context) error {
// Disable the lock file scanning // Disable the lock file scanning
opt.DisabledAnalyzers = analyzer.TypeLockfiles opt.DisabledAnalyzers = analyzer.TypeLockfiles
return Run(ctx.Context, opt, filesystemScanner, initFSCache) // client/server mode
if opt.RemoteAddr != "" {
return Run(ctx.Context, opt, filesystemRemoteScanner, initCache)
}
// standalone mode
return Run(ctx.Context, opt, filesystemStandaloneScanner, initCache)
} }

View File

@@ -7,36 +7,67 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer" "github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
) )
func archiveScanner(ctx context.Context, input string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, // imageScanner initializes a container image scanner in standalone mode
_ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { // $ trivy image alpine:3.15
s, err := initializeArchiveScanner(ctx, input, ac, lac, artifactOpt, scannerOpt) func imageScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
if err != nil { dockerOpt, err := types.GetDockerOption(conf.ArtifactOption.InsecureSkipTLS)
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,
insecure bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) {
dockerOpt, err := types.GetDockerOption(insecure)
if err != nil { if err != nil {
return scanner.Scanner{}, nil, err return scanner.Scanner{}, nil, err
} }
s, cleanup, err := initializeDockerScanner(ctx, imageName, ac, lac, dockerOpt, artifactOpt, scannerOpt) s, cleanup, err := initializeDockerScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
dockerOpt, conf.ArtifactOption, conf.MisconfOption)
if err != nil { if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err) return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err)
} }
return s, cleanup, nil return s, cleanup, nil
} }
// ImageRun runs scan on docker image // archiveScanner initializes an image archive scanner in standalone mode
// $ trivy image --input alpine.tar
func archiveScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
s, err := initializeArchiveScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
conf.ArtifactOption, conf.MisconfOption)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize the archive scanner: %w", err)
}
return s, func() {}, nil
}
// remoteImageScanner initializes a container image scanner in client/server mode
// $ trivy image --server localhost:4954 alpine:3.15
func remoteImageScanner(ctx context.Context, conf scannerConfig) (
scanner.Scanner, func(), error) {
// Scan an image in Docker Engine, Docker Registry, etc.
dockerOpt, err := types.GetDockerOption(conf.ArtifactOption.InsecureSkipTLS)
if err != nil {
return scanner.Scanner{}, nil, err
}
s, cleanup, err := initializeRemoteDockerScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption,
dockerOpt, conf.ArtifactOption, conf.MisconfOption)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the docker scanner: %w", err)
}
return s, cleanup, nil
}
// remoteArchiveScanner initializes an image archive scanner in client/server mode
// $ trivy image --server localhost:4954 --input alpine.tar
func remoteArchiveScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
// Scan tar file
s, err := initializeRemoteArchiveScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption,
conf.ArtifactOption, conf.MisconfOption)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the archive scanner: %w", err)
}
return s, func() {}, nil
}
// ImageRun runs scan on container image
func ImageRun(ctx *cli.Context) error { func ImageRun(ctx *cli.Context) error {
opt, err := initOption(ctx) opt, err := initOption(ctx)
if err != nil { if err != nil {
@@ -47,9 +78,34 @@ func ImageRun(ctx *cli.Context) error {
opt.DisabledAnalyzers = analyzer.TypeLockfiles opt.DisabledAnalyzers = analyzer.TypeLockfiles
if opt.Input != "" { if opt.Input != "" {
// scan tar file return archiveImageRun(ctx.Context, opt)
return Run(ctx.Context, opt, archiveScanner, initFSCache)
} }
return Run(ctx.Context, opt, dockerScanner, initFSCache) return imageRun(ctx.Context, opt)
}
func archiveImageRun(ctx context.Context, opt Option) error {
// standalone mode
scanner := archiveScanner
if opt.RemoteAddr != "" {
// client/server mode
scanner = remoteArchiveScanner
}
// scan tar file
return Run(ctx, opt, scanner, initCache)
}
func imageRun(ctx context.Context, opt Option) error {
// standalone mode
scanner := imageScanner
if opt.RemoteAddr != "" {
// client/server mode
scanner = remoteImageScanner
}
// scan container image
return Run(ctx, opt, scanner, initCache)
} }

View File

@@ -13,9 +13,16 @@ import (
"github.com/aquasecurity/fanal/cache" "github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/types" "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy/pkg/result" "github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
) )
//////////////
// Standalone
//////////////
// initializeDockerScanner is for container image scanning in standalone mode
// e.g. dockerd, container registry, podman, etc.
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option, localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option,
configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) { configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
@@ -23,6 +30,8 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach
return scanner.Scanner{}, nil, nil return scanner.Scanner{}, nil, nil
} }
// initializeArchiveScanner is for container image archive scanning in standalone mode
// e.g. docker save -o alpine.tar alpine:3.15
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option,
configScannerOption config.ScannerOption) (scanner.Scanner, error) { configScannerOption config.ScannerOption) (scanner.Scanner, error) {
@@ -30,6 +39,7 @@ func initializeArchiveScanner(ctx context.Context, filePath string, artifactCach
return scanner.Scanner{}, nil return scanner.Scanner{}, nil
} }
// initializeFilesystemScanner is for filesystem scanning in standalone mode
func initializeFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, func initializeFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache,
localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option,
configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) { configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
@@ -48,3 +58,39 @@ func initializeResultClient() result.Client {
wire.Build(result.SuperSet) wire.Build(result.SuperSet)
return result.Client{} return result.Client{}
} }
/////////////////
// Client/Server
/////////////////
// initializeRemoteDockerScanner is for container image scanning in client/server mode
// e.g. dockerd, container registry, podman, etc.
func initializeRemoteDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache,
remoteScanOptions client.ScannerOption, dockerOpt types.DockerOption, artifactOption artifact.Option,
configScannerOption config.ScannerOption) (
scanner.Scanner, func(), error) {
wire.Build(scanner.RemoteDockerSet)
return scanner.Scanner{}, nil, nil
}
// initializeRemoteArchiveScanner is for container image archive scanning in client/server mode
// e.g. docker save -o alpine.tar alpine:3.15
func initializeRemoteArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache,
remoteScanOptions client.ScannerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (
scanner.Scanner, error) {
wire.Build(scanner.RemoteArchiveSet)
return scanner.Scanner{}, nil
}
// initializeRemoteFilesystemScanner is for filesystem scanning in client/server mode
func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache,
remoteScanOptions client.ScannerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (
scanner.Scanner, func(), error) {
wire.Build(scanner.RemoteFilesystemSet)
return scanner.Scanner{}, nil, nil
}
func initializeRemoteResultClient() result.Client {
wire.Build(result.SuperSet)
return result.Client{}
}

View File

@@ -17,6 +17,7 @@ type Option struct {
option.ReportOption option.ReportOption
option.CacheOption option.CacheOption
option.ConfigOption option.ConfigOption
option.RemoteOption
// We don't want to allow disabled analyzers to be passed by users, // We don't want to allow disabled analyzers to be passed by users,
// but it differs depending on scanning modes. // but it differs depending on scanning modes.
@@ -38,6 +39,7 @@ func NewOption(c *cli.Context) (Option, error) {
ReportOption: option.NewReportOption(c), ReportOption: option.NewReportOption(c),
CacheOption: option.NewCacheOption(c), CacheOption: option.NewCacheOption(c),
ConfigOption: option.NewConfigOption(c), ConfigOption: option.NewConfigOption(c),
RemoteOption: option.NewRemoteOption(c),
}, nil }, nil
} }
@@ -55,7 +57,6 @@ func (c *Option) Init() error {
if err := c.ArtifactOption.Init(c.Context, c.Logger); err != nil { if err := c.ArtifactOption.Init(c.Context, c.Logger); err != nil {
return err return err
} }
return nil return nil
} }
@@ -69,6 +70,7 @@ func (c *Option) initPreScanOptions() error {
if err := c.CacheOption.Init(); err != nil { if err := c.CacheOption.Init(); err != nil {
return err return err
} }
c.RemoteOption.Init(c.Logger)
return nil return nil
} }

View File

@@ -2,6 +2,7 @@ package artifact
import ( import (
"flag" "flag"
"net/http"
"os" "os"
"testing" "testing"
@@ -60,6 +61,81 @@ func TestOption_Init(t *testing.T) {
}, },
}, },
}, },
{
name: "happy path with token and token header",
args: []string{"--server", "http://localhost:8080", "--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.11",
},
RemoteOption: option.RemoteOption{
RemoteAddr: "http://localhost:8080",
CustomHeaders: http.Header{
"X-Trivy-Token": []string{"secret"},
},
},
},
},
{
name: "invalid option combination: token and token header without server",
args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"},
logs: []string{
"'--token', '--token-header' and 'custom-header' can be used only with '--server'",
},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.11",
},
},
},
{
name: "happy path with good custom headers",
args: []string{"--server", "http://localhost:8080", "--custom-headers", "foo:bar", "alpine:3.11"},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.11",
},
RemoteOption: option.RemoteOption{
RemoteAddr: "http://localhost:8080",
CustomHeaders: http.Header{
"Foo": []string{"bar"},
}},
},
},
{
name: "happy path with bad custom headers",
args: []string{"--server", "http://localhost:8080", "--custom-headers", "foobaz", "alpine:3.11"},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.11",
},
RemoteOption: option.RemoteOption{RemoteAddr: "http://localhost:8080", CustomHeaders: http.Header{}},
},
},
{ {
name: "happy path: reset", name: "happy path: reset",
args: []string{"--reset"}, args: []string{"--reset"},
@@ -182,6 +258,10 @@ func TestOption_Init(t *testing.T) {
set.String("security-checks", "vuln", "") set.String("security-checks", "vuln", "")
set.String("template", "", "") set.String("template", "", "")
set.String("format", "", "") set.String("format", "", "")
set.String("server", "", "")
set.String("token", "", "")
set.String("token-header", "", "")
set.Var(&cli.StringSlice{}, "custom-headers", "")
ctx := cli.NewContext(app, set, nil) ctx := cli.NewContext(app, set, nil)
_ = set.Parse(tt.args) _ = set.Parse(tt.args)

View File

@@ -7,16 +7,14 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer" "github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
) )
func repositoryScanner(ctx context.Context, dir string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, // filesystemStandaloneScanner initializes a repository scanner in standalone mode
_ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { func repositoryScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) {
s, cleanup, err := initializeRepositoryScanner(ctx, dir, ac, lac, artifactOpt, scannerOpt) s, cleanup, err := initializeRepositoryScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
conf.ArtifactOption, conf.MisconfOption)
if err != nil { if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err)
} }
@@ -36,5 +34,5 @@ func RepositoryRun(ctx *cli.Context) error {
// Disable the OS analyzers and individual package analyzers // Disable the OS analyzers and individual package analyzers
opt.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...) opt.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...)
return Run(ctx.Context, opt, repositoryScanner, initFSCache) return Run(ctx.Context, opt, repositoryScanner, initCache)
} }

View File

@@ -13,9 +13,11 @@ import (
"github.com/aquasecurity/fanal/artifact" "github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache" "github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy-db/pkg/db"
tcache "github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/commands/operation" "github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/log"
pkgReport "github.com/aquasecurity/trivy/pkg/report" pkgReport "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils" "github.com/aquasecurity/trivy/pkg/utils"
@@ -25,9 +27,26 @@ const defaultPolicyNamespace = "appshield"
var errSkipScan = errors.New("skip subsequent processes") var errSkipScan = errors.New("skip subsequent processes")
type scannerConfig struct {
// e.g. image name and file path
Target string
// Cache
ArtifactCache cache.ArtifactCache
LocalArtifactCache cache.LocalArtifactCache
// Client/Server options
RemoteOption client.ScannerOption
// Artifact options
ArtifactOption artifact.Option
// Misconfiguration scanning options
MisconfOption config.ScannerOption
}
// InitializeScanner defines the initialize function signature of scanner // InitializeScanner defines the initialize function signature of scanner
type InitializeScanner func(context.Context, string, cache.ArtifactCache, cache.LocalArtifactCache, bool, type InitializeScanner func(context.Context, scannerConfig) (scanner.Scanner, func(), error)
artifact.Option, config.ScannerOption) (scanner.Scanner, func(), error)
// InitCache defines cache initializer // InitCache defines cache initializer
type InitCache func(c Option) (cache.Cache, error) type InitCache func(c Option) (cache.Cache, error)
@@ -37,7 +56,11 @@ func Run(ctx context.Context, opt Option, initializeScanner InitializeScanner, i
ctx, cancel := context.WithTimeout(ctx, opt.Timeout) ctx, cancel := context.WithTimeout(ctx, opt.Timeout)
defer cancel() defer cancel()
return runWithTimeout(ctx, opt, initializeScanner, initCache) err := runWithTimeout(ctx, opt, initializeScanner, initCache)
if xerrors.Is(err, context.DeadlineExceeded) {
log.Logger.Warn("Increase --timeout value")
}
return err
} }
func runWithTimeout(ctx context.Context, opt Option, initializeScanner InitializeScanner, initCache InitCache) error { func runWithTimeout(ctx context.Context, opt Option, initializeScanner InitializeScanner, initCache InitCache) error {
@@ -54,8 +77,8 @@ func runWithTimeout(ctx context.Context, opt Option, initializeScanner Initializ
} }
defer cacheClient.Close() defer cacheClient.Close()
// When scanning config files, it doesn't need to download the vulnerability database. // When scanning config files or running as client mode, it doesn't need to download the vulnerability database.
if utils.StringInSlice(types.SecurityCheckVulnerability, opt.SecurityChecks) { if opt.RemoteAddr == "" && utils.StringInSlice(types.SecurityCheckVulnerability, opt.SecurityChecks) {
if err = initDB(opt); err != nil { if err = initDB(opt); err != nil {
if errors.Is(err, errSkipScan) { if errors.Is(err, errSkipScan) {
return nil return nil
@@ -92,7 +115,14 @@ func runWithTimeout(ctx context.Context, opt Option, initializeScanner Initializ
return nil return nil
} }
func initFSCache(c Option) (cache.Cache, error) { func initCache(c Option) (cache.Cache, error) {
// client/server mode
if c.RemoteAddr != "" {
remoteCache := tcache.NewRemoteCache(c.RemoteAddr, c.CustomHeaders, c.Insecure)
return tcache.NopCache(remoteCache), nil
}
// standalone mode
utils.SetCacheDir(c.CacheDir) utils.SetCacheDir(c.CacheDir)
cache, err := operation.NewCache(c.CacheOption) cache, err := operation.NewCache(c.CacheOption)
if err != nil { if err != nil {
@@ -181,7 +211,7 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner,
} }
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType) log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
// ScannerOptions is filled only when config scanning is enabled. // ScannerOption is filled only when config scanning is enabled.
var configScannerOptions config.ScannerOption var configScannerOptions config.ScannerOption
if utils.StringInSlice(types.SecurityCheckConfig, opt.SecurityChecks) { if utils.StringInSlice(types.SecurityCheckConfig, opt.SecurityChecks) {
noProgress := opt.Quiet || opt.NoProgress noProgress := opt.Quiet || opt.NoProgress
@@ -199,16 +229,25 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner,
} }
} }
artifactOpt := artifact.Option{ s, cleanup, err := initializeScanner(ctx, scannerConfig{
DisabledAnalyzers: disabledAnalyzers(opt), Target: target,
SkipFiles: opt.SkipFiles, ArtifactCache: cacheClient,
SkipDirs: opt.SkipDirs, LocalArtifactCache: cacheClient,
InsecureSkipTLS: opt.Insecure, RemoteOption: client.ScannerOption{
Offline: opt.OfflineScan, RemoteURL: opt.RemoteAddr,
NoProgress: opt.NoProgress || opt.Quiet, CustomHeaders: opt.CustomHeaders,
} Insecure: opt.Insecure,
},
s, cleanup, err := initializeScanner(ctx, target, cacheClient, cacheClient, opt.Insecure, artifactOpt, configScannerOptions) ArtifactOption: artifact.Option{
DisabledAnalyzers: disabledAnalyzers(opt),
SkipFiles: opt.SkipFiles,
SkipDirs: opt.SkipDirs,
InsecureSkipTLS: opt.Insecure,
Offline: opt.OfflineScan,
NoProgress: opt.NoProgress || opt.Quiet,
},
MisconfOption: configScannerOptions,
})
if err != nil { if err != nil {
return types.Report{}, xerrors.Errorf("unable to initialize a scanner: %w", err) return types.Report{}, xerrors.Errorf("unable to initialize a scanner: %w", err)
} }
@@ -225,7 +264,10 @@ func filter(ctx context.Context, opt Option, report types.Report) (types.Report,
resultClient := initializeResultClient() resultClient := initializeResultClient()
results := report.Results results := report.Results
for i := range results { for i := range results {
resultClient.FillVulnerabilityInfo(results[i].Vulnerabilities, results[i].Type) // Fill vulnerability info only in standalone mode
if opt.RemoteAddr == "" {
resultClient.FillVulnerabilityInfo(results[i].Vulnerabilities, results[i].Type)
}
vulns, misconfSummary, misconfs, err := resultClient.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations, vulns, misconfSummary, misconfs, err := resultClient.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations,
opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy) opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy)
if err != nil { if err != nil {

View File

@@ -20,12 +20,15 @@ import (
"github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/detector/ospkg" "github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/result" "github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/scanner/local" "github.com/aquasecurity/trivy/pkg/scanner/local"
) )
// Injectors from inject.go: // Injectors from inject.go:
// initializeDockerScanner is for container image scanning in standalone mode
// e.g. dockerd, container registry, podman, etc.
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) { func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache) applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{} detector := ospkg.Detector{}
@@ -45,6 +48,8 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach
}, nil }, nil
} }
// initializeArchiveScanner is for container image archive scanning in standalone mode
// e.g. docker save -o alpine.tar alpine:3.15
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, error) { func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, error) {
applierApplier := applier.NewApplier(localArtifactCache) applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{} detector := ospkg.Detector{}
@@ -61,6 +66,7 @@ func initializeArchiveScanner(ctx context.Context, filePath string, artifactCach
return scannerScanner, nil return scannerScanner, nil
} }
// initializeFilesystemScanner is for filesystem scanning in standalone mode
func initializeFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) { func initializeFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache) applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{} detector := ospkg.Detector{}
@@ -93,3 +99,56 @@ func initializeResultClient() result.Client {
client := result.NewClient(dbConfig) client := result.NewClient(dbConfig)
return client return client
} }
// initializeRemoteDockerScanner is for container image scanning in client/server mode
// e.g. dockerd, container registry, podman, etc.
func initializeRemoteDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
clientScanner := client.NewScanner(remoteScanOptions)
typesImage, cleanup, err := image.NewDockerImage(ctx, imageName, dockerOpt)
if err != nil {
return scanner.Scanner{}, nil, err
}
artifactArtifact, err := image2.NewArtifact(typesImage, artifactCache, artifactOption, configScannerOption)
if err != nil {
cleanup()
return scanner.Scanner{}, nil, err
}
scannerScanner := scanner.NewScanner(clientScanner, artifactArtifact)
return scannerScanner, func() {
cleanup()
}, nil
}
// initializeRemoteArchiveScanner is for container image archive scanning in client/server mode
// e.g. docker save -o alpine.tar alpine:3.15
func initializeRemoteArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, error) {
clientScanner := client.NewScanner(remoteScanOptions)
typesImage, err := image.NewArchiveImage(filePath)
if err != nil {
return scanner.Scanner{}, err
}
artifactArtifact, err := image2.NewArtifact(typesImage, artifactCache, artifactOption, configScannerOption)
if err != nil {
return scanner.Scanner{}, err
}
scannerScanner := scanner.NewScanner(clientScanner, artifactArtifact)
return scannerScanner, nil
}
// initializeRemoteFilesystemScanner is for filesystem scanning in client/server mode
func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
clientScanner := client.NewScanner(remoteScanOptions)
artifactArtifact, err := local2.NewArtifact(path, artifactCache, artifactOption, configScannerOption)
if err != nil {
return scanner.Scanner{}, nil, err
}
scannerScanner := scanner.NewScanner(clientScanner, artifactArtifact)
return scannerScanner, func() {
}, nil
}
func initializeRemoteResultClient() result.Client {
dbConfig := db.Config{}
resultClient := result.NewClient(dbConfig)
return resultClient
}

View File

@@ -1,37 +0,0 @@
//go:build wireinject
// +build wireinject
package client
import (
"context"
"github.com/google/wire"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
)
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders,
url client.RemoteURL, insecure client.Insecure, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (
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, insecure client.Insecure, artifactOption artifact.Option,
configScannerOption config.ScannerOption) (scanner.Scanner, error) {
wire.Build(scanner.RemoteArchiveSet)
return scanner.Scanner{}, nil
}
func initializeResultClient() result.Client {
wire.Build(result.SuperSet)
return result.Client{}
}

View File

@@ -1,94 +0,0 @@
package client
import (
"net/http"
"strings"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/commands/option"
)
// Option holds the Trivy client options
type Option struct {
option.GlobalOption
option.ArtifactOption
option.ImageOption
option.ReportOption
option.ConfigOption
// For policy downloading
NoProgress bool
// We don't want to allow disabled analyzers to be passed by users,
// but it differs depending on scanning modes.
DisabledAnalyzers []analyzer.Type
RemoteAddr string
token string
tokenHeader string
customHeaders []string
// this field is populated in Init()
CustomHeaders http.Header
}
// NewOption is the factory method for Option
func NewOption(c *cli.Context) (Option, error) {
gc, err := option.NewGlobalOption(c)
if err != nil {
return Option{}, xerrors.Errorf("failed to initialize global options: %w", err)
}
return Option{
GlobalOption: gc,
ArtifactOption: option.NewArtifactOption(c),
ImageOption: option.NewImageOption(c),
ReportOption: option.NewReportOption(c),
ConfigOption: option.NewConfigOption(c),
NoProgress: c.Bool("no-progress"),
RemoteAddr: c.String("remote"),
token: c.String("token"),
tokenHeader: c.String("token-header"),
customHeaders: c.StringSlice("custom-headers"),
}, nil
}
// Init initializes the options
func (c *Option) 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.ReportOption.Init(c.Context.App.Writer, c.Logger); err != nil {
return err
}
if err = c.ArtifactOption.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,309 +0,0 @@
package client
import (
"flag"
"net/http"
"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/pkg/commands/option"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestConfig_Init(t *testing.T) {
tests := []struct {
name string
args []string
logs []string
want Option
wantErr string
}{
{
name: "happy path",
args: []string{"--severity", "CRITICAL", "--vuln-type", "os", "--quiet", "alpine:3.10"},
want: Option{
GlobalOption: option.GlobalOption{
Quiet: true,
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.10",
},
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{types.VulnTypeOS},
SecurityChecks: []string{types.SecurityCheckVulnerability},
Output: os.Stdout,
},
CustomHeaders: http.Header{},
},
},
{
name: "config scanning",
args: []string{"--severity", "CRITICAL", "--security-checks", "config", "--quiet", "alpine:3.10"},
want: Option{
GlobalOption: option.GlobalOption{
Quiet: true,
},
ArtifactOption: option.ArtifactOption{
Target: "alpine:3.10",
},
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckConfig},
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
ArtifactOption: option.ArtifactOption{
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
Template: "@contrib/gitlab.tpl",
},
ArtifactOption: option.ArtifactOption{
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: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
Template: "@contrib/gitlab.tpl",
Format: "json",
},
ArtifactOption: option.ArtifactOption{
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' is not specified. Specify '--template' option when you use '--format template'.",
},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
Format: "template",
},
ArtifactOption: option.ArtifactOption{
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' is not specified. Specify '--template' option when you use '--format template'.",
},
want: Option{
ReportOption: option.ReportOption{
Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
Output: os.Stdout,
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
Format: "template",
},
ArtifactOption: option.ArtifactOption{
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("security-checks", "vuln", "")
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 := NewOption(ctx)
require.NoError(t, err, err)
c.GlobalOption.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 != "":
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
default:
assert.NoError(t, err, tt.name)
}
tt.want.GlobalOption.Context = ctx
tt.want.GlobalOption.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) {
got := splitCustomHeaders(tt.args.headers)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -1,201 +0,0 @@
package client
import (
"context"
"os"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/log"
pkgReport "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"
)
const defaultPolicyNamespace = "appshield"
// Run runs the scan
func Run(cliCtx *cli.Context) error {
opt, err := NewOption(cliCtx)
if err != nil {
return xerrors.Errorf("option error: %w", err)
}
ctx, cancel := context.WithTimeout(cliCtx.Context, opt.Timeout)
defer cancel()
// Disable the lock file scanning
opt.DisabledAnalyzers = analyzer.TypeLockfiles
err = runWithTimeout(ctx, opt)
if xerrors.Is(err, context.DeadlineExceeded) {
log.Logger.Warn("Increase --timeout value")
}
return err
}
func runWithTimeout(ctx context.Context, opt Option) error {
if err := initialize(&opt); err != nil {
return xerrors.Errorf("initialize error: %w", err)
}
if opt.ClearCache {
log.Logger.Warn("A client doesn't have image cache")
return nil
}
s, cleanup, err := initializeScanner(ctx, opt)
if err != nil {
return xerrors.Errorf("scanner initialize error: %w", err)
}
defer cleanup()
scanOptions := types.ScanOptions{
VulnType: opt.VulnType,
SecurityChecks: opt.SecurityChecks,
ScanRemovedPackages: opt.ScanRemovedPkgs,
ListAllPackages: opt.ListAllPkgs,
}
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
report, err := s.ScanArtifact(ctx, scanOptions)
if err != nil {
return xerrors.Errorf("error in image scan: %w", err)
}
resultClient := initializeResultClient()
results := report.Results
for i := range results {
vulns, misconfSummary, misconfs, err := resultClient.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations,
opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy)
if err != nil {
return xerrors.Errorf("filter error: %w", err)
}
results[i].Vulnerabilities = vulns
results[i].Misconfigurations = misconfs
results[i].MisconfSummary = misconfSummary
}
if err = pkgReport.Write(report, pkgReport.Option{
AppVersion: opt.GlobalOption.AppVersion,
Format: opt.Format,
Output: opt.Output,
Severities: opt.Severities,
OutputTemplate: opt.Template,
IncludeNonFailures: opt.IncludeNonFailures,
Trace: opt.Trace,
}); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
exit(opt, results)
return nil
}
func initialize(opt *Option) error {
// Initialize logger
if err := log.InitLogger(opt.Debug, opt.Quiet); err != nil {
return xerrors.Errorf("failed to initialize a logger: %w", err)
}
// Initialize options
if err := opt.Init(); err != nil {
return xerrors.Errorf("failed to initialize options: %w", err)
}
// configure cache dir
utils.SetCacheDir(opt.CacheDir)
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
return nil
}
func disabledAnalyzers(opt Option) []analyzer.Type {
// Specified analyzers to be disabled depending on scanning modes
// e.g. The 'image' subcommand should disable the lock file scanning.
analyzers := opt.DisabledAnalyzers
// It doesn't analyze apk commands by default.
if !opt.ScanRemovedPkgs {
analyzers = append(analyzers, analyzer.TypeApkCommand)
}
// Don't analyze programming language packages when not running in 'library' mode
if !utils.StringInSlice(types.VulnTypeLibrary, opt.VulnType) {
analyzers = append(analyzers, analyzer.TypeLanguages...)
}
return analyzers
}
func initializeScanner(ctx context.Context, opt Option) (scanner.Scanner, func(), error) {
remoteCache := cache.NewRemoteCache(opt.RemoteAddr, opt.CustomHeaders, opt.Insecure)
// ScannerOptions is filled only when config scanning is enabled.
var configScannerOptions config.ScannerOption
if utils.StringInSlice(types.SecurityCheckConfig, opt.SecurityChecks) {
noProgress := opt.Quiet || opt.NoProgress
builtinPolicyPaths, err := operation.InitBuiltinPolicies(ctx, opt.CacheDir, noProgress, opt.SkipPolicyUpdate)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("failed to initialize default policies: %w", err)
}
configScannerOptions = config.ScannerOption{
Trace: opt.Trace,
Namespaces: append(opt.PolicyNamespaces, defaultPolicyNamespace),
PolicyPaths: append(opt.PolicyPaths, builtinPolicyPaths...),
DataPaths: opt.DataPaths,
FilePatterns: opt.FilePatterns,
}
}
artifactOpt := artifact.Option{
DisabledAnalyzers: disabledAnalyzers(opt),
SkipFiles: opt.SkipFiles,
SkipDirs: opt.SkipDirs,
Offline: opt.OfflineScan,
}
if opt.Input != "" {
// Scan tar file
s, err := initializeArchiveScanner(ctx, opt.Input, remoteCache, client.CustomHeaders(opt.CustomHeaders),
client.RemoteURL(opt.RemoteAddr), client.Insecure(opt.Insecure), artifactOpt, configScannerOptions)
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
dockerOpt, err := types.GetDockerOption(opt.Insecure)
if err != nil {
return scanner.Scanner{}, nil, err
}
s, cleanup, err := initializeDockerScanner(ctx, opt.Target, remoteCache, client.CustomHeaders(opt.CustomHeaders),
client.RemoteURL(opt.RemoteAddr), client.Insecure(opt.Insecure), dockerOpt, artifactOpt, configScannerOptions)
if err != nil {
return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the docker scanner: %w", err)
}
return s, cleanup, nil
}
func exit(c Option, results types.Results) {
if c.ExitCode != 0 {
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(c.ExitCode)
}
}
}
}

View File

@@ -1,62 +0,0 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//go:build !wireinject
// +build !wireinject
package client
import (
"context"
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/fanal/artifact"
image2 "github.com/aquasecurity/fanal/artifact/image"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/image"
"github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
)
// Injectors from inject.go:
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders, url client.RemoteURL, insecure client.Insecure, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) {
scannerScanner := client.NewProtobufClient(url, insecure)
clientScanner := client.NewScanner(customHeaders, scannerScanner)
typesImage, cleanup, err := image.NewDockerImage(ctx, imageName, dockerOpt)
if err != nil {
return scanner.Scanner{}, nil, err
}
artifactArtifact, err := image2.NewArtifact(typesImage, artifactCache, artifactOption, configScannerOption)
if err != nil {
cleanup()
return scanner.Scanner{}, nil, err
}
scanner2 := scanner.NewScanner(clientScanner, artifactArtifact)
return scanner2, func() {
cleanup()
}, nil
}
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, customHeaders client.CustomHeaders, url client.RemoteURL, insecure client.Insecure, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, error) {
scannerScanner := client.NewProtobufClient(url, insecure)
clientScanner := client.NewScanner(customHeaders, scannerScanner)
typesImage, err := image.NewArchiveImage(filePath)
if err != nil {
return scanner.Scanner{}, err
}
artifactArtifact, err := image2.NewArtifact(typesImage, artifactCache, artifactOption, configScannerOption)
if err != nil {
return scanner.Scanner{}, err
}
scanner2 := scanner.NewScanner(clientScanner, artifactArtifact)
return scanner2, nil
}
func initializeResultClient() result.Client {
dbConfig := db.Config{}
resultClient := result.NewClient(dbConfig)
return resultClient
}

View File

@@ -56,6 +56,8 @@ func NewCache(c option.CacheOption) (Cache, error) {
redisCache := cache.NewRedisCache(options) redisCache := cache.NewRedisCache(options)
return Cache{Cache: redisCache}, nil return Cache{Cache: redisCache}, nil
} }
// standalone mode
fsCache, err := cache.NewFSCache(utils.CacheDir()) fsCache, err := cache.NewFSCache(utils.CacheDir())
if err != nil { if err != nil {
return Cache{}, xerrors.Errorf("unable to initialize fs cache: %w", err) return Cache{}, xerrors.Errorf("unable to initialize fs cache: %w", err)

View File

@@ -0,0 +1,74 @@
package option
import (
"net/http"
"strings"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
)
// RemoteOption holds options for client/server
type RemoteOption struct {
RemoteAddr string
customHeaders []string
token string
tokenHeader string
remote string // deprecated
// this field is populated in Init()
CustomHeaders http.Header
}
func NewRemoteOption(c *cli.Context) RemoteOption {
r := RemoteOption{
RemoteAddr: c.String("server"),
customHeaders: c.StringSlice("custom-headers"),
token: c.String("token"),
tokenHeader: c.String("token-header"),
remote: c.String("remote"), // deprecated
}
return r
}
// Init initialize the options for client/server mode
func (c *RemoteOption) Init(logger *zap.SugaredLogger) {
// for testability
defer func() {
c.token = ""
c.tokenHeader = ""
c.remote = ""
c.customHeaders = nil
}()
// for backward compatibility, should be removed in the future
if c.remote != "" {
c.RemoteAddr = c.remote
}
if c.RemoteAddr == "" {
if len(c.customHeaders) > 0 || c.token != "" || c.tokenHeader != "" {
logger.Warn(`'--token', '--token-header' and 'custom-header' can be used only with '--server'`)
}
return
}
c.CustomHeaders = splitCustomHeaders(c.customHeaders)
if c.token != "" {
c.CustomHeaders.Set(c.tokenHeader, c.token)
}
}
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

@@ -0,0 +1,36 @@
package option
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
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) {
got := splitCustomHeaders(tt.args.headers)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -5,58 +5,63 @@ import (
"crypto/tls" "crypto/tls"
"net/http" "net/http"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/google/wire"
"golang.org/x/xerrors" "golang.org/x/xerrors"
ftypes "github.com/aquasecurity/fanal/types" ftypes "github.com/aquasecurity/fanal/types"
r "github.com/aquasecurity/trivy/pkg/rpc" r "github.com/aquasecurity/trivy/pkg/rpc"
"github.com/aquasecurity/trivy/pkg/types"
rpc "github.com/aquasecurity/trivy/rpc/scanner" rpc "github.com/aquasecurity/trivy/rpc/scanner"
) )
// SuperSet binds the dependencies for RPC client type options struct {
var SuperSet = wire.NewSet( rpcClient rpc.Scanner
NewProtobufClient,
NewScanner,
)
// RemoteURL for RPC remote host
type RemoteURL string
// Insecure for RPC remote host
type Insecure bool
// NewProtobufClient is the factory method to return RPC scanner
func NewProtobufClient(remoteURL RemoteURL, insecure Insecure) rpc.Scanner {
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: bool(insecure),
},
},
}
return rpc.NewScannerProtobufClient(string(remoteURL), httpClient)
} }
// CustomHeaders for holding HTTP headers type option func(*options)
type CustomHeaders http.Header
// WithRPCClient takes rpc client for testability
func WithRPCClient(c rpc.Scanner) option {
return func(opts *options) {
opts.rpcClient = c
}
}
// ScannerOption holds options for RPC client
type ScannerOption struct {
RemoteURL string
Insecure bool
CustomHeaders http.Header
}
// Scanner implements the RPC scanner // Scanner implements the RPC scanner
type Scanner struct { type Scanner struct {
customHeaders CustomHeaders customHeaders http.Header
client rpc.Scanner client rpc.Scanner
} }
// NewScanner is the factory method to return RPC Scanner // NewScanner is the factory method to return RPC Scanner
func NewScanner(customHeaders CustomHeaders, s rpc.Scanner) Scanner { func NewScanner(scannerOptions ScannerOption, opts ...option) Scanner {
return Scanner{customHeaders: customHeaders, client: s} httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: scannerOptions.Insecure,
},
},
}
c := rpc.NewScannerProtobufClient(scannerOptions.RemoteURL, httpClient)
o := &options{rpcClient: c}
for _, opt := range opts {
opt(o)
}
return Scanner{customHeaders: scannerOptions.CustomHeaders, client: o.rpcClient}
} }
// Scan scans the image // Scan scans the image
func (s Scanner) Scan(target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) { func (s Scanner) Scan(target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
ctx := WithCustomHeaders(context.Background(), http.Header(s.customHeaders)) ctx := WithCustomHeaders(context.Background(), s.customHeaders)
var res *rpc.ScanResponse var res *rpc.ScanResponse
err := r.Retry(func() error { err := r.Retry(func() error {

View File

@@ -1,94 +1,27 @@
package client package client
import ( import (
"context" "crypto/tls"
"errors" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/golang/protobuf/ptypes/timestamp" "github.com/golang/protobuf/ptypes/timestamp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
ftypes "github.com/aquasecurity/fanal/types" ftypes "github.com/aquasecurity/fanal/types"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types" dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/utils" "github.com/aquasecurity/trivy-db/pkg/utils"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/rpc/common" "github.com/aquasecurity/trivy/rpc/common"
"github.com/aquasecurity/trivy/rpc/scanner" rpc "github.com/aquasecurity/trivy/rpc/scanner"
) )
type mockScanner struct {
mock.Mock
}
type scanArgs struct {
Ctx context.Context
CtxAnything bool
Request *scanner.ScanRequest
RequestAnything bool
}
type scanReturns struct {
Res *scanner.ScanResponse
Err error
}
type scanExpectation struct {
Args scanArgs
Returns scanReturns
}
func (_m *mockScanner) ApplyScanExpectation(e scanExpectation) {
var args []interface{}
if e.Args.CtxAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Ctx)
}
if e.Args.RequestAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Request)
}
_m.On("Scan", args...).Return(e.Returns.Res, e.Returns.Err)
}
func (_m *mockScanner) ApplyScanExpectations(expectations []scanExpectation) {
for _, e := range expectations {
_m.ApplyScanExpectation(e)
}
}
// Scan provides a mock function with given fields: Ctx, Request
func (_m *mockScanner) Scan(Ctx context.Context, Request *scanner.ScanRequest) (*scanner.ScanResponse, error) {
ret := _m.Called(Ctx, Request)
var r0 *scanner.ScanResponse
if rf, ok := ret.Get(0).(func(context.Context, *scanner.ScanRequest) *scanner.ScanResponse); ok {
r0 = rf(Ctx, Request)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scanner.ScanResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *scanner.ScanRequest) error); ok {
r1 = rf(Ctx, Request)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
func TestScanner_Scan(t *testing.T) { func TestScanner_Scan(t *testing.T) {
type fields struct {
customHeaders CustomHeaders
}
type args struct { type args struct {
target string target string
imageID string imageID string
@@ -96,21 +29,19 @@ func TestScanner_Scan(t *testing.T) {
options types.ScanOptions options types.ScanOptions
} }
tests := []struct { tests := []struct {
name string name string
fields fields customHeaders http.Header
args args args args
scanExpectation scanExpectation expectation *rpc.ScanResponse
wantResults types.Results wantResults types.Results
wantOS *ftypes.OS wantOS *ftypes.OS
wantEosl bool wantEosl bool
wantErr string wantErr string
}{ }{
{ {
name: "happy path", name: "happy path",
fields: fields{ customHeaders: http.Header{
customHeaders: CustomHeaders{ "Trivy-Token": []string{"foo"},
"Trivy-Token": []string{"foo"},
},
}, },
args: args{ args: args{
target: "alpine:3.11", target: "alpine:3.11",
@@ -120,64 +51,49 @@ func TestScanner_Scan(t *testing.T) {
VulnType: []string{"os"}, VulnType: []string{"os"},
}, },
}, },
scanExpectation: scanExpectation{ expectation: &rpc.ScanResponse{
Args: scanArgs{ Os: &common.OS{
CtxAnything: true, Family: "alpine",
Request: &scanner.ScanRequest{ Name: "3.11",
Target: "alpine:3.11", Eosl: true,
ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
BlobIds: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
Options: &scanner.ScanOptions{
VulnType: []string{"os"},
},
},
}, },
Returns: scanReturns{ Results: []*rpc.Result{
Res: &scanner.ScanResponse{ {
Os: &common.OS{ Target: "alpine:3.11",
Family: "alpine", Vulnerabilities: []*common.Vulnerability{
Name: "3.11",
Eosl: true,
},
Results: []*scanner.Result{
{ {
Target: "alpine:3.11", VulnerabilityId: "CVE-2020-0001",
Vulnerabilities: []*common.Vulnerability{ PkgName: "musl",
{ InstalledVersion: "1.2.3",
VulnerabilityId: "CVE-2020-0001", FixedVersion: "1.2.4",
PkgName: "musl", Title: "DoS",
InstalledVersion: "1.2.3", Description: "Denial os Service",
FixedVersion: "1.2.4", Severity: common.Severity_CRITICAL,
Title: "DoS", References: []string{"http://exammple.com"},
Description: "Denial os Service", SeveritySource: "nvd",
Severity: common.Severity_CRITICAL, Cvss: map[string]*common.CVSS{
References: []string{"http://exammple.com"}, "nvd": {
SeveritySource: "nvd", V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C",
Cvss: map[string]*common.CVSS{ V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"nvd": { V2Score: 7.2,
V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", V3Score: 7.8,
V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
V2Score: 7.2,
V3Score: 7.8,
},
"redhat": {
V2Vector: "AV:H/AC:L/Au:N/C:C/I:C/A:C",
V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
V2Score: 4.2,
V3Score: 2.8,
},
},
CweIds: []string{"CWE-78"},
Layer: &common.Layer{
DiffId: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10",
},
LastModifiedDate: &timestamp.Timestamp{
Seconds: 1577840460,
},
PublishedDate: &timestamp.Timestamp{
Seconds: 978310860,
},
}, },
"redhat": {
V2Vector: "AV:H/AC:L/Au:N/C:C/I:C/A:C",
V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
V2Score: 4.2,
V3Score: 2.8,
},
},
CweIds: []string{"CWE-78"},
Layer: &common.Layer{
DiffId: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10",
},
LastModifiedDate: &timestamp.Timestamp{
Seconds: 1577840460,
},
PublishedDate: &timestamp.Timestamp{
Seconds: 978310860,
}, },
}, },
}, },
@@ -232,10 +148,8 @@ func TestScanner_Scan(t *testing.T) {
}, },
{ {
name: "sad path: Scan returns an error", name: "sad path: Scan returns an error",
fields: fields{ customHeaders: http.Header{
customHeaders: CustomHeaders{ "Trivy-Token": []string{"foo"},
"Trivy-Token": []string{"foo"},
},
}, },
args: args{ args: args{
target: "alpine:3.11", target: "alpine:3.11",
@@ -245,41 +159,44 @@ func TestScanner_Scan(t *testing.T) {
VulnType: []string{"os"}, VulnType: []string{"os"},
}, },
}, },
scanExpectation: scanExpectation{
Args: scanArgs{
CtxAnything: true,
Request: &scanner.ScanRequest{
Target: "alpine:3.11",
ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
BlobIds: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
Options: &scanner.ScanOptions{
VulnType: []string{"os"},
},
},
},
Returns: scanReturns{
Err: errors.New("error"),
},
},
wantErr: "failed to detect vulnerabilities via RPC", wantErr: "failed to detect vulnerabilities via RPC",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockClient := new(mockScanner) ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mockClient.ApplyScanExpectation(tt.scanExpectation) if tt.expectation == nil {
e := map[string]interface{}{
"code": "not_found",
"msg": "expectation is empty",
}
b, _ := json.Marshal(e)
w.WriteHeader(http.StatusBadGateway)
w.Write(b)
return
}
b, err := protojson.Marshal(tt.expectation)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "json marshalling error: %v", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}))
client := rpc.NewScannerJSONClient(ts.URL, ts.Client())
s := NewScanner(ScannerOption{CustomHeaders: tt.customHeaders}, WithRPCClient(client))
s := NewScanner(tt.fields.customHeaders, mockClient)
gotResults, gotOS, err := s.Scan(tt.args.target, tt.args.imageID, tt.args.layerIDs, tt.args.options) gotResults, gotOS, err := s.Scan(tt.args.target, tt.args.imageID, tt.args.layerIDs, tt.args.options)
if tt.wantErr != "" { if tt.wantErr != "" {
require.NotNil(t, err, tt.name) require.NotNil(t, err, tt.name)
require.Contains(t, err.Error(), tt.wantErr, tt.name) require.Contains(t, err.Error(), tt.wantErr, tt.name)
return return
} else {
require.NoError(t, err, tt.name)
} }
require.NoError(t, err, tt.name)
assert.Equal(t, tt.wantResults, gotResults) assert.Equal(t, tt.wantResults, gotResults)
assert.Equal(t, tt.wantOS, gotOS) assert.Equal(t, tt.wantOS, gotOS)
}) })
@@ -290,36 +207,32 @@ func TestScanner_ScanServerInsecure(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer ts.Close() defer ts.Close()
type args struct {
request *scanner.ScanRequest
insecure bool
}
tests := []struct { tests := []struct {
name string name string
args args insecure bool
wantErr string wantErr string
}{ }{
{ {
name: "happy path", name: "happy path",
args: args{ insecure: true,
request: &scanner.ScanRequest{},
insecure: true,
},
}, },
{ {
name: "sad path", name: "sad path",
args: args{ insecure: false,
request: &scanner.ScanRequest{}, wantErr: "certificate signed by unknown authority",
insecure: false,
},
wantErr: "certificate signed by unknown authority",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := rpc.NewScannerProtobufClient(ts.URL, &http.Client{
s := NewProtobufClient(RemoteURL(ts.URL), Insecure(tt.args.insecure)) Transport: &http.Transport{
_, err := s.Scan(context.Background(), tt.args.request) TLSClientConfig: &tls.Config{
InsecureSkipVerify: tt.insecure,
},
},
})
s := NewScanner(ScannerOption{Insecure: tt.insecure}, WithRPCClient(c))
_, _, err := s.Scan("dummy", "", nil, types.ScanOptions{})
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)

View File

@@ -19,6 +19,10 @@ import (
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
) )
///////////////
// Standalone
///////////////
// StandaloneSuperSet is used in the standalone mode // StandaloneSuperSet is used in the standalone mode
var StandaloneSuperSet = wire.NewSet( var StandaloneSuperSet = wire.NewSet(
local.SuperSet, local.SuperSet,
@@ -52,22 +56,33 @@ var StandaloneRepositorySet = wire.NewSet(
StandaloneSuperSet, StandaloneSuperSet,
) )
/////////////////
// Client/Server
/////////////////
// RemoteSuperSet is used in the client mode // RemoteSuperSet is used in the client mode
var RemoteSuperSet = wire.NewSet( var RemoteSuperSet = wire.NewSet(
aimage.NewArtifact, client.NewScanner,
client.SuperSet,
wire.Bind(new(Driver), new(client.Scanner)), wire.Bind(new(Driver), new(client.Scanner)),
NewScanner, NewScanner,
) )
// RemoteFilesystemSet binds filesystem dependencies for client/server mode
var RemoteFilesystemSet = wire.NewSet(
flocal.NewArtifact,
RemoteSuperSet,
)
// RemoteDockerSet binds remote docker dependencies // RemoteDockerSet binds remote docker dependencies
var RemoteDockerSet = wire.NewSet( var RemoteDockerSet = wire.NewSet(
aimage.NewArtifact,
image.NewDockerImage, image.NewDockerImage,
RemoteSuperSet, RemoteSuperSet,
) )
// RemoteArchiveSet binds remote archive dependencies // RemoteArchiveSet binds remote archive dependencies
var RemoteArchiveSet = wire.NewSet( var RemoteArchiveSet = wire.NewSet(
aimage.NewArtifact,
image.NewArchiveImage, image.NewArchiveImage,
RemoteSuperSet, RemoteSuperSet,
) )