diff --git a/docs/getting-started/cli/fs.md b/docs/getting-started/cli/fs.md index 9b53639cdcc0..a40c67dbc365 100644 --- a/docs/getting-started/cli/fs.md +++ b/docs/getting-started/cli/fs.md @@ -31,5 +31,9 @@ OPTIONS: --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] --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) ``` \ No newline at end of file diff --git a/docs/vulnerability/scanning/filesystem.md b/docs/vulnerability/scanning/filesystem.md index 60a504171213..ee63d96e4a8c 100644 --- a/docs/vulnerability/scanning/filesystem.md +++ b/docs/vulnerability/scanning/filesystem.md @@ -6,7 +6,8 @@ Scan a local project including language-specific files. $ 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. ``` @@ -53,4 +54,50 @@ It's also possible to scan a single file. ``` $ trivy fs ~/src/github.com/aquasecurity/trivy-ci-test/Pipfile.lock -``` \ No newline at end of file +``` + +## 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/ +``` + +
+Result + +``` +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 | ++---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+ +``` +
+ diff --git a/integration/client_server_test.go b/integration/client_server_test.go index 47cd35592685..4a6422759754 100644 --- a/integration/client_server_test.go +++ b/integration/client_server_test.go @@ -26,6 +26,8 @@ import ( ) type csArgs struct { + Command string + RemoteAddrOption string Format string TemplatePath string IgnoreUnfixed bool @@ -35,6 +37,7 @@ type csArgs struct { ClientToken string ClientTokenHeader string ListAllPackages bool + Target string } func TestClientServer(t *testing.T) { @@ -220,6 +223,15 @@ func TestClientServer(t *testing.T) { }, 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{}) @@ -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) { + if c.Command == "" { + c.Command = "client" + } + if c.RemoteAddrOption == "" { + c.RemoteAddrOption = "--remote" + } 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 != "" { 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) + if c.Target != "" { + osArgs = append(osArgs, c.Target) + } + return osArgs, outputFile } diff --git a/integration/testdata/almalinux-8.json.golden b/integration/testdata/almalinux-8.json.golden index ab88cfb8c53d..809ead3f6e15 100644 --- a/integration/testdata/almalinux-8.json.golden +++ b/integration/testdata/almalinux-8.json.golden @@ -119,4 +119,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/alpine-310.json.golden b/integration/testdata/alpine-310.json.golden index 76397cb6f130..ad9343ed04d6 100644 --- a/integration/testdata/alpine-310.json.golden +++ b/integration/testdata/alpine-310.json.golden @@ -308,4 +308,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/alpine-39-high-critical.json.golden b/integration/testdata/alpine-39-high-critical.json.golden index da10766c317d..4bb09f76af47 100644 --- a/integration/testdata/alpine-39-high-critical.json.golden +++ b/integration/testdata/alpine-39-high-critical.json.golden @@ -128,4 +128,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/alpine-39-ignore-cveids.json.golden b/integration/testdata/alpine-39-ignore-cveids.json.golden index bb5060ad0a87..3505bb203e6e 100644 --- a/integration/testdata/alpine-39-ignore-cveids.json.golden +++ b/integration/testdata/alpine-39-ignore-cveids.json.golden @@ -192,4 +192,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/amazon-1.json.golden b/integration/testdata/amazon-1.json.golden index 0e5c8d99097e..7ddc215155e6 100644 --- a/integration/testdata/amazon-1.json.golden +++ b/integration/testdata/amazon-1.json.golden @@ -111,4 +111,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/amazon-2.json.golden b/integration/testdata/amazon-2.json.golden index 327c3b0d14ec..d26b2cc99a7c 100644 --- a/integration/testdata/amazon-2.json.golden +++ b/integration/testdata/amazon-2.json.golden @@ -169,4 +169,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/busybox-with-lockfile.json.golden b/integration/testdata/busybox-with-lockfile.json.golden index 9b9760933ac9..c1826972765b 100644 --- a/integration/testdata/busybox-with-lockfile.json.golden +++ b/integration/testdata/busybox-with-lockfile.json.golden @@ -129,4 +129,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/centos-6.json.golden b/integration/testdata/centos-6.json.golden index 80d89ff3fd58..92ee56bb2405 100644 --- a/integration/testdata/centos-6.json.golden +++ b/integration/testdata/centos-6.json.golden @@ -196,4 +196,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/centos-7-ignore-unfixed.json.golden b/integration/testdata/centos-7-ignore-unfixed.json.golden index c8911adc745b..e22a00e4265c 100644 --- a/integration/testdata/centos-7-ignore-unfixed.json.golden +++ b/integration/testdata/centos-7-ignore-unfixed.json.golden @@ -217,4 +217,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/centos-7-medium.json.golden b/integration/testdata/centos-7-medium.json.golden index afb93838585a..dacb5e54ae97 100644 --- a/integration/testdata/centos-7-medium.json.golden +++ b/integration/testdata/centos-7-medium.json.golden @@ -146,4 +146,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/centos-7.json.golden b/integration/testdata/centos-7.json.golden index 662597b139dc..81257e619a2c 100644 --- a/integration/testdata/centos-7.json.golden +++ b/integration/testdata/centos-7.json.golden @@ -260,4 +260,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/debian-buster-ignore-unfixed.json.golden b/integration/testdata/debian-buster-ignore-unfixed.json.golden index f91b2f478fed..30edd4b493d7 100644 --- a/integration/testdata/debian-buster-ignore-unfixed.json.golden +++ b/integration/testdata/debian-buster-ignore-unfixed.json.golden @@ -110,4 +110,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/debian-buster.json.golden b/integration/testdata/debian-buster.json.golden index 745f7802e5c1..28f1e640a7cb 100644 --- a/integration/testdata/debian-buster.json.golden +++ b/integration/testdata/debian-buster.json.golden @@ -158,4 +158,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/debian-stretch.json.golden b/integration/testdata/debian-stretch.json.golden index 87111dc34c92..dfa4f20ef401 100644 --- a/integration/testdata/debian-stretch.json.golden +++ b/integration/testdata/debian-stretch.json.golden @@ -335,4 +335,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/distroless-base.json.golden b/integration/testdata/distroless-base.json.golden index bbbe79bb48b5..93c699687897 100644 --- a/integration/testdata/distroless-base.json.golden +++ b/integration/testdata/distroless-base.json.golden @@ -345,4 +345,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/distroless-python27.json.golden b/integration/testdata/distroless-python27.json.golden index 118a83df38aa..900e885d232f 100644 --- a/integration/testdata/distroless-python27.json.golden +++ b/integration/testdata/distroless-python27.json.golden @@ -362,4 +362,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/mariner-1.0.json.golden b/integration/testdata/mariner-1.0.json.golden index cfd2be34f276..e0ff4f6d660c 100644 --- a/integration/testdata/mariner-1.0.json.golden +++ b/integration/testdata/mariner-1.0.json.golden @@ -113,4 +113,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/opensuse-leap-151.json.golden b/integration/testdata/opensuse-leap-151.json.golden index 3a68cc03144c..484e2bb15ee4 100644 --- a/integration/testdata/opensuse-leap-151.json.golden +++ b/integration/testdata/opensuse-leap-151.json.golden @@ -109,4 +109,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/oraclelinux-8-slim.json.golden b/integration/testdata/oraclelinux-8-slim.json.golden index a1824b010b53..647642ec1dab 100644 --- a/integration/testdata/oraclelinux-8-slim.json.golden +++ b/integration/testdata/oraclelinux-8-slim.json.golden @@ -177,4 +177,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/photon-30.json.golden b/integration/testdata/photon-30.json.golden index 644252c44fb9..f11404f78738 100644 --- a/integration/testdata/photon-30.json.golden +++ b/integration/testdata/photon-30.json.golden @@ -226,4 +226,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/rockylinux-8.json.golden b/integration/testdata/rockylinux-8.json.golden index 4dea9e99dbfa..840b35cf8af1 100644 --- a/integration/testdata/rockylinux-8.json.golden +++ b/integration/testdata/rockylinux-8.json.golden @@ -124,4 +124,4 @@ "Type": "python-pkg" } ] -} \ No newline at end of file +} diff --git a/integration/testdata/ubi-7.json.golden b/integration/testdata/ubi-7.json.golden index 7b095c4016b0..c9a54f40bc19 100644 --- a/integration/testdata/ubi-7.json.golden +++ b/integration/testdata/ubi-7.json.golden @@ -121,4 +121,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/integration/testdata/ubuntu-1804.json.golden b/integration/testdata/ubuntu-1804.json.golden index 367f0cc9ad38..08d62b4a2597 100644 --- a/integration/testdata/ubuntu-1804.json.golden +++ b/integration/testdata/ubuntu-1804.json.golden @@ -341,4 +341,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/pkg/cache/nop.go b/pkg/cache/nop.go new file mode 100644 index 000000000000..127aac2e0904 --- /dev/null +++ b/pkg/cache/nop.go @@ -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 +} diff --git a/pkg/cache/remote.go b/pkg/cache/remote.go index 888d464bf193..7963ad355090 100644 --- a/pkg/cache/remote.go +++ b/pkg/cache/remote.go @@ -31,7 +31,6 @@ func NewRemoteCache(url string, customHeaders http.Header, insecure bool) cache. }, }, } - c := rpcCache.NewCacheProtobufClient(url, httpClient) return &RemoteCache{ctx: ctx, client: c} } diff --git a/pkg/commands/app.go b/pkg/commands/app.go index b2815f5b9186..16245f2922ab 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -13,7 +13,6 @@ import ( "github.com/aquasecurity/trivy-db/pkg/metadata" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy/pkg/commands/artifact" - "github.com/aquasecurity/trivy/pkg/commands/client" "github.com/aquasecurity/trivy/pkg/commands/plugin" "github.com/aquasecurity/trivy/pkg/commands/server" "github.com/aquasecurity/trivy/pkg/result" @@ -210,14 +209,14 @@ var ( token = cli.StringFlag{ Name: "token", - Usage: "for authentication", + Usage: "for authentication in client/server mode", EnvVars: []string{"TRIVY_TOKEN"}, } tokenHeader = cli.StringFlag{ Name: "token-header", 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"}, } @@ -313,6 +312,18 @@ var ( 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 globalFlags = []cli.Flag{ &quietFlag, @@ -473,9 +484,17 @@ func NewFilesystemCommand() *cli.Command { &offlineScan, stringSliceFlag(skipFiles), stringSliceFlag(skipDirs), + + // for misconfiguration stringSliceFlag(configPolicy), stringSliceFlag(configData), stringSliceFlag(policyNamespaces), + + // for client/server + &remoteServer, + &token, + &tokenHeader, + &customHeaders, }, } } @@ -565,7 +584,7 @@ func NewClientCommand() *cli.Command { Aliases: []string{"c"}, ArgsUsage: "image_name", Usage: "client mode", - Action: client.Run, + Action: artifact.ImageRun, Flags: []cli.Flag{ &templateFlag, &formatFlag, @@ -589,20 +608,17 @@ func NewClientCommand() *cli.Command { &offlineScan, &insecureFlag, - // original flags &token, &tokenHeader, + &customHeaders, + + // original flags &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"}, - }, }, } } diff --git a/pkg/commands/artifact/config.go b/pkg/commands/artifact/config.go index a832a9b443ed..5e13c17e701c 100644 --- a/pkg/commands/artifact/config.go +++ b/pkg/commands/artifact/config.go @@ -26,5 +26,5 @@ func ConfigRun(ctx *cli.Context) error { opt.SkipDBUpdate = true // Run filesystem command internally - return Run(ctx.Context, opt, filesystemScanner, initFSCache) + return Run(ctx.Context, opt, filesystemStandaloneScanner, initCache) } diff --git a/pkg/commands/artifact/fs.go b/pkg/commands/artifact/fs.go index a0a48c6127b5..a34cce3270f5 100644 --- a/pkg/commands/artifact/fs.go +++ b/pkg/commands/artifact/fs.go @@ -7,15 +7,23 @@ import ( "golang.org/x/xerrors" "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" ) -func filesystemScanner(ctx context.Context, path string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, - _ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeFilesystemScanner(ctx, path, ac, lac, artifactOpt, scannerOpt) +// filesystemStandaloneScanner initializes a filesystem scanner in standalone mode +func filesystemStandaloneScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) { + 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 { 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 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. @@ -45,5 +59,11 @@ func RootfsRun(ctx *cli.Context) error { // Disable the lock file scanning 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) } diff --git a/pkg/commands/artifact/image.go b/pkg/commands/artifact/image.go index 5775bdd97612..ea7b62c4fbf6 100644 --- a/pkg/commands/artifact/image.go +++ b/pkg/commands/artifact/image.go @@ -7,36 +7,67 @@ import ( "golang.org/x/xerrors" "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/types" ) -func archiveScanner(ctx context.Context, input string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, - _ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { - s, err := initializeArchiveScanner(ctx, input, ac, lac, artifactOpt, scannerOpt) +// imageScanner initializes a container image scanner in standalone mode +// $ trivy image alpine:3.15 +func imageScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) { + dockerOpt, err := types.GetDockerOption(conf.ArtifactOption.InsecureSkipTLS) + if err != nil { + return scanner.Scanner{}, nil, err + } + s, cleanup, err := initializeDockerScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, + dockerOpt, conf.ArtifactOption, conf.MisconfOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err) + } + return s, cleanup, nil +} + +// 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 } -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) +// 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 := initializeDockerScanner(ctx, imageName, ac, lac, dockerOpt, artifactOpt, scannerOpt) + + s, cleanup, err := initializeRemoteDockerScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, + dockerOpt, conf.ArtifactOption, conf.MisconfOption) if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err) + return scanner.Scanner{}, nil, xerrors.Errorf("unable to initialize the docker scanner: %w", err) } return s, cleanup, nil } -// ImageRun runs scan on docker image +// 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 { opt, err := initOption(ctx) if err != nil { @@ -47,9 +78,34 @@ func ImageRun(ctx *cli.Context) error { opt.DisabledAnalyzers = analyzer.TypeLockfiles if opt.Input != "" { - // scan tar file - return Run(ctx.Context, opt, archiveScanner, initFSCache) + return archiveImageRun(ctx.Context, opt) + } + + 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 } - return Run(ctx.Context, opt, dockerScanner, initFSCache) + // scan container image + return Run(ctx, opt, scanner, initCache) } diff --git a/pkg/commands/artifact/inject.go b/pkg/commands/artifact/inject.go index 629b24b2d07d..b528205dfb45 100644 --- a/pkg/commands/artifact/inject.go +++ b/pkg/commands/artifact/inject.go @@ -13,9 +13,16 @@ import ( "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" ) +////////////// +// 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, localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, func(), error) { @@ -23,6 +30,8 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach 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, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option, configScannerOption config.ScannerOption) (scanner.Scanner, error) { @@ -30,6 +39,7 @@ func initializeArchiveScanner(ctx context.Context, filePath string, artifactCach return scanner.Scanner{}, 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) { @@ -48,3 +58,39 @@ func initializeResultClient() result.Client { wire.Build(result.SuperSet) 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{} +} diff --git a/pkg/commands/artifact/option.go b/pkg/commands/artifact/option.go index 95c817c47833..1671dac598b0 100644 --- a/pkg/commands/artifact/option.go +++ b/pkg/commands/artifact/option.go @@ -17,6 +17,7 @@ type Option struct { option.ReportOption option.CacheOption option.ConfigOption + option.RemoteOption // We don't want to allow disabled analyzers to be passed by users, // but it differs depending on scanning modes. @@ -38,6 +39,7 @@ func NewOption(c *cli.Context) (Option, error) { ReportOption: option.NewReportOption(c), CacheOption: option.NewCacheOption(c), ConfigOption: option.NewConfigOption(c), + RemoteOption: option.NewRemoteOption(c), }, nil } @@ -55,7 +57,6 @@ func (c *Option) Init() error { if err := c.ArtifactOption.Init(c.Context, c.Logger); err != nil { return err } - return nil } @@ -69,6 +70,7 @@ func (c *Option) initPreScanOptions() error { if err := c.CacheOption.Init(); err != nil { return err } + c.RemoteOption.Init(c.Logger) return nil } diff --git a/pkg/commands/artifact/option_test.go b/pkg/commands/artifact/option_test.go index 4ed00fa290ac..d9caf6084101 100644 --- a/pkg/commands/artifact/option_test.go +++ b/pkg/commands/artifact/option_test.go @@ -2,6 +2,7 @@ package artifact import ( "flag" + "net/http" "os" "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", args: []string{"--reset"}, @@ -182,6 +258,10 @@ func TestOption_Init(t *testing.T) { set.String("security-checks", "vuln", "") set.String("template", "", "") 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) _ = set.Parse(tt.args) diff --git a/pkg/commands/artifact/repository.go b/pkg/commands/artifact/repository.go index 9ed83cf93129..f7df2e822a22 100644 --- a/pkg/commands/artifact/repository.go +++ b/pkg/commands/artifact/repository.go @@ -7,16 +7,14 @@ import ( "golang.org/x/xerrors" "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/types" ) -func repositoryScanner(ctx context.Context, dir string, ac cache.ArtifactCache, lac cache.LocalArtifactCache, - _ bool, artifactOpt artifact.Option, scannerOpt config.ScannerOption) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeRepositoryScanner(ctx, dir, ac, lac, artifactOpt, scannerOpt) +// filesystemStandaloneScanner initializes a repository scanner in standalone mode +func repositoryScanner(ctx context.Context, conf scannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeRepositoryScanner(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) } @@ -36,5 +34,5 @@ func RepositoryRun(ctx *cli.Context) error { // Disable the OS analyzers and individual package analyzers opt.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...) - return Run(ctx.Context, opt, repositoryScanner, initFSCache) + return Run(ctx.Context, opt, repositoryScanner, initCache) } diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 1b5e1e3d146e..79d0470f8931 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -13,9 +13,11 @@ import ( "github.com/aquasecurity/fanal/artifact" "github.com/aquasecurity/fanal/cache" "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/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" @@ -25,9 +27,26 @@ const defaultPolicyNamespace = "appshield" 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 -type InitializeScanner func(context.Context, string, cache.ArtifactCache, cache.LocalArtifactCache, bool, - artifact.Option, config.ScannerOption) (scanner.Scanner, func(), error) +type InitializeScanner func(context.Context, scannerConfig) (scanner.Scanner, func(), error) // InitCache defines cache initializer 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) 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 { @@ -54,8 +77,8 @@ func runWithTimeout(ctx context.Context, opt Option, initializeScanner Initializ } defer cacheClient.Close() - // When scanning config files, it doesn't need to download the vulnerability database. - if utils.StringInSlice(types.SecurityCheckVulnerability, opt.SecurityChecks) { + // When scanning config files or running as client mode, it doesn't need to download the vulnerability database. + if opt.RemoteAddr == "" && utils.StringInSlice(types.SecurityCheckVulnerability, opt.SecurityChecks) { if err = initDB(opt); err != nil { if errors.Is(err, errSkipScan) { return nil @@ -92,7 +115,14 @@ func runWithTimeout(ctx context.Context, opt Option, initializeScanner Initializ 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) cache, err := operation.NewCache(c.CacheOption) if err != nil { @@ -181,7 +211,7 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, } 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 if utils.StringInSlice(types.SecurityCheckConfig, opt.SecurityChecks) { noProgress := opt.Quiet || opt.NoProgress @@ -199,16 +229,25 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, } } - artifactOpt := artifact.Option{ - DisabledAnalyzers: disabledAnalyzers(opt), - SkipFiles: opt.SkipFiles, - SkipDirs: opt.SkipDirs, - InsecureSkipTLS: opt.Insecure, - Offline: opt.OfflineScan, - NoProgress: opt.NoProgress || opt.Quiet, - } - - s, cleanup, err := initializeScanner(ctx, target, cacheClient, cacheClient, opt.Insecure, artifactOpt, configScannerOptions) + s, cleanup, err := initializeScanner(ctx, scannerConfig{ + Target: target, + ArtifactCache: cacheClient, + LocalArtifactCache: cacheClient, + RemoteOption: client.ScannerOption{ + RemoteURL: opt.RemoteAddr, + CustomHeaders: opt.CustomHeaders, + Insecure: opt.Insecure, + }, + 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 { 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() results := report.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, opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy) if err != nil { diff --git a/pkg/commands/artifact/wire_gen.go b/pkg/commands/artifact/wire_gen.go index 62091f704628..a703825de4af 100644 --- a/pkg/commands/artifact/wire_gen.go +++ b/pkg/commands/artifact/wire_gen.go @@ -20,12 +20,15 @@ import ( "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy/pkg/detector/ospkg" "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/local" ) // 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) { applierApplier := applier.NewApplier(localArtifactCache) detector := ospkg.Detector{} @@ -45,6 +48,8 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach }, 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) { applierApplier := applier.NewApplier(localArtifactCache) detector := ospkg.Detector{} @@ -61,6 +66,7 @@ func initializeArchiveScanner(ctx context.Context, filePath string, artifactCach 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) { applierApplier := applier.NewApplier(localArtifactCache) detector := ospkg.Detector{} @@ -93,3 +99,56 @@ func initializeResultClient() result.Client { client := result.NewClient(dbConfig) 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 +} diff --git a/pkg/commands/client/inject.go b/pkg/commands/client/inject.go deleted file mode 100644 index 697bef93cc32..000000000000 --- a/pkg/commands/client/inject.go +++ /dev/null @@ -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{} -} diff --git a/pkg/commands/client/option.go b/pkg/commands/client/option.go deleted file mode 100644 index 150204ff2a5d..000000000000 --- a/pkg/commands/client/option.go +++ /dev/null @@ -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 -} diff --git a/pkg/commands/client/option_test.go b/pkg/commands/client/option_test.go deleted file mode 100644 index 491ebe07cac8..000000000000 --- a/pkg/commands/client/option_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/pkg/commands/client/run.go b/pkg/commands/client/run.go deleted file mode 100644 index b4a41acb1977..000000000000 --- a/pkg/commands/client/run.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/pkg/commands/client/wire_gen.go b/pkg/commands/client/wire_gen.go deleted file mode 100644 index 4e1a77cefc77..000000000000 --- a/pkg/commands/client/wire_gen.go +++ /dev/null @@ -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 -} diff --git a/pkg/commands/operation/operation.go b/pkg/commands/operation/operation.go index 87a1afa68566..06ac1d8a5e64 100644 --- a/pkg/commands/operation/operation.go +++ b/pkg/commands/operation/operation.go @@ -56,6 +56,8 @@ func NewCache(c option.CacheOption) (Cache, error) { redisCache := cache.NewRedisCache(options) return Cache{Cache: redisCache}, nil } + + // standalone mode fsCache, err := cache.NewFSCache(utils.CacheDir()) if err != nil { return Cache{}, xerrors.Errorf("unable to initialize fs cache: %w", err) diff --git a/pkg/commands/option/remote.go b/pkg/commands/option/remote.go new file mode 100644 index 000000000000..8cf79555822d --- /dev/null +++ b/pkg/commands/option/remote.go @@ -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 +} diff --git a/pkg/commands/option/remote_test.go b/pkg/commands/option/remote_test.go new file mode 100644 index 000000000000..2fa5c0cfa74b --- /dev/null +++ b/pkg/commands/option/remote_test.go @@ -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) + }) + } +} diff --git a/pkg/rpc/client/client.go b/pkg/rpc/client/client.go index 985a0511b89b..77189052604f 100644 --- a/pkg/rpc/client/client.go +++ b/pkg/rpc/client/client.go @@ -5,58 +5,63 @@ import ( "crypto/tls" "net/http" - "github.com/aquasecurity/trivy/pkg/types" - - "github.com/google/wire" "golang.org/x/xerrors" ftypes "github.com/aquasecurity/fanal/types" r "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/types" rpc "github.com/aquasecurity/trivy/rpc/scanner" ) -// SuperSet binds the dependencies for RPC client -var SuperSet = wire.NewSet( - NewProtobufClient, - NewScanner, -) - -// RemoteURL for RPC remote host -type RemoteURL string +type options struct { + rpcClient rpc.Scanner +} -// Insecure for RPC remote host -type Insecure bool +type option func(*options) -// 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), - }, - }, +// WithRPCClient takes rpc client for testability +func WithRPCClient(c rpc.Scanner) option { + return func(opts *options) { + opts.rpcClient = c } - - return rpc.NewScannerProtobufClient(string(remoteURL), httpClient) } -// CustomHeaders for holding HTTP headers -type CustomHeaders http.Header +// ScannerOption holds options for RPC client +type ScannerOption struct { + RemoteURL string + Insecure bool + CustomHeaders http.Header +} // Scanner implements the RPC scanner type Scanner struct { - customHeaders CustomHeaders + customHeaders http.Header client rpc.Scanner } // NewScanner is the factory method to return RPC Scanner -func NewScanner(customHeaders CustomHeaders, s rpc.Scanner) Scanner { - return Scanner{customHeaders: customHeaders, client: s} +func NewScanner(scannerOptions ScannerOption, opts ...option) Scanner { + 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 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 err := r.Retry(func() error { diff --git a/pkg/rpc/client/client_test.go b/pkg/rpc/client/client_test.go index ebcef0e2e83e..6219403d4b64 100644 --- a/pkg/rpc/client/client_test.go +++ b/pkg/rpc/client/client_test.go @@ -1,94 +1,27 @@ package client import ( - "context" - "errors" + "crypto/tls" + "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" "github.com/golang/protobuf/ptypes/timestamp" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" ftypes "github.com/aquasecurity/fanal/types" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/utils" "github.com/aquasecurity/trivy/pkg/types" "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) { - type fields struct { - customHeaders CustomHeaders - } type args struct { target string imageID string @@ -96,21 +29,19 @@ func TestScanner_Scan(t *testing.T) { options types.ScanOptions } tests := []struct { - name string - fields fields - args args - scanExpectation scanExpectation - wantResults types.Results - wantOS *ftypes.OS - wantEosl bool - wantErr string + name string + customHeaders http.Header + args args + expectation *rpc.ScanResponse + wantResults types.Results + wantOS *ftypes.OS + wantEosl bool + wantErr string }{ { name: "happy path", - fields: fields{ - customHeaders: CustomHeaders{ - "Trivy-Token": []string{"foo"}, - }, + customHeaders: http.Header{ + "Trivy-Token": []string{"foo"}, }, args: args{ target: "alpine:3.11", @@ -120,65 +51,50 @@ func TestScanner_Scan(t *testing.T) { 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"}, - }, - }, + expectation: &rpc.ScanResponse{ + Os: &common.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, }, - Returns: scanReturns{ - Res: &scanner.ScanResponse{ - Os: &common.OS{ - Family: "alpine", - Name: "3.11", - Eosl: true, - }, - Results: []*scanner.Result{ + Results: []*rpc.Result{ + { + Target: "alpine:3.11", + Vulnerabilities: []*common.Vulnerability{ { - Target: "alpine:3.11", - Vulnerabilities: []*common.Vulnerability{ - { - VulnerabilityId: "CVE-2020-0001", - PkgName: "musl", - InstalledVersion: "1.2.3", - FixedVersion: "1.2.4", - Title: "DoS", - Description: "Denial os Service", - Severity: common.Severity_CRITICAL, - References: []string{"http://exammple.com"}, - SeveritySource: "nvd", - Cvss: map[string]*common.CVSS{ - "nvd": { - V2Vector: "AV:L/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: 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: ×tamp.Timestamp{ - Seconds: 1577840460, - }, - PublishedDate: ×tamp.Timestamp{ - Seconds: 978310860, - }, + VulnerabilityId: "CVE-2020-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial os Service", + Severity: common.Severity_CRITICAL, + References: []string{"http://exammple.com"}, + SeveritySource: "nvd", + Cvss: map[string]*common.CVSS{ + "nvd": { + V2Vector: "AV:L/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: 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: ×tamp.Timestamp{ + Seconds: 1577840460, + }, + PublishedDate: ×tamp.Timestamp{ + Seconds: 978310860, + }, }, }, }, @@ -232,10 +148,8 @@ func TestScanner_Scan(t *testing.T) { }, { name: "sad path: Scan returns an error", - fields: fields{ - customHeaders: CustomHeaders{ - "Trivy-Token": []string{"foo"}, - }, + customHeaders: http.Header{ + "Trivy-Token": []string{"foo"}, }, args: args{ target: "alpine:3.11", @@ -245,41 +159,44 @@ func TestScanner_Scan(t *testing.T) { 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", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockClient := new(mockScanner) - mockClient.ApplyScanExpectation(tt.scanExpectation) + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + 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) if tt.wantErr != "" { require.NotNil(t, err, tt.name) require.Contains(t, err.Error(), tt.wantErr, tt.name) return - } else { - require.NoError(t, err, tt.name) } + require.NoError(t, err, tt.name) assert.Equal(t, tt.wantResults, gotResults) 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) {})) defer ts.Close() - type args struct { - request *scanner.ScanRequest - insecure bool - } tests := []struct { - name string - args args - wantErr string + name string + insecure bool + wantErr string }{ { - name: "happy path", - args: args{ - request: &scanner.ScanRequest{}, - insecure: true, - }, + name: "happy path", + insecure: true, }, { - name: "sad path", - args: args{ - request: &scanner.ScanRequest{}, - insecure: false, - }, - wantErr: "certificate signed by unknown authority", + name: "sad path", + insecure: false, + wantErr: "certificate signed by unknown authority", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - s := NewProtobufClient(RemoteURL(ts.URL), Insecure(tt.args.insecure)) - _, err := s.Scan(context.Background(), tt.args.request) + c := rpc.NewScannerProtobufClient(ts.URL, &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: tt.insecure, + }, + }, + }) + s := NewScanner(ScannerOption{Insecure: tt.insecure}, WithRPCClient(c)) + _, _, err := s.Scan("dummy", "", nil, types.ScanOptions{}) if tt.wantErr != "" { require.Error(t, err) diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index 0b89e4f9ffa1..07b595df927e 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -19,6 +19,10 @@ import ( "github.com/aquasecurity/trivy/pkg/types" ) +/////////////// +// Standalone +/////////////// + // StandaloneSuperSet is used in the standalone mode var StandaloneSuperSet = wire.NewSet( local.SuperSet, @@ -52,22 +56,33 @@ var StandaloneRepositorySet = wire.NewSet( StandaloneSuperSet, ) +///////////////// +// Client/Server +///////////////// + // RemoteSuperSet is used in the client mode var RemoteSuperSet = wire.NewSet( - aimage.NewArtifact, - client.SuperSet, + client.NewScanner, wire.Bind(new(Driver), new(client.Scanner)), NewScanner, ) +// RemoteFilesystemSet binds filesystem dependencies for client/server mode +var RemoteFilesystemSet = wire.NewSet( + flocal.NewArtifact, + RemoteSuperSet, +) + // RemoteDockerSet binds remote docker dependencies var RemoteDockerSet = wire.NewSet( + aimage.NewArtifact, image.NewDockerImage, RemoteSuperSet, ) // RemoteArchiveSet binds remote archive dependencies var RemoteArchiveSet = wire.NewSet( + aimage.NewArtifact, image.NewArchiveImage, RemoteSuperSet, )