diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000000..9f28501b3999 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +Language: Proto +BasedOnStyle: Google +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true diff --git a/Makefile b/Makefile index 2decf9c4b776..4f83f36890c9 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,17 @@ LDFLAGS=-ldflags "-s -w -X=main.version=$(VERSION)" GOPATH=$(shell go env GOPATH) GOBIN=$(GOPATH)/bin +GOSRC=$(GOPATH)/src u := $(if $(update),-u) +$(GOBIN)/wire: + GO111MODULE=off go get github.com/google/wire/cmd/wire + +.PHONY: wire +wire: $(GOBIN)/wire + wire gen ./... + .PHONY: deps deps: go get ${u} -d @@ -29,10 +37,18 @@ test-integration: integration/testdata/fixtures/*.tar.gz lint: $(GOBIN)/golangci-lint $(GOBIN)/golangci-lint run +.PHONY: fmt +fmt: + find ./ -name "*.proto" | xargs clang-format -i + .PHONY: build build: go build $(LDFLAGS) ./cmd/trivy +.PHONY: protoc +protoc: + protoc --proto_path=$(GOSRC):. --twirp_out=. --go_out=. ./rpc/detector/service.proto + .PHONY: install install: go install $(LDFLAGS) ./cmd/trivy diff --git a/README.md b/README.md index 203e6f1f60a8..218e0b0615f1 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,22 @@ A Simple and Comprehensive Vulnerability Scanner for Containers, Suitable for CI - [Basic](#basic) - [Docker](#docker) - [Examples](#examples) - - [Scan an image](#scan-an-image) - - [Scan an image file](#scan-an-image-file) - - [Save the results as JSON](#save-the-results-as-json) - - [Filter the vulnerabilities by severities](#filter-the-vulnerabilities-by-severities) - - [Filter the vulnerabilities by type](#filter-the-vulnerabilities-by-type) - - [Skip an update of vulnerability DB](#skip-update-of-vulnerability-db) - - [Ignore unfixed vulnerabilities](#ignore-unfixed-vulnerabilities) - - [Specify exit code](#specify-exit-code) - - [Ignore the specified vulnerabilities](#ignore-the-specified-vulnerabilities) - - [Clear image caches](#clear-image-caches) - - [Reset](#reset) + - [Standalone](#standalone) + - [Scan an image](#scan-an-image) + - [Scan an image file](#scan-an-image-file) + - [Save the results as JSON](#save-the-results-as-json) + - [Filter the vulnerabilities by severities](#filter-the-vulnerabilities-by-severities) + - [Filter the vulnerabilities by type](#filter-the-vulnerabilities-by-type) + - [Skip an update of vulnerability DB](#skip-update-of-vulnerability-db) + - [Ignore unfixed vulnerabilities](#ignore-unfixed-vulnerabilities) + - [Specify exit code](#specify-exit-code) + - [Ignore the specified vulnerabilities](#ignore-the-specified-vulnerabilities) + - [Clear image caches](#clear-image-caches) + - [Reset](#reset) + - [Lightweight DB](#use-lightweight-db) + - [Client/Server](#client--server) + - [Server](#server) + - [Client](#client) - [Continuous Integration (CI)](#continuous-integration-ci) - [Travis CI](#travis-ci) - [CircleCI](#circleci) @@ -254,6 +259,8 @@ Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) # Examples +## Standalone + ### Scan an image Simply specify an image name (and a tag). @@ -1078,6 +1085,46 @@ Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 0) ``` + +## Client / Server +Trivy has client/server mode. Trivy server has vulnerability database and Trivy client doesn't have to download vulnerability database. It is useful if you want to scan images at multiple locations and do not want to download the database at every location. + +### Server +At first, you need to launch Trivy server. It downloads vulnerability database automatically and continue to fetch the latest DB in the background. +``` +$ trivy server --listen localhost:8080 +2019-12-12T15:17:06.551+0200 INFO Need to update DB +2019-12-12T15:17:56.706+0200 INFO Reopening DB... +2019-12-12T15:17:56.707+0200 INFO Listening localhost:8080... +``` + +### Client +Then, specify the remote address. +``` +$ trivy client --remote http://localhost:8080 alpine:3.10 +``` + +
+Result + +``` +alpine:3.10 (alpine 3.10.2) +=========================== +Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 0) + ++---------+------------------+----------+-------------------+---------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | ++---------+------------------+----------+-------------------+---------------+ +| openssl | CVE-2019-1549 | MEDIUM | 1.1.1c-r0 | 1.1.1d-r0 | ++ +------------------+ + + + +| | CVE-2019-1563 | | | | ++ +------------------+----------+ + + +| | CVE-2019-1547 | LOW | | | ++---------+------------------+----------+-------------------+---------------+ +``` +
+ + ### Deprecated options `--only-update`, `--refresh` and `--auto-refresh` are deprecated since they are unnecessary now. These options will be removed at the next version @@ -1297,6 +1344,7 @@ Trivy scans a tar image with the following format. - https://github.com/RustSec/advisory-db # Usage +## Standalone ``` NAME: @@ -1333,6 +1381,53 @@ OPTIONS: ``` +## Sub commands +Trivy has two sub commands, client and server. + +``` +NAME: + trivy client - client mode + +USAGE: + trivy client [command options] [arguments...] + +OPTIONS: + --template value, -t value output template [$TRIVY_TEMPLATE] + --format value, -f value format (table, json, template) (default: "table") [$TRIVY_FORMAT] + --input value, -i value input file path instead of image name [$TRIVY_INPUT] + --severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL") [$TRIVY_SEVERITY] + --output value, -o value output file name [$TRIVY_OUTPUT] + --exit-code value Exit code when vulnerabilities were found (default: 0) [$TRIVY_EXIT_CODE] + --clear-cache, -c clear image caches without scanning [$TRIVY_CLEAR_CACHE] + --quiet, -q suppress progress bar and log output [$TRIVY_QUIET] + --ignore-unfixed display only fixed vulnerabilities [$TRIVY_IGNORE_UNFIXED] + --debug, -d debug mode [$TRIVY_DEBUG] + --vuln-type value comma-separated list of vulnerability types (os,library) (default: "os,library") [$TRIVY_VULN_TYPE] + --ignorefile value specify .trivyignore file (default: ".trivyignore") [$TRIVY_IGNOREFILE] + --cache-dir value use as cache directory, but image cache is stored in /path/to/cache/fanal (default: "/Users/teppei/Library/Caches/trivy") [$TRIVY_CACHE_DIR] + --timeout value docker timeout (default: 1m0s) [$TRIVY_TIMEOUT] + --token value for authentication [$TRIVY_TOKEN] + --remote value server address (default: "http://localhost:4954") [$TRIVY_REMOTE] +``` + +``` +NAME: + trivy server - server mode + +USAGE: + trivy server [command options] [arguments...] + +OPTIONS: + --skip-update skip db update [$TRIVY_SKIP_UPDATE] + --download-db-only download/update vulnerability database but don't run a scan [$TRIVY_DOWNLOAD_DB_ONLY] + --reset remove all caches and database [$TRIVY_RESET] + --quiet, -q suppress progress bar and log output [$TRIVY_QUIET] + --debug, -d debug mode [$TRIVY_DEBUG] + --cache-dir value use as cache directory, but image cache is stored in /path/to/cache/fanal (default: "/Users/teppei/Library/Caches/trivy") [$TRIVY_CACHE_DIR] + --token value for authentication [$TRIVY_TOKEN] + --listen value listen address (default: "localhost:4954") [$TRIVY_LISTEN] +``` + # Comparison with other scanners ## Overview diff --git a/cmd/trivy/main.go b/cmd/trivy/main.go index 9deae3b87d9b..87f9f5dd5dc5 100644 --- a/cmd/trivy/main.go +++ b/cmd/trivy/main.go @@ -4,7 +4,7 @@ import ( l "log" "os" - "github.com/aquasecurity/trivy/internal/standalone" + "github.com/aquasecurity/trivy/internal" "github.com/aquasecurity/trivy/pkg/log" ) @@ -14,7 +14,7 @@ var ( ) func main() { - app := standalone.NewApp(version) + app := internal.NewApp(version) err := app.Run(os.Args) if err != nil { if log.Logger != nil { diff --git a/go.mod b/go.mod index cfb5888736a4..f7160421b66f 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,16 @@ require ( github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91 github.com/caarlos0/env/v6 v6.0.0 github.com/genuinetools/reg v0.16.1 + github.com/golang/protobuf v1.3.1 github.com/google/go-github/v28 v28.1.1 + github.com/google/wire v0.3.0 github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 github.com/knqyf263/go-version v1.1.1 github.com/kylelemons/godebug v1.1.0 github.com/olekukonko/tablewriter v0.0.2-0.20190607075207-195002e6e56a github.com/stretchr/testify v1.4.0 + github.com/twitchtv/twirp v5.9.0+incompatible github.com/urfave/cli v1.20.0 go.etcd.io/bbolt v1.3.3 // indirect go.uber.org/atomic v1.5.1 // indirect diff --git a/go.sum b/go.sum index 0a38d421bd83..4447b190005f 100644 --- a/go.sum +++ b/go.sum @@ -19,7 +19,9 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -127,6 +129,8 @@ github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= @@ -140,6 +144,9 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60= +github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -192,8 +199,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-jsonpointer v0.0.0-20180225143300-37667080efed h1:fCWISZq4YN4ulCJx7x0KB15rqxLEe3mtNJL8cSOGKZU= github.com/mattn/go-jsonpointer v0.0.0-20180225143300-37667080efed/go.mod h1:SDJ4hurDYyQ9/7nc+eCYtXqdufgK4Cq9TJlwPklqEYA= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= @@ -280,6 +291,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/twitchtv/twirp v5.9.0+incompatible h1:KBCo4NYCpE9alO1HAEcgninDnw/0AhPT1rZnHkkSqi8= +github.com/twitchtv/twirp v5.9.0+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= @@ -379,6 +392,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/integration/client_server_test.go b/integration/client_server_test.go new file mode 100644 index 000000000000..59bb9710beb2 --- /dev/null +++ b/integration/client_server_test.go @@ -0,0 +1,381 @@ +// +build integration + +package integration + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/aquasecurity/trivy/internal" + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestClientServer(t *testing.T) { + type args struct { + Version string + IgnoreUnfixed bool + Severity []string + IgnoreIDs []string + Input string + ClientToken string + ServerToken string + } + cases := []struct { + name string + testArgs args + golden string + wantErr string + }{ + { + name: "alpine 3.10 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/alpine-310.tar.gz", + }, + golden: "testdata/alpine-310.json.golden", + }, + { + name: "alpine 3.10 integration with token", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/alpine-310.tar.gz", + ClientToken: "token", + ServerToken: "token", + }, + golden: "testdata/alpine-310.json.golden", + }, + { + name: "alpine 3.10 integration with --ignore-unfixed option", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Input: "testdata/fixtures/alpine-310.tar.gz", + }, + golden: "testdata/alpine-310-ignore-unfixed.json.golden", + }, + { + name: "alpine 3.10 integration with medium and high severity", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Severity: []string{"MEDIUM", "HIGH"}, + Input: "testdata/fixtures/alpine-310.tar.gz", + }, + golden: "testdata/alpine-310-medium-high.json.golden", + }, + { + name: "alpine 3.10 integration with .trivyignore", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: false, + IgnoreIDs: []string{"CVE-2019-1549", "CVE-2019-1563"}, + Input: "testdata/fixtures/alpine-310.tar.gz", + }, + golden: "testdata/alpine-310-ignore-cveids.json.golden", + }, + { + name: "alpine 3.9 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/alpine-39.tar.gz", + }, + golden: "testdata/alpine-39.json.golden", + }, + { + name: "debian buster integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/debian-buster.tar.gz", + }, + golden: "testdata/debian-buster.json.golden", + }, + { + name: "debian buster integration with --ignore-unfixed option", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Input: "testdata/fixtures/debian-buster.tar.gz", + }, + golden: "testdata/debian-buster-ignore-unfixed.json.golden", + }, + { + name: "debian stretch integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/debian-stretch.tar.gz", + }, + golden: "testdata/debian-stretch.json.golden", + }, + { + name: "ubuntu 18.04 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/ubuntu-1804.tar.gz", + }, + golden: "testdata/ubuntu-1804.json.golden", + }, + { + name: "ubuntu 18.04 integration with --ignore-unfixed option", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Input: "testdata/fixtures/ubuntu-1804.tar.gz", + }, + golden: "testdata/ubuntu-1804-ignore-unfixed.json.golden", + }, + { + name: "ubuntu 16.04 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/ubuntu-1604.tar.gz", + }, + golden: "testdata/ubuntu-1604.json.golden", + }, + { + name: "centos 7 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/centos-7.tar.gz", + }, + golden: "testdata/centos-7.json.golden", + }, + { + name: "centos 7 integration with --ignore-unfixed option", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Input: "testdata/fixtures/centos-7.tar.gz", + }, + golden: "testdata/centos-7-ignore-unfixed.json.golden", + }, + { + name: "centos 7 integration with critical severity", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Severity: []string{"CRITICAL"}, + Input: "testdata/fixtures/centos-7.tar.gz", + }, + golden: "testdata/centos-7-critical.json.golden", + }, + { + name: "centos 7 integration with low and high severity", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Severity: []string{"LOW", "HIGH"}, + Input: "testdata/fixtures/centos-7.tar.gz", + }, + golden: "testdata/centos-7-low-high.json.golden", + }, + { + name: "centos 6 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/centos-6.tar.gz", + }, + golden: "testdata/centos-6.json.golden", + }, + { + name: "ubi 7 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/ubi-7.tar.gz", + }, + golden: "testdata/ubi-7.json.golden", + }, + { + name: "distroless base integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/distroless-base.tar.gz", + }, + golden: "testdata/distroless-base.json.golden", + }, + { + name: "distroless base integration with --ignore-unfixed option", + testArgs: args{ + Version: "dev", + IgnoreUnfixed: true, + Input: "testdata/fixtures/distroless-base.tar.gz", + }, + golden: "testdata/distroless-base-ignore-unfixed.json.golden", + }, + { + name: "distroless python27 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/distroless-python27.tar.gz", + }, + golden: "testdata/distroless-python27.json.golden", + }, + { + name: "amazon 1 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/amazon-1.tar.gz", + }, + golden: "testdata/amazon-1.json.golden", + }, + { + name: "amazon 2 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/amazon-2.tar.gz", + }, + golden: "testdata/amazon-2.json.golden", + }, + { + name: "oracle 6 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/oraclelinux-6-slim.tar.gz", + }, + golden: "testdata/oraclelinux-6-slim.json.golden", + }, + { + name: "oracle 7 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/oraclelinux-7-slim.tar.gz", + }, + golden: "testdata/oraclelinux-7-slim.json.golden", + }, + { + name: "oracle 8 integration", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/oraclelinux-8-slim.tar.gz", + }, + golden: "testdata/oraclelinux-8-slim.json.golden", + }, + { + name: "invalid token", + testArgs: args{ + Version: "dev", + Input: "testdata/fixtures/distroless-base.tar.gz", + ClientToken: "invalidtoken", + ServerToken: "token", + }, + wantErr: "twirp error unauthenticated: invalid token", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // Copy DB file + cacheDir := gunzipDB() + defer os.RemoveAll(cacheDir) + + port, err := getFreePort() + require.NoError(t, err, c.name) + addr := fmt.Sprintf("localhost:%d", port) + + go func() { + // Setup CLI App + app := internal.NewApp(c.testArgs.Version) + app.Writer = ioutil.Discard + osArgs := setupServer(addr, c.testArgs.ServerToken, cacheDir) + + // Run Trivy server + require.NoError(t, app.Run(osArgs), c.name) + }() + + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + require.NoError(t, waitPort(ctx, addr), c.name) + + // Setup CLI App + app := internal.NewApp(c.testArgs.Version) + app.Writer = ioutil.Discard + + osArgs, outputFile, cleanup := setupClient(t, c.testArgs.IgnoreUnfixed, c.testArgs.Severity, + c.testArgs.IgnoreIDs, addr, c.testArgs.ClientToken, c.testArgs.Input, cacheDir, c.golden) + defer cleanup() + + // Run Trivy client + err = app.Run(osArgs) + + if c.wantErr != "" { + require.NotNil(t, err, c.name) + assert.Contains(t, err.Error(), c.wantErr, c.name) + return + } else { + assert.NoError(t, err, c.name) + } + + // Compare want and got + want, err := ioutil.ReadFile(c.golden) + assert.NoError(t, err) + got, err := ioutil.ReadFile(outputFile) + assert.NoError(t, err) + + assert.JSONEq(t, string(want), string(got)) + }) + } +} + +func setupServer(addr, token, cacheDir string) []string { + osArgs := []string{"trivy", "server", "--skip-update", "--cache-dir", cacheDir, "--listen", addr} + if token != "" { + osArgs = append(osArgs, []string{"--token", token}...) + } + return osArgs +} + +func setupClient(t *testing.T, ignoreUnfixed bool, severity, ignoreIDs []string, + addr, token, input, cacheDir, golden string) ([]string, string, func()) { + t.Helper() + osArgs := []string{"trivy", "client", "--cache-dir", cacheDir, + "--format", "json", "--remote", "http://" + addr} + if ignoreUnfixed { + osArgs = append(osArgs, "--ignore-unfixed") + } + if len(severity) != 0 { + osArgs = append(osArgs, + []string{"--severity", strings.Join(severity, ",")}..., + ) + } + + var err error + var ignoreTmpDir string + if len(ignoreIDs) != 0 { + ignoreTmpDir, err = ioutil.TempDir("", "ignore") + require.NoError(t, err, "failed to create a temp dir") + trivyIgnore := filepath.Join(ignoreTmpDir, ".trivyignore") + err = ioutil.WriteFile(trivyIgnore, []byte(strings.Join(ignoreIDs, "\n")), 0444) + require.NoError(t, err, "failed to write .trivyignore") + osArgs = append(osArgs, []string{"--ignorefile", trivyIgnore}...) + } + if token != "" { + osArgs = append(osArgs, []string{"--token", token}...) + } + if input != "" { + osArgs = append(osArgs, []string{"--input", input}...) + } + + // Setup the output file + var outputFile string + if *update { + outputFile = golden + } else { + output, _ := ioutil.TempFile("", "integration") + assert.Nil(t, output.Close()) + outputFile = output.Name() + } + + cleanup := func() { + _ = os.Remove(ignoreTmpDir) + _ = os.Remove(outputFile) + } + + osArgs = append(osArgs, []string{"--output", outputFile}...) + return osArgs, outputFile, cleanup +} diff --git a/integration/integration_test.go b/integration/integration_test.go new file mode 100644 index 000000000000..13fa2532820d --- /dev/null +++ b/integration/integration_test.go @@ -0,0 +1,80 @@ +// +build integration + +package integration + +import ( + "compress/gzip" + "context" + "flag" + "io" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "time" +) + +var update = flag.Bool("update", false, "update golden files") + +func gunzipDB() string { + gz, err := os.Open("testdata/trivy.db.gz") + if err != nil { + log.Panic(err) + } + zr, err := gzip.NewReader(gz) + if err != nil { + log.Panic(err) + } + + tmpDir, err := ioutil.TempDir("", "integration") + if err != nil { + log.Panic(err) + } + dbDir := filepath.Join(tmpDir, "db") + err = os.MkdirAll(dbDir, 0700) + if err != nil { + log.Panic(err) + } + + file, err := os.Create(filepath.Join(dbDir, "trivy.db")) + if err != nil { + log.Panic(err) + } + defer file.Close() + + _, err = io.Copy(file, zr) + if err != nil { + log.Panic(err) + } + return tmpDir +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func waitPort(ctx context.Context, addr string) error { + for { + conn, err := net.Dial("tcp", addr) + if err == nil && conn != nil { + return nil + } + select { + case <-ctx.Done(): + return err + default: + time.Sleep(1 * time.Second) + } + } +} diff --git a/integration/tar_input_test.go b/integration/standalone_test.go similarity index 91% rename from integration/tar_input_test.go rename to integration/standalone_test.go index 907ab712242e..6368e788c760 100644 --- a/integration/tar_input_test.go +++ b/integration/standalone_test.go @@ -1,58 +1,18 @@ // +build integration -package integration_test +package integration import ( - "compress/gzip" - "flag" - "io" "io/ioutil" - "log" "os" - "path/filepath" "strings" "testing" - "github.com/aquasecurity/trivy/internal/standalone" + "github.com/aquasecurity/trivy/internal" "github.com/stretchr/testify/assert" ) -var update = flag.Bool("update", false, "update golden files") - -func gunzipDB() string { - gz, err := os.Open("testdata/trivy.db.gz") - if err != nil { - log.Panic(err) - } - zr, err := gzip.NewReader(gz) - if err != nil { - log.Panic(err) - } - - tmpDir, err := ioutil.TempDir("", "integration") - if err != nil { - log.Panic(err) - } - dbDir := filepath.Join(tmpDir, "db") - err = os.MkdirAll(dbDir, 0700) - if err != nil { - log.Panic(err) - } - - file, err := os.Create(filepath.Join(dbDir, "trivy.db")) - if err != nil { - log.Panic(err) - } - defer file.Close() - - _, err = io.Copy(file, zr) - if err != nil { - log.Panic(err) - } - return tmpDir -} - func TestRun_WithTar(t *testing.T) { type args struct { Version string @@ -340,7 +300,7 @@ func TestRun_WithTar(t *testing.T) { defer os.RemoveAll(cacheDir) // Setup CLI App - app := standalone.NewApp(c.testArgs.Version) + app := internal.NewApp(c.testArgs.Version) app.Writer = ioutil.Discard osArgs := []string{"trivy", "--cache-dir", cacheDir, "--format", c.testArgs.Format} diff --git a/internal/app.go b/internal/app.go new file mode 100644 index 000000000000..d076ff28267d --- /dev/null +++ b/internal/app.go @@ -0,0 +1,280 @@ +package internal + +import ( + "strings" + "time" + + "github.com/aquasecurity/trivy/internal/client" + "github.com/aquasecurity/trivy/internal/server" + + "github.com/aquasecurity/trivy/internal/standalone" + "github.com/aquasecurity/trivy/pkg/vulnerability" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" + "github.com/urfave/cli" +) + +var ( + templateFlag = cli.StringFlag{ + Name: "template, t", + Value: "", + Usage: "output template", + EnvVar: "TRIVY_TEMPLATE", + } + + formatFlag = cli.StringFlag{ + Name: "format, f", + Value: "table", + Usage: "format (table, json, template)", + EnvVar: "TRIVY_FORMAT", + } + + inputFlag = cli.StringFlag{ + Name: "input, i", + Value: "", + Usage: "input file path instead of image name", + EnvVar: "TRIVY_INPUT", + } + + severityFlag = cli.StringFlag{ + Name: "severity, s", + Value: strings.Join(types.SeverityNames, ","), + Usage: "severities of vulnerabilities to be displayed (comma separated)", + EnvVar: "TRIVY_SEVERITY", + } + + outputFlag = cli.StringFlag{ + Name: "output, o", + Usage: "output file name", + EnvVar: "TRIVY_OUTPUT", + } + + exitCodeFlag = cli.IntFlag{ + Name: "exit-code", + Usage: "Exit code when vulnerabilities were found", + Value: 0, + EnvVar: "TRIVY_EXIT_CODE", + } + + skipUpdateFlag = cli.BoolFlag{ + Name: "skip-update", + Usage: "skip db update", + EnvVar: "TRIVY_SKIP_UPDATE", + } + + downloadDBOnlyFlag = cli.BoolFlag{ + Name: "download-db-only", + Usage: "download/update vulnerability database but don't run a scan", + EnvVar: "TRIVY_DOWNLOAD_DB_ONLY", + } + + resetFlag = cli.BoolFlag{ + Name: "reset", + Usage: "remove all caches and database", + EnvVar: "TRIVY_RESET", + } + + clearCacheFlag = cli.BoolFlag{ + Name: "clear-cache, c", + Usage: "clear image caches without scanning", + EnvVar: "TRIVY_CLEAR_CACHE", + } + + quietFlag = cli.BoolFlag{ + Name: "quiet, q", + Usage: "suppress progress bar and log output", + EnvVar: "TRIVY_QUIET", + } + + noProgressFlag = cli.BoolFlag{ + Name: "no-progress", + Usage: "suppress progress bar", + EnvVar: "TRIVY_NO_PROGRESS", + } + + ignoreUnfixedFlag = cli.BoolFlag{ + Name: "ignore-unfixed", + Usage: "display only fixed vulnerabilities", + EnvVar: "TRIVY_IGNORE_UNFIXED", + } + + debugFlag = cli.BoolFlag{ + Name: "debug, d", + Usage: "debug mode", + EnvVar: "TRIVY_DEBUG", + } + + vulnTypeFlag = cli.StringFlag{ + Name: "vuln-type", + Value: "os,library", + Usage: "comma-separated list of vulnerability types (os,library)", + EnvVar: "TRIVY_VULN_TYPE", + } + + cacheDirFlag = cli.StringFlag{ + Name: "cache-dir", + Value: utils.DefaultCacheDir(), + Usage: "use as cache directory, but image cache is stored in /path/to/cache/fanal", + EnvVar: "TRIVY_CACHE_DIR", + } + + ignoreFileFlag = cli.StringFlag{ + Name: "ignorefile", + Value: vulnerability.DefaultIgnoreFile, + Usage: "specify .trivyignore file", + EnvVar: "TRIVY_IGNOREFILE", + } + + timeoutFlag = cli.DurationFlag{ + Name: "timeout", + Value: time.Second * 60, + Usage: "docker timeout", + EnvVar: "TRIVY_TIMEOUT", + } + + lightFlag = cli.BoolFlag{ + Name: "light", + Usage: "light mode: it's faster, but vulnerability descriptions and references are not displayed", + EnvVar: "TRIVY_LIGHT", + } + + token = cli.StringFlag{ + Name: "token", + Usage: "for authentication", + EnvVar: "TRIVY_TOKEN", + } +) + +func NewApp(version string) *cli.App { + cli.AppHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} +OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}} +` + app := cli.NewApp() + app.Name = "trivy" + app.Version = version + app.ArgsUsage = "image_name" + + app.Usage = "A simple and comprehensive vulnerability scanner for containers" + + app.EnableBashCompletion = true + + app.Flags = []cli.Flag{ + templateFlag, + formatFlag, + inputFlag, + severityFlag, + outputFlag, + exitCodeFlag, + skipUpdateFlag, + downloadDBOnlyFlag, + resetFlag, + clearCacheFlag, + quietFlag, + noProgressFlag, + ignoreUnfixedFlag, + debugFlag, + vulnTypeFlag, + cacheDirFlag, + ignoreFileFlag, + timeoutFlag, + lightFlag, + + // deprecated options + cli.StringFlag{ + Name: "only-update", + Usage: "deprecated", + EnvVar: "TRIVY_ONLY_UPDATE", + }, + cli.BoolFlag{ + Name: "refresh", + Usage: "deprecated", + EnvVar: "TRIVY_REFRESH", + }, + cli.BoolFlag{ + Name: "auto-refresh", + Usage: "deprecated", + EnvVar: "TRIVY_AUTO_REFRESH", + }, + } + + app.Commands = []cli.Command{ + NewClientCommand(), + NewServerCommand(), + } + + app.Action = standalone.Run + return app +} + +func NewClientCommand() cli.Command { + return cli.Command{ + Name: "client", + Aliases: []string{"c"}, + Usage: "client mode", + Action: client.Run, + Flags: []cli.Flag{ + templateFlag, + formatFlag, + inputFlag, + severityFlag, + outputFlag, + exitCodeFlag, + clearCacheFlag, + quietFlag, + ignoreUnfixedFlag, + debugFlag, + vulnTypeFlag, + ignoreFileFlag, + cacheDirFlag, + timeoutFlag, + + // original flags + token, + cli.StringFlag{ + Name: "remote", + Value: "http://localhost:4954", + Usage: "server address", + EnvVar: "TRIVY_REMOTE", + }, + }, + } +} + +func NewServerCommand() cli.Command { + return cli.Command{ + Name: "server", + Aliases: []string{"s"}, + Usage: "server mode", + Action: server.Run, + Flags: []cli.Flag{ + skipUpdateFlag, + downloadDBOnlyFlag, + resetFlag, + quietFlag, + debugFlag, + cacheDirFlag, + + // original flags + token, + cli.StringFlag{ + Name: "listen", + Value: "localhost:4954", + Usage: "listen address", + EnvVar: "TRIVY_LISTEN", + }, + }, + } +} diff --git a/internal/client/config/config.go b/internal/client/config/config.go new file mode 100644 index 000000000000..9927c0052b2e --- /dev/null +++ b/internal/client/config/config.go @@ -0,0 +1,142 @@ +package config + +import ( + "os" + "strings" + "time" + + "github.com/genuinetools/reg/registry" + "github.com/urfave/cli" + "go.uber.org/zap" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils" +) + +type Config struct { + context *cli.Context + logger *zap.SugaredLogger + + Quiet bool + Debug bool + + CacheDir string + ClearCache bool + + Input string + output string + Format string + Template string + + Timeout time.Duration + vulnType string + severities string + IgnoreFile string + IgnoreUnfixed bool + ExitCode int + + RemoteAddr string + Token string + + // these variables are generated by Init() + ImageName string + VulnType []string + Output *os.File + Severities []dbTypes.Severity + AppVersion string +} + +func New(c *cli.Context) (Config, error) { + debug := c.Bool("debug") + quiet := c.Bool("quiet") + logger, err := log.NewLogger(debug, quiet) + if err != nil { + return Config{}, xerrors.New("failed to create a logger") + } + return Config{ + context: c, + logger: logger, + + Quiet: quiet, + Debug: debug, + + CacheDir: c.String("cache-dir"), + ClearCache: c.Bool("clear-cache"), + + Input: c.String("input"), + output: c.String("output"), + Format: c.String("format"), + Template: c.String("template"), + + Timeout: c.Duration("timeout"), + vulnType: c.String("vuln-type"), + severities: c.String("severity"), + IgnoreFile: c.String("ignorefile"), + IgnoreUnfixed: c.Bool("ignore-unfixed"), + ExitCode: c.Int("exit-code"), + + RemoteAddr: c.String("remote"), + Token: c.String("token"), + }, nil +} + +func (c *Config) Init() (err error) { + c.Severities = c.splitSeverity(c.severities) + c.VulnType = strings.Split(c.vulnType, ",") + c.AppVersion = c.context.App.Version + + if c.Quiet { + utils.Quiet = true + } + + // --clear-cache doesn't conduct the scan + if c.ClearCache { + return nil + } + + args := c.context.Args() + if c.Input == "" && len(args) == 0 { + c.logger.Error(`trivy requires at least 1 argument or --input option`) + cli.ShowAppHelp(c.context) + return xerrors.New("arguments error") + } + + c.Output = os.Stdout + if c.output != "" { + if c.Output, err = os.Create(c.output); err != nil { + return xerrors.Errorf("failed to create an output file: %w", err) + } + } + + if c.Input == "" { + c.ImageName = args[0] + } + + // Check whether 'latest' tag is used + if c.ImageName != "" { + image, err := registry.ParseImage(c.ImageName) + if err != nil { + return xerrors.Errorf("invalid image: %w", err) + } + if image.Tag == "latest" { + c.logger.Warn("You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed") + } + } + + return nil +} + +func (c *Config) splitSeverity(severity string) []dbTypes.Severity { + c.logger.Debugf("Severities: %s", severity) + var severities []dbTypes.Severity + for _, s := range strings.Split(severity, ",") { + severity, err := dbTypes.NewSeverity(s) + if err != nil { + c.logger.Warnf("unknown severity option: %s", err) + } + severities = append(severities, severity) + } + return severities +} diff --git a/internal/client/config/config_test.go b/internal/client/config/config_test.go new file mode 100644 index 000000000000..bf2d3fc9ca91 --- /dev/null +++ b/internal/client/config/config_test.go @@ -0,0 +1,229 @@ +package config + +import ( + "flag" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + args []string + want Config + }{ + { + name: "happy path", + args: []string{"-quiet", "--cache-dir", "/tmp/test"}, + want: Config{ + Quiet: true, + CacheDir: "/tmp/test", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := &cli.App{} + set := flag.NewFlagSet("test", 0) + set.Bool("quiet", false, "") + set.String("cache-dir", "", "") + + c := cli.NewContext(app, set, nil) + _ = set.Parse(tt.args) + + got, err := New(c) + + // avoid to compare these values because these values are pointer + tt.want.context = c + tt.want.logger = got.logger + + assert.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConfig_Init(t *testing.T) { + type fields struct { + context *cli.Context + Quiet bool + NoProgress bool + Debug bool + CacheDir string + Reset bool + DownloadDBOnly bool + SkipUpdate bool + ClearCache bool + Input string + output string + Format string + Template string + Timeout time.Duration + vulnType string + Light bool + severities string + IgnoreFile string + IgnoreUnfixed bool + ExitCode int + ImageName string + VulnType []string + Output *os.File + Severities []dbTypes.Severity + AppVersion string + onlyUpdate string + refresh bool + autoRefresh bool + } + tests := []struct { + name string + fields fields + args []string + logs []string + want Config + wantErr string + }{ + { + name: "happy path", + fields: fields{ + severities: "CRITICAL", + vulnType: "os", + Quiet: true, + }, + args: []string{"alpine:3.10"}, + want: Config{ + AppVersion: "0.0.0", + Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, + severities: "CRITICAL", + ImageName: "alpine:3.10", + VulnType: []string{"os"}, + vulnType: "os", + Output: os.Stdout, + Quiet: true, + }, + }, + { + name: "happy path with an unknown severity", + fields: fields{ + severities: "CRITICAL,INVALID", + vulnType: "os,library", + }, + args: []string{"centos:7"}, + logs: []string{ + "unknown severity option: unknown severity: INVALID", + }, + want: Config{ + AppVersion: "0.0.0", + Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown}, + severities: "CRITICAL,INVALID", + ImageName: "centos:7", + VulnType: []string{"os", "library"}, + vulnType: "os,library", + Output: os.Stdout, + }, + }, + { + name: "with latest tag", + fields: fields{ + onlyUpdate: "alpine", + severities: "LOW", + vulnType: "os,library", + }, + args: []string{"gcr.io/distroless/base"}, + logs: []string{ + "You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed", + }, + want: Config{ + AppVersion: "0.0.0", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + severities: "LOW", + ImageName: "gcr.io/distroless/base", + VulnType: []string{"os", "library"}, + vulnType: "os,library", + Output: os.Stdout, + }, + }, + { + name: "sad: no image name", + fields: fields{ + severities: "MEDIUM", + }, + logs: []string{ + "trivy requires at least 1 argument or --input option", + }, + wantErr: "arguments error", + }, + { + name: "sad: invalid image name", + fields: fields{ + severities: "HIGH", + }, + args: []string{`!"#$%&'()`}, + wantErr: "invalid image: parsing image", + }, + } + 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) + ctx := cli.NewContext(app, set, nil) + _ = set.Parse(tt.args) + + c := &Config{ + context: ctx, + logger: logger.Sugar(), + Quiet: tt.fields.Quiet, + Debug: tt.fields.Debug, + CacheDir: tt.fields.CacheDir, + ClearCache: tt.fields.ClearCache, + Input: tt.fields.Input, + output: tt.fields.output, + Format: tt.fields.Format, + Template: tt.fields.Template, + Timeout: tt.fields.Timeout, + vulnType: tt.fields.vulnType, + severities: tt.fields.severities, + IgnoreFile: tt.fields.IgnoreFile, + IgnoreUnfixed: tt.fields.IgnoreUnfixed, + ExitCode: tt.fields.ExitCode, + ImageName: tt.fields.ImageName, + Output: tt.fields.Output, + } + + 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, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + default: + assert.NoError(t, err, tt.name) + } + + tt.want.context = ctx + tt.want.logger = logger.Sugar() + assert.Equal(t, &tt.want, c, tt.name) + }) + } +} diff --git a/internal/client/inject.go b/internal/client/inject.go new file mode 100644 index 000000000000..0e8cd813eece --- /dev/null +++ b/internal/client/inject.go @@ -0,0 +1,21 @@ +// +build wireinject + +package client + +import ( + "github.com/aquasecurity/trivy/pkg/rpc/client/library" + "github.com/aquasecurity/trivy/pkg/rpc/client/ospkg" + "github.com/aquasecurity/trivy/pkg/scanner" + "github.com/aquasecurity/trivy/pkg/vulnerability" + "github.com/google/wire" +) + +func initializeScanner(ospkgToken ospkg.Token, libToken library.Token, ospkgURL ospkg.RemoteURL, libURL library.RemoteURL) scanner.Scanner { + wire.Build(scanner.ClientSet) + return scanner.Scanner{} +} + +func initializeVulnerabilityClient() vulnerability.Client { + wire.Build(vulnerability.SuperSet) + return vulnerability.Client{} +} diff --git a/internal/client/run.go b/internal/client/run.go new file mode 100644 index 000000000000..a5ef3db856fd --- /dev/null +++ b/internal/client/run.go @@ -0,0 +1,73 @@ +package client + +import ( + "os" + + "github.com/urfave/cli" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/internal/client/config" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/rpc/client/library" + "github.com/aquasecurity/trivy/pkg/rpc/client/ospkg" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func Run(cliCtx *cli.Context) error { + c, err := config.New(cliCtx) + if err != nil { + return err + } + return run(c) +} + +func run(c config.Config) (err error) { + if err = log.InitLogger(c.Debug, c.Quiet); err != nil { + return xerrors.Errorf("failed to initialize a logger: %w", err) + } + + // initialize config + if err = c.Init(); err != nil { + return xerrors.Errorf("failed to initialize options: %w", err) + } + + // configure cache dir + utils.SetCacheDir(c.CacheDir) + log.Logger.Debugf("cache dir: %s", utils.CacheDir()) + + scanOptions := types.ScanOptions{ + VulnType: c.VulnType, + Timeout: c.Timeout, + RemoteURL: c.RemoteAddr, + Token: c.Token, + } + log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType) + + scanner := initializeScanner(ospkg.Token(c.Token), library.Token(c.Token), + ospkg.RemoteURL(c.RemoteAddr), library.RemoteURL(c.RemoteAddr)) + results, err := scanner.ScanImage(c.ImageName, c.Input, scanOptions) + if err != nil { + return xerrors.Errorf("error in image scan: %w", err) + } + + vulnClient := initializeVulnerabilityClient() + for i := range results { + results[i].Vulnerabilities = vulnClient.Filter(results[i].Vulnerabilities, + c.Severities, c.IgnoreUnfixed, c.IgnoreFile) + } + + if err = report.WriteResults(c.Format, c.Output, results, c.Template, false); err != nil { + return xerrors.Errorf("unable to write results: %w", err) + } + + if c.ExitCode != 0 { + for _, result := range results { + if len(result.Vulnerabilities) > 0 { + os.Exit(c.ExitCode) + } + } + } + return nil +} diff --git a/internal/client/wire_gen.go b/internal/client/wire_gen.go new file mode 100644 index 000000000000..c79e8f0831ef --- /dev/null +++ b/internal/client/wire_gen.go @@ -0,0 +1,35 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package client + +import ( + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/rpc/client/library" + "github.com/aquasecurity/trivy/pkg/rpc/client/ospkg" + "github.com/aquasecurity/trivy/pkg/scanner" + library2 "github.com/aquasecurity/trivy/pkg/scanner/library" + ospkg2 "github.com/aquasecurity/trivy/pkg/scanner/ospkg" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +// Injectors from inject.go: + +func initializeScanner(ospkgToken ospkg.Token, libToken library.Token, ospkgURL ospkg.RemoteURL, libURL library.RemoteURL) scanner.Scanner { + osDetector := ospkg.NewProtobufClient(ospkgURL) + detector := ospkg.NewDetector(ospkgToken, osDetector) + ospkgScanner := ospkg2.NewScanner(detector) + libDetector := library.NewProtobufClient(libURL) + libraryDetector := library.NewDetector(libToken, libDetector) + libraryScanner := library2.NewScanner(libraryDetector) + scannerScanner := scanner.NewScanner(ospkgScanner, libraryScanner) + return scannerScanner +} + +func initializeVulnerabilityClient() vulnerability.Client { + config := db.Config{} + client := vulnerability.NewClient(config) + return client +} diff --git a/internal/operation/inject.go b/internal/operation/inject.go new file mode 100644 index 000000000000..7dcfe6235791 --- /dev/null +++ b/internal/operation/inject.go @@ -0,0 +1,13 @@ +// +build wireinject + +package operation + +import ( + "github.com/aquasecurity/trivy/pkg/db" + "github.com/google/wire" +) + +func initializeDBClient() db.Client { + wire.Build(db.SuperSet) + return db.Client{} +} diff --git a/internal/operation/operation.go b/internal/operation/operation.go new file mode 100644 index 000000000000..7c5dfa7aa620 --- /dev/null +++ b/internal/operation/operation.go @@ -0,0 +1,72 @@ +package operation + +import ( + "context" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/fanal/cache" + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func Reset() (err error) { + log.Logger.Info("Resetting...") + if err = cache.Clear(); err != nil { + return xerrors.New("failed to remove image layer cache") + } + if err = os.RemoveAll(utils.CacheDir()); err != nil { + return xerrors.New("failed to remove cache") + } + return nil +} + +func ClearCache() error { + log.Logger.Info("Removing image caches...") + if err := cache.Clear(); err != nil { + return xerrors.New("failed to remove image layer cache") + } + return nil +} + +func DownloadDB(appVersion, cacheDir string, light, skipUpdate bool) error { + client := initializeDBClient() + ctx := context.Background() + needsUpdate, err := client.NeedsUpdate(ctx, appVersion, light, skipUpdate) + if err != nil { + return xerrors.Errorf("database error: %w", err) + } + + if needsUpdate { + log.Logger.Info("Need to update DB") + if err = db.Close(); err != nil { + return xerrors.Errorf("failed db close: %w", err) + } + if err := client.Download(ctx, cacheDir, light); err != nil { + return xerrors.Errorf("failed to download vulnerability DB: %w", err) + } + + log.Logger.Info("Reopening DB...") + if err = db.Init(cacheDir); err != nil { + return xerrors.Errorf("failed db close: %w", err) + } + } + + // for debug + if err := showDBInfo(); err != nil { + return xerrors.Errorf("failed to show database info") + } + return nil +} + +func showDBInfo() error { + metadata, err := db.Config{}.GetMetadata() + if err != nil { + return xerrors.Errorf("something wrong with DB: %w", err) + } + log.Logger.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s", + metadata.Version, metadata.Type, metadata.UpdatedAt, metadata.NextUpdate) + return nil +} diff --git a/internal/operation/wire_gen.go b/internal/operation/wire_gen.go new file mode 100644 index 000000000000..3e6e1e4ac539 --- /dev/null +++ b/internal/operation/wire_gen.go @@ -0,0 +1,23 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package operation + +import ( + db2 "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/github" + "k8s.io/utils/clock" +) + +// Injectors from inject.go: + +func initializeDBClient() db.Client { + config := db2.Config{} + client := github.NewClient() + realClock := clock.RealClock{} + dbClient := db.NewClient(config, client, realClock) + return dbClient +} diff --git a/internal/server/config/config.go b/internal/server/config/config.go new file mode 100644 index 000000000000..8dcda814a4b1 --- /dev/null +++ b/internal/server/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "github.com/urfave/cli" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/utils" +) + +type Config struct { + context *cli.Context + + Quiet bool + Debug bool + CacheDir string + Reset bool + DownloadDBOnly bool + SkipUpdate bool + + Listen string + Token string + + // these variables are generated by Init() + AppVersion string +} + +func New(c *cli.Context) Config { + debug := c.Bool("debug") + quiet := c.Bool("quiet") + return Config{ + context: c, + + Quiet: quiet, + Debug: debug, + CacheDir: c.String("cache-dir"), + Reset: c.Bool("reset"), + DownloadDBOnly: c.Bool("download-db-only"), + SkipUpdate: c.Bool("skip-update"), + Listen: c.String("listen"), + Token: c.String("token"), + } +} + +func (c *Config) Init() (err error) { + if c.SkipUpdate && c.DownloadDBOnly { + return xerrors.New("The --skip-update and --download-db-only option can not be specified both") + } + + c.AppVersion = c.context.App.Version + + // A server always suppresses a progress bar + utils.Quiet = true + + return nil +} diff --git a/internal/server/config/config_test.go b/internal/server/config/config_test.go new file mode 100644 index 000000000000..35e068927218 --- /dev/null +++ b/internal/server/config/config_test.go @@ -0,0 +1,159 @@ +package config + +import ( + "flag" + "os" + "testing" + "time" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + args []string + want Config + }{ + { + name: "happy path", + args: []string{"-quiet", "--no-progress", "--reset", "--skip-update"}, + want: Config{ + Quiet: true, + Reset: true, + SkipUpdate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := &cli.App{} + set := flag.NewFlagSet("test", 0) + set.Bool("quiet", false, "") + set.Bool("no-progress", false, "") + set.Bool("reset", false, "") + set.Bool("skip-update", false, "") + + c := cli.NewContext(app, set, nil) + _ = set.Parse(tt.args) + + tt.want.context = c + + got := New(c) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConfig_Init(t *testing.T) { + type fields struct { + context *cli.Context + Quiet bool + Debug bool + CacheDir string + Reset bool + DownloadDBOnly bool + SkipUpdate bool + ClearCache bool + Input string + output string + Format string + Template string + Timeout time.Duration + vulnType string + Light bool + severities string + IgnoreFile string + IgnoreUnfixed bool + ExitCode int + ImageName string + VulnType []string + Output *os.File + Severities []dbTypes.Severity + AppVersion string + onlyUpdate string + refresh bool + autoRefresh bool + } + tests := []struct { + name string + fields fields + args []string + logs []string + want Config + wantErr string + }{ + { + name: "happy path", + fields: fields{ + severities: "CRITICAL", + vulnType: "os", + Quiet: true, + }, + args: []string{"alpine:3.10"}, + want: Config{ + AppVersion: "0.0.0", + Quiet: true, + }, + }, + { + name: "happy path: reset", + fields: fields{ + severities: "CRITICAL", + vulnType: "os", + Reset: true, + }, + args: []string{"alpine:3.10"}, + want: Config{ + AppVersion: "0.0.0", + Reset: true, + }, + }, + { + name: "sad: skip and download db", + fields: fields{ + refresh: true, + SkipUpdate: true, + DownloadDBOnly: true, + }, + args: []string{"alpine:3.10"}, + wantErr: "The --skip-update and --download-db-only option can not be specified both", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ctx := cli.NewContext(app, set, nil) + _ = set.Parse(tt.args) + + c := &Config{ + context: ctx, + Quiet: tt.fields.Quiet, + Debug: tt.fields.Debug, + CacheDir: tt.fields.CacheDir, + Reset: tt.fields.Reset, + DownloadDBOnly: tt.fields.DownloadDBOnly, + SkipUpdate: tt.fields.SkipUpdate, + } + + err := c.Init() + + // test the error + switch { + case tt.wantErr != "": + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + default: + assert.NoError(t, err, tt.name) + } + + tt.want.context = ctx + assert.Equal(t, &tt.want, c, tt.name) + }) + } +} diff --git a/internal/server/run.go b/internal/server/run.go new file mode 100644 index 000000000000..a485e2ca004b --- /dev/null +++ b/internal/server/run.go @@ -0,0 +1,51 @@ +package server + +import ( + "github.com/aquasecurity/trivy/pkg/rpc/server" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/internal/operation" + "github.com/aquasecurity/trivy/internal/server/config" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils" + "github.com/urfave/cli" + "golang.org/x/xerrors" +) + +func Run(ctx *cli.Context) error { + return run(config.New(ctx)) +} + +func run(c config.Config) (err error) { + if err = log.InitLogger(c.Debug, c.Quiet); err != nil { + return xerrors.Errorf("failed to initialize a logger: %w", err) + } + + // initialize config + if err = c.Init(); err != nil { + return xerrors.Errorf("failed to initialize options: %w", err) + } + + // configure cache dir + utils.SetCacheDir(c.CacheDir) + log.Logger.Debugf("cache dir: %s", utils.CacheDir()) + + if c.Reset { + return operation.Reset() + } + + if err = db.Init(c.CacheDir); err != nil { + return xerrors.Errorf("error in vulnerability DB initialize: %w", err) + } + + // download the database file + if err = operation.DownloadDB(c.AppVersion, c.CacheDir, false, c.SkipUpdate); err != nil { + return err + } + + if c.DownloadDBOnly { + return nil + } + + return server.ListenAndServe(c.Listen, c) +} diff --git a/internal/standalone/app.go b/internal/standalone/app.go deleted file mode 100644 index 41d3307351f0..000000000000 --- a/internal/standalone/app.go +++ /dev/null @@ -1,164 +0,0 @@ -package standalone - -import ( - "strings" - "time" - - "github.com/aquasecurity/trivy/pkg/vulnerability" - - "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/utils" - "github.com/urfave/cli" -) - -func NewApp(version string) *cli.App { - cli.AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} -OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}} -` - app := cli.NewApp() - app.Name = "trivy" - app.Version = version - app.ArgsUsage = "image_name" - - app.Usage = "A simple and comprehensive vulnerability scanner for containers" - - app.EnableBashCompletion = true - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "template, t", - Value: "", - Usage: "output template", - EnvVar: "TRIVY_TEMPLATE", - }, - cli.StringFlag{ - Name: "format, f", - Value: "table", - Usage: "format (table, json, template)", - EnvVar: "TRIVY_FORMAT", - }, - cli.StringFlag{ - Name: "input, i", - Value: "", - Usage: "input file path instead of image name", - EnvVar: "TRIVY_INPUT", - }, - cli.StringFlag{ - Name: "severity, s", - Value: strings.Join(types.SeverityNames, ","), - Usage: "severities of vulnerabilities to be displayed (comma separated)", - EnvVar: "TRIVY_SEVERITY", - }, - cli.StringFlag{ - Name: "output, o", - Usage: "output file name", - EnvVar: "TRIVY_OUTPUT", - }, - cli.IntFlag{ - Name: "exit-code", - Usage: "Exit code when vulnerabilities were found", - Value: 0, - EnvVar: "TRIVY_EXIT_CODE", - }, - cli.BoolFlag{ - Name: "skip-update", - Usage: "skip db update", - EnvVar: "TRIVY_SKIP_UPDATE", - }, - cli.BoolFlag{ - Name: "download-db-only", - Usage: "download/update vulnerability database but don't run a scan", - EnvVar: "TRIVY_DOWNLOAD_DB_ONLY", - }, - cli.BoolFlag{ - Name: "reset", - Usage: "remove all caches and database", - EnvVar: "TRIVY_RESET", - }, - cli.BoolFlag{ - Name: "clear-cache, c", - Usage: "clear image caches without scanning", - EnvVar: "TRIVY_CLEAR_CACHE", - }, - cli.BoolFlag{ - Name: "quiet, q", - Usage: "suppress progress bar and log output", - EnvVar: "TRIVY_QUIET", - }, - cli.BoolFlag{ - Name: "no-progress", - Usage: "suppress progress bar", - EnvVar: "TRIVY_NO_PROGRESS", - }, - cli.BoolFlag{ - Name: "ignore-unfixed", - Usage: "display only fixed vulnerabilities", - EnvVar: "TRIVY_IGNORE_UNFIXED", - }, - cli.BoolFlag{ - Name: "debug, d", - Usage: "debug mode", - EnvVar: "TRIVY_DEBUG", - }, - cli.StringFlag{ - Name: "vuln-type", - Value: "os,library", - Usage: "comma-separated list of vulnerability types (os,library)", - EnvVar: "TRIVY_VULN_TYPE", - }, - cli.StringFlag{ - Name: "cache-dir", - Value: utils.DefaultCacheDir(), - Usage: "use as cache directory, but image cache is stored in /path/to/cache/fanal", - EnvVar: "TRIVY_CACHE_DIR", - }, - cli.StringFlag{ - Name: "ignorefile", - Value: vulnerability.DefaultIgnoreFile, - Usage: "specify .trivyignore file", - EnvVar: "TRIVY_IGNOREFILE", - }, - cli.DurationFlag{ - Name: "timeout", - Value: time.Second * 60, - Usage: "docker timeout", - EnvVar: "TRIVY_TIMEOUT", - }, - cli.BoolFlag{ - Name: "light", - Usage: "light mode: it's faster, but vulnerability descriptions and references are not displayed", - }, - - // deprecated options - cli.StringFlag{ - Name: "only-update", - Usage: "deprecated", - EnvVar: "TRIVY_ONLY_UPDATE", - }, - cli.BoolFlag{ - Name: "refresh", - Usage: "deprecated", - EnvVar: "TRIVY_REFRESH", - }, - cli.BoolFlag{ - Name: "auto-refresh", - Usage: "deprecated", - EnvVar: "TRIVY_AUTO_REFRESH", - }, - } - - app.Action = Run - return app -} diff --git a/internal/standalone/inject.go b/internal/standalone/inject.go new file mode 100644 index 000000000000..cfa1bf8ee790 --- /dev/null +++ b/internal/standalone/inject.go @@ -0,0 +1,19 @@ +// +build wireinject + +package standalone + +import ( + "github.com/aquasecurity/trivy/pkg/scanner" + "github.com/aquasecurity/trivy/pkg/vulnerability" + "github.com/google/wire" +) + +func initializeScanner() scanner.Scanner { + wire.Build(scanner.StandaloneSet) + return scanner.Scanner{} +} + +func initializeVulnerabilityClient() vulnerability.Client { + wire.Build(vulnerability.SuperSet) + return vulnerability.Client{} +} diff --git a/internal/standalone/run.go b/internal/standalone/run.go index c5ac3d8d9134..59a5bc9f6909 100644 --- a/internal/standalone/run.go +++ b/internal/standalone/run.go @@ -1,20 +1,16 @@ package standalone import ( - "context" l "log" "os" - "github.com/aquasecurity/fanal/cache" "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/internal/operation" "github.com/aquasecurity/trivy/internal/standalone/config" - dbFile "github.com/aquasecurity/trivy/pkg/db" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/report" - "github.com/aquasecurity/trivy/pkg/scanner" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/utils" - "github.com/aquasecurity/trivy/pkg/vulnerability" "github.com/urfave/cli" "golang.org/x/xerrors" ) @@ -42,11 +38,11 @@ func run(c config.Config) (err error) { log.Logger.Debugf("cache dir: %s", utils.CacheDir()) if c.Reset { - return reset() + return operation.Reset() } if c.ClearCache { - return clearCache() + return operation.ClearCache() } if err = db.Init(c.CacheDir); err != nil { @@ -54,7 +50,7 @@ func run(c config.Config) (err error) { } // download the database file - if err = downloadDB(c.AppVersion, c.CacheDir, c.Light, c.SkipUpdate); err != nil { + if err = operation.DownloadDB(c.AppVersion, c.CacheDir, c.Light, c.SkipUpdate); err != nil { return err } @@ -68,15 +64,17 @@ func run(c config.Config) (err error) { } log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType) + scanner := initializeScanner() results, err := scanner.ScanImage(c.ImageName, c.Input, scanOptions) if err != nil { return xerrors.Errorf("error in image scan: %w", err) } - vulnClient := vulnerability.NewClient() + vulnClient := initializeVulnerabilityClient() for i := range results { - results[i].Vulnerabilities = vulnClient.FillAndFilter(results[i].Vulnerabilities, - c.Severities, c.IgnoreUnfixed, c.IgnoreFile, c.Light) + vulnClient.FillInfo(results[i].Vulnerabilities, c.Light) + results[i].Vulnerabilities = vulnClient.Filter(results[i].Vulnerabilities, + c.Severities, c.IgnoreUnfixed, c.IgnoreFile) } if err = report.WriteResults(c.Format, c.Output, results, c.Template, c.Light); err != nil { @@ -92,45 +90,3 @@ func run(c config.Config) (err error) { } return nil } - -func reset() (err error) { - log.Logger.Info("Resetting...") - if err = cache.Clear(); err != nil { - return xerrors.New("failed to remove image layer cache") - } - if err = os.RemoveAll(utils.CacheDir()); err != nil { - return xerrors.New("failed to remove cache") - } - return nil -} - -func clearCache() error { - log.Logger.Info("Removing image caches...") - if err := cache.Clear(); err != nil { - return xerrors.New("failed to remove image layer cache") - } - return nil -} - -func downloadDB(appVersion, cacheDir string, light, skipUpdate bool) error { - client := dbFile.NewClient() - ctx := context.Background() - if err := client.Download(ctx, appVersion, cacheDir, light, skipUpdate); err != nil { - return xerrors.Errorf("failed to download vulnerability DB: %w", err) - } - // for debug - if err := showDBInfo(); err != nil { - return xerrors.Errorf("failed to show database info") - } - return nil -} - -func showDBInfo() error { - metadata, err := db.Config{}.GetMetadata() - if err != nil { - return xerrors.Errorf("something wrong with DB: %w", err) - } - log.Logger.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s", - metadata.Version, metadata.Type, metadata.UpdatedAt, metadata.NextUpdate) - return nil -} diff --git a/internal/standalone/wire_gen.go b/internal/standalone/wire_gen.go new file mode 100644 index 000000000000..baf4f980538c --- /dev/null +++ b/internal/standalone/wire_gen.go @@ -0,0 +1,34 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package standalone + +import ( + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/detector/library" + "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/scanner" + library2 "github.com/aquasecurity/trivy/pkg/scanner/library" + ospkg2 "github.com/aquasecurity/trivy/pkg/scanner/ospkg" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +// Injectors from inject.go: + +func initializeScanner() scanner.Scanner { + detector := ospkg.Detector{} + ospkgScanner := ospkg2.NewScanner(detector) + driverFactory := library.DriverFactory{} + libraryDetector := library.NewDetector(driverFactory) + libraryScanner := library2.NewScanner(libraryDetector) + scannerScanner := scanner.NewScanner(ospkgScanner, libraryScanner) + return scannerScanner +} + +func initializeVulnerabilityClient() vulnerability.Client { + config := db.Config{} + client := vulnerability.NewClient(config) + return client +} diff --git a/pkg/db/db.go b/pkg/db/db.go index 92089d28d9e9..bb5bb81c7a1f 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -5,10 +5,11 @@ import ( "context" "io" "os" + "path/filepath" - "k8s.io/utils/clock" - + "github.com/google/wire" "golang.org/x/xerrors" + "k8s.io/utils/clock" "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy/pkg/github" @@ -21,35 +22,42 @@ const ( lightDB = "trivy-light.db.gz" ) +var SuperSet = wire.NewSet( + wire.Struct(new(clock.RealClock)), + wire.Bind(new(clock.Clock), new(clock.RealClock)), + wire.Struct(new(db.Config)), + github.NewClient, + wire.Bind(new(github.Operation), new(github.Client)), + NewClient, + wire.Bind(new(Operation), new(Client)), +) + type Operation interface { - GetMetadata() (db.Metadata, error) + NeedsUpdate(ctx context.Context, cliVersion string, light, skip bool) (bool, error) + Download(ctx context.Context, cacheDir string, light bool) error } -type GitHubOperation interface { - DownloadDB(ctx context.Context, fileName string) (io.ReadCloser, error) +type dbOperation interface { + GetMetadata() (db.Metadata, error) } type Client struct { - dbc Operation + dbc dbOperation + githubClient github.Operation clock clock.Clock - githubClient GitHubOperation } -func NewClient() Client { +func NewClient(dbc db.Config, githubClient github.Operation, clock clock.Clock) Client { return Client{ - dbc: db.Config{}, - clock: clock.RealClock{}, - githubClient: github.NewClient(), + dbc: dbc, + githubClient: githubClient, + clock: clock, } } -func (c Client) Download(ctx context.Context, cliVersion, cacheDir string, light, skip bool) error { +func (c Client) NeedsUpdate(ctx context.Context, cliVersion string, light, skip bool) (bool, error) { dbType := db.TypeFull - dbFile := fullDB - message := " Downloading Full DB file..." if light { - dbFile = lightDB - message = " Downloading Lightweight DB file..." dbType = db.TypeLight } @@ -58,54 +66,48 @@ func (c Client) Download(ctx context.Context, cliVersion, cacheDir string, light log.Logger.Debug("This is the first run") if skip { log.Logger.Error("The first run cannot skip downloading DB") - return xerrors.New("--skip-update cannot be specified on the first run") + return false, xerrors.New("--skip-update cannot be specified on the first run") } metadata = db.Metadata{} // suppress a warning } if db.SchemaVersion < metadata.Version { log.Logger.Errorf("Trivy version (%s) is old. Update to the latest version.", cliVersion) - return xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d", + return false, xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d", metadata.Version, db.SchemaVersion) } if skip { if db.SchemaVersion != metadata.Version { log.Logger.Error("The local DB is old and needs to be updated") - return xerrors.New("--skip-update cannot be specified with the old DB") + return false, xerrors.New("--skip-update cannot be specified with the old DB") } else if metadata.Type != dbType { if dbType == db.TypeFull { log.Logger.Error("The local DB is a lightweight DB. You have to download a full DB") } else { log.Logger.Error("The local DB is a full DB. You have to download a lightweight DB") } - return xerrors.New("--skip-update cannot be specified with the different schema DB") + return false, xerrors.New("--skip-update cannot be specified with the different schema DB") } - return nil + return false, nil } if db.SchemaVersion == metadata.Version && metadata.Type == dbType && c.clock.Now().Before(metadata.NextUpdate) { log.Logger.Debug("DB update was skipped because DB is the latest") - return nil - } - - if err = c.download(ctx, cacheDir, message, dbFile); err != nil { - return xerrors.Errorf("failed to download the DB file: %w", err) + return false, nil } + return true, nil +} - log.Logger.Info("Reopening vulnerability DB") - if err = db.Close(); err != nil { - return xerrors.Errorf("unable to close old DB: %w", err) - } - if err = db.Init(cacheDir); err != nil { - return xerrors.Errorf("unable to open new DB: %w", err) +func (c Client) Download(ctx context.Context, cacheDir string, light bool) error { + dbFile := fullDB + message := " Downloading Full DB file..." + if light { + dbFile = lightDB + message = " Downloading Lightweight DB file..." } - return nil -} - -func (c Client) download(ctx context.Context, cacheDir, message, dbFile string) error { spinner := utils.NewSpinner(message) spinner.Start() defer spinner.Stop() @@ -122,6 +124,11 @@ func (c Client) download(ctx context.Context, cacheDir, message, dbFile string) } dbPath := db.Path(cacheDir) + dbDir := filepath.Dir(dbPath) + if err = os.MkdirAll(dbDir, 0700); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + file, err := os.Create(dbPath) if err != nil { return xerrors.Errorf("unable to open DB file: %w", err) diff --git a/pkg/db/db_mock.go b/pkg/db/db_mock.go new file mode 100644 index 000000000000..3700bd2cb82c --- /dev/null +++ b/pkg/db/db_mock.go @@ -0,0 +1,22 @@ +package db + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type MockClient struct { + mock.Mock +} + +func (_m *MockClient) NeedsUpdate(a context.Context, b string, c, d bool) (bool, error) { + ret := _m.Called(a, b, c, d) + return ret.Bool(0), ret.Error(1) +} + +func (_m *MockClient) Download(a context.Context, b string, c bool) error { + ret := _m.Called(a, b, c) + return ret.Error(0) + +} diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go index fd80cbaa1d85..50dcf09191d5 100644 --- a/pkg/db/db_test.go +++ b/pkg/db/db_test.go @@ -17,6 +17,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/github" "github.com/aquasecurity/trivy/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -39,47 +40,20 @@ func (_m *MockConfig) GetMetadata() (db.Metadata, error) { return metadata, ret.Error(1) } -type MockGitHubClient struct { - mock.Mock -} - -func (_m *MockGitHubClient) DownloadDB(ctx context.Context, fileName string) (io.ReadCloser, error) { - ret := _m.Called(ctx, fileName) - ret0 := ret.Get(0) - if ret0 == nil { - return nil, ret.Error(1) - } - rc, ok := ret0.(io.ReadCloser) - if !ok { - return nil, ret.Error(1) - } - return rc, ret.Error(1) -} - -func TestClient_Download(t *testing.T) { +func TestClient_NeedsUpdate(t *testing.T) { type getMetadataOutput struct { metadata db.Metadata err error } - type downloadDBOutput struct { - fileName string - err error - } - type downloadDB struct { - input string - output downloadDBOutput - } - testCases := []struct { - name string - light bool - skip bool - clock clock.Clock - getMetadata getMetadataOutput - downloadDB []downloadDB - expectedContent []byte - expectedError error + name string + light bool + skip bool + clock clock.Clock + getMetadata getMetadataOutput + expected bool + expectedError error }{ { name: "happy path", @@ -92,14 +66,7 @@ func TestClient_Download(t *testing.T) { NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), }, }, - downloadDB: []downloadDB{ - { - input: fullDB, - output: downloadDBOutput{ - fileName: "testdata/test.db.gz", - }, - }, - }, + expected: true, }, { name: "happy path for first run", @@ -109,14 +76,7 @@ func TestClient_Download(t *testing.T) { metadata: db.Metadata{}, err: errors.New("get metadata failed"), }, - downloadDB: []downloadDB{ - { - input: fullDB, - output: downloadDBOutput{ - fileName: "testdata/test.db.gz", - }, - }, - }, + expected: true, }, { name: "happy path with different type", @@ -129,14 +89,7 @@ func TestClient_Download(t *testing.T) { NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), }, }, - downloadDB: []downloadDB{ - { - input: lightDB, - output: downloadDBOutput{ - fileName: "testdata/test.db.gz", - }, - }, - }, + expected: true, }, { name: "happy path with old schema version", @@ -149,14 +102,7 @@ func TestClient_Download(t *testing.T) { NextUpdate: time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC), }, }, - downloadDB: []downloadDB{ - { - input: lightDB, - output: downloadDBOutput{ - fileName: "testdata/test.db.gz", - }, - }, - }, + expected: true, }, { name: "happy path with --skip-update", @@ -169,7 +115,8 @@ func TestClient_Download(t *testing.T) { NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), }, }, - skip: true, + skip: true, + expected: false, }, { name: "skip downloading DB", @@ -182,6 +129,7 @@ func TestClient_Download(t *testing.T) { NextUpdate: time.Date(2019, 10, 2, 0, 0, 0, 0, time.UTC), }, }, + expected: false, }, { name: "newer schema version", @@ -197,84 +145,131 @@ func TestClient_Download(t *testing.T) { expectedError: xerrors.New("the version of DB schema doesn't match. Local DB: 2, Expected: 1"), }, { - name: "DownloadDB returns an error", + name: "--skip-update on the first run", light: false, clock: clocktesting.NewFakeClock(time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)), getMetadata: getMetadataOutput{ - metadata: db.Metadata{ - Version: 1, - Type: db.TypeFull, - NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), - }, - }, - downloadDB: []downloadDB{ - { - input: fullDB, - output: downloadDBOutput{ - err: xerrors.New("download failed"), - }, - }, + err: xerrors.New("this is the first run"), }, - expectedError: xerrors.New("failed to download the DB file: failed to download vulnerability DB: download failed"), + skip: true, + expectedError: xerrors.New("--skip-update cannot be specified on the first run"), }, { - name: "invalid gzip", + name: "--skip-update with different schema version", light: false, clock: clocktesting.NewFakeClock(time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)), getMetadata: getMetadataOutput{ metadata: db.Metadata{ - Version: 1, + Version: 0, Type: db.TypeFull, NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), }, }, + skip: true, + expectedError: xerrors.New("--skip-update cannot be specified with the old DB"), + }, + } + + if err := log.InitLogger(false, true); err != nil { + require.NoError(t, err, "failed to init logger") + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockConfig := new(MockConfig) + mockConfig.On("GetMetadata").Return( + tc.getMetadata.metadata, tc.getMetadata.err) + + dir, err := ioutil.TempDir("", "db") + require.NoError(t, err, tc.name) + defer os.RemoveAll(dir) + + err = db.Init(dir) + require.NoError(t, err, tc.name) + + client := Client{ + dbc: mockConfig, + clock: tc.clock, + } + + needsUpdate, err := client.NeedsUpdate(context.Background(), "test", tc.light, tc.skip) + + switch { + case tc.expectedError != nil: + assert.EqualError(t, err, tc.expectedError.Error(), tc.name) + default: + assert.NoError(t, err, tc.name) + } + + assert.Equal(t, tc.expected, needsUpdate) + mockConfig.AssertExpectations(t) + }) + } +} + +func TestClient_Download(t *testing.T) { + type downloadDBOutput struct { + fileName string + err error + } + type downloadDB struct { + input string + output downloadDBOutput + } + + testCases := []struct { + name string + light bool + downloadDB []downloadDB + expectedContent []byte + expectedError error + }{ + { + name: "happy path", + light: false, downloadDB: []downloadDB{ { input: fullDB, output: downloadDBOutput{ - fileName: "testdata/invalid.db.gz", + fileName: "testdata/test.db.gz", }, }, }, - expectedError: xerrors.New("unable to open new DB: failed to open db: invalid database"), }, { - name: "--skip-update on the first run", + name: "DownloadDB returns an error", light: false, - clock: clocktesting.NewFakeClock(time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)), - getMetadata: getMetadataOutput{ - err: xerrors.New("this is the first run"), + downloadDB: []downloadDB{ + { + input: fullDB, + output: downloadDBOutput{ + err: xerrors.New("download failed"), + }, + }, }, - skip: true, - expectedError: xerrors.New("--skip-update cannot be specified on the first run"), + expectedError: xerrors.New("failed to download vulnerability DB: download failed"), }, { - name: "--skip-update with different schema version", + name: "invalid gzip", light: false, - clock: clocktesting.NewFakeClock(time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)), - getMetadata: getMetadataOutput{ - metadata: db.Metadata{ - Version: 0, - Type: db.TypeFull, - NextUpdate: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), + downloadDB: []downloadDB{ + { + input: fullDB, + output: downloadDBOutput{ + fileName: "testdata/invalid.db.gz", + }, }, }, - skip: true, - expectedError: xerrors.New("--skip-update cannot be specified with the old DB"), + expectedError: xerrors.New("invalid gzip file: unexpected EOF"), }, } - if err := log.InitLogger(false, true); err != nil { - require.NoError(t, err, "failed to init logger") - } + err := log.InitLogger(false, true) + require.NoError(t, err, "failed to init logger") for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - mockConfig := new(MockConfig) - mockConfig.On("GetMetadata").Return( - tc.getMetadata.metadata, tc.getMetadata.err) - - mockGitHubConfig := new(MockGitHubClient) + mockGitHubClient := new(github.MockClient) for _, dd := range tc.downloadDB { var rc io.ReadCloser if dd.output.fileName != "" { @@ -283,7 +278,7 @@ func TestClient_Download(t *testing.T) { rc = f } - mockGitHubConfig.On("DownloadDB", mock.Anything, dd.input).Return( + mockGitHubClient.On("DownloadDB", mock.Anything, dd.input).Return( rc, dd.output.err, ) } @@ -295,14 +290,9 @@ func TestClient_Download(t *testing.T) { err = db.Init(dir) require.NoError(t, err, tc.name) - client := Client{ - dbc: mockConfig, - clock: tc.clock, - githubClient: mockGitHubConfig, - } - + client := NewClient(db.Config{}, mockGitHubClient, nil) ctx := context.Background() - err = client.Download(ctx, "test", dir, tc.light, tc.skip) + err = client.Download(ctx, dir, tc.light) switch { case tc.expectedError != nil: @@ -311,8 +301,7 @@ func TestClient_Download(t *testing.T) { assert.NoError(t, err, tc.name) } - mockConfig.AssertExpectations(t) - mockGitHubConfig.AssertExpectations(t) + mockGitHubClient.AssertExpectations(t) }) } } diff --git a/pkg/db/testdata/invalid.db.gz b/pkg/db/testdata/invalid.db.gz index 629123ed5ce4..257cc5642cb1 100644 Binary files a/pkg/db/testdata/invalid.db.gz and b/pkg/db/testdata/invalid.db.gz differ diff --git a/pkg/scanner/library/bundler/scan.go b/pkg/detector/library/bundler/scan.go similarity index 100% rename from pkg/scanner/library/bundler/scan.go rename to pkg/detector/library/bundler/scan.go diff --git a/pkg/scanner/library/bundler/scan_test.go b/pkg/detector/library/bundler/scan_test.go similarity index 100% rename from pkg/scanner/library/bundler/scan_test.go rename to pkg/detector/library/bundler/scan_test.go diff --git a/pkg/scanner/library/cargo/scan.go b/pkg/detector/library/cargo/scan.go similarity index 100% rename from pkg/scanner/library/cargo/scan.go rename to pkg/detector/library/cargo/scan.go diff --git a/pkg/scanner/library/composer/scan.go b/pkg/detector/library/composer/scan.go similarity index 100% rename from pkg/scanner/library/composer/scan.go rename to pkg/detector/library/composer/scan.go diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go new file mode 100644 index 000000000000..9a7501f6b92e --- /dev/null +++ b/pkg/detector/library/detect.go @@ -0,0 +1,71 @@ +package library + +import ( + "path/filepath" + + "github.com/google/wire" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/knqyf263/go-version" + + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/types" +) + +var SuperSet = wire.NewSet( + wire.Struct(new(DriverFactory)), + wire.Bind(new(Factory), new(DriverFactory)), + NewDetector, + wire.Bind(new(Operation), new(Detector)), +) + +type Operation interface { + Detect(string, []ptypes.Library) ([]types.DetectedVulnerability, error) +} + +type Detector struct { + driverFactory Factory +} + +func NewDetector(factory Factory) Detector { + return Detector{driverFactory: factory} +} + +func (d Detector) Detect(filePath string, pkgs []ptypes.Library) ([]types.DetectedVulnerability, error) { + log.Logger.Debugf("Detecting library vulnerabilities, path: %s", filePath) + driver := d.driverFactory.NewDriver(filepath.Base(filePath)) + if driver == nil { + return nil, xerrors.New("unknown file type") + } + + vulns, err := detect(driver, pkgs) + if err != nil { + return nil, xerrors.Errorf("failed to scan %s vulnerabilities: %w", driver.Type(), err) + } + + return vulns, nil +} + +func detect(driver Driver, libs []ptypes.Library) ([]types.DetectedVulnerability, error) { + log.Logger.Infof("Detecting %s vulnerabilities...", driver.Type()) + var vulnerabilities []types.DetectedVulnerability + for _, lib := range libs { + v, err := version.NewVersion(lib.Version) + if err != nil { + log.Logger.Debugf("invalid version, library: %s, version: %s, error: %s\n", + lib.Name, lib.Version, err) + continue + } + + vulns, err := driver.Detect(lib.Name, v) + if err != nil { + return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", driver.Type(), err) + } + vulnerabilities = append(vulnerabilities, vulns...) + } + + return vulnerabilities, nil +} diff --git a/pkg/detector/library/detector_mock.go b/pkg/detector/library/detector_mock.go new file mode 100644 index 000000000000..9307cbca0629 --- /dev/null +++ b/pkg/detector/library/detector_mock.go @@ -0,0 +1,46 @@ +package library + +import ( + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/stretchr/testify/mock" +) + +type MockDetector struct { + mock.Mock +} + +type DetectInput struct { + FilePath string + Libs []ptypes.Library +} +type DetectOutput struct { + Vulns []types.DetectedVulnerability + Err error +} +type DetectExpectation struct { + Args DetectInput + ReturnArgs DetectOutput +} + +func NewMockDetector(detectExpectations []DetectExpectation) *MockDetector { + mockDetector := new(MockDetector) + for _, e := range detectExpectations { + mockDetector.On("Detect", e.Args.FilePath, e.Args.Libs).Return( + e.ReturnArgs.Vulns, e.ReturnArgs.Err) + } + return mockDetector +} + +func (_m *MockDetector) Detect(a string, b []ptypes.Library) ([]types.DetectedVulnerability, error) { + ret := _m.Called(a, b) + ret0 := ret.Get(0) + if ret0 == nil { + return nil, ret.Error(1) + } + vulns, ok := ret0.([]types.DetectedVulnerability) + if !ok { + return nil, ret.Error(1) + } + return vulns, ret.Error(1) +} diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go new file mode 100644 index 000000000000..dfb32237717f --- /dev/null +++ b/pkg/detector/library/driver.go @@ -0,0 +1,50 @@ +package library + +import ( + "os" + + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/bundler" + "github.com/aquasecurity/trivy/pkg/detector/library/cargo" + "github.com/aquasecurity/trivy/pkg/detector/library/composer" + "github.com/aquasecurity/trivy/pkg/detector/library/node" + "github.com/aquasecurity/trivy/pkg/detector/library/python" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/knqyf263/go-version" +) + +type Driver interface { + ParseLockfile(*os.File) ([]ptypes.Library, error) + Detect(string, *version.Version) ([]types.DetectedVulnerability, error) + Type() string +} + +type Factory interface { + NewDriver(filename string) Driver +} + +type DriverFactory struct{} + +func (d DriverFactory) NewDriver(filename string) Driver { + // TODO: use DI + var scanner Driver + switch filename { + case "Gemfile.lock": + scanner = bundler.NewScanner() + case "Cargo.lock": + scanner = cargo.NewScanner() + case "composer.lock": + scanner = composer.NewScanner() + case "package-lock.json": + scanner = node.NewScanner(node.ScannerTypeNpm) + case "yarn.lock": + scanner = node.NewScanner(node.ScannerTypeYarn) + case "Pipfile.lock": + scanner = python.NewScanner(python.ScannerTypePipenv) + case "poetry.lock": + scanner = python.NewScanner(python.ScannerTypePoetry) + default: + return nil + } + return scanner +} diff --git a/pkg/scanner/library/node/scan.go b/pkg/detector/library/node/scan.go similarity index 100% rename from pkg/scanner/library/node/scan.go rename to pkg/detector/library/node/scan.go diff --git a/pkg/scanner/library/python/scan.go b/pkg/detector/library/python/scan.go similarity index 100% rename from pkg/scanner/library/python/scan.go rename to pkg/detector/library/python/scan.go diff --git a/pkg/scanner/ospkg/alpine/alpine.go b/pkg/detector/ospkg/alpine/alpine.go similarity index 100% rename from pkg/scanner/ospkg/alpine/alpine.go rename to pkg/detector/ospkg/alpine/alpine.go diff --git a/pkg/scanner/ospkg/alpine/alpine_test.go b/pkg/detector/ospkg/alpine/alpine_test.go similarity index 100% rename from pkg/scanner/ospkg/alpine/alpine_test.go rename to pkg/detector/ospkg/alpine/alpine_test.go diff --git a/pkg/scanner/ospkg/amazon/amazon.go b/pkg/detector/ospkg/amazon/amazon.go similarity index 100% rename from pkg/scanner/ospkg/amazon/amazon.go rename to pkg/detector/ospkg/amazon/amazon.go diff --git a/pkg/scanner/ospkg/amazon/amazon_test.go b/pkg/detector/ospkg/amazon/amazon_test.go similarity index 100% rename from pkg/scanner/ospkg/amazon/amazon_test.go rename to pkg/detector/ospkg/amazon/amazon_test.go diff --git a/pkg/scanner/ospkg/debian/debian.go b/pkg/detector/ospkg/debian/debian.go similarity index 100% rename from pkg/scanner/ospkg/debian/debian.go rename to pkg/detector/ospkg/debian/debian.go diff --git a/pkg/scanner/ospkg/debian/debian_test.go b/pkg/detector/ospkg/debian/debian_test.go similarity index 100% rename from pkg/scanner/ospkg/debian/debian_test.go rename to pkg/detector/ospkg/debian/debian_test.go diff --git a/pkg/detector/ospkg/detect.go b/pkg/detector/ospkg/detect.go new file mode 100644 index 000000000000..2c2e23b3daec --- /dev/null +++ b/pkg/detector/ospkg/detect.go @@ -0,0 +1,76 @@ +package ospkg + +import ( + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alpine" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/amazon" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/debian" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/google/wire" + "golang.org/x/xerrors" + + "github.com/aquasecurity/fanal/analyzer" + fos "github.com/aquasecurity/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + ErrUnsupportedOS = xerrors.New("unsupported os") + + SuperSet = wire.NewSet( + wire.Struct(new(Detector)), + wire.Bind(new(Operation), new(Detector)), + ) +) + +type Operation interface { + Detect(string, string, []analyzer.Package) ([]types.DetectedVulnerability, bool, error) +} + +type Driver interface { + Detect(string, []analyzer.Package) ([]types.DetectedVulnerability, error) + IsSupportedVersion(string, string) bool +} + +type Detector struct{} + +func (d Detector) Detect(osFamily, osName string, pkgs []analyzer.Package) ([]types.DetectedVulnerability, bool, error) { + driver := newDriver(osFamily, osName) + if driver == nil { + return nil, false, ErrUnsupportedOS + } + + eosl := !driver.IsSupportedVersion(osFamily, osName) + + vulns, err := driver.Detect(osName, pkgs) + if err != nil { + return nil, false, xerrors.Errorf("failed detection: %w", err) + } + + return vulns, eosl, nil +} + +func newDriver(osFamily, osName string) Driver { + // TODO: use DI and change struct names + var d Driver + switch osFamily { + case fos.Alpine: + d = alpine.NewScanner() + case fos.Debian: + d = debian.NewScanner() + case fos.Ubuntu: + d = ubuntu.NewScanner() + case fos.RedHat, fos.CentOS: + d = redhat.NewScanner() + case fos.Amazon: + d = amazon.NewScanner() + case fos.Oracle: + d = oracle.NewScanner() + default: + log.Logger.Warnf("unsupported os : %s", osFamily) + return nil + } + return d +} diff --git a/pkg/detector/ospkg/detector_mock.go b/pkg/detector/ospkg/detector_mock.go new file mode 100644 index 000000000000..ca8b7a1a8ef5 --- /dev/null +++ b/pkg/detector/ospkg/detector_mock.go @@ -0,0 +1,48 @@ +package ospkg + +import ( + "github.com/aquasecurity/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/stretchr/testify/mock" +) + +type MockDetector struct { + mock.Mock +} + +type DetectInput struct { + OSFamily string + OSName string + Pkgs []analyzer.Package +} +type DetectOutput struct { + Vulns []types.DetectedVulnerability + Eosl bool + Err error +} +type DetectExpectation struct { + Args DetectInput + ReturnArgs DetectOutput +} + +func NewMockDetector(detectExpectations []DetectExpectation) *MockDetector { + mockDetector := new(MockDetector) + for _, e := range detectExpectations { + mockDetector.On("Detect", e.Args.OSFamily, e.Args.OSName, e.Args.Pkgs).Return( + e.ReturnArgs.Vulns, e.ReturnArgs.Eosl, e.ReturnArgs.Err) + } + return mockDetector +} + +func (_m *MockDetector) Detect(a, b string, c []analyzer.Package) ([]types.DetectedVulnerability, bool, error) { + ret := _m.Called(a, b, c) + ret0 := ret.Get(0) + if ret0 == nil { + return nil, false, ret.Error(2) + } + vulns, ok := ret0.([]types.DetectedVulnerability) + if !ok { + return nil, false, ret.Error(2) + } + return vulns, ret.Bool(1), ret.Error(2) +} diff --git a/pkg/scanner/ospkg/oracle/oracle.go b/pkg/detector/ospkg/oracle/oracle.go similarity index 100% rename from pkg/scanner/ospkg/oracle/oracle.go rename to pkg/detector/ospkg/oracle/oracle.go diff --git a/pkg/scanner/ospkg/oracle/oracle_test.go b/pkg/detector/ospkg/oracle/oracle_test.go similarity index 100% rename from pkg/scanner/ospkg/oracle/oracle_test.go rename to pkg/detector/ospkg/oracle/oracle_test.go diff --git a/pkg/scanner/ospkg/redhat/redhat.go b/pkg/detector/ospkg/redhat/redhat.go similarity index 100% rename from pkg/scanner/ospkg/redhat/redhat.go rename to pkg/detector/ospkg/redhat/redhat.go diff --git a/pkg/scanner/ospkg/redhat/redhat_test.go b/pkg/detector/ospkg/redhat/redhat_test.go similarity index 100% rename from pkg/scanner/ospkg/redhat/redhat_test.go rename to pkg/detector/ospkg/redhat/redhat_test.go diff --git a/pkg/scanner/ospkg/ubuntu/ubnutu_test.go b/pkg/detector/ospkg/ubuntu/ubnutu_test.go similarity index 100% rename from pkg/scanner/ospkg/ubuntu/ubnutu_test.go rename to pkg/detector/ospkg/ubuntu/ubnutu_test.go diff --git a/pkg/scanner/ospkg/ubuntu/ubuntu.go b/pkg/detector/ospkg/ubuntu/ubuntu.go similarity index 100% rename from pkg/scanner/ospkg/ubuntu/ubuntu.go rename to pkg/detector/ospkg/ubuntu/ubuntu.go diff --git a/pkg/github/github.go b/pkg/github/github.go index a6a00ee8c61f..2883319d6ead 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -41,6 +41,10 @@ func (r Repository) DownloadAsset(ctx context.Context, id int64) (io.ReadCloser, return r.repository.DownloadReleaseAsset(ctx, r.owner, r.repoName, id) } +type Operation interface { + DownloadDB(ctx context.Context, fileName string) (io.ReadCloser, error) +} + type Client struct { Repository RepositoryInterface } diff --git a/pkg/github/github_mock.go b/pkg/github/github_mock.go new file mode 100644 index 000000000000..1bb1f4d41cd8 --- /dev/null +++ b/pkg/github/github_mock.go @@ -0,0 +1,25 @@ +package github + +import ( + "context" + "io" + + "github.com/stretchr/testify/mock" +) + +type MockClient struct { + mock.Mock +} + +func (_m *MockClient) DownloadDB(ctx context.Context, fileName string) (io.ReadCloser, error) { + ret := _m.Called(ctx, fileName) + ret0 := ret.Get(0) + if ret0 == nil { + return nil, ret.Error(1) + } + rc, ok := ret0.(io.ReadCloser) + if !ok { + return nil, ret.Error(1) + } + return rc, ret.Error(1) +} diff --git a/pkg/rpc/client/library/client.go b/pkg/rpc/client/library/client.go new file mode 100644 index 000000000000..fb2f76e99503 --- /dev/null +++ b/pkg/rpc/client/library/client.go @@ -0,0 +1,53 @@ +package library + +import ( + "context" + "net/http" + + "github.com/aquasecurity/trivy/pkg/rpc/client" + + "github.com/google/wire" + "golang.org/x/xerrors" + + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + detector "github.com/aquasecurity/trivy/pkg/detector/library" + r "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/types" + rpc "github.com/aquasecurity/trivy/rpc/detector" +) + +var SuperSet = wire.NewSet( + NewProtobufClient, + NewDetector, + wire.Bind(new(detector.Operation), new(Detector)), +) + +type RemoteURL string + +func NewProtobufClient(remoteURL RemoteURL) rpc.LibDetector { + return rpc.NewLibDetectorProtobufClient(string(remoteURL), &http.Client{}) +} + +type Token string + +type Detector struct { + token Token + client rpc.LibDetector +} + +func NewDetector(token Token, detector rpc.LibDetector) Detector { + return Detector{token: token, client: detector} +} + +func (d Detector) Detect(filePath string, libs []ptypes.Library) ([]types.DetectedVulnerability, error) { + ctx := client.WithToken(context.Background(), string(d.token)) + res, err := d.client.Detect(ctx, &rpc.LibDetectRequest{ + FilePath: filePath, + Libraries: r.ConvertToRpcLibraries(libs), + }) + if err != nil { + return nil, xerrors.Errorf("failed to detect vulnerabilities via RPC: %w", err) + } + + return r.ConvertFromRpcVulns(res.Vulnerabilities), nil +} diff --git a/pkg/rpc/client/library/client_test.go b/pkg/rpc/client/library/client_test.go new file mode 100644 index 000000000000..64b1c2ea2889 --- /dev/null +++ b/pkg/rpc/client/library/client_test.go @@ -0,0 +1,157 @@ +package library + +import ( + "context" + "testing" + + "golang.org/x/xerrors" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/rpc/detector" + "github.com/stretchr/testify/mock" +) + +type mockDetector struct { + mock.Mock +} + +func (_m *mockDetector) Detect(a context.Context, b *detector.LibDetectRequest) (*detector.DetectResponse, error) { + ret := _m.Called(a, b) + ret0 := ret.Get(0) + if ret0 == nil { + return nil, ret.Error(1) + } + res, ok := ret0.(*detector.DetectResponse) + if !ok { + return nil, ret.Error(1) + } + return res, ret.Error(1) +} + +func TestDetectClient_Detect(t *testing.T) { + type detectInput struct { + req *detector.LibDetectRequest + } + type detectOutput struct { + res *detector.DetectResponse + err error + } + type detect struct { + input detectInput + output detectOutput + } + + type fields struct { + token Token + } + type args struct { + filePath string + libs []ptypes.Library + } + tests := []struct { + name string + fields fields + args args + detect detect + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fields: fields{ + token: "token", + }, + args: args{ + filePath: "app/Pipfile.lock", + libs: []ptypes.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + detect: detect{ + input: detectInput{req: &detector.LibDetectRequest{ + FilePath: "app/Pipfile.lock", + Libraries: []*detector.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + }, + output: detectOutput{ + res: &detector.DetectResponse{ + Vulnerabilities: []*detector.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "django", + InstalledVersion: "3.0.0", + FixedVersion: "3.0.1", + Title: "RCE", + Description: "Remote Code Execution", + Severity: detector.Severity_CRITICAL, + }, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "django", + InstalledVersion: "3.0.0", + FixedVersion: "3.0.1", + Vulnerability: dbTypes.Vulnerability{ + Title: "RCE", + Description: "Remote Code Execution", + Severity: "CRITICAL", + }, + }, + }, + }, + { + name: "Detect returns an error", + fields: fields{}, + args: args{ + filePath: "app/Pipfile.lock", + libs: []ptypes.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + detect: detect{ + input: detectInput{req: &detector.LibDetectRequest{ + FilePath: "app/Pipfile.lock", + Libraries: []*detector.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + }, + output: detectOutput{ + err: xerrors.New("error"), + }, + }, + wantErr: "failed to detect vulnerabilities via RPC", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := new(mockDetector) + mockDetector.On("Detect", mock.Anything, tt.detect.input.req).Return( + tt.detect.output.res, tt.detect.output.err) + + d := NewDetector(tt.fields.token, mockDetector) + got, err := d.Detect(tt.args.filePath, tt.args.libs) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + assert.Equal(t, tt.want, got, tt.name) + mockDetector.AssertExpectations(t) + }) + } +} diff --git a/pkg/rpc/client/ospkg/client.go b/pkg/rpc/client/ospkg/client.go new file mode 100644 index 000000000000..8c1706bf762e --- /dev/null +++ b/pkg/rpc/client/ospkg/client.go @@ -0,0 +1,54 @@ +package ospkg + +import ( + "context" + "net/http" + + "github.com/aquasecurity/trivy/pkg/rpc/client" + + "github.com/google/wire" + "golang.org/x/xerrors" + + "github.com/aquasecurity/fanal/analyzer" + detector "github.com/aquasecurity/trivy/pkg/detector/ospkg" + r "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/types" + rpc "github.com/aquasecurity/trivy/rpc/detector" +) + +var SuperSet = wire.NewSet( + NewProtobufClient, + NewDetector, + wire.Bind(new(detector.Operation), new(Detector)), +) + +type RemoteURL string + +func NewProtobufClient(remoteURL RemoteURL) rpc.OSDetector { + return rpc.NewOSDetectorProtobufClient(string(remoteURL), &http.Client{}) +} + +type Token string + +type Detector struct { + token Token + client rpc.OSDetector +} + +func NewDetector(token Token, detector rpc.OSDetector) Detector { + return Detector{token: token, client: detector} +} + +func (d Detector) Detect(osFamily, osName string, pkgs []analyzer.Package) ([]types.DetectedVulnerability, bool, error) { + ctx := client.WithToken(context.Background(), string(d.token)) + res, err := d.client.Detect(ctx, &rpc.OSDetectRequest{ + OsFamily: osFamily, + OsName: osName, + Packages: r.ConvertToRpcPkgs(pkgs), + }) + if err != nil { + return nil, false, xerrors.Errorf("failed to detect vulnerabilities via RPC: %w", err) + } + + return r.ConvertFromRpcVulns(res.Vulnerabilities), res.Eosl, nil +} diff --git a/pkg/rpc/client/ospkg/client_test.go b/pkg/rpc/client/ospkg/client_test.go new file mode 100644 index 000000000000..6f8d34f67d34 --- /dev/null +++ b/pkg/rpc/client/ospkg/client_test.go @@ -0,0 +1,184 @@ +package ospkg + +import ( + "context" + "testing" + + "golang.org/x/xerrors" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/fanal/analyzer" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/rpc/detector" + "github.com/stretchr/testify/mock" +) + +type mockDetector struct { + mock.Mock +} + +func (_m *mockDetector) Detect(a context.Context, b *detector.OSDetectRequest) (*detector.DetectResponse, error) { + ret := _m.Called(a, b) + ret0 := ret.Get(0) + if ret0 == nil { + return nil, ret.Error(1) + } + res, ok := ret0.(*detector.DetectResponse) + if !ok { + return nil, ret.Error(1) + } + return res, ret.Error(1) +} + +func TestDetectClient_Detect(t *testing.T) { + type detectInput struct { + req *detector.OSDetectRequest + } + type detectOutput struct { + res *detector.DetectResponse + err error + } + type detect struct { + input detectInput + output detectOutput + } + + type fields struct { + token Token + } + type args struct { + osFamily string + osName string + pkgs []analyzer.Package + } + tests := []struct { + name string + fields fields + args args + detect detect + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fields: fields{ + token: "token", + }, + args: args{ + osFamily: "alpine", + osName: "3.10.2", + pkgs: []analyzer.Package{ + { + Name: "openssl", + Version: "1.0.1e", + Release: "1", + Epoch: 0, + }, + }, + }, + detect: detect{ + input: detectInput{ + req: &detector.OSDetectRequest{ + OsFamily: "alpine", + OsName: "3.10.2", + Packages: []*detector.Package{ + { + Name: "openssl", + Version: "1.0.1e", + Release: "1", + Epoch: 0, + }, + }, + }, + }, + output: detectOutput{ + res: &detector.DetectResponse{ + Vulnerabilities: []*detector.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "bash", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "RCE", + Description: "Remote Code Execution", + Severity: detector.Severity_HIGH, + }, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "bash", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "RCE", + Description: "Remote Code Execution", + Severity: "HIGH", + }, + }, + }, + }, + { + name: "Detect returns an error", + fields: fields{}, + args: args{ + osFamily: "alpine", + osName: "3.10.2", + pkgs: []analyzer.Package{ + { + Name: "openssl", + Version: "1.0.1e", + Release: "1", + Epoch: 0, + }, + }, + }, + detect: detect{ + input: detectInput{ + req: &detector.OSDetectRequest{ + OsFamily: "alpine", + OsName: "3.10.2", + Packages: []*detector.Package{ + { + Name: "openssl", + Version: "1.0.1e", + Release: "1", + Epoch: 0, + }, + }, + }, + }, + output: detectOutput{ + err: xerrors.New("error"), + }, + }, + wantErr: "failed to detect vulnerabilities via RPC", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := new(mockDetector) + mockDetector.On("Detect", mock.Anything, tt.detect.input.req).Return( + tt.detect.output.res, tt.detect.output.err) + + d := NewDetector(tt.fields.token, mockDetector) + got, _, err := d.Detect(tt.args.osFamily, tt.args.osName, tt.args.pkgs) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + assert.Equal(t, tt.want, got, tt.name) + mockDetector.AssertExpectations(t) + }) + } +} diff --git a/pkg/rpc/client/token.go b/pkg/rpc/client/token.go new file mode 100644 index 000000000000..fa71f83f7961 --- /dev/null +++ b/pkg/rpc/client/token.go @@ -0,0 +1,35 @@ +package client + +import ( + "context" + "net/http" + + "github.com/twitchtv/twirp" + + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + buildRequestHeaderFunc = buildRequestHeader +) + +func buildRequestHeader(inputHeaders map[string]string) http.Header { + header := make(http.Header) + for k, v := range inputHeaders { + header.Set(k, v) + } + return header +} + +func WithToken(ctx context.Context, token string) context.Context { + // Prepare custom header + header := buildRequestHeaderFunc(map[string]string{"Trivy-Token": token}) + + // Attach the headers to a context + ctxWithToken, err := twirp.WithHTTPRequestHeaders(ctx, header) + if err != nil { + log.Logger.Warnf("twirp error setting headers: %s", err) + return ctx + } + return ctxWithToken +} diff --git a/pkg/rpc/client/token_test.go b/pkg/rpc/client/token_test.go new file mode 100644 index 000000000000..2ce79da3d469 --- /dev/null +++ b/pkg/rpc/client/token_test.go @@ -0,0 +1,72 @@ +package client + +import ( + "context" + "net/http" + "os" + "testing" + + "github.com/twitchtv/twirp" + + "github.com/aquasecurity/trivy/pkg/log" + + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + _ = log.InitLogger(false, true) + os.Exit(m.Run()) +} + +func TestWithToken(t *testing.T) { + type args struct { + ctx context.Context + token string + } + tests := []struct { + name string + args args + buildRequestHeaderFunc func(map[string]string) http.Header + want http.Header + }{ + { + name: "happy path", + args: args{ + ctx: context.Background(), + token: "token", + }, + want: http.Header{ + "Trivy-Token": []string{"token"}, + }, + buildRequestHeaderFunc: buildRequestHeader, + }, + { + name: "sad path, invalid headers passed in", + args: args{ + ctx: context.Background(), + token: "token", + }, + want: http.Header(nil), + buildRequestHeaderFunc: func(m map[string]string) http.Header { + header := make(http.Header) + for k, v := range m { + header.Set(k, v) + } + + // add an extra header that is reserved for twirp + header.Set("Content-Type", "foobar") + return header + }, + }, + } + for _, tt := range tests { + oldbuildRequestHeaderFunc := buildRequestHeaderFunc + defer func() { + buildRequestHeaderFunc = oldbuildRequestHeaderFunc + }() + buildRequestHeaderFunc = tt.buildRequestHeaderFunc + gotCtx := WithToken(tt.args.ctx, tt.args.token) + header, _ := twirp.HTTPRequestHeaders(gotCtx) + assert.Equal(t, tt.want, header, tt.name) + } +} diff --git a/pkg/rpc/convert.go b/pkg/rpc/convert.go new file mode 100644 index 000000000000..eaf4df7b5231 --- /dev/null +++ b/pkg/rpc/convert.go @@ -0,0 +1,110 @@ +package rpc + +import ( + "github.com/aquasecurity/fanal/analyzer" + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/rpc/detector" +) + +func ConvertToRpcPkgs(pkgs []analyzer.Package) []*detector.Package { + var rpcPkgs []*detector.Package + for _, pkg := range pkgs { + rpcPkgs = append(rpcPkgs, &detector.Package{ + Name: pkg.Name, + Version: pkg.Version, + Release: pkg.Release, + Epoch: int32(pkg.Epoch), + Arch: pkg.Arch, + SrcName: pkg.SrcName, + SrcVersion: pkg.SrcVersion, + SrcRelease: pkg.SrcRelease, + SrcEpoch: int32(pkg.SrcEpoch), + }) + } + return rpcPkgs +} + +func ConvertFromRpcPkgs(rpcPkgs []*detector.Package) []analyzer.Package { + var pkgs []analyzer.Package + for _, pkg := range rpcPkgs { + pkgs = append(pkgs, analyzer.Package{ + Name: pkg.Name, + Version: pkg.Version, + Release: pkg.Release, + Epoch: int(pkg.Epoch), + Arch: pkg.Arch, + SrcName: pkg.SrcName, + SrcVersion: pkg.SrcVersion, + SrcRelease: pkg.SrcRelease, + SrcEpoch: int(pkg.SrcEpoch), + }) + } + return pkgs +} + +func ConvertFromRpcLibraries(rpcLibs []*detector.Library) []ptypes.Library { + var libs []ptypes.Library + for _, l := range rpcLibs { + libs = append(libs, ptypes.Library{ + Name: l.Name, + Version: l.Version, + }) + } + return libs +} + +func ConvertToRpcLibraries(libs []ptypes.Library) []*detector.Library { + var rpcLibs []*detector.Library + for _, l := range libs { + rpcLibs = append(rpcLibs, &detector.Library{ + Name: l.Name, + Version: l.Version, + }) + } + return rpcLibs +} + +func ConvertFromRpcVulns(rpcVulns []*detector.Vulnerability) []types.DetectedVulnerability { + var vulns []types.DetectedVulnerability + for _, vuln := range rpcVulns { + severity := dbTypes.Severity(vuln.Severity) + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: vuln.VulnerabilityId, + PkgName: vuln.PkgName, + InstalledVersion: vuln.InstalledVersion, + FixedVersion: vuln.FixedVersion, + Vulnerability: dbTypes.Vulnerability{ + Title: vuln.Title, + Description: vuln.Description, + Severity: severity.String(), + References: vuln.References, + }, + }) + } + return vulns +} + +func ConvertToRpcVulns(vulns []types.DetectedVulnerability) []*detector.Vulnerability { + var rpcVulns []*detector.Vulnerability + for _, vuln := range vulns { + severity, err := dbTypes.NewSeverity(vuln.Severity) + if err != nil { + log.Logger.Warn(err) + } + + rpcVulns = append(rpcVulns, &detector.Vulnerability{ + VulnerabilityId: vuln.VulnerabilityID, + PkgName: vuln.PkgName, + InstalledVersion: vuln.InstalledVersion, + FixedVersion: vuln.FixedVersion, + Title: vuln.Title, + Description: vuln.Description, + Severity: detector.Severity(severity), + References: vuln.References, + }) + } + return rpcVulns +} diff --git a/pkg/rpc/convert_test.go b/pkg/rpc/convert_test.go new file mode 100644 index 000000000000..c31be065efc1 --- /dev/null +++ b/pkg/rpc/convert_test.go @@ -0,0 +1,309 @@ +package rpc + +import ( + "os" + "testing" + + "github.com/aquasecurity/trivy/pkg/log" + + "github.com/aquasecurity/fanal/analyzer" + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/rpc/detector" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, false) + code := m.Run() + os.Exit(code) +} + +func TestConvertToRpcPkgs(t *testing.T) { + type args struct { + pkgs []analyzer.Package + } + tests := []struct { + name string + args args + want []*detector.Package + }{ + { + name: "happy path", + args: args{ + pkgs: []analyzer.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + }, + }, + }, + want: []*detector.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRpcPkgs(tt.args.pkgs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRpcPkgs(t *testing.T) { + type args struct { + rpcPkgs []*detector.Package + } + tests := []struct { + name string + args args + want []analyzer.Package + }{ + { + args: args{ + rpcPkgs: []*detector.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + }, + }, + }, + want: []analyzer.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRpcPkgs(tt.args.rpcPkgs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRpcLibraries(t *testing.T) { + type args struct { + rpcLibs []*detector.Library + } + tests := []struct { + name string + args args + want []ptypes.Library + }{ + { + name: "happy path", + args: args{ + rpcLibs: []*detector.Library{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "4.5.6"}, + }, + }, + want: []ptypes.Library{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "4.5.6"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRpcLibraries(tt.args.rpcLibs) + assert.Equal(t, got, tt.want, tt.name) + }) + } +} + +func TestConvertToRpcLibraries(t *testing.T) { + type args struct { + libs []ptypes.Library + } + tests := []struct { + name string + args args + want []*detector.Library + }{ + { + name: "happy path", + args: args{ + libs: []ptypes.Library{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "4.5.6"}, + }, + }, + want: []*detector.Library{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "4.5.6"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRpcLibraries(tt.args.libs) + assert.Equal(t, got, tt.want, tt.name) + }) + } +} + +func TestConvertFromRpcVulns(t *testing.T) { + type args struct { + rpcVulns []*detector.Vulnerability + } + tests := []struct { + name string + args args + want []types.DetectedVulnerability + }{ + { + name: "happy path", + args: args{ + rpcVulns: []*detector.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: detector.Severity_CRITICAL, + References: []string{"http://example.com"}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: "CRITICAL", + References: []string{"http://example.com"}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRpcVulns(tt.args.rpcVulns) + assert.Equal(t, got, tt.want, tt.name) + }) + } +} + +func TestConvertToRpcVulns(t *testing.T) { + type args struct { + vulns []types.DetectedVulnerability + } + tests := []struct { + name string + args args + want []*detector.Vulnerability + }{ + { + name: "happy path", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: "MEDIUM", + References: []string{"http://example.com"}, + }, + }, + }, + }, + want: []*detector.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: detector.Severity_MEDIUM, + References: []string{"http://example.com"}, + }, + }, + }, + { + name: "invalid severity", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: "INVALID", + References: []string{"http://example.com"}, + }, + }, + }, + }, + want: []*detector.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: detector.Severity_UNKNOWN, + References: []string{"http://example.com"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRpcVulns(tt.args.vulns) + assert.Equal(t, got, tt.want, tt.name) + }) + } +} diff --git a/pkg/rpc/server/inject.go b/pkg/rpc/server/inject.go new file mode 100644 index 000000000000..d5d989eeaec8 --- /dev/null +++ b/pkg/rpc/server/inject.go @@ -0,0 +1,25 @@ +// +build wireinject + +package server + +import ( + "github.com/google/wire" + + "github.com/aquasecurity/trivy/pkg/rpc/server/library" + "github.com/aquasecurity/trivy/pkg/rpc/server/ospkg" +) + +func initializeOspkgServer() *ospkg.Server { + wire.Build(ospkg.SuperSet) + return &ospkg.Server{} +} + +func initializeLibServer() *library.Server { + wire.Build(library.SuperSet) + return &library.Server{} +} + +func initializeDBWorker() dbWorker { + wire.Build(SuperSet) + return dbWorker{} +} diff --git a/pkg/rpc/server/library/server.go b/pkg/rpc/server/library/server.go new file mode 100644 index 000000000000..737ea2f8c6f4 --- /dev/null +++ b/pkg/rpc/server/library/server.go @@ -0,0 +1,42 @@ +package library + +import ( + "context" + + "github.com/google/wire" + "golang.org/x/xerrors" + + detector "github.com/aquasecurity/trivy/pkg/detector/library" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/vulnerability" + proto "github.com/aquasecurity/trivy/rpc/detector" +) + +var SuperSet = wire.NewSet( + detector.SuperSet, + vulnerability.SuperSet, + NewServer, +) + +type Server struct { + detector detector.Operation + vulnClient vulnerability.Operation +} + +func NewServer(detector detector.Operation, vulnClient vulnerability.Operation) *Server { + return &Server{detector: detector, vulnClient: vulnClient} +} + +func (s *Server) Detect(ctx context.Context, req *proto.LibDetectRequest) (res *proto.DetectResponse, err error) { + vulns, err := s.detector.Detect(req.FilePath, rpc.ConvertFromRpcLibraries(req.Libraries)) + if err != nil { + err = xerrors.Errorf("failed to detect library vulnerabilities: %w", err) + log.Logger.Error(err) + return nil, err + } + + s.vulnClient.FillInfo(vulns, false) + + return &proto.DetectResponse{Vulnerabilities: rpc.ConvertToRpcVulns(vulns)}, nil +} diff --git a/pkg/rpc/server/library/server_test.go b/pkg/rpc/server/library/server_test.go new file mode 100644 index 000000000000..1414bd85c476 --- /dev/null +++ b/pkg/rpc/server/library/server_test.go @@ -0,0 +1,135 @@ +package library + +import ( + "context" + "os" + "testing" + + "github.com/aquasecurity/trivy/pkg/detector/library" + + "github.com/aquasecurity/trivy/pkg/log" + + "golang.org/x/xerrors" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" + proto "github.com/aquasecurity/trivy/rpc/detector" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, false) + code := m.Run() + os.Exit(code) +} + +func TestServer_Detect(t *testing.T) { + type args struct { + req *proto.LibDetectRequest + } + tests := []struct { + name string + args args + detect library.DetectExpectation + wantRes *proto.DetectResponse + wantErr string + }{ + { + name: "happy path", + args: args{ + req: &proto.LibDetectRequest{ + FilePath: "app/Pipfile.lock", + Libraries: []*proto.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + }, + detect: library.DetectExpectation{ + Args: library.DetectInput{ + FilePath: "app/Pipfile.lock", + Libs: []ptypes.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + ReturnArgs: library.DetectOutput{ + Vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "test", + InstalledVersion: "1", + FixedVersion: "2", + Vulnerability: dbTypes.Vulnerability{ + Title: "title", + Description: "description", + Severity: "MEDIUM", + References: []string{"http://example.com"}, + }, + }, + }, + }, + }, + wantRes: &proto.DetectResponse{ + Vulnerabilities: []*proto.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "test", + InstalledVersion: "1", + FixedVersion: "2", + Title: "title", + Description: "description", + Severity: proto.Severity_MEDIUM, + References: []string{"http://example.com"}, + }, + }, + }, + }, + { + name: "Detect returns an error", + args: args{ + req: &proto.LibDetectRequest{ + FilePath: "app/Pipfile.lock", + Libraries: []*proto.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + }, + detect: library.DetectExpectation{ + Args: library.DetectInput{ + FilePath: "app/Pipfile.lock", + Libs: []ptypes.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + ReturnArgs: library.DetectOutput{ + Err: xerrors.New("error"), + }, + }, + wantErr: "failed to detect library vulnerabilities", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := library.NewMockDetector([]library.DetectExpectation{tt.detect}) + mockVulnClient := vulnerability.NewMockVulnClient() + + s := NewServer(mockDetector, mockVulnClient) + ctx := context.TODO() + gotRes, err := s.Detect(ctx, tt.args.req) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.wantRes, gotRes, tt.name) + mockDetector.AssertExpectations(t) + mockVulnClient.AssertExpectations(t) + }) + } +} diff --git a/pkg/rpc/server/ospkg/server.go b/pkg/rpc/server/ospkg/server.go new file mode 100644 index 000000000000..4c376066b6a0 --- /dev/null +++ b/pkg/rpc/server/ospkg/server.go @@ -0,0 +1,42 @@ +package ospkg + +import ( + "context" + + "github.com/google/wire" + "golang.org/x/xerrors" + + detector "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/vulnerability" + proto "github.com/aquasecurity/trivy/rpc/detector" +) + +var SuperSet = wire.NewSet( + detector.SuperSet, + vulnerability.SuperSet, + NewServer, +) + +type Server struct { + detector detector.Operation + vulnClient vulnerability.Operation +} + +func NewServer(detector detector.Operation, vulnClient vulnerability.Operation) *Server { + return &Server{detector: detector, vulnClient: vulnClient} +} + +func (s *Server) Detect(ctx context.Context, req *proto.OSDetectRequest) (res *proto.DetectResponse, err error) { + vulns, eosl, err := s.detector.Detect(req.OsFamily, req.OsName, rpc.ConvertFromRpcPkgs(req.Packages)) + if err != nil { + err = xerrors.Errorf("failed to detect vulnerabilities of OS packages: %w", err) + log.Logger.Error(err) + return nil, err + } + + s.vulnClient.FillInfo(vulns, false) + + return &proto.DetectResponse{Vulnerabilities: rpc.ConvertToRpcVulns(vulns), Eosl: eosl}, nil +} diff --git a/pkg/rpc/server/ospkg/server_test.go b/pkg/rpc/server/ospkg/server_test.go new file mode 100644 index 000000000000..9eedc0932639 --- /dev/null +++ b/pkg/rpc/server/ospkg/server_test.go @@ -0,0 +1,125 @@ +package ospkg + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/aquasecurity/fanal/analyzer" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" + proto "github.com/aquasecurity/trivy/rpc/detector" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, false) + code := m.Run() + os.Exit(code) +} + +func TestServer_Detect(t *testing.T) { + type args struct { + req *proto.OSDetectRequest + } + tests := []struct { + name string + args args + detect ospkg.DetectExpectation + wantRes *proto.DetectResponse + wantErr string + }{ + { + name: "happy path", + args: args{ + req: &proto.OSDetectRequest{ + OsFamily: "alpine", + OsName: "3.10.2", + Packages: []*proto.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + }, + detect: ospkg.DetectExpectation{ + Args: ospkg.DetectInput{ + OSFamily: "alpine", + OSName: "3.10.2", + Pkgs: []analyzer.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + ReturnArgs: ospkg.DetectOutput{ + Eosl: false, + Vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "musl", + Vulnerability: dbTypes.Vulnerability{ + Severity: "HIGH", + }}, + }, + }, + }, + wantRes: &proto.DetectResponse{ + Vulnerabilities: []*proto.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "musl", + Severity: proto.Severity_HIGH, + }, + }, + }, + }, + { + name: "Detect returns an error", + args: args{ + req: &proto.OSDetectRequest{ + OsFamily: "alpine", + OsName: "3.10.2", + Packages: []*proto.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + }, + detect: ospkg.DetectExpectation{ + Args: ospkg.DetectInput{ + OSFamily: "alpine", + OSName: "3.10.2", + Pkgs: []analyzer.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + ReturnArgs: ospkg.DetectOutput{ + Err: xerrors.New("error"), + }, + }, + wantErr: "failed to detect vulnerabilities of OS packages: error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := ospkg.NewMockDetector([]ospkg.DetectExpectation{tt.detect}) + mockVulnClient := vulnerability.NewMockVulnClient() + + s := NewServer(mockDetector, mockVulnClient) + gotRes, err := s.Detect(context.TODO(), tt.args.req) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.wantRes, gotRes, tt.name) + mockDetector.AssertExpectations(t) + mockVulnClient.AssertExpectations(t) + }) + } +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go new file mode 100644 index 000000000000..bca8adfb9f36 --- /dev/null +++ b/pkg/rpc/server/server.go @@ -0,0 +1,137 @@ +package server + +import ( + "context" + "io/ioutil" + "net/http" + "os" + "sync" + "time" + + "github.com/google/wire" + + "github.com/twitchtv/twirp" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/internal/server/config" + dbFile "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils" + rpc "github.com/aquasecurity/trivy/rpc/detector" +) + +var SuperSet = wire.NewSet( + dbFile.SuperSet, + newDBWorker, +) + +func ListenAndServe(addr string, c config.Config) error { + requestWg := &sync.WaitGroup{} + dbUpdateWg := &sync.WaitGroup{} + + withWaitGroup := func(base http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Stop processing requests during DB update + dbUpdateWg.Wait() + + // Wait for all requests to be processed before DB update + requestWg.Add(1) + defer requestWg.Done() + + base.ServeHTTP(w, r) + + }) + } + + go func() { + worker := initializeDBWorker() + ctx := context.Background() + for { + time.Sleep(1 * time.Hour) + if err := worker.update(ctx, c.AppVersion, c.CacheDir, dbUpdateWg, requestWg); err != nil { + log.Logger.Errorf("%+v\n", err) + } + } + }() + + mux := http.NewServeMux() + + osHandler := rpc.NewOSDetectorServer(initializeOspkgServer(), nil) + mux.Handle(rpc.OSDetectorPathPrefix, withToken(withWaitGroup(osHandler), c.Token)) + + libHandler := rpc.NewLibDetectorServer(initializeLibServer(), nil) + mux.Handle(rpc.LibDetectorPathPrefix, withToken(withWaitGroup(libHandler), c.Token)) + + log.Logger.Infof("Listening %s...", addr) + + return http.ListenAndServe(addr, mux) +} + +func withToken(base http.Handler, token string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if token != "" && token != r.Header.Get("Trivy-Token") { + rpc.WriteError(w, twirp.NewError(twirp.Unauthenticated, "invalid token")) + return + } + base.ServeHTTP(w, r) + }) +} + +type dbWorker struct { + dbClient dbFile.Operation +} + +func newDBWorker(dbClient dbFile.Operation) dbWorker { + return dbWorker{dbClient: dbClient} +} + +func (w dbWorker) update(ctx context.Context, appVersion, cacheDir string, + dbUpdateWg, requestWg *sync.WaitGroup) error { + needsUpdate, err := w.dbClient.NeedsUpdate(ctx, appVersion, false, false) + if err != nil { + return xerrors.Errorf("failed to check if db needs an update") + } else if !needsUpdate { + return nil + } + + log.Logger.Info("Updating DB...") + if err = w.hotUpdate(ctx, cacheDir, dbUpdateWg, requestWg); err != nil { + return xerrors.Errorf("failed DB hot update") + } + return nil +} + +func (w dbWorker) hotUpdate(ctx context.Context, cacheDir string, dbUpdateWg, requestWg *sync.WaitGroup) error { + tmpDir, err := ioutil.TempDir("", "db") + if err != nil { + return xerrors.Errorf("failed to create a temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err := w.dbClient.Download(ctx, tmpDir, false); err != nil { + return xerrors.Errorf("failed to download vulnerability DB: %w", err) + } + + log.Logger.Info("Suspending all requests during DB update") + dbUpdateWg.Add(1) + defer dbUpdateWg.Done() + + log.Logger.Info("Waiting for all requests to be processed before DB update...") + requestWg.Wait() + + if err = db.Close(); err != nil { + return xerrors.Errorf("failed to close DB: %w", err) + } + + if _, err = utils.CopyFile(db.Path(tmpDir), db.Path(cacheDir)); err != nil { + return xerrors.Errorf("failed to copy the database file: %w", err) + } + + log.Logger.Info("Reopening DB...") + if err = db.Init(cacheDir); err != nil { + return xerrors.Errorf("failed to open DB: %w", err) + } + + return nil +} diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go new file mode 100644 index 000000000000..497930f5e09a --- /dev/null +++ b/pkg/rpc/server/server_test.go @@ -0,0 +1,157 @@ +package server + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbFile "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, false) + os.Exit(m.Run()) +} + +func Test_dbWorker_update(t *testing.T) { + type needsUpdateInput struct { + appVersion string + skip bool + } + type needsUpdateOutput struct { + needsUpdate bool + err error + } + type needsUpdate struct { + input needsUpdateInput + output needsUpdateOutput + } + + type download struct { + call bool + err error + } + + type args struct { + appVersion string + } + tests := []struct { + name string + needsUpdate needsUpdate + download download + args args + want db.Metadata + wantErr string + }{ + { + name: "happy path", + needsUpdate: needsUpdate{ + input: needsUpdateInput{appVersion: "1", skip: false}, + output: needsUpdateOutput{needsUpdate: true}, + }, + download: download{ + call: true, + }, + args: args{appVersion: "1"}, + want: db.Metadata{ + Version: 1, + Type: db.TypeFull, + NextUpdate: time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "not update", + needsUpdate: needsUpdate{ + input: needsUpdateInput{appVersion: "1", skip: false}, + output: needsUpdateOutput{needsUpdate: false}, + }, + args: args{appVersion: "1"}, + }, + { + name: "NeedsUpdate returns an error", + needsUpdate: needsUpdate{ + input: needsUpdateInput{appVersion: "1", skip: false}, + output: needsUpdateOutput{err: xerrors.New("fail")}, + }, + args: args{appVersion: "1"}, + wantErr: "failed to check if db needs an update", + }, + { + name: "Download returns an error", + needsUpdate: needsUpdate{ + input: needsUpdateInput{appVersion: "1", skip: false}, + output: needsUpdateOutput{needsUpdate: true}, + }, + download: download{ + call: true, + err: xerrors.New("fail"), + }, + args: args{appVersion: "1"}, + wantErr: "failed DB hot update", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cacheDir, err := ioutil.TempDir("", "server-test") + require.NoError(t, err, tt.name) + + require.NoError(t, db.Init(cacheDir), tt.name) + + mockDBClient := new(dbFile.MockClient) + mockDBClient.On("NeedsUpdate", mock.Anything, + tt.needsUpdate.input.appVersion, false, tt.needsUpdate.input.skip).Return( + tt.needsUpdate.output.needsUpdate, tt.needsUpdate.output.err) + + if tt.download.call { + mockDBClient.On("Download", mock.Anything, mock.Anything, false).Run( + func(args mock.Arguments) { + // fake download: copy testdata/new.db to tmpDir/db/trivy.db + content, err := ioutil.ReadFile("testdata/new.db") + require.NoError(t, err, tt.name) + + tmpDir := args.String(1) + dbPath := db.Path(tmpDir) + require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0777), tt.name) + err = ioutil.WriteFile(dbPath, content, 0444) + require.NoError(t, err, tt.name) + }).Return(tt.download.err) + } + + w := newDBWorker(mockDBClient) + + var dbUpdateWg, requestWg sync.WaitGroup + err = w.update(context.Background(), tt.args.appVersion, cacheDir, + &dbUpdateWg, &requestWg) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + + if !tt.download.call { + return + } + + dbc := db.Config{} + got, err := dbc.GetMetadata() + assert.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got, tt.name) + + mockDBClient.AssertExpectations(t) + }) + } +} diff --git a/pkg/rpc/server/testdata/new.db b/pkg/rpc/server/testdata/new.db new file mode 100644 index 000000000000..4ee279a13d3a Binary files /dev/null and b/pkg/rpc/server/testdata/new.db differ diff --git a/pkg/rpc/server/wire_gen.go b/pkg/rpc/server/wire_gen.go new file mode 100644 index 000000000000..150c75773495 --- /dev/null +++ b/pkg/rpc/server/wire_gen.go @@ -0,0 +1,46 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package server + +import ( + "github.com/aquasecurity/trivy-db/pkg/db" + db2 "github.com/aquasecurity/trivy/pkg/db" + library2 "github.com/aquasecurity/trivy/pkg/detector/library" + ospkg2 "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/github" + "github.com/aquasecurity/trivy/pkg/rpc/server/library" + "github.com/aquasecurity/trivy/pkg/rpc/server/ospkg" + "github.com/aquasecurity/trivy/pkg/vulnerability" + "k8s.io/utils/clock" +) + +// Injectors from inject.go: + +func initializeOspkgServer() *ospkg.Server { + detector := ospkg2.Detector{} + config := db.Config{} + client := vulnerability.NewClient(config) + server := ospkg.NewServer(detector, client) + return server +} + +func initializeLibServer() *library.Server { + driverFactory := library2.DriverFactory{} + detector := library2.NewDetector(driverFactory) + config := db.Config{} + client := vulnerability.NewClient(config) + server := library.NewServer(detector, client) + return server +} + +func initializeDBWorker() dbWorker { + config := db.Config{} + client := github.NewClient() + realClock := clock.RealClock{} + dbClient := db2.NewClient(config, client, realClock) + serverDbWorker := newDBWorker(dbClient) + return serverDbWorker +} diff --git a/pkg/scanner/library/scan.go b/pkg/scanner/library/scan.go index 1ecdc8738aa8..2d1394b51225 100644 --- a/pkg/scanner/library/scan.go +++ b/pkg/scanner/library/scan.go @@ -1,8 +1,10 @@ package library import ( + "io/ioutil" "os" - "path/filepath" + + detector "github.com/aquasecurity/trivy/pkg/detector/library" "github.com/aquasecurity/fanal/analyzer" _ "github.com/aquasecurity/fanal/analyzer/library/bundler" @@ -13,64 +15,29 @@ import ( _ "github.com/aquasecurity/fanal/analyzer/library/poetry" _ "github.com/aquasecurity/fanal/analyzer/library/yarn" "github.com/aquasecurity/fanal/extractor" - ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" - "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/scanner/library/bundler" - "github.com/aquasecurity/trivy/pkg/scanner/library/cargo" - "github.com/aquasecurity/trivy/pkg/scanner/library/composer" - "github.com/aquasecurity/trivy/pkg/scanner/library/node" - "github.com/aquasecurity/trivy/pkg/scanner/library/python" "github.com/aquasecurity/trivy/pkg/types" - "github.com/knqyf263/go-version" "golang.org/x/xerrors" ) -type Scanner interface { - ParseLockfile(*os.File) ([]ptypes.Library, error) - Detect(string, *version.Version) ([]types.DetectedVulnerability, error) - Type() string +type Scanner struct { + detector detector.Operation } -func NewScanner(filename string) Scanner { - var scanner Scanner - switch filename { - case "Gemfile.lock": - scanner = bundler.NewScanner() - case "Cargo.lock": - scanner = cargo.NewScanner() - case "composer.lock": - scanner = composer.NewScanner() - case "package-lock.json": - scanner = node.NewScanner(node.ScannerTypeNpm) - case "yarn.lock": - scanner = node.NewScanner(node.ScannerTypeYarn) - case "Pipfile.lock": - scanner = python.NewScanner(python.ScannerTypePipenv) - case "poetry.lock": - scanner = python.NewScanner(python.ScannerTypePoetry) - default: - return nil - } - return scanner +func NewScanner(detector detector.Operation) Scanner { + return Scanner{detector: detector} } -func Scan(files extractor.FileMap, scanOptions types.ScanOptions) (map[string][]types.DetectedVulnerability, error) { +func (s Scanner) Scan(files extractor.FileMap) (map[string][]types.DetectedVulnerability, error) { results, err := analyzer.GetLibraries(files) if err != nil { return nil, xerrors.Errorf("failed to analyze libraries: %w", err) } vulnerabilities := map[string][]types.DetectedVulnerability{} - for path, pkgs := range results { - log.Logger.Debugf("Detecting library vulnerabilities, path: %s", path) - scanner := NewScanner(filepath.Base(string(path))) - if scanner == nil { - return nil, xerrors.New("unknown file type") - } - - vulns, err := scan(scanner, pkgs) + for path, libs := range results { + vulns, err := s.detector.Detect(string(path), libs) if err != nil { - return nil, xerrors.Errorf("failed to scan %s vulnerabilities: %w", scanner.Type(), err) + return nil, xerrors.Errorf("failed library scan: %w", err) } vulnerabilities[string(path)] = vulns @@ -78,40 +45,23 @@ func Scan(files extractor.FileMap, scanOptions types.ScanOptions) (map[string][] return vulnerabilities, nil } -func ScanFile(f *os.File) ([]types.DetectedVulnerability, error) { - scanner := NewScanner(filepath.Base(f.Name())) - if scanner == nil { - return nil, xerrors.New("unknown file type") - } - - pkgs, err := scanner.ParseLockfile(f) +func (s Scanner) ScanFile(f *os.File) ([]types.DetectedVulnerability, error) { + content, err := ioutil.ReadAll(f) if err != nil { return nil, err } + files := extractor.FileMap{ + f.Name(): content, + } - vulns, err := scan(scanner, pkgs) + results, err := s.Scan(files) if err != nil { return nil, err } - return vulns, nil -} -func scan(scanner Scanner, pkgs []ptypes.Library) ([]types.DetectedVulnerability, error) { - log.Logger.Infof("Detecting %s vulnerabilities...", scanner.Type()) - var vulnerabilities []types.DetectedVulnerability - for _, pkg := range pkgs { - v, err := version.NewVersion(pkg.Version) - if err != nil { - log.Logger.Debug(err) - continue - } - - vulns, err := scanner.Detect(pkg.Name, v) - if err != nil { - return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", scanner.Type(), err) - } - vulnerabilities = append(vulnerabilities, vulns...) + // need only 1 result + for _, vulns := range results { + return vulns, nil } - - return vulnerabilities, nil + return nil, nil } diff --git a/pkg/scanner/library/scan_test.go b/pkg/scanner/library/scan_test.go new file mode 100644 index 000000000000..c57031961120 --- /dev/null +++ b/pkg/scanner/library/scan_test.go @@ -0,0 +1,201 @@ +package library + +import ( + "testing" + + library2 "github.com/aquasecurity/trivy/pkg/detector/library" + + "golang.org/x/xerrors" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/fanal/extractor" + ptypes "github.com/aquasecurity/go-dep-parser/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Scan(t *testing.T) { + type detectInput struct { + filePath string + libs []ptypes.Library + } + type detectOutput struct { + vulns []types.DetectedVulnerability + err error + } + type detect struct { + input detectInput + output detectOutput + } + type args struct { + files extractor.FileMap + } + tests := []struct { + name string + args args + detect []detect + want map[string][]types.DetectedVulnerability + wantErr string + }{ + { + name: "happy", + args: args{ + files: extractor.FileMap{ + "app/Pipfile.lock": []byte(`{ + "_meta": { + "hash": { + "sha256": "ad1805ab0e16cf08032c3fe45eeaa29b79e9c196650411977af14e31b12ff0cd" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "django": { + "hashes": [ + "sha256:665457d4146bbd34ae9d2970fa3b37082d7b225b0671bfd24c337458f229db78", + "sha256:bde46d4dbc410678e89bc95ea5d312dd6eb4c37d0fa0e19c9415cad94addf22f" + ], + "index": "pypi", + "version": "==3.0.0" + } + } +} +`), + "app/package-lock.json": []byte(`{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "react": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + } + } +}`), + }, + }, + detect: []detect{ + { + input: detectInput{ + filePath: "app/Pipfile.lock", + libs: []ptypes.Library{ + {Name: "django", Version: "3.0.0"}, + }, + }, + output: detectOutput{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001"}, + }, + }, + }, + { + input: detectInput{ + filePath: "app/package-lock.json", + libs: []ptypes.Library{ + {Name: "react", Version: "16.8.6"}, + }, + }, + output: detectOutput{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0002"}, + {VulnerabilityID: "CVE-2019-0003"}, + }, + }, + }, + }, + want: map[string][]types.DetectedVulnerability{ + "app/Pipfile.lock": {{VulnerabilityID: "CVE-2019-0001"}}, + "app/package-lock.json": { + {VulnerabilityID: "CVE-2019-0002"}, + {VulnerabilityID: "CVE-2019-0003"}, + }, + }, + }, + { + name: "broken lock file", + args: args{ + files: extractor.FileMap{ + "app/Pipfile.lock": []byte(`{broken}`), + }, + }, + wantErr: "failed to analyze libraries", + }, + { + name: "Detect returns an error", + args: args{ + files: extractor.FileMap{ + "app/package-lock.json": []byte(`{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "react": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + } + } +}`), + }, + }, + detect: []detect{ + { + input: detectInput{ + filePath: "app/package-lock.json", + libs: []ptypes.Library{ + {Name: "react", Version: "16.8.6"}, + }, + }, + output: detectOutput{err: xerrors.New("error")}, + }, + }, + wantErr: "failed library scan", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := new(library2.MockDetector) + for _, d := range tt.detect { + mockDetector.On("Detect", d.input.filePath, d.input.libs).Return( + d.output.vulns, d.output.err) + } + + s := Scanner{ + detector: mockDetector, + } + got, err := s.Scan(tt.args.files) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + assert.Equal(t, tt.want, got, tt.name) + mockDetector.AssertExpectations(t) + }) + } +} diff --git a/pkg/scanner/ospkg/scan.go b/pkg/scanner/ospkg/scan.go index 1677106aeaa5..26c1db338f27 100644 --- a/pkg/scanner/ospkg/scan.go +++ b/pkg/scanner/ospkg/scan.go @@ -1,9 +1,10 @@ package ospkg import ( + "golang.org/x/xerrors" + "github.com/aquasecurity/fanal/analyzer" _ "github.com/aquasecurity/fanal/analyzer/command/apk" - fos "github.com/aquasecurity/fanal/analyzer/os" _ "github.com/aquasecurity/fanal/analyzer/os/alpine" _ "github.com/aquasecurity/fanal/analyzer/os/amazonlinux" _ "github.com/aquasecurity/fanal/analyzer/os/debianbase" @@ -13,47 +14,26 @@ import ( _ "github.com/aquasecurity/fanal/analyzer/pkg/dpkg" "github.com/aquasecurity/fanal/extractor" ftypes "github.com/aquasecurity/fanal/types" + detector "github.com/aquasecurity/trivy/pkg/detector/ospkg" "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/alpine" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/amazon" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/debian" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/oracle" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/redhat" - "github.com/aquasecurity/trivy/pkg/scanner/ospkg/ubuntu" "github.com/aquasecurity/trivy/pkg/types" - "golang.org/x/xerrors" ) -type Scanner interface { - Detect(string, []analyzer.Package) ([]types.DetectedVulnerability, error) - IsSupportedVersion(string, string) bool +type Scanner struct { + detector detector.Operation } -func Scan(files extractor.FileMap) (string, string, []types.DetectedVulnerability, error) { +func NewScanner(detector detector.Operation) Scanner { + return Scanner{detector: detector} +} + +func (s Scanner) Scan(files extractor.FileMap) (string, string, []types.DetectedVulnerability, error) { os, err := analyzer.GetOS(files) if err != nil { return "", "", nil, xerrors.Errorf("failed to analyze OS: %w", err) } log.Logger.Debugf("OS family: %s, OS version: %s", os.Family, os.Name) - var s Scanner - switch os.Family { - case fos.Alpine: - s = alpine.NewScanner() - case fos.Debian: - s = debian.NewScanner() - case fos.Ubuntu: - s = ubuntu.NewScanner() - case fos.RedHat, fos.CentOS: - s = redhat.NewScanner() - case fos.Amazon: - s = amazon.NewScanner() - case fos.Oracle: - s = oracle.NewScanner() - default: - log.Logger.Warnf("unsupported os : %s", os.Family) - return "", "", nil, nil - } pkgs, err := analyzer.GetPackages(files) if err != nil { if xerrors.Is(err, ftypes.ErrNoRpmCmd) { @@ -72,15 +52,14 @@ func Scan(files extractor.FileMap) (string, string, []types.DetectedVulnerabilit pkgs = mergePkgs(pkgs, pkgsFromCommands) log.Logger.Debugf("the number of packages: %d", len(pkgs)) - if !s.IsSupportedVersion(os.Family, os.Name) { - log.Logger.Warnf("This OS version is no longer supported by the distribution: %s %s", os.Family, os.Name) - log.Logger.Warnf("The vulnerability detection may be insufficient because security updates are not provided") - } - - vulns, err := s.Detect(os.Name, pkgs) + vulns, eosl, err := s.detector.Detect(os.Family, os.Name, pkgs) if err != nil { return "", "", nil, xerrors.Errorf("failed to detect vulnerabilities: %w", err) } + if eosl { + log.Logger.Warnf("This OS version is no longer supported by the distribution: %s %s", os.Family, os.Name) + log.Logger.Warnf("The vulnerability detection may be insufficient because security updates are not provided") + } return os.Family, os.Name, vulns, nil } diff --git a/pkg/scanner/ospkg/scan_test.go b/pkg/scanner/ospkg/scan_test.go new file mode 100644 index 000000000000..78361257f761 --- /dev/null +++ b/pkg/scanner/ospkg/scan_test.go @@ -0,0 +1,213 @@ +package ospkg + +import ( + "os" + "testing" + + ospkg2 "github.com/aquasecurity/trivy/pkg/detector/ospkg" + + "golang.org/x/xerrors" + + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/fanal/analyzer" + "github.com/aquasecurity/fanal/extractor" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + code := m.Run() + os.Exit(code) +} + +func TestScanner_Scan(t *testing.T) { + type detectInput struct { + osFamily string + osName string + pkgs []analyzer.Package + } + type detectOutput struct { + vulns []types.DetectedVulnerability + eosl bool + err error + } + type detect struct { + input detectInput + output detectOutput + } + + type fields struct { + files extractor.FileMap + } + type want struct { + osFamily string + osName string + vulns []types.DetectedVulnerability + err string + } + tests := []struct { + name string + fields fields + detect detect + want want + }{ + { + name: "happy path", + fields: fields{ + files: extractor.FileMap{ + "etc/alpine-release": []byte("3.10.2"), + "lib/apk/db/installed": []byte(`C:Q11Ing8/u1VIdY9czSxaDO9wJg72I= +P:musl +V:1.1.22-r3 +A:x86_64 +S:368204 +I:598016 +T:the musl c library (libc) implementation +U:http://www.musl-libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1565162130 +c:0c777cf840e82cdc528651e3f3f8f9dda6b1b028 +p:so:libc.musl-x86_64.so.1=1 +F:lib +R:libc.musl-x86_64.so.1 +a:0:0:777 +Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI= +R:ld-musl-x86_64.so.1 +a:0:0:755 +Z:Q1TTLtUopPeiF9JrA0cgKQZYggG+c= +F:usr +F:usr/lib +`), + }, + }, + detect: detect{ + input: detectInput{ + osFamily: "alpine", + osName: "3.10.2", + pkgs: []analyzer.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + output: detectOutput{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001", PkgName: "musl"}, + }, + err: nil, + }, + }, + want: want{ + osFamily: "alpine", + osName: "3.10.2", + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001", PkgName: "musl"}, + }, + }, + }, + { + name: "sad path", + fields: fields{ + files: extractor.FileMap{ + "etc/alpine-release": []byte("3.10.2"), + "invalid": []byte(`invalid`), + }, + }, + want: want{err: analyzer.ErrPkgAnalysis.Error()}, + }, + { + name: "Detect returns an error", + fields: fields{ + files: extractor.FileMap{ + "etc/alpine-release": []byte("3.10.2"), + "lib/apk/db/installed": []byte(`C:Q11Ing8/u1VIdY9czSxaDO9wJg72I= +P:musl +V:1.1.22-r3 +A:x86_64 +`), + }, + }, + detect: detect{ + input: detectInput{ + osFamily: "alpine", + osName: "3.10.2", + pkgs: []analyzer.Package{ + {Name: "musl", Version: "1.1.22-r3"}, + }, + }, + output: detectOutput{ + err: xerrors.New("error"), + }, + }, + want: want{ + err: "failed to detect vulnerabilities", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDetector := new(ospkg2.MockDetector) + mockDetector.On("Detect", tt.detect.input.osFamily, tt.detect.input.osName, + tt.detect.input.pkgs).Return(tt.detect.output.vulns, tt.detect.output.eosl, tt.detect.output.err) + + s := NewScanner(mockDetector) + got, got1, got2, err := s.Scan(tt.fields.files) + + if tt.want.err != "" { + require.NotNil(t, err, tt.name) + assert.Contains(t, err.Error(), tt.want.err, tt.name) + return + } else { + assert.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.want.osFamily, got) + assert.Equal(t, tt.want.osName, got1) + assert.Equal(t, tt.want.vulns, got2) + mockDetector.AssertExpectations(t) + }) + } +} + +func Test_mergePkgs(t *testing.T) { + type args struct { + pkgs []analyzer.Package + pkgsFromCommands []analyzer.Package + } + tests := []struct { + name string + args args + want []analyzer.Package + }{ + { + name: "happy path", + args: args{ + pkgs: []analyzer.Package{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "3.4.5"}, + {Name: "baz", Version: "6.7.8"}, + }, + pkgsFromCommands: []analyzer.Package{ + {Name: "bar", Version: "1.1.1"}, + {Name: "hoge", Version: "9.0.1"}, + }, + }, + want: []analyzer.Package{ + {Name: "foo", Version: "1.2.3"}, + {Name: "bar", Version: "3.4.5"}, + {Name: "baz", Version: "6.7.8"}, + {Name: "hoge", Version: "9.0.1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mergePkgs(tt.args.pkgs, tt.args.pkgsFromCommands) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index ad6d7a315d32..c6478b3fb47e 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -7,19 +7,52 @@ import ( "os" "sort" + "github.com/google/wire" + "github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/fanal/analyzer" "github.com/aquasecurity/fanal/extractor" + libDetector "github.com/aquasecurity/trivy/pkg/detector/library" + ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg" + rpcLibDetector "github.com/aquasecurity/trivy/pkg/rpc/client/library" + rpcOSDetector "github.com/aquasecurity/trivy/pkg/rpc/client/ospkg" "github.com/aquasecurity/trivy/pkg/scanner/library" + libScanner "github.com/aquasecurity/trivy/pkg/scanner/library" "github.com/aquasecurity/trivy/pkg/scanner/ospkg" + ospkgScanner "github.com/aquasecurity/trivy/pkg/scanner/ospkg" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/utils" "golang.org/x/crypto/ssh/terminal" "golang.org/x/xerrors" ) -func ScanImage(imageName, filePath string, scanOptions types.ScanOptions) (report.Results, error) { +var StandaloneSet = wire.NewSet( + ospkgDetector.SuperSet, + ospkgScanner.NewScanner, + libDetector.SuperSet, + libScanner.NewScanner, + NewScanner, +) + +var ClientSet = wire.NewSet( + rpcOSDetector.SuperSet, + ospkgScanner.NewScanner, + rpcLibDetector.SuperSet, + libScanner.NewScanner, + NewScanner, +) + +type Scanner struct { + ospkgScanner ospkg.Scanner + libScanner library.Scanner +} + +func NewScanner(ospkgScanner ospkg.Scanner, libScanner library.Scanner) Scanner { + return Scanner{ospkgScanner: ospkgScanner, libScanner: libScanner} +} + +func (s Scanner) ScanImage(imageName, filePath string, scanOptions types.ScanOptions) (report.Results, error) { results := report.Results{} ctx := context.Background() @@ -53,9 +86,9 @@ func ScanImage(imageName, filePath string, scanOptions types.ScanOptions) (repor } if utils.StringInSlice("os", scanOptions.VulnType) { - osFamily, osVersion, osVulns, err := ospkg.Scan(files) - if err != nil { - return nil, xerrors.Errorf("failed to scan image: %w", err) + osFamily, osVersion, osVulns, err := s.ospkgScanner.Scan(files) + if err != nil && err != ospkgDetector.ErrUnsupportedOS { + return nil, xerrors.Errorf("failed to scan the image: %w", err) } if osFamily != "" { imageDetail := fmt.Sprintf("%s (%s %s)", target, osFamily, osVersion) @@ -67,7 +100,7 @@ func ScanImage(imageName, filePath string, scanOptions types.ScanOptions) (repor } if utils.StringInSlice("library", scanOptions.VulnType) { - libVulns, err := library.Scan(files, scanOptions) + libVulns, err := s.libScanner.Scan(files) if err != nil { return nil, xerrors.Errorf("failed to scan libraries: %w", err) } @@ -88,8 +121,8 @@ func ScanImage(imageName, filePath string, scanOptions types.ScanOptions) (repor return results, nil } -func ScanFile(f *os.File) (report.Results, error) { - vulns, err := library.ScanFile(f) +func (s Scanner) ScanFile(f *os.File) (report.Results, error) { + vulns, err := s.libScanner.ScanFile(f) if err != nil { return nil, xerrors.Errorf("failed to scan libraries in file: %w", err) } diff --git a/pkg/types/scanoptions.go b/pkg/types/scanoptions.go index 02243aa9c79c..b280d41365b7 100644 --- a/pkg/types/scanoptions.go +++ b/pkg/types/scanoptions.go @@ -5,4 +5,8 @@ import "time" type ScanOptions struct { VulnType []string Timeout time.Duration + + // for client/server + RemoteURL string + Token string } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a8fbd140db38..c3bdd40319ee 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "bytes" + "fmt" "io" "os" "os/exec" @@ -126,3 +127,28 @@ func FilterTargets(prefixPath string, targets map[string]struct{}) (map[string]s } return filtered, nil } + +func CopyFile(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + n, err := io.Copy(destination, source) + return n, err +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 5759c11cd065..63c1ab00beb6 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/aquasecurity/trivy/pkg/log" "github.com/kylelemons/godebug/pretty" ) @@ -142,3 +145,51 @@ func TestFilterTargets(t *testing.T) { }) } } + +func TestCopyFile(t *testing.T) { + type args struct { + src string + dst string + } + tests := []struct { + name string + args args + content []byte + want string + wantErr string + }{ + { + name: "happy path", + content: []byte("this is a content"), + args: args{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + src := tt.args.src + if tt.args.src == "" { + s, err := ioutil.TempFile("", "src") + require.NoError(t, err, tt.name) + _, err = s.Write(tt.content) + require.NoError(t, err, tt.name) + src = s.Name() + } + + dst := tt.args.dst + if tt.args.dst == "" { + d, err := ioutil.TempFile("", "dst") + require.NoError(t, err, tt.name) + dst = d.Name() + require.NoError(t, d.Close(), tt.name) + } + + _, err := CopyFile(src, dst) + if tt.wantErr != "" { + require.NotNil(t, err, tt.name) + assert.Equal(t, err.Error(), tt.wantErr, tt.name) + } else { + assert.NoError(t, err, tt.name) + } + }) + } +} diff --git a/pkg/vulnerability/testdata/.trivyignore b/pkg/vulnerability/testdata/.trivyignore new file mode 100644 index 000000000000..b0fe9854d2bc --- /dev/null +++ b/pkg/vulnerability/testdata/.trivyignore @@ -0,0 +1,3 @@ +# test +CVE-2019-0001 +CVE-2019-0002 diff --git a/pkg/vulnerability/vulnerability.go b/pkg/vulnerability/vulnerability.go index 37709157a5fd..c9abc7753e5b 100644 --- a/pkg/vulnerability/vulnerability.go +++ b/pkg/vulnerability/vulnerability.go @@ -6,6 +6,8 @@ import ( "sort" "strings" + "github.com/google/wire" + "github.com/aquasecurity/trivy-db/pkg/db" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" @@ -18,41 +20,52 @@ const ( DefaultIgnoreFile = ".trivyignore" ) +var SuperSet = wire.NewSet( + wire.Struct(new(db.Config)), + NewClient, + wire.Bind(new(Operation), new(Client)), +) + +type Operation interface { + FillInfo(vulns []types.DetectedVulnerability, light bool) + Filter(vulns []types.DetectedVulnerability, severities []dbTypes.Severity, + ignoreUnfixed bool, ignoreFile string) []types.DetectedVulnerability +} + type Client struct { dbc db.Operations } -func NewClient() Client { - return Client{ - dbc: db.Config{}, - } +func NewClient(dbc db.Config) Client { + return Client{dbc: dbc} } -func (c Client) FillAndFilter(vulns []types.DetectedVulnerability, severities []dbTypes.Severity, - ignoreUnfixed bool, ignoreFile string, light bool) []types.DetectedVulnerability { +func (c Client) FillInfo(vulns []types.DetectedVulnerability, light bool) { var err error var severity dbTypes.Severity - ignoredIDs := getIgnoredIDs(ignoreFile) - var vulnerabilities []types.DetectedVulnerability - for _, vuln := range vulns { - var vulnerability dbTypes.Vulnerability + for i := range vulns { if light { - severity, err = c.dbc.GetSeverity(vuln.VulnerabilityID) - vulnerability.Severity = severity.String() + severity, err = c.dbc.GetSeverity(vulns[i].VulnerabilityID) + vulns[i].Vulnerability.Severity = severity.String() } else { - vulnerability, err = c.dbc.GetVulnerability(vuln.VulnerabilityID) + vulns[i].Vulnerability, err = c.dbc.GetVulnerability(vulns[i].VulnerabilityID) } if err != nil { - log.Logger.Warn(err) + log.Logger.Warnf("Error while getting vulnerability details: %s\n", err) continue } + } +} +func (c Client) Filter(vulns []types.DetectedVulnerability, severities []dbTypes.Severity, + ignoreUnfixed bool, ignoreFile string) []types.DetectedVulnerability { + ignoredIDs := getIgnoredIDs(ignoreFile) + var vulnerabilities []types.DetectedVulnerability + for _, vuln := range vulns { // Filter vulnerabilities by severity for _, s := range severities { - if s.String() == vulnerability.Severity { - vuln.Vulnerability = vulnerability - + if s.String() == vuln.Severity { // Ignore unfixed vulnerabilities if ignoreUnfixed && vuln.FixedVersion == "" { continue diff --git a/pkg/vulnerability/vulnerability_mock.go b/pkg/vulnerability/vulnerability_mock.go new file mode 100644 index 000000000000..652dcf57d8a7 --- /dev/null +++ b/pkg/vulnerability/vulnerability_mock.go @@ -0,0 +1,35 @@ +package vulnerability + +import ( + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/stretchr/testify/mock" +) + +type MockVulnClient struct { + mock.Mock +} + +func NewMockVulnClient() *MockVulnClient { + mockVulnClient := new(MockVulnClient) + mockVulnClient.On("FillInfo", mock.Anything, mock.Anything) + return mockVulnClient +} + +func (_m *MockVulnClient) FillInfo(a []types.DetectedVulnerability, b bool) { + _m.Called(a, b) +} + +func (_m *MockVulnClient) Filter(a []types.DetectedVulnerability, b []dbTypes.Severity, + c bool, d string) []types.DetectedVulnerability { + ret := _m.Called(a, b, c, d) + ret0 := ret.Get(0) + if ret0 == nil { + return nil + } + vulns, ok := ret0.([]types.DetectedVulnerability) + if !ok { + return nil + } + return vulns +} diff --git a/pkg/vulnerability/vulnerability_test.go b/pkg/vulnerability/vulnerability_test.go index 6f32b651ac1b..aaed1667a542 100644 --- a/pkg/vulnerability/vulnerability_test.go +++ b/pkg/vulnerability/vulnerability_test.go @@ -1,190 +1,386 @@ package vulnerability import ( + "os" "testing" + "github.com/aquasecurity/trivy/pkg/log" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/db" "github.com/stretchr/testify/assert" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/types" ) -func TestFillAndFilter(t *testing.T) { - detectedVulns := []types.DetectedVulnerability{ - { - VulnerabilityID: "foo", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityHigh], - }, - }, - { - VulnerabilityID: "piyo", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityCritical], - }, - }, - { - VulnerabilityID: "bar", - PkgName: "barpkg", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityLow], - }, - }, - { - VulnerabilityID: "hoge", - }, - { - VulnerabilityID: "baz", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityMedium], - }, - }, +func TestMain(m *testing.M) { + if err := log.InitLogger(false, true); err != nil { + log.Fatal(err) } - severities := []dbTypes.Severity{dbTypes.SeverityLow, dbTypes.SeverityCritical, - dbTypes.SeverityMedium, dbTypes.SeverityHigh, dbTypes.SeverityUnknown} + code := m.Run() + os.Exit(code) +} - mockDBConfig := new(db.MockDBConfig) - getVulnerability := map[string]dbTypes.Vulnerability{ - "foo": { - Title: "footitle", - Description: "foodesc", - Severity: dbTypes.SeverityHigh.String(), - References: []string{"fooref"}, - }, - "bar": { - Title: "bartitle", - Description: "bardesc", - Severity: dbTypes.SeverityLow.String(), - References: []string{"barref"}, - }, - "baz": { - Title: "baztitle", - Description: "bazdesc", - Severity: dbTypes.SeverityMedium.String(), - References: []string{"bazref"}, - }, - "piyo": { - Title: "piyotitle", - Description: "piyodesc", - Severity: dbTypes.SeverityCritical.String(), - References: []string{"piyoref"}, - }, - "hoge": { - Title: "hogetitle", - Description: "hogedesc", - Severity: dbTypes.SeverityUnknown.String(), - References: []string{"hogeref"}, - }, +func TestClient_FillInfo(t *testing.T) { + type getSeverityOutput struct { + severity dbTypes.Severity + err error } - - for pkgName, vulnerability := range getVulnerability { - mockDBConfig.On("GetVulnerability", pkgName).Return(vulnerability, nil) - + type getSeverity struct { + input string + output getSeverityOutput } - getSeverity := map[string]dbTypes.Severity{ - "foo": dbTypes.SeverityHigh, - "bar": dbTypes.SeverityLow, - "baz": dbTypes.SeverityMedium, - "piyo": dbTypes.SeverityCritical, - "hoge": dbTypes.SeverityUnknown, + type getVulnerabilityOutput struct { + vulnerability dbTypes.Vulnerability + err error } - - for pkgName, severity := range getSeverity { - mockDBConfig.On("GetSeverity", pkgName).Return(severity, nil) + type getVulnerability struct { + input string + output getVulnerabilityOutput } - expected := []types.DetectedVulnerability{ + type args struct { + vulns []types.DetectedVulnerability + light bool + } + tests := []struct { + name string + getSeverity []getSeverity + getVulnerability []getVulnerability + args args + expected []types.DetectedVulnerability + }{ { - VulnerabilityID: "piyo", - Vulnerability: dbTypes.Vulnerability{ - Title: "piyotitle", - Description: "piyodesc", - Severity: dbTypes.SeverityNames[dbTypes.SeverityCritical], - References: []string{"piyoref"}, + name: "happy path", + getVulnerability: []getVulnerability{ + { + input: "CVE-2019-0001", + output: getVulnerabilityOutput{ + vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityMedium.String(), + References: []string{"http://example.com"}, + }, + }, + }, }, - }, - { - VulnerabilityID: "foo", - Vulnerability: dbTypes.Vulnerability{ - Title: "footitle", - Description: "foodesc", - Severity: dbTypes.SeverityNames[dbTypes.SeverityHigh], - References: []string{"fooref"}, + args: args{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001"}, + }, + light: false, + }, + expected: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityMedium.String(), + References: []string{"http://example.com"}, + }, + }, }, }, { - VulnerabilityID: "baz", - Vulnerability: dbTypes.Vulnerability{ - Title: "baztitle", - Description: "bazdesc", - Severity: dbTypes.SeverityNames[dbTypes.SeverityMedium], - References: []string{"bazref"}, + name: "happy path with light option", + getSeverity: []getSeverity{ + { + input: "CVE-2019-0001", + output: getSeverityOutput{ + severity: dbTypes.SeverityCritical, + }, + }, + { + input: "CVE-2019-0002", + output: getSeverityOutput{ + severity: dbTypes.SeverityHigh, + }, + }, + }, + args: args{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001"}, + {VulnerabilityID: "CVE-2019-0002"}, + }, + light: true, + }, + expected: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, }, }, { - VulnerabilityID: "hoge", - Vulnerability: dbTypes.Vulnerability{ - Title: "hogetitle", - Description: "hogedesc", - Severity: dbTypes.SeverityNames[dbTypes.SeverityUnknown], - References: []string{"hogeref"}, + name: "GetVulnerability returns an error", + getVulnerability: []getVulnerability{ + { + input: "CVE-2019-0004", + output: getVulnerabilityOutput{ + err: xerrors.New("failed"), + }, + }, + }, + args: args{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0004"}, + }, + light: false, + }, + expected: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0004"}, }, }, { - VulnerabilityID: "bar", - PkgName: "barpkg", - Vulnerability: dbTypes.Vulnerability{ - Title: "bartitle", - Description: "bardesc", - Severity: dbTypes.SeverityNames[dbTypes.SeverityLow], - References: []string{"barref"}, + name: "GetSeverity returns an error", + getSeverity: []getSeverity{ + { + input: "CVE-2019-0003", + output: getSeverityOutput{ + err: xerrors.New("failed"), + }, + }, + }, + args: args{ + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0003"}, + }, + light: true, + }, + expected: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0003", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityUnknown.String(), + }, + }, }, }, } - client := Client{ - dbc: mockDBConfig, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDBConfig := new(db.MockDBConfig) + c := Client{ + dbc: mockDBConfig, + } + for _, gs := range tt.getSeverity { + mockDBConfig.On("GetSeverity", gs.input).Return( + gs.output.severity, gs.output.err) + } + for _, gv := range tt.getVulnerability { + mockDBConfig.On("GetVulnerability", gv.input).Return( + gv.output.vulnerability, gv.output.err) + } + + c.FillInfo(tt.args.vulns, tt.args.light) + assert.Equal(t, tt.expected, tt.args.vulns, tt.name) + mockDBConfig.AssertExpectations(t) + }) } - actual := client.FillAndFilter(detectedVulns, severities, false, ".trivyignore", false) - assert.Equal(t, expected, actual, "full db") +} - expected = []types.DetectedVulnerability{ +func TestClient_Filter(t *testing.T) { + type args struct { + vulns []types.DetectedVulnerability + severities []dbTypes.Severity + ignoreUnfixed bool + ignoreFile string + } + tests := []struct { + name string + args args + want []types.DetectedVulnerability + }{ { - VulnerabilityID: "piyo", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityCritical], + name: "happy path", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityHigh}, + ignoreUnfixed: false, }, - }, - { - VulnerabilityID: "foo", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityHigh], + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, }, }, { - VulnerabilityID: "baz", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityMedium], + name: "happy path with ignore-unfixed", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + severities: []dbTypes.Severity{dbTypes.SeverityHigh}, + ignoreUnfixed: true, }, }, { - VulnerabilityID: "hoge", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityUnknown], + name: "happy path with ignore-file", + args: args{ + vulns: []types.DetectedVulnerability{ + { + // this vulnerability is ignored + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + // this vulnerability is ignored + VulnerabilityID: "CVE-2019-0002", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + }, + severities: []dbTypes.Severity{dbTypes.SeverityLow}, + ignoreUnfixed: false, + ignoreFile: "testdata/.trivyignore", }, - }, - { - VulnerabilityID: "bar", - PkgName: "barpkg", - Vulnerability: dbTypes.Vulnerability{ - Severity: dbTypes.SeverityNames[dbTypes.SeverityLow], + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, }, }, } - - actual = client.FillAndFilter(detectedVulns, severities, false, ".trivyignore", true) - assert.Equal(t, expected, actual, "light db") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := Client{} + got := c.Filter(tt.args.vulns, tt.args.severities, tt.args.ignoreUnfixed, tt.args.ignoreFile) + assert.Equal(t, tt.want, got, tt.name) + }) + } } diff --git a/rpc/detector/service.pb.go b/rpc/detector/service.pb.go new file mode 100644 index 000000000000..4383051c4ffc --- /dev/null +++ b/rpc/detector/service.pb.go @@ -0,0 +1,508 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: rpc/detector/service.proto + +package detector + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Severity int32 + +const ( + Severity_UNKNOWN Severity = 0 + Severity_LOW Severity = 1 + Severity_MEDIUM Severity = 2 + Severity_HIGH Severity = 3 + Severity_CRITICAL Severity = 4 +) + +var Severity_name = map[int32]string{ + 0: "UNKNOWN", + 1: "LOW", + 2: "MEDIUM", + 3: "HIGH", + 4: "CRITICAL", +} + +var Severity_value = map[string]int32{ + "UNKNOWN": 0, + "LOW": 1, + "MEDIUM": 2, + "HIGH": 3, + "CRITICAL": 4, +} + +func (x Severity) String() string { + return proto.EnumName(Severity_name, int32(x)) +} + +func (Severity) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{0} +} + +type OSDetectRequest struct { + OsFamily string `protobuf:"bytes,1,opt,name=os_family,json=osFamily,proto3" json:"os_family,omitempty"` + OsName string `protobuf:"bytes,2,opt,name=os_name,json=osName,proto3" json:"os_name,omitempty"` + Packages []*Package `protobuf:"bytes,3,rep,name=packages,proto3" json:"packages,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OSDetectRequest) Reset() { *m = OSDetectRequest{} } +func (m *OSDetectRequest) String() string { return proto.CompactTextString(m) } +func (*OSDetectRequest) ProtoMessage() {} +func (*OSDetectRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{0} +} + +func (m *OSDetectRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OSDetectRequest.Unmarshal(m, b) +} +func (m *OSDetectRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OSDetectRequest.Marshal(b, m, deterministic) +} +func (m *OSDetectRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_OSDetectRequest.Merge(m, src) +} +func (m *OSDetectRequest) XXX_Size() int { + return xxx_messageInfo_OSDetectRequest.Size(m) +} +func (m *OSDetectRequest) XXX_DiscardUnknown() { + xxx_messageInfo_OSDetectRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_OSDetectRequest proto.InternalMessageInfo + +func (m *OSDetectRequest) GetOsFamily() string { + if m != nil { + return m.OsFamily + } + return "" +} + +func (m *OSDetectRequest) GetOsName() string { + if m != nil { + return m.OsName + } + return "" +} + +func (m *OSDetectRequest) GetPackages() []*Package { + if m != nil { + return m.Packages + } + return nil +} + +type DetectResponse struct { + Vulnerabilities []*Vulnerability `protobuf:"bytes,1,rep,name=vulnerabilities,proto3" json:"vulnerabilities,omitempty"` + Eosl bool `protobuf:"varint,2,opt,name=eosl,proto3" json:"eosl,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DetectResponse) Reset() { *m = DetectResponse{} } +func (m *DetectResponse) String() string { return proto.CompactTextString(m) } +func (*DetectResponse) ProtoMessage() {} +func (*DetectResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{1} +} + +func (m *DetectResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DetectResponse.Unmarshal(m, b) +} +func (m *DetectResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DetectResponse.Marshal(b, m, deterministic) +} +func (m *DetectResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_DetectResponse.Merge(m, src) +} +func (m *DetectResponse) XXX_Size() int { + return xxx_messageInfo_DetectResponse.Size(m) +} +func (m *DetectResponse) XXX_DiscardUnknown() { + xxx_messageInfo_DetectResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_DetectResponse proto.InternalMessageInfo + +func (m *DetectResponse) GetVulnerabilities() []*Vulnerability { + if m != nil { + return m.Vulnerabilities + } + return nil +} + +func (m *DetectResponse) GetEosl() bool { + if m != nil { + return m.Eosl + } + return false +} + +type Package struct { + // binary package + // e.g. bind-utils + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` + Epoch int32 `protobuf:"varint,4,opt,name=epoch,proto3" json:"epoch,omitempty"` + Arch string `protobuf:"bytes,5,opt,name=arch,proto3" json:"arch,omitempty"` + // src package containing some binary packages + // e.g. bind + SrcName string `protobuf:"bytes,6,opt,name=src_name,json=srcName,proto3" json:"src_name,omitempty"` + SrcVersion string `protobuf:"bytes,7,opt,name=src_version,json=srcVersion,proto3" json:"src_version,omitempty"` + SrcRelease string `protobuf:"bytes,8,opt,name=src_release,json=srcRelease,proto3" json:"src_release,omitempty"` + SrcEpoch int32 `protobuf:"varint,9,opt,name=src_epoch,json=srcEpoch,proto3" json:"src_epoch,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Package) Reset() { *m = Package{} } +func (m *Package) String() string { return proto.CompactTextString(m) } +func (*Package) ProtoMessage() {} +func (*Package) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{2} +} + +func (m *Package) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Package.Unmarshal(m, b) +} +func (m *Package) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Package.Marshal(b, m, deterministic) +} +func (m *Package) XXX_Merge(src proto.Message) { + xxx_messageInfo_Package.Merge(m, src) +} +func (m *Package) XXX_Size() int { + return xxx_messageInfo_Package.Size(m) +} +func (m *Package) XXX_DiscardUnknown() { + xxx_messageInfo_Package.DiscardUnknown(m) +} + +var xxx_messageInfo_Package proto.InternalMessageInfo + +func (m *Package) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Package) GetVersion() string { + if m != nil { + return m.Version + } + return "" +} + +func (m *Package) GetRelease() string { + if m != nil { + return m.Release + } + return "" +} + +func (m *Package) GetEpoch() int32 { + if m != nil { + return m.Epoch + } + return 0 +} + +func (m *Package) GetArch() string { + if m != nil { + return m.Arch + } + return "" +} + +func (m *Package) GetSrcName() string { + if m != nil { + return m.SrcName + } + return "" +} + +func (m *Package) GetSrcVersion() string { + if m != nil { + return m.SrcVersion + } + return "" +} + +func (m *Package) GetSrcRelease() string { + if m != nil { + return m.SrcRelease + } + return "" +} + +func (m *Package) GetSrcEpoch() int32 { + if m != nil { + return m.SrcEpoch + } + return 0 +} + +type LibDetectRequest struct { + FilePath string `protobuf:"bytes,1,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Libraries []*Library `protobuf:"bytes,2,rep,name=libraries,proto3" json:"libraries,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LibDetectRequest) Reset() { *m = LibDetectRequest{} } +func (m *LibDetectRequest) String() string { return proto.CompactTextString(m) } +func (*LibDetectRequest) ProtoMessage() {} +func (*LibDetectRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{3} +} + +func (m *LibDetectRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LibDetectRequest.Unmarshal(m, b) +} +func (m *LibDetectRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LibDetectRequest.Marshal(b, m, deterministic) +} +func (m *LibDetectRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_LibDetectRequest.Merge(m, src) +} +func (m *LibDetectRequest) XXX_Size() int { + return xxx_messageInfo_LibDetectRequest.Size(m) +} +func (m *LibDetectRequest) XXX_DiscardUnknown() { + xxx_messageInfo_LibDetectRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_LibDetectRequest proto.InternalMessageInfo + +func (m *LibDetectRequest) GetFilePath() string { + if m != nil { + return m.FilePath + } + return "" +} + +func (m *LibDetectRequest) GetLibraries() []*Library { + if m != nil { + return m.Libraries + } + return nil +} + +type Library struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Library) Reset() { *m = Library{} } +func (m *Library) String() string { return proto.CompactTextString(m) } +func (*Library) ProtoMessage() {} +func (*Library) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{4} +} + +func (m *Library) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Library.Unmarshal(m, b) +} +func (m *Library) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Library.Marshal(b, m, deterministic) +} +func (m *Library) XXX_Merge(src proto.Message) { + xxx_messageInfo_Library.Merge(m, src) +} +func (m *Library) XXX_Size() int { + return xxx_messageInfo_Library.Size(m) +} +func (m *Library) XXX_DiscardUnknown() { + xxx_messageInfo_Library.DiscardUnknown(m) +} + +var xxx_messageInfo_Library proto.InternalMessageInfo + +func (m *Library) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Library) GetVersion() string { + if m != nil { + return m.Version + } + return "" +} + +type Vulnerability struct { + VulnerabilityId string `protobuf:"bytes,1,opt,name=vulnerability_id,json=vulnerabilityId,proto3" json:"vulnerability_id,omitempty"` + PkgName string `protobuf:"bytes,2,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` + InstalledVersion string `protobuf:"bytes,3,opt,name=installed_version,json=installedVersion,proto3" json:"installed_version,omitempty"` + FixedVersion string `protobuf:"bytes,4,opt,name=fixed_version,json=fixedVersion,proto3" json:"fixed_version,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + Severity Severity `protobuf:"varint,7,opt,name=severity,proto3,enum=trivy.detector.Severity" json:"severity,omitempty"` + References []string `protobuf:"bytes,8,rep,name=references,proto3" json:"references,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Vulnerability) Reset() { *m = Vulnerability{} } +func (m *Vulnerability) String() string { return proto.CompactTextString(m) } +func (*Vulnerability) ProtoMessage() {} +func (*Vulnerability) Descriptor() ([]byte, []int) { + return fileDescriptor_93e16dbd737b8924, []int{5} +} + +func (m *Vulnerability) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Vulnerability.Unmarshal(m, b) +} +func (m *Vulnerability) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Vulnerability.Marshal(b, m, deterministic) +} +func (m *Vulnerability) XXX_Merge(src proto.Message) { + xxx_messageInfo_Vulnerability.Merge(m, src) +} +func (m *Vulnerability) XXX_Size() int { + return xxx_messageInfo_Vulnerability.Size(m) +} +func (m *Vulnerability) XXX_DiscardUnknown() { + xxx_messageInfo_Vulnerability.DiscardUnknown(m) +} + +var xxx_messageInfo_Vulnerability proto.InternalMessageInfo + +func (m *Vulnerability) GetVulnerabilityId() string { + if m != nil { + return m.VulnerabilityId + } + return "" +} + +func (m *Vulnerability) GetPkgName() string { + if m != nil { + return m.PkgName + } + return "" +} + +func (m *Vulnerability) GetInstalledVersion() string { + if m != nil { + return m.InstalledVersion + } + return "" +} + +func (m *Vulnerability) GetFixedVersion() string { + if m != nil { + return m.FixedVersion + } + return "" +} + +func (m *Vulnerability) GetTitle() string { + if m != nil { + return m.Title + } + return "" +} + +func (m *Vulnerability) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +func (m *Vulnerability) GetSeverity() Severity { + if m != nil { + return m.Severity + } + return Severity_UNKNOWN +} + +func (m *Vulnerability) GetReferences() []string { + if m != nil { + return m.References + } + return nil +} + +func init() { + proto.RegisterEnum("trivy.detector.Severity", Severity_name, Severity_value) + proto.RegisterType((*OSDetectRequest)(nil), "trivy.detector.OSDetectRequest") + proto.RegisterType((*DetectResponse)(nil), "trivy.detector.DetectResponse") + proto.RegisterType((*Package)(nil), "trivy.detector.Package") + proto.RegisterType((*LibDetectRequest)(nil), "trivy.detector.LibDetectRequest") + proto.RegisterType((*Library)(nil), "trivy.detector.Library") + proto.RegisterType((*Vulnerability)(nil), "trivy.detector.Vulnerability") +} + +func init() { proto.RegisterFile("rpc/detector/service.proto", fileDescriptor_93e16dbd737b8924) } + +var fileDescriptor_93e16dbd737b8924 = []byte{ + // 618 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x4d, 0x4f, 0xdb, 0x40, + 0x10, 0x6d, 0x3e, 0xed, 0x4c, 0xf8, 0x70, 0x57, 0x48, 0xb8, 0xa0, 0x42, 0x94, 0x5e, 0x68, 0x2b, + 0x05, 0x29, 0xb4, 0xea, 0xb9, 0x05, 0x0a, 0x69, 0x43, 0x40, 0xa6, 0x80, 0xda, 0x4b, 0xb4, 0x71, + 0x26, 0x64, 0x85, 0x93, 0x75, 0x77, 0x97, 0xa8, 0x96, 0xfa, 0xb7, 0xfa, 0xd3, 0x7a, 0xaf, 0x76, + 0xd7, 0x36, 0x49, 0xca, 0x85, 0xdb, 0xcc, 0xbc, 0xe7, 0x99, 0xd9, 0xf7, 0xd6, 0x0b, 0x5b, 0x22, + 0x0e, 0xf7, 0x87, 0xa8, 0x30, 0x54, 0x5c, 0xec, 0x4b, 0x14, 0x33, 0x16, 0x62, 0x2b, 0x16, 0x5c, + 0x71, 0xb2, 0xa6, 0x04, 0x9b, 0x25, 0xad, 0x0c, 0x6d, 0xfe, 0x86, 0xf5, 0xf3, 0xcb, 0x23, 0x93, + 0x05, 0xf8, 0xf3, 0x1e, 0xa5, 0x22, 0xdb, 0x50, 0xe3, 0xb2, 0x3f, 0xa2, 0x13, 0x16, 0x25, 0x7e, + 0xa1, 0x51, 0xd8, 0xab, 0x05, 0x2e, 0x97, 0x9f, 0x4d, 0x4e, 0x36, 0xc1, 0xe1, 0xb2, 0x3f, 0xa5, + 0x13, 0xf4, 0x8b, 0x06, 0xaa, 0x72, 0xd9, 0xa3, 0x13, 0x24, 0x07, 0xe0, 0xc6, 0x34, 0xbc, 0xa3, + 0xb7, 0x28, 0xfd, 0x52, 0xa3, 0xb4, 0x57, 0x6f, 0x6f, 0xb6, 0x16, 0x67, 0xb5, 0x2e, 0x2c, 0x1e, + 0xe4, 0xc4, 0xe6, 0x04, 0xd6, 0xb2, 0xd9, 0x32, 0xe6, 0x53, 0x89, 0xe4, 0x04, 0xd6, 0x67, 0xf7, + 0xd1, 0x14, 0x05, 0x1d, 0xb0, 0x88, 0x29, 0x86, 0xd2, 0x2f, 0x98, 0x6e, 0x2f, 0x97, 0xbb, 0x5d, + 0xcf, 0xd1, 0x92, 0x60, 0xf9, 0x2b, 0x42, 0xa0, 0x8c, 0x5c, 0x46, 0x66, 0x4b, 0x37, 0x30, 0x71, + 0xf3, 0x6f, 0x01, 0x9c, 0x74, 0x09, 0x8d, 0x9b, 0x53, 0xd8, 0x03, 0x9a, 0x98, 0xf8, 0xe0, 0xcc, + 0x50, 0x48, 0xc6, 0xa7, 0xe9, 0xe1, 0xb2, 0x54, 0x23, 0x02, 0x23, 0xa4, 0x12, 0xfd, 0x92, 0x45, + 0xd2, 0x94, 0x6c, 0x40, 0x05, 0x63, 0x1e, 0x8e, 0xfd, 0x72, 0xa3, 0xb0, 0x57, 0x09, 0x6c, 0xa2, + 0xbb, 0x53, 0x11, 0x8e, 0xfd, 0x8a, 0xed, 0xae, 0x63, 0xf2, 0x02, 0x5c, 0x29, 0x42, 0xab, 0x5d, + 0xd5, 0x36, 0x91, 0x22, 0x34, 0xe2, 0xed, 0x42, 0x5d, 0x43, 0xd9, 0x70, 0xc7, 0xa0, 0x20, 0x45, + 0x78, 0x9d, 0xce, 0x4f, 0x09, 0xd9, 0x0e, 0x6e, 0x4e, 0x08, 0xd2, 0x35, 0xb6, 0xa1, 0xa6, 0x09, + 0x76, 0x95, 0x9a, 0x59, 0x45, 0x4f, 0x3b, 0xd6, 0x79, 0x73, 0x04, 0x5e, 0x97, 0x0d, 0xfe, 0x73, + 0x79, 0xc4, 0x22, 0xec, 0xc7, 0x54, 0x8d, 0x33, 0x97, 0x75, 0xe1, 0x82, 0xaa, 0x31, 0x79, 0x0f, + 0xb5, 0x88, 0x0d, 0x04, 0x15, 0x5a, 0xff, 0xe2, 0xe3, 0x6e, 0x76, 0x0d, 0x21, 0x09, 0x1e, 0x98, + 0xcd, 0x0f, 0xe0, 0xa4, 0xd5, 0xa7, 0xc9, 0xdb, 0xfc, 0x53, 0x84, 0xd5, 0x05, 0x3f, 0xc9, 0x6b, + 0xf0, 0xe6, 0x1d, 0x4d, 0xfa, 0x6c, 0x98, 0xf6, 0x5a, 0x70, 0x3a, 0xe9, 0x0c, 0xb5, 0xae, 0xf1, + 0xdd, 0xed, 0xfc, 0x9d, 0x74, 0xe2, 0xbb, 0x5b, 0xa3, 0xeb, 0x5b, 0x78, 0xce, 0xa6, 0x52, 0xd1, + 0x28, 0xc2, 0x61, 0xae, 0xae, 0x35, 0xd0, 0xcb, 0x81, 0x4c, 0xe3, 0x57, 0xb0, 0x3a, 0x62, 0xbf, + 0xe6, 0x88, 0x65, 0x43, 0x5c, 0x31, 0xc5, 0x8c, 0xb4, 0x01, 0x15, 0xc5, 0x54, 0x84, 0xa9, 0xb3, + 0x36, 0x21, 0x0d, 0xa8, 0x0f, 0x51, 0x86, 0x82, 0xc5, 0x4a, 0x7f, 0x68, 0xdd, 0x9d, 0x2f, 0x91, + 0x77, 0xe0, 0x4a, 0x9c, 0xa1, 0x60, 0x2a, 0x31, 0xf6, 0xae, 0xb5, 0xfd, 0x65, 0x41, 0x2f, 0x53, + 0x3c, 0xc8, 0x99, 0x64, 0x07, 0x40, 0xe0, 0x08, 0x05, 0x4e, 0x43, 0x94, 0xbe, 0xdb, 0x28, 0x69, + 0xd7, 0x1f, 0x2a, 0x6f, 0x8e, 0xc0, 0xcd, 0xbe, 0x22, 0x75, 0x70, 0xae, 0x7a, 0x5f, 0x7b, 0xe7, + 0x37, 0x3d, 0xef, 0x19, 0x71, 0xa0, 0xd4, 0x3d, 0xbf, 0xf1, 0x0a, 0x04, 0xa0, 0x7a, 0x76, 0x7c, + 0xd4, 0xb9, 0x3a, 0xf3, 0x8a, 0xc4, 0x85, 0xf2, 0x69, 0xe7, 0xe4, 0xd4, 0x2b, 0x91, 0x15, 0x70, + 0x0f, 0x83, 0xce, 0xb7, 0xce, 0xe1, 0xc7, 0xae, 0x57, 0x6e, 0xdf, 0x00, 0x64, 0x6f, 0x00, 0x17, + 0xa4, 0x03, 0x55, 0x1b, 0x93, 0xdd, 0xe5, 0x0d, 0x97, 0x5e, 0x8a, 0xad, 0x9d, 0x65, 0xc2, 0xe2, + 0xcf, 0xdc, 0xfe, 0x0e, 0xf5, 0xfc, 0xde, 0x71, 0x41, 0xbe, 0xe4, 0x9d, 0x1b, 0x8f, 0x5c, 0xa6, + 0x27, 0xb5, 0xfe, 0x04, 0x3f, 0xdc, 0x0c, 0x1a, 0x54, 0xcd, 0xd3, 0x76, 0xf0, 0x2f, 0x00, 0x00, + 0xff, 0xff, 0xde, 0x1c, 0x85, 0x42, 0xf8, 0x04, 0x00, 0x00, +} diff --git a/rpc/detector/service.proto b/rpc/detector/service.proto new file mode 100644 index 000000000000..607028b2d50f --- /dev/null +++ b/rpc/detector/service.proto @@ -0,0 +1,68 @@ +syntax = "proto3"; + +package trivy.detector; +option go_package = "detector"; + +service OSDetector { + rpc Detect(OSDetectRequest) returns (DetectResponse); +} + +message OSDetectRequest { + string os_family = 1; + string os_name = 2; + repeated Package packages = 3; +} + +message DetectResponse { + repeated Vulnerability vulnerabilities = 1; + bool eosl = 2; +} + +message Package { + // binary package + // e.g. bind-utils + string name = 1; + string version = 2; + string release = 3; + int32 epoch = 4; + string arch = 5; + // src package containing some binary packages + // e.g. bind + string src_name = 6; + string src_version = 7; + string src_release = 8; + int32 src_epoch = 9; +} + +service LibDetector { + rpc Detect(LibDetectRequest) returns (DetectResponse); +} + +message LibDetectRequest { + string file_path = 1; + repeated Library libraries = 2; +} + +message Library { + string name = 1; + string version = 2; +} + +message Vulnerability { + string vulnerability_id = 1; + string pkg_name = 2; + string installed_version = 3; + string fixed_version = 4; + string title = 5; + string description = 6; + Severity severity = 7; + repeated string references = 8; +} + +enum Severity { + UNKNOWN = 0; + LOW = 1; + MEDIUM = 2; + HIGH = 3; + CRITICAL = 4; +} diff --git a/rpc/detector/service.twirp.go b/rpc/detector/service.twirp.go new file mode 100644 index 000000000000..2cc6b3181976 --- /dev/null +++ b/rpc/detector/service.twirp.go @@ -0,0 +1,1127 @@ +// Code generated by protoc-gen-twirp v5.9.0, DO NOT EDIT. +// source: rpc/detector/service.proto + +/* +Package detector is a generated twirp stub package. +This code was generated with github.com/twitchtv/twirp/protoc-gen-twirp v5.9.0. + +It is generated from these files: + rpc/detector/service.proto +*/ +package detector + +import bytes "bytes" +import strings "strings" +import context "context" +import fmt "fmt" +import ioutil "io/ioutil" +import http "net/http" +import strconv "strconv" + +import jsonpb "github.com/golang/protobuf/jsonpb" +import proto "github.com/golang/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +// Imports only used by utility functions: +import io "io" +import json "encoding/json" +import url "net/url" + +// ==================== +// OSDetector Interface +// ==================== + +type OSDetector interface { + Detect(context.Context, *OSDetectRequest) (*DetectResponse, error) +} + +// ========================== +// OSDetector Protobuf Client +// ========================== + +type oSDetectorProtobufClient struct { + client HTTPClient + urls [1]string +} + +// NewOSDetectorProtobufClient creates a Protobuf client that implements the OSDetector interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewOSDetectorProtobufClient(addr string, client HTTPClient) OSDetector { + prefix := urlBase(addr) + OSDetectorPathPrefix + urls := [1]string{ + prefix + "Detect", + } + if httpClient, ok := client.(*http.Client); ok { + return &oSDetectorProtobufClient{ + client: withoutRedirects(httpClient), + urls: urls, + } + } + return &oSDetectorProtobufClient{ + client: client, + urls: urls, + } +} + +func (c *oSDetectorProtobufClient) Detect(ctx context.Context, in *OSDetectRequest) (*DetectResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "OSDetector") + ctx = ctxsetters.WithMethodName(ctx, "Detect") + out := new(DetectResponse) + err := doProtobufRequest(ctx, c.client, c.urls[0], in, out) + if err != nil { + return nil, err + } + return out, nil +} + +// ====================== +// OSDetector JSON Client +// ====================== + +type oSDetectorJSONClient struct { + client HTTPClient + urls [1]string +} + +// NewOSDetectorJSONClient creates a JSON client that implements the OSDetector interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewOSDetectorJSONClient(addr string, client HTTPClient) OSDetector { + prefix := urlBase(addr) + OSDetectorPathPrefix + urls := [1]string{ + prefix + "Detect", + } + if httpClient, ok := client.(*http.Client); ok { + return &oSDetectorJSONClient{ + client: withoutRedirects(httpClient), + urls: urls, + } + } + return &oSDetectorJSONClient{ + client: client, + urls: urls, + } +} + +func (c *oSDetectorJSONClient) Detect(ctx context.Context, in *OSDetectRequest) (*DetectResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "OSDetector") + ctx = ctxsetters.WithMethodName(ctx, "Detect") + out := new(DetectResponse) + err := doJSONRequest(ctx, c.client, c.urls[0], in, out) + if err != nil { + return nil, err + } + return out, nil +} + +// ========================= +// OSDetector Server Handler +// ========================= + +type oSDetectorServer struct { + OSDetector + hooks *twirp.ServerHooks +} + +func NewOSDetectorServer(svc OSDetector, hooks *twirp.ServerHooks) TwirpServer { + return &oSDetectorServer{ + OSDetector: svc, + hooks: hooks, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *oSDetectorServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// OSDetectorPathPrefix is used for all URL paths on a twirp OSDetector server. +// Requests are always: POST OSDetectorPathPrefix/method +// It can be used in an HTTP mux to route twirp requests along with non-twirp requests on other routes. +const OSDetectorPathPrefix = "/twirp/trivy.detector.OSDetector/" + +func (s *oSDetectorServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "OSDetector") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + err = badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, err) + return + } + + switch req.URL.Path { + case "/twirp/trivy.detector.OSDetector/Detect": + s.serveDetect(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + err = badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, err) + return + } +} + +func (s *oSDetectorServer) serveDetect(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveDetectJSON(ctx, resp, req) + case "application/protobuf": + s.serveDetectProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *oSDetectorServer) serveDetectJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Detect") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + reqContent := new(OSDetectRequest) + unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true} + if err = unmarshaler.Unmarshal(req.Body, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the json request could not be decoded")) + return + } + + // Call service method + var respContent *DetectResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.OSDetector.Detect(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetectResponse and nil error while calling Detect. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + var buf bytes.Buffer + marshaler := &jsonpb.Marshaler{OrigName: true} + if err = marshaler.Marshal(&buf, respContent); err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + respBytes := buf.Bytes() + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *oSDetectorServer) serveDetectProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Detect") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to read request body")) + return + } + reqContent := new(OSDetectRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + // Call service method + var respContent *DetectResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.OSDetector.Detect(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetectResponse and nil error while calling Detect. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *oSDetectorServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 0 +} + +func (s *oSDetectorServer) ProtocGenTwirpVersion() string { + return "v5.9.0" +} + +func (s *oSDetectorServer) PathPrefix() string { + return OSDetectorPathPrefix +} + +// ===================== +// LibDetector Interface +// ===================== + +type LibDetector interface { + Detect(context.Context, *LibDetectRequest) (*DetectResponse, error) +} + +// =========================== +// LibDetector Protobuf Client +// =========================== + +type libDetectorProtobufClient struct { + client HTTPClient + urls [1]string +} + +// NewLibDetectorProtobufClient creates a Protobuf client that implements the LibDetector interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewLibDetectorProtobufClient(addr string, client HTTPClient) LibDetector { + prefix := urlBase(addr) + LibDetectorPathPrefix + urls := [1]string{ + prefix + "Detect", + } + if httpClient, ok := client.(*http.Client); ok { + return &libDetectorProtobufClient{ + client: withoutRedirects(httpClient), + urls: urls, + } + } + return &libDetectorProtobufClient{ + client: client, + urls: urls, + } +} + +func (c *libDetectorProtobufClient) Detect(ctx context.Context, in *LibDetectRequest) (*DetectResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "LibDetector") + ctx = ctxsetters.WithMethodName(ctx, "Detect") + out := new(DetectResponse) + err := doProtobufRequest(ctx, c.client, c.urls[0], in, out) + if err != nil { + return nil, err + } + return out, nil +} + +// ======================= +// LibDetector JSON Client +// ======================= + +type libDetectorJSONClient struct { + client HTTPClient + urls [1]string +} + +// NewLibDetectorJSONClient creates a JSON client that implements the LibDetector interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewLibDetectorJSONClient(addr string, client HTTPClient) LibDetector { + prefix := urlBase(addr) + LibDetectorPathPrefix + urls := [1]string{ + prefix + "Detect", + } + if httpClient, ok := client.(*http.Client); ok { + return &libDetectorJSONClient{ + client: withoutRedirects(httpClient), + urls: urls, + } + } + return &libDetectorJSONClient{ + client: client, + urls: urls, + } +} + +func (c *libDetectorJSONClient) Detect(ctx context.Context, in *LibDetectRequest) (*DetectResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "LibDetector") + ctx = ctxsetters.WithMethodName(ctx, "Detect") + out := new(DetectResponse) + err := doJSONRequest(ctx, c.client, c.urls[0], in, out) + if err != nil { + return nil, err + } + return out, nil +} + +// ========================== +// LibDetector Server Handler +// ========================== + +type libDetectorServer struct { + LibDetector + hooks *twirp.ServerHooks +} + +func NewLibDetectorServer(svc LibDetector, hooks *twirp.ServerHooks) TwirpServer { + return &libDetectorServer{ + LibDetector: svc, + hooks: hooks, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *libDetectorServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// LibDetectorPathPrefix is used for all URL paths on a twirp LibDetector server. +// Requests are always: POST LibDetectorPathPrefix/method +// It can be used in an HTTP mux to route twirp requests along with non-twirp requests on other routes. +const LibDetectorPathPrefix = "/twirp/trivy.detector.LibDetector/" + +func (s *libDetectorServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "trivy.detector") + ctx = ctxsetters.WithServiceName(ctx, "LibDetector") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + err = badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, err) + return + } + + switch req.URL.Path { + case "/twirp/trivy.detector.LibDetector/Detect": + s.serveDetect(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + err = badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, err) + return + } +} + +func (s *libDetectorServer) serveDetect(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveDetectJSON(ctx, resp, req) + case "application/protobuf": + s.serveDetectProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *libDetectorServer) serveDetectJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Detect") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + reqContent := new(LibDetectRequest) + unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true} + if err = unmarshaler.Unmarshal(req.Body, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the json request could not be decoded")) + return + } + + // Call service method + var respContent *DetectResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.LibDetector.Detect(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetectResponse and nil error while calling Detect. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + var buf bytes.Buffer + marshaler := &jsonpb.Marshaler{OrigName: true} + if err = marshaler.Marshal(&buf, respContent); err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + respBytes := buf.Bytes() + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *libDetectorServer) serveDetectProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Detect") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to read request body")) + return + } + reqContent := new(LibDetectRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + // Call service method + var respContent *DetectResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.LibDetector.Detect(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetectResponse and nil error while calling Detect. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *libDetectorServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 1 +} + +func (s *libDetectorServer) ProtocGenTwirpVersion() string { + return "v5.9.0" +} + +func (s *libDetectorServer) PathPrefix() string { + return LibDetectorPathPrefix +} + +// ===== +// Utils +// ===== + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +// +// HTTPClient implementations should not follow redirects. Redirects are +// automatically disabled if *(net/http).Client is passed to client +// constructors. See the withoutRedirects function in this file for more +// details. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// TwirpServer is the interface generated server structs will support: they're +// HTTP handlers with additional methods for accessing metadata about the +// service. Those accessors are a low-level API for building reflection tools. +// Most people can think of TwirpServers as just http.Handlers. +type TwirpServer interface { + http.Handler + // ServiceDescriptor returns gzipped bytes describing the .proto file that + // this service was generated from. Once unzipped, the bytes can be + // unmarshalled as a + // github.com/golang/protobuf/protoc-gen-go/descriptor.FileDescriptorProto. + // + // The returned integer is the index of this particular service within that + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a + // low-level field, expected to be used for reflection. + ServiceDescriptor() ([]byte, int) + // ProtocGenTwirpVersion is the semantic version string of the version of + // twirp used to generate this file. + ProtocGenTwirpVersion() string + // PathPrefix returns the HTTP URL path prefix for all methods handled by this + // service. This can be used with an HTTP mux to route twirp requests + // alongside non-twirp requests on one HTTP listener. + PathPrefix() string +} + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) { + writeError(context.Background(), resp, err, nil) +} + +// writeError writes Twirp errors in the response and triggers hooks. +func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { + // Non-twirp errors are wrapped as Internal (default) + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) + ctx = ctxsetters.WithStatusCode(ctx, statusCode) + ctx = callError(ctx, hooks, twerr) + + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + // We have three options here. We could log the error, call the Error + // hook, or just silently ignore the error. + // + // Logging is unacceptable because we don't have a user-controlled + // logger; writing out to stderr without permission is too rude. + // + // Calling the Error hook would confuse users: it would mean the Error + // hook got called twice for one request, which is likely to lead to + // duplicated log messages and metrics, no matter how well we document + // the behavior. + // + // Silently ignoring the error is our least-bad option. It's highly + // likely that the connection is broken and the original 'err' says + // so anyway. + _ = writeErr + } + + callResponseSent(ctx, hooks) +} + +// urlBase helps ensure that addr specifies a scheme. If it is unparsable +// as a URL, it returns addr unchanged. +func urlBase(addr string) string { + // If the addr specifies a scheme, use it. If not, default to + // http. If url.Parse fails on it, return it unchanged. + url, err := url.Parse(addr) + if err != nil { + return addr + } + if url.Scheme == "" { + url.Scheme = "http" + } + return url.String() +} + +// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in +// a context through the twirp.WithHTTPRequestHeaders function. +// If there are no headers set, or if they have the wrong type, nil is returned. +func getCustomHTTPReqHeaders(ctx context.Context) http.Header { + header, ok := twirp.HTTPRequestHeaders(ctx) + if !ok || header == nil { + return nil + } + copied := make(http.Header) + for k, vv := range header { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + return copied +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequest("POST", url, reqBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { + req.Header = customHeader + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Twirp-Version", "v5.9.0") + return req, nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr twirp.Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} + +// errorFromResponse builds a twirp.Error from a non-200 HTTP response. +// If the response has a valid serialized Twirp error, then it's returned. +// If not, the response status code is used to generate a similar twirp +// error. See twirpErrorFromIntermediary for more info on intermediary errors. +func errorFromResponse(resp *http.Response) twirp.Error { + statusCode := resp.StatusCode + statusText := http.StatusText(statusCode) + + if isHTTPRedirect(statusCode) { + // Unexpected redirect: it must be an error from an intermediary. + // Twirp clients don't follow redirects automatically, Twirp only handles + // POST requests, redirects should only happen on GET and HEAD requests. + location := resp.Header.Get("Location") + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) + return twirpErrorFromIntermediary(statusCode, msg, location) + } + + respBodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read server error response body") + } + + var tj twerrJSON + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) + dec.DisallowUnknownFields() + if err := dec.Decode(&tj); err != nil || tj.Code == "" { + // Invalid JSON response; it must be an error from an intermediary. + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) + } + + errorCode := twirp.ErrorCode(tj.Code) + if !twirp.IsValidErrorCode(errorCode) { + msg := "invalid type returned from server error response: " + tj.Code + return twirp.InternalError(msg) + } + + twerr := twirp.NewError(errorCode, tj.Msg) + for k, v := range tj.Meta { + twerr = twerr.WithMeta(k, v) + } + return twerr +} + +// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. +// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. +// Returned twirp Errors have some additional metadata for inspection. +func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { + var code twirp.ErrorCode + if isHTTPRedirect(status) { // 3xx + code = twirp.Internal + } else { + switch status { + case 400: // Bad Request + code = twirp.Internal + case 401: // Unauthorized + code = twirp.Unauthenticated + case 403: // Forbidden + code = twirp.PermissionDenied + case 404: // Not Found + code = twirp.BadRoute + case 429, 502, 503, 504: // Too Many Requests, Bad Gateway, Service Unavailable, Gateway Timeout + code = twirp.Unavailable + default: // All other codes + code = twirp.Unknown + } + } + + twerr := twirp.NewError(code, msg) + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) + if isHTTPRedirect(status) { + twerr = twerr.WithMeta("location", bodyOrLocation) + } else { + twerr = twerr.WithMeta("body", bodyOrLocation) + } + return twerr +} + +func isHTTPRedirect(status int) bool { + return status >= 300 && status <= 399 +} + +// wrapInternal wraps an error with a prefix as an Internal error. +// The original error cause is accessible by github.com/pkg/errors.Cause. +func wrapInternal(err error, prefix string) twirp.Error { + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) +} + +type wrappedError struct { + prefix string + cause error +} + +func (e *wrappedError) Cause() error { return e.cause } +func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } + +// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal +// error response (status 500), and error hooks are properly called with the panic wrapped as an error. +// The panic is re-raised so it can be handled normally with middleware. +func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { + if r := recover(); r != nil { + // Wrap the panic as an error so it can be passed to error hooks. + // The original error is accessible from error hooks, but not visible in the response. + err := errFromPanic(r) + twerr := &internalWithCause{msg: "Internal service panic", cause: err} + // Actually write the error + writeError(ctx, resp, twerr, hooks) + // If possible, flush the error to the wire. + f, ok := resp.(http.Flusher) + if ok { + f.Flush() + } + + panic(r) + } +} + +// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. +func errFromPanic(p interface{}) error { + if err, ok := p.(error); ok { + return err + } + return fmt.Errorf("panic: %v", p) +} + +// internalWithCause is a Twirp Internal error wrapping an original error cause, accessible +// by github.com/pkg/errors.Cause, but the original error message is not exposed on Msg(). +type internalWithCause struct { + msg string + cause error +} + +func (e *internalWithCause) Cause() error { return e.cause } +func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } +func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } +func (e *internalWithCause) Msg() string { return e.msg } +func (e *internalWithCause) Meta(key string) string { return "" } +func (e *internalWithCause) MetaMap() map[string]string { return nil } +func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } + +// malformedRequestError is used when the twirp server cannot unmarshal a request +func malformedRequestError(msg string) twirp.Error { + return twirp.NewError(twirp.Malformed, msg) +} + +// badRouteError is used when the twirp server cannot route a request +func badRouteError(msg string, method, url string) twirp.Error { + err := twirp.NewError(twirp.BadRoute, msg) + err = err.WithMeta("twirp_invalid_route", method+" "+url) + return err +} + +// withoutRedirects makes sure that the POST request can not be redirected. +// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or +// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the +// method to GET and removing the body. This produces very confusing error messages, so instead we +// set a redirect policy that always errors. This stops Go from executing the redirect. +// +// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect +// policy - if so, we'll run through that policy first. +// +// Because this requires modifying the http.Client, we make a new copy of the client and return it. +func withoutRedirects(in *http.Client) *http.Client { + copy := *in + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if in.CheckRedirect != nil { + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it + // returns, since we want to use ErrUseLastResponse. + err := in.CheckRedirect(req, via) + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. + } + return http.ErrUseLastResponse + } + return © +} + +// doProtobufRequest makes a Protobuf request to the remote Twirp service. +func doProtobufRequest(ctx context.Context, client HTTPClient, url string, in, out proto.Message) (err error) { + reqBodyBytes, err := proto.Marshal(in) + if err != nil { + return wrapInternal(err, "failed to marshal proto request") + } + reqBody := bytes.NewBuffer(reqBodyBytes) + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/protobuf") + if err != nil { + return wrapInternal(err, "could not build request") + } + resp, err := client.Do(req) + if err != nil { + return wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return errorFromResponse(resp) + } + + respBodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read response body") + } + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + + if err = proto.Unmarshal(respBodyBytes, out); err != nil { + return wrapInternal(err, "failed to unmarshal proto response") + } + return nil +} + +// doJSONRequest makes a JSON request to the remote Twirp service. +func doJSONRequest(ctx context.Context, client HTTPClient, url string, in, out proto.Message) (err error) { + reqBody := bytes.NewBuffer(nil) + marshaler := &jsonpb.Marshaler{OrigName: true} + if err = marshaler.Marshal(reqBody, in); err != nil { + return wrapInternal(err, "failed to marshal json request") + } + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/json") + if err != nil { + return wrapInternal(err, "could not build request") + } + resp, err := client.Do(req) + if err != nil { + return wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return errorFromResponse(resp) + } + + unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true} + if err = unmarshaler.Unmarshal(resp.Body, out); err != nil { + return wrapInternal(err, "failed to unmarshal json response") + } + if err = ctx.Err(); err != nil { + return wrapInternal(err, "aborted because context was done") + } + return nil +} + +// Call twirp.ServerHooks.RequestReceived if the hook is available +func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestReceived == nil { + return ctx, nil + } + return h.RequestReceived(ctx) +} + +// Call twirp.ServerHooks.RequestRouted if the hook is available +func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestRouted == nil { + return ctx, nil + } + return h.RequestRouted(ctx) +} + +// Call twirp.ServerHooks.ResponsePrepared if the hook is available +func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { + if h == nil || h.ResponsePrepared == nil { + return ctx + } + return h.ResponsePrepared(ctx) +} + +// Call twirp.ServerHooks.ResponseSent if the hook is available +func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { + if h == nil || h.ResponseSent == nil { + return + } + h.ResponseSent(ctx) +} + +// Call twirp.ServerHooks.Error if the hook is available +func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { + if h == nil || h.Error == nil { + return ctx + } + return h.Error(ctx, err) +} + +var twirpFileDescriptor0 = []byte{ + // 618 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x4d, 0x4f, 0xdb, 0x40, + 0x10, 0x6d, 0x3e, 0xed, 0x4c, 0xf8, 0x70, 0x57, 0x48, 0xb8, 0xa0, 0x42, 0x94, 0x5e, 0x68, 0x2b, + 0x05, 0x29, 0xb4, 0xea, 0xb9, 0x05, 0x0a, 0x69, 0x43, 0x40, 0xa6, 0x80, 0xda, 0x4b, 0xb4, 0x71, + 0x26, 0x64, 0x85, 0x93, 0x75, 0x77, 0x97, 0xa8, 0x96, 0xfa, 0xb7, 0xfa, 0xd3, 0x7a, 0xaf, 0x76, + 0xd7, 0x36, 0x49, 0xca, 0x85, 0xdb, 0xcc, 0xbc, 0xe7, 0x99, 0xd9, 0xf7, 0xd6, 0x0b, 0x5b, 0x22, + 0x0e, 0xf7, 0x87, 0xa8, 0x30, 0x54, 0x5c, 0xec, 0x4b, 0x14, 0x33, 0x16, 0x62, 0x2b, 0x16, 0x5c, + 0x71, 0xb2, 0xa6, 0x04, 0x9b, 0x25, 0xad, 0x0c, 0x6d, 0xfe, 0x86, 0xf5, 0xf3, 0xcb, 0x23, 0x93, + 0x05, 0xf8, 0xf3, 0x1e, 0xa5, 0x22, 0xdb, 0x50, 0xe3, 0xb2, 0x3f, 0xa2, 0x13, 0x16, 0x25, 0x7e, + 0xa1, 0x51, 0xd8, 0xab, 0x05, 0x2e, 0x97, 0x9f, 0x4d, 0x4e, 0x36, 0xc1, 0xe1, 0xb2, 0x3f, 0xa5, + 0x13, 0xf4, 0x8b, 0x06, 0xaa, 0x72, 0xd9, 0xa3, 0x13, 0x24, 0x07, 0xe0, 0xc6, 0x34, 0xbc, 0xa3, + 0xb7, 0x28, 0xfd, 0x52, 0xa3, 0xb4, 0x57, 0x6f, 0x6f, 0xb6, 0x16, 0x67, 0xb5, 0x2e, 0x2c, 0x1e, + 0xe4, 0xc4, 0xe6, 0x04, 0xd6, 0xb2, 0xd9, 0x32, 0xe6, 0x53, 0x89, 0xe4, 0x04, 0xd6, 0x67, 0xf7, + 0xd1, 0x14, 0x05, 0x1d, 0xb0, 0x88, 0x29, 0x86, 0xd2, 0x2f, 0x98, 0x6e, 0x2f, 0x97, 0xbb, 0x5d, + 0xcf, 0xd1, 0x92, 0x60, 0xf9, 0x2b, 0x42, 0xa0, 0x8c, 0x5c, 0x46, 0x66, 0x4b, 0x37, 0x30, 0x71, + 0xf3, 0x6f, 0x01, 0x9c, 0x74, 0x09, 0x8d, 0x9b, 0x53, 0xd8, 0x03, 0x9a, 0x98, 0xf8, 0xe0, 0xcc, + 0x50, 0x48, 0xc6, 0xa7, 0xe9, 0xe1, 0xb2, 0x54, 0x23, 0x02, 0x23, 0xa4, 0x12, 0xfd, 0x92, 0x45, + 0xd2, 0x94, 0x6c, 0x40, 0x05, 0x63, 0x1e, 0x8e, 0xfd, 0x72, 0xa3, 0xb0, 0x57, 0x09, 0x6c, 0xa2, + 0xbb, 0x53, 0x11, 0x8e, 0xfd, 0x8a, 0xed, 0xae, 0x63, 0xf2, 0x02, 0x5c, 0x29, 0x42, 0xab, 0x5d, + 0xd5, 0x36, 0x91, 0x22, 0x34, 0xe2, 0xed, 0x42, 0x5d, 0x43, 0xd9, 0x70, 0xc7, 0xa0, 0x20, 0x45, + 0x78, 0x9d, 0xce, 0x4f, 0x09, 0xd9, 0x0e, 0x6e, 0x4e, 0x08, 0xd2, 0x35, 0xb6, 0xa1, 0xa6, 0x09, + 0x76, 0x95, 0x9a, 0x59, 0x45, 0x4f, 0x3b, 0xd6, 0x79, 0x73, 0x04, 0x5e, 0x97, 0x0d, 0xfe, 0x73, + 0x79, 0xc4, 0x22, 0xec, 0xc7, 0x54, 0x8d, 0x33, 0x97, 0x75, 0xe1, 0x82, 0xaa, 0x31, 0x79, 0x0f, + 0xb5, 0x88, 0x0d, 0x04, 0x15, 0x5a, 0xff, 0xe2, 0xe3, 0x6e, 0x76, 0x0d, 0x21, 0x09, 0x1e, 0x98, + 0xcd, 0x0f, 0xe0, 0xa4, 0xd5, 0xa7, 0xc9, 0xdb, 0xfc, 0x53, 0x84, 0xd5, 0x05, 0x3f, 0xc9, 0x6b, + 0xf0, 0xe6, 0x1d, 0x4d, 0xfa, 0x6c, 0x98, 0xf6, 0x5a, 0x70, 0x3a, 0xe9, 0x0c, 0xb5, 0xae, 0xf1, + 0xdd, 0xed, 0xfc, 0x9d, 0x74, 0xe2, 0xbb, 0x5b, 0xa3, 0xeb, 0x5b, 0x78, 0xce, 0xa6, 0x52, 0xd1, + 0x28, 0xc2, 0x61, 0xae, 0xae, 0x35, 0xd0, 0xcb, 0x81, 0x4c, 0xe3, 0x57, 0xb0, 0x3a, 0x62, 0xbf, + 0xe6, 0x88, 0x65, 0x43, 0x5c, 0x31, 0xc5, 0x8c, 0xb4, 0x01, 0x15, 0xc5, 0x54, 0x84, 0xa9, 0xb3, + 0x36, 0x21, 0x0d, 0xa8, 0x0f, 0x51, 0x86, 0x82, 0xc5, 0x4a, 0x7f, 0x68, 0xdd, 0x9d, 0x2f, 0x91, + 0x77, 0xe0, 0x4a, 0x9c, 0xa1, 0x60, 0x2a, 0x31, 0xf6, 0xae, 0xb5, 0xfd, 0x65, 0x41, 0x2f, 0x53, + 0x3c, 0xc8, 0x99, 0x64, 0x07, 0x40, 0xe0, 0x08, 0x05, 0x4e, 0x43, 0x94, 0xbe, 0xdb, 0x28, 0x69, + 0xd7, 0x1f, 0x2a, 0x6f, 0x8e, 0xc0, 0xcd, 0xbe, 0x22, 0x75, 0x70, 0xae, 0x7a, 0x5f, 0x7b, 0xe7, + 0x37, 0x3d, 0xef, 0x19, 0x71, 0xa0, 0xd4, 0x3d, 0xbf, 0xf1, 0x0a, 0x04, 0xa0, 0x7a, 0x76, 0x7c, + 0xd4, 0xb9, 0x3a, 0xf3, 0x8a, 0xc4, 0x85, 0xf2, 0x69, 0xe7, 0xe4, 0xd4, 0x2b, 0x91, 0x15, 0x70, + 0x0f, 0x83, 0xce, 0xb7, 0xce, 0xe1, 0xc7, 0xae, 0x57, 0x6e, 0xdf, 0x00, 0x64, 0x6f, 0x00, 0x17, + 0xa4, 0x03, 0x55, 0x1b, 0x93, 0xdd, 0xe5, 0x0d, 0x97, 0x5e, 0x8a, 0xad, 0x9d, 0x65, 0xc2, 0xe2, + 0xcf, 0xdc, 0xfe, 0x0e, 0xf5, 0xfc, 0xde, 0x71, 0x41, 0xbe, 0xe4, 0x9d, 0x1b, 0x8f, 0x5c, 0xa6, + 0x27, 0xb5, 0xfe, 0x04, 0x3f, 0xdc, 0x0c, 0x1a, 0x54, 0xcd, 0xd3, 0x76, 0xf0, 0x2f, 0x00, 0x00, + 0xff, 0xff, 0xde, 0x1c, 0x85, 0x42, 0xf8, 0x04, 0x00, 0x00, +}