diff --git a/.golangci.yaml b/.golangci.yaml index 8dc42989ca..844a86c026 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -31,7 +31,6 @@ linters: - ineffassign - typecheck - govet - - errcheck - varcheck - deadcode - revive diff --git a/cmd/trivy/main.go b/cmd/trivy/main.go index d33addd010..4e45b40ad5 100644 --- a/cmd/trivy/main.go +++ b/cmd/trivy/main.go @@ -1,8 +1,6 @@ package main import ( - "os" - "github.com/aquasecurity/trivy/pkg/commands" "github.com/aquasecurity/trivy/pkg/log" ) @@ -13,8 +11,7 @@ var ( func main() { app := commands.NewApp(version) - err := app.Run(os.Args) - if err != nil { + if err := app.Execute(); err != nil { log.Fatal(err) } } diff --git a/go.mod b/go.mod index 2b6d0c57cc..27a01a4a8d 100644 --- a/go.mod +++ b/go.mod @@ -36,30 +36,37 @@ require ( github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 github.com/kylelemons/godebug v1.1.0 + github.com/liamg/memoryfs v1.4.2 + github.com/liamg/tml v0.6.0 github.com/mailru/easyjson v0.7.7 github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/open-policy-agent/opa v0.41.0 github.com/owenrumney/go-sarif/v2 v2.1.1 github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a - github.com/samber/lo v1.21.0 + github.com/samber/lo v1.24.0 github.com/sosedoff/gitkit v0.3.0 + github.com/spf13/cobra v1.4.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.8.0 github.com/testcontainers/testcontainers-go v0.13.0 github.com/tetratelabs/wazero v0.0.0-20220701105919-891761ac1ee2 github.com/twitchtv/twirp v8.1.2+incompatible - github.com/urfave/cli/v2 v2.8.1 github.com/xlab/treeprint v1.1.0 + go.etcd.io/bbolt v1.3.6 go.uber.org/zap v1.21.0 golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 + golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 ) require ( - cloud.google.com/go v0.99.0 // indirect + cloud.google.com/go v0.100.2 // indirect + cloud.google.com/go/compute v1.6.1 // indirect + cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/storage v1.14.0 // indirect github.com/Azure/azure-sdk-for-go v66.0.0+incompatible github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -108,7 +115,6 @@ require ( github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3 // indirect github.com/containerd/typeurl v1.0.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -124,6 +130,7 @@ require ( github.com/emirpasic/gods v1.12.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect @@ -146,7 +153,7 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/googleapis/gax-go/v2 v2.1.1 // indirect + github.com/googleapis/gax-go/v2 v2.4.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect @@ -157,6 +164,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.4.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl/v2 v2.12.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect @@ -175,11 +183,9 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/liamg/iamgo v0.0.6 // indirect github.com/liamg/jfather v0.0.7 // indirect - github.com/liamg/memoryfs v1.4.2 - github.com/liamg/tml v0.6.0 github.com/lib/pq v1.10.4 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/magiconair/properties v1.8.5 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect @@ -188,6 +194,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/buildkit v0.10.3 github.com/moby/locker v1.0.1 // indirect @@ -208,6 +215,7 @@ require ( github.com/opencontainers/runtime-spec v1.0.3-0.20220311020903-6969a0a09ab1 // indirect github.com/opencontainers/selinux v1.10.1 // indirect github.com/owenrumney/squealer v1.0.1-0.20220510063705-c0be93f0edea // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -220,16 +228,16 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/rubenv/sql-migrate v1.1.1 // indirect github.com/russross/blackfriday v1.6.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e github.com/sergi/go-diff v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spdx/tools-golang v0.3.0 - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.4.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stretchr/objx v0.4.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/ulikunitz/xz v0.5.8 // indirect github.com/vbatts/tar-split v0.11.2 // indirect github.com/vektah/gqlparser/v2 v2.4.4 // indirect @@ -237,12 +245,10 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect github.com/zclconf/go-cty v1.10.0 // indirect github.com/zclconf/go-cty-yaml v1.0.2 // indirect - go.etcd.io/bbolt v1.3.6 go.opencensus.io v0.23.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.7.0 // indirect @@ -250,20 +256,21 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect - google.golang.org/api v0.62.0 // indirect + google.golang.org/api v0.81.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect google.golang.org/grpc v1.47.0 // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools v2.2.0+incompatible diff --git a/go.sum b/go.sum index d4265209c0..b2d1f71add 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -28,18 +29,26 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -454,7 +463,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -565,6 +573,7 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= @@ -801,11 +810,15 @@ github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= @@ -866,6 +879,7 @@ github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4= github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= @@ -957,8 +971,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -991,8 +1005,9 @@ github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1074,6 +1089,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -1209,6 +1226,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= @@ -1221,6 +1240,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -1278,6 +1298,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rubenv/sql-migrate v1.1.1 h1:haR5Hn8hbW9/SpAICrXoZqXnywS7Q5WijwkQENPeNWY= github.com/rubenv/sql-migrate v1.1.1/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= @@ -1285,13 +1306,12 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/samber/lo v1.21.0 h1:FSby8pJQtX4KmyddTCCGhc3JvnnIVrDA+NW37rG+7G8= -github.com/samber/lo v1.21.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.24.0 h1:8BtUIUpAK2UfLv4/yI+1+1ux8brGwjhTpSndNWjRsjs= +github.com/samber/lo v1.24.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e h1:NO86zOn5ScSKW8wRbMaSIcjDZUFpWdCQQnexRqZ9h9A= github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e/go.mod h1:G0Z6yVPru183i2MuRJx1DcR4dgIZtLcTdaaE/pC1BJU= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1331,10 +1351,12 @@ github.com/spdx/tools-golang v0.3.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAP github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -1344,6 +1366,7 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6 github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1355,6 +1378,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1375,6 +1399,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1400,8 +1426,6 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4= -github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= github.com/vektah/gqlparser/v2 v2.4.4 h1:rh9hwZ5Jx9cCq88zXz2YHKmuQBuwY1JErHU8GywFdwE= @@ -1434,8 +1458,6 @@ github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6Ut github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1565,6 +1587,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1677,6 +1700,11 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1695,8 +1723,11 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1708,6 +1739,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1829,11 +1861,15 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= @@ -1943,8 +1979,10 @@ golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -1977,8 +2015,15 @@ google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2054,11 +2099,24 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f h1:hJ/Y5SqPXbarffmAsApliUlcvMU+wScNGfyop4bZm8o= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -2093,7 +2151,10 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2136,6 +2197,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/integration/client_server_test.go b/integration/client_server_test.go index d0feb1bc35..08fa0334a6 100644 --- a/integration/client_server_test.go +++ b/integration/client_server_test.go @@ -1,5 +1,4 @@ //go:build integration -// +build integration package integration @@ -7,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "path/filepath" "strings" @@ -19,10 +17,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" testcontainers "github.com/testcontainers/testcontainers-go" - "github.com/urfave/cli/v2" "github.com/aquasecurity/trivy/pkg/clock" - "github.com/aquasecurity/trivy/pkg/commands" "github.com/aquasecurity/trivy/pkg/report" ) @@ -242,14 +238,14 @@ func TestClientServer(t *testing.T) { }, } - app, addr, cacheDir := setup(t, setupOptions{}) + addr, cacheDir := setup(t, setupOptions{}) for _, c := range tests { t.Run(c.name, func(t *testing.T) { osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden) - // Run Trivy client - err := app.Run(osArgs) + // + err := execute(osArgs) require.NoError(t, err) compareReports(t, c.golden, outputFile) @@ -340,7 +336,7 @@ func TestClientServerWithFormat(t *testing.T) { report.CustomTemplateFuncMap = map[string]interface{}{} }) - app, addr, cacheDir := setup(t, setupOptions{}) + addr, cacheDir := setup(t, setupOptions{}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -349,7 +345,7 @@ func TestClientServerWithFormat(t *testing.T) { osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden) // Run Trivy client - err := app.Run(osArgs) + err := execute(osArgs) require.NoError(t, err) want, err := os.ReadFile(tt.golden) @@ -386,13 +382,13 @@ func TestClientServerWithCycloneDX(t *testing.T) { }, } - app, addr, cacheDir := setup(t, setupOptions{}) + addr, cacheDir := setup(t, setupOptions{}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, "") // Run Trivy client - err := app.Run(osArgs) + err := execute(osArgs) require.NoError(t, err) f, err := os.Open(outputFile) @@ -450,7 +446,7 @@ func TestClientServerWithToken(t *testing.T) { serverToken := "token" serverTokenHeader := "Trivy-Token" - app, addr, cacheDir := setup(t, setupOptions{ + addr, cacheDir := setup(t, setupOptions{ token: serverToken, tokenHeader: serverTokenHeader, }) @@ -460,16 +456,14 @@ func TestClientServerWithToken(t *testing.T) { osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden) // Run Trivy client - err := app.Run(osArgs) - + err := execute(osArgs) if c.wantErr != "" { - require.NotNil(t, err, c.name) + require.Error(t, err, c.name) assert.Contains(t, err.Error(), c.wantErr, c.name) return - } else { - assert.NoError(t, err, c.name) } + require.NoError(t, err, c.name) compareReports(t, c.golden, outputFile) }) } @@ -481,7 +475,7 @@ func TestClientServerWithRedis(t *testing.T) { redisC, addr := setupRedis(t, ctx) // Set up Trivy server - app, addr, cacheDir := setup(t, setupOptions{cacheBackend: addr}) + addr, cacheDir := setup(t, setupOptions{cacheBackend: addr}) t.Cleanup(func() { os.RemoveAll(cacheDir) }) // Test parameters @@ -494,7 +488,7 @@ func TestClientServerWithRedis(t *testing.T) { osArgs, outputFile := setupClient(t, testArgs, addr, cacheDir, golden) // Run Trivy client - err := app.Run(osArgs) + err := execute(osArgs) require.NoError(t, err) compareReports(t, golden, outputFile) @@ -507,8 +501,8 @@ func TestClientServerWithRedis(t *testing.T) { osArgs, _ := setupClient(t, testArgs, addr, cacheDir, golden) // Run Trivy client - err := app.Run(osArgs) - require.NotNil(t, err) + err := execute(osArgs) + require.Error(t, err) assert.Contains(t, err.Error(), "connect: connection refused") }) } @@ -519,9 +513,8 @@ type setupOptions struct { cacheBackend string } -func setup(t *testing.T, options setupOptions) (*cli.App, string, string) { +func setup(t *testing.T, options setupOptions) (string, string) { t.Helper() - version := "dev" // Set up testing DB cacheDir := initDB(t) @@ -534,28 +527,21 @@ func setup(t *testing.T, options setupOptions) (*cli.App, string, string) { addr := fmt.Sprintf("localhost:%d", port) go func() { - // Setup CLI App - app := commands.NewApp(version) - app.Writer = io.Discard osArgs := setupServer(addr, options.token, options.tokenHeader, cacheDir, options.cacheBackend) // Run Trivy server - app.Run(osArgs) + require.NoError(t, execute(osArgs)) }() ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) err = waitPort(ctx, addr) assert.NoError(t, err) - // Setup CLI App - app := commands.NewApp(version) - app.Writer = io.Discard - - return app, addr, cacheDir + return addr, cacheDir } func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string { - osArgs := []string{"trivy", "--cache-dir", cacheDir, "server", "--skip-update", "--listen", addr} + osArgs := []string{"--cache-dir", cacheDir, "server", "--skip-update", "--listen", addr} if token != "" { osArgs = append(osArgs, []string{"--token", token, "--token-header", tokenHeader}...) } @@ -573,7 +559,7 @@ func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden st c.RemoteAddrOption = "--server" } t.Helper() - osArgs := []string{"trivy", "--cache-dir", cacheDir, c.Command, c.RemoteAddrOption, "http://" + addr} + osArgs := []string{"--cache-dir", cacheDir, c.Command, c.RemoteAddrOption, "http://" + addr} if c.Format != "" { osArgs = append(osArgs, "--format", c.Format) diff --git a/integration/docker_engine_test.go b/integration/docker_engine_test.go index 35c442ab97..ea1fb92722 100644 --- a/integration/docker_engine_test.go +++ b/integration/docker_engine_test.go @@ -15,8 +15,6 @@ import ( "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/aquasecurity/trivy/pkg/commands" ) func TestDockerEngine(t *testing.T) { @@ -233,16 +231,14 @@ func TestDockerEngine(t *testing.T) { tmpDir := t.TempDir() output := filepath.Join(tmpDir, "result.json") - // run trivy - app := commands.NewApp("dev") - trivyArgs := []string{"trivy", "--cache-dir", cacheDir, "image", + osArgs := []string{"--cache-dir", cacheDir, "image", "--skip-update", "--format=json", "--output", output} if tt.ignoreUnfixed { - trivyArgs = append(trivyArgs, "--ignore-unfixed") + osArgs = append(osArgs, "--ignore-unfixed") } if len(tt.severity) != 0 { - trivyArgs = append(trivyArgs, + osArgs = append(osArgs, []string{"--severity", strings.Join(tt.severity, ",")}..., ) } @@ -252,11 +248,12 @@ func TestDockerEngine(t *testing.T) { assert.NoError(t, err, "failed to write .trivyignore") defer os.Remove(trivyIgnore) } - trivyArgs = append(trivyArgs, tt.input) + osArgs = append(osArgs, tt.input) - err = app.Run(trivyArgs) + // Run Trivy + err = execute(osArgs) if tt.wantErr != "" { - require.NotNil(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr, tt.name) return } diff --git a/integration/fs_test.go b/integration/fs_test.go index 2626b7f3a4..c49fddb431 100644 --- a/integration/fs_test.go +++ b/integration/fs_test.go @@ -4,15 +4,13 @@ package integration import ( - "io" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" - - "github.com/aquasecurity/trivy/pkg/commands" + "github.com/stretchr/testify/require" ) func TestFilesystem(t *testing.T) { @@ -145,7 +143,7 @@ func TestFilesystem(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { osArgs := []string{ - "trivy", "--cache-dir", cacheDir, "fs", "--skip-db-update", "--skip-policy-update", + "-q", "--cache-dir", cacheDir, "fs", "--skip-db-update", "--skip-policy-update", "--format", "json", "--offline-scan", "--security-checks", tt.args.securityChecks, } @@ -189,12 +187,9 @@ func TestFilesystem(t *testing.T) { osArgs = append(osArgs, "--output", outputFile) osArgs = append(osArgs, tt.args.input) - // Setup CLI App - app := commands.NewApp("dev") - app.Writer = io.Discard - // Run "trivy fs" - assert.Nil(t, app.Run(osArgs)) + err := execute(osArgs) + require.NoError(t, err) // Compare want and got compareReports(t, tt.golden, outputFile) diff --git a/integration/integration_test.go b/integration/integration_test.go index ed8dde5e8a..2c188aa13d 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "flag" + "io" "net" "os" "path/filepath" @@ -18,6 +19,7 @@ import ( "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/pkg/commands" "github.com/aquasecurity/trivy/pkg/dbtest" "github.com/aquasecurity/trivy/pkg/types" ) @@ -120,6 +122,16 @@ func readReport(t *testing.T, filePath string) types.Report { return report } +func execute(osArgs []string) error { + // Setup CLI App + app := commands.NewApp("dev") + app.SetOut(io.Discard) + + // Run Trivy + app.SetArgs(osArgs) + return app.Execute() +} + func compareReports(t *testing.T, wantFile, gotFile string) { want := readReport(t, wantFile) got := readReport(t, gotFile) diff --git a/integration/module_test.go b/integration/module_test.go index aa2c87b62c..4894eff43e 100644 --- a/integration/module_test.go +++ b/integration/module_test.go @@ -3,7 +3,6 @@ package integration import ( - "io" "os" "path/filepath" "testing" @@ -11,7 +10,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/aquasecurity/trivy/pkg/commands" "github.com/aquasecurity/trivy/pkg/module" "github.com/aquasecurity/trivy/pkg/utils" ) @@ -48,13 +46,9 @@ func TestModule(t *testing.T) { filepath.Join(moduleDir, "spring4shell.wasm")) require.NoError(t, err) - // Setup CLI App - app := commands.NewApp("dev") - app.Writer = io.Discard - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - osArgs := []string{"trivy", "--cache-dir", cacheDir, "image", "--ignore-unfixed", "--format", "json", + osArgs := []string{"--cache-dir", cacheDir, "image", "--ignore-unfixed", "--format", "json", "--skip-update", "--offline-scan", "--input", tt.input} // Set up the output file @@ -66,7 +60,8 @@ func TestModule(t *testing.T) { osArgs = append(osArgs, []string{"--output", outputFile}...) // Run Trivy - assert.Nil(t, app.Run(osArgs)) + err = execute(osArgs) + assert.NoError(t, err) // Compare want and got compareReports(t, tt.golden, outputFile) diff --git a/integration/registry_test.go b/integration/registry_test.go index b13d1a0561..881fbfa137 100644 --- a/integration/registry_test.go +++ b/integration/registry_test.go @@ -27,8 +27,6 @@ import ( "github.com/stretchr/testify/require" testcontainers "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" - - "github.com/aquasecurity/trivy/pkg/commands" ) const ( @@ -235,15 +233,11 @@ func scan(t *testing.T, imageRef name.Reference, baseDir, goldenFile string, opt return "", err } - // Setup CLI App - app := commands.NewApp("dev") - app.Writer = io.Discard - - osArgs := []string{"trivy", "--cache-dir", cacheDir, "image", "--format", "json", "--skip-update", + osArgs := []string{"-q", "--cache-dir", cacheDir, "image", "--format", "json", "--skip-update", "--output", outputFile, imageRef.Name()} // Run Trivy - if err := app.Run(osArgs); err != nil { + if err := execute(osArgs); err != nil { return "", err } return outputFile, nil diff --git a/integration/sbom_test.go b/integration/sbom_test.go index eb5b0ddb78..431514a189 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -1,10 +1,8 @@ //go:build integration -// +build integration package integration import ( - "io" "os" "path/filepath" "testing" @@ -12,8 +10,6 @@ import ( cdx "github.com/CycloneDX/cyclonedx-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/aquasecurity/trivy/pkg/commands" ) func TestCycloneDX(t *testing.T) { @@ -53,7 +49,7 @@ func TestCycloneDX(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { osArgs := []string{ - "trivy", "--cache-dir", cacheDir, "sbom", "--skip-db-update", "--format", tt.args.format, + "--cache-dir", cacheDir, "sbom", "-q", "--skip-db-update", "--format", tt.args.format, } // Setup the output file @@ -65,12 +61,9 @@ func TestCycloneDX(t *testing.T) { osArgs = append(osArgs, "--output", outputFile) osArgs = append(osArgs, tt.args.input) - // Setup CLI App - app := commands.NewApp("dev") - app.Writer = io.Discard - // Run "trivy sbom" - assert.Nil(t, app.Run(osArgs)) + err := execute(osArgs) + assert.NoError(t, err) // Compare want and got want := decodeCycloneDX(t, tt.golden) diff --git a/integration/standalone_tar_test.go b/integration/standalone_tar_test.go index 8779549aad..a980b950bd 100644 --- a/integration/standalone_tar_test.go +++ b/integration/standalone_tar_test.go @@ -1,18 +1,15 @@ //go:build integration -// +build integration package integration import ( - "io" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" - - "github.com/aquasecurity/trivy/pkg/commands" + "github.com/stretchr/testify/require" ) func TestTar(t *testing.T) { @@ -264,13 +261,9 @@ func TestTar(t *testing.T) { // Set a temp dir so that modules will not be loaded t.Setenv("XDG_DATA_HOME", cacheDir) - // Setup CLI App - app := commands.NewApp("dev") - app.Writer = io.Discard - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - osArgs := []string{"trivy", "--cache-dir", cacheDir, "image", "--format", tt.testArgs.Format, "--skip-update"} + osArgs := []string{"--cache-dir", cacheDir, "image", "-q", "--format", tt.testArgs.Format, "--skip-update"} if tt.testArgs.IgnoreUnfixed { osArgs = append(osArgs, "--ignore-unfixed") @@ -310,7 +303,8 @@ func TestTar(t *testing.T) { osArgs = append(osArgs, []string{"--output", outputFile}...) // Run Trivy - assert.Nil(t, app.Run(osArgs)) + err := execute(osArgs) + require.NoError(t, err) // Compare want and got compareReports(t, tt.golden, outputFile) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index fd344785cf..7e14ccd162 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -2,28 +2,26 @@ package commands import ( "encoding/json" + "errors" "fmt" "io" "os" "strings" - "time" - "github.com/samber/lo" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/metadata" - dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy/pkg/commands/artifact" - "github.com/aquasecurity/trivy/pkg/commands/module" - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/commands/plugin" "github.com/aquasecurity/trivy/pkg/commands/server" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/flag" k8scommands "github.com/aquasecurity/trivy/pkg/k8s/commands" "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/report" - "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/module" + "github.com/aquasecurity/trivy/pkg/plugin" "github.com/aquasecurity/trivy/pkg/types" - "github.com/aquasecurity/trivy/pkg/utils" ) // VersionInfo holds the trivy DB version Info @@ -33,404 +31,790 @@ type VersionInfo struct { } var ( - templateFlag = cli.StringFlag{ - Name: "template", - Aliases: []string{"t"}, - Value: "", - Usage: "output template", - EnvVars: []string{"TRIVY_TEMPLATE"}, - } - - formatFlag = cli.StringFlag{ - Name: "format", - Aliases: []string{"f"}, - Value: report.FormatTable, - Usage: "format (table, json, sarif, template, cyclonedx, spdx, spdx-json, github)", - EnvVars: []string{"TRIVY_FORMAT"}, - } - - inputFlag = cli.StringFlag{ - Name: "input", - Aliases: []string{"i"}, - Value: "", - Usage: "input file path instead of image name", - EnvVars: []string{"TRIVY_INPUT"}, - } - - severityFlag = cli.StringFlag{ - Name: "severity", - Aliases: []string{"s"}, - Value: strings.Join(dbTypes.SeverityNames, ","), - Usage: "severities of vulnerabilities to be displayed (comma separated)", - EnvVars: []string{"TRIVY_SEVERITY"}, - } - - outputFlag = cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "output file name", - EnvVars: []string{"TRIVY_OUTPUT"}, - } - - exitCodeFlag = cli.IntFlag{ - Name: "exit-code", - Usage: "Exit code when vulnerabilities were found", - Value: 0, - EnvVars: []string{"TRIVY_EXIT_CODE"}, - } - - skipDBUpdateFlag = cli.BoolFlag{ - Name: "skip-db-update", - Aliases: []string{"skip-update"}, - Usage: "skip updating vulnerability database", - EnvVars: []string{"TRIVY_SKIP_UPDATE", "TRIVY_SKIP_DB_UPDATE"}, - } - - skipPolicyUpdateFlag = cli.BoolFlag{ - Name: "skip-policy-update", - Usage: "skip updating built-in policies", - EnvVars: []string{"TRIVY_SKIP_POLICY_UPDATE"}, - } - - downloadDBOnlyFlag = cli.BoolFlag{ - Name: "download-db-only", - Usage: "download/update vulnerability database but don't run a scan", - EnvVars: []string{"TRIVY_DOWNLOAD_DB_ONLY"}, - } - - resetFlag = cli.BoolFlag{ - Name: "reset", - Usage: "remove all caches and database", - EnvVars: []string{"TRIVY_RESET"}, - } - - clearCacheFlag = cli.BoolFlag{ - Name: "clear-cache", - Aliases: []string{"c"}, - Usage: "clear image caches without scanning", - EnvVars: []string{"TRIVY_CLEAR_CACHE"}, - } - - quietFlag = cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "suppress progress bar and log output", - EnvVars: []string{"TRIVY_QUIET"}, - } - - noProgressFlag = cli.BoolFlag{ - Name: "no-progress", - Usage: "suppress progress bar", - EnvVars: []string{"TRIVY_NO_PROGRESS"}, - } - - ignoreUnfixedFlag = cli.BoolFlag{ - Name: "ignore-unfixed", - Usage: "display only fixed vulnerabilities", - EnvVars: []string{"TRIVY_IGNORE_UNFIXED"}, - } - - debugFlag = cli.BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Usage: "debug mode", - EnvVars: []string{"TRIVY_DEBUG"}, - } - - removedPkgsFlag = cli.BoolFlag{ - Name: "removed-pkgs", - Usage: "detect vulnerabilities of removed packages (only for Alpine)", - EnvVars: []string{"TRIVY_REMOVED_PKGS"}, - } - - vulnTypeFlag = cli.StringFlag{ - Name: "vuln-type", - Value: strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), - Usage: "comma-separated list of vulnerability types (os,library)", - EnvVars: []string{"TRIVY_VULN_TYPE"}, - } - - securityChecksFlag = cli.StringFlag{ - Name: "security-checks", - Value: fmt.Sprintf("%s,%s", types.SecurityCheckVulnerability, types.SecurityCheckSecret), - Usage: "comma-separated list of what security issues to detect (vuln,config,secret)", - EnvVars: []string{"TRIVY_SECURITY_CHECKS"}, - } - - cacheDirFlag = cli.StringFlag{ - Name: "cache-dir", - Value: utils.DefaultCacheDir(), - Usage: "cache directory", - EnvVars: []string{"TRIVY_CACHE_DIR"}, - } - - cacheBackendFlag = cli.StringFlag{ - Name: "cache-backend", - Value: "fs", - Usage: "cache backend (e.g. redis://localhost:6379)", - EnvVars: []string{"TRIVY_CACHE_BACKEND"}, - } - - cacheTTL = cli.DurationFlag{ - Name: "cache-ttl", - Usage: "cache TTL when using redis as cache backend", - EnvVars: []string{"TRIVY_CACHE_TTL"}, - } - - redisBackendCACert = cli.StringFlag{ - Name: "redis-ca", - Usage: "redis ca file location, if using redis as cache backend", - EnvVars: []string{"TRIVY_REDIS_BACKEND_CA"}, - Hidden: true, - } - - redisBackendCert = cli.StringFlag{ - Name: "redis-cert", - Usage: "redis certificate file location, if using redis as cache backend", - EnvVars: []string{"TRIVY_REDIS_BACKEND_CERT"}, - Hidden: true, - } - - redisBackendKey = cli.StringFlag{ - Name: "redis-key", - Usage: "redis key file location, if using redis as cache backend", - EnvVars: []string{"TRIVY_REDIS_BACKEND_KEY"}, - Hidden: true, - } - - ignoreFileFlag = cli.StringFlag{ - Name: "ignorefile", - Value: result.DefaultIgnoreFile, - Usage: "specify .trivyignore file", - EnvVars: []string{"TRIVY_IGNOREFILE"}, - } - - timeoutFlag = cli.DurationFlag{ - Name: "timeout", - Value: time.Second * 300, - Usage: "timeout", - EnvVars: []string{"TRIVY_TIMEOUT"}, - } - - namespaceFlag = cli.StringFlag{ - Name: "namespace", - Aliases: []string{"n"}, - Value: "", - Usage: "specify a namespace to scan", - EnvVars: []string{"TRIVY_K8S_NAMESPACE"}, - } - - contextFlag = cli.StringFlag{ - Name: "context", - Aliases: []string{"ctx"}, - Value: "", - Usage: "specify a context to scan", - EnvVars: []string{"TRIVY_K8S_CONTEXT"}, - } - - reportFlag = cli.StringFlag{ - Name: "report", - Value: "all", - Usage: "specify a report format for the output. (all,summary default: all)", - } - - // TODO: remove this flag after a sufficient deprecation period. - lightFlag = cli.BoolFlag{ - Name: "light", - Usage: "deprecated", - EnvVars: []string{"TRIVY_LIGHT"}, - } - - token = cli.StringFlag{ - Name: "token", - Usage: "for authentication in client/server mode", - EnvVars: []string{"TRIVY_TOKEN"}, - } - - tokenHeader = cli.StringFlag{ - Name: "token-header", - Value: option.DefaultTokenHeader, - Usage: "specify a header name for token in client/server mode", - EnvVars: []string{"TRIVY_TOKEN_HEADER"}, - } - - ignorePolicy = cli.StringFlag{ - Name: "ignore-policy", - Usage: "specify the Rego file to evaluate each vulnerability", - EnvVars: []string{"TRIVY_IGNORE_POLICY"}, - } - - listAllPackages = cli.BoolFlag{ - Name: "list-all-pkgs", - Usage: "enabling the option will output all packages regardless of vulnerability", - EnvVars: []string{"TRIVY_LIST_ALL_PKGS"}, - } - - skipFiles = cli.StringSliceFlag{ - Name: "skip-files", - Usage: "specify the file paths to skip traversal", - EnvVars: []string{"TRIVY_SKIP_FILES"}, - } - - skipDirs = cli.StringSliceFlag{ - Name: "skip-dirs", - Usage: "specify the directories where the traversal is skipped", - EnvVars: []string{"TRIVY_SKIP_DIRS"}, - } - - offlineScan = cli.BoolFlag{ - Name: "offline-scan", - Usage: "do not issue API requests to identify dependencies", - EnvVars: []string{"TRIVY_OFFLINE_SCAN"}, - } - - // For misconfigurations - configPolicy = cli.StringSliceFlag{ - Name: "config-policy", - Usage: "specify paths to the Rego policy files directory, applying config files", - EnvVars: []string{"TRIVY_CONFIG_POLICY"}, - } - - configPolicyAlias = cli.StringSliceFlag{ - Name: "policy", - Aliases: []string{"config-policy"}, - Usage: "specify paths to the Rego policy files directory, applying config files", - EnvVars: []string{"TRIVY_POLICY"}, - } - - configData = cli.StringSliceFlag{ - Name: "config-data", - Usage: "specify paths from which data for the Rego policies will be recursively loaded", - EnvVars: []string{"TRIVY_CONFIG_DATA"}, - } - - configDataAlias = cli.StringSliceFlag{ - Name: "data", - Aliases: []string{"config-data"}, - Usage: "specify paths from which data for the Rego policies will be recursively loaded", - EnvVars: []string{"TRIVY_DATA"}, - } - - filePatterns = cli.StringSliceFlag{ - Name: "file-patterns", - Usage: "specify file patterns", - EnvVars: []string{"TRIVY_FILE_PATTERNS"}, - } - - policyNamespaces = cli.StringSliceFlag{ - Name: "policy-namespaces", - Aliases: []string{"namespaces"}, - Usage: "Rego namespaces", - Value: cli.NewStringSlice("users"), - EnvVars: []string{"TRIVY_POLICY_NAMESPACES"}, - } - - includeNonFailures = cli.BoolFlag{ - Name: "include-non-failures", - Usage: "include successes and exceptions", - Value: false, - EnvVars: []string{"TRIVY_INCLUDE_NON_FAILURES"}, - } - - traceFlag = cli.BoolFlag{ - Name: "trace", - Usage: "enable more verbose trace output for custom queries", - Value: false, - EnvVars: []string{"TRIVY_TRACE"}, - } - - insecureFlag = cli.BoolFlag{ - Name: "insecure", - Usage: "allow insecure server connections when using SSL", - Value: false, - EnvVars: []string{"TRIVY_INSECURE"}, - } - - remoteServer = cli.StringFlag{ - Name: "server", - Usage: "server address", - EnvVars: []string{"TRIVY_SERVER"}, - } - - customHeaders = cli.StringSliceFlag{ - Name: "custom-headers", - Usage: "custom headers in client/server mode", - EnvVars: []string{"TRIVY_CUSTOM_HEADERS"}, - } - - dbRepositoryFlag = cli.StringFlag{ - Name: "db-repository", - Usage: "OCI repository to retrieve trivy-db from", - Value: "ghcr.io/aquasecurity/trivy-db", - EnvVars: []string{"TRIVY_DB_REPOSITORY"}, - } - - secretConfig = cli.StringFlag{ - Name: "secret-config", - Usage: "specify a path to config file for secret scanning", - Value: "trivy-secret.yaml", - EnvVars: []string{"TRIVY_SECRET_CONFIG"}, - } - - dependencyTree = cli.BoolFlag{ - Name: "dependency-tree", - Usage: "show dependency origin tree (EXPERIMENTAL)", - EnvVars: []string{"TRIVY_DEPENDENCY_TREE"}, - } - - // Global flags - globalFlags = []cli.Flag{ - &quietFlag, - &debugFlag, - &cacheDirFlag, - } + outputWriter io.Writer = os.Stdout ) -// NewApp is the factory method to return Trivy CLI -func NewApp(version string) *cli.App { - cli.VersionPrinter = func(c *cli.Context) { - showVersion(c.String("cache-dir"), c.String("format"), c.App.Version, c.App.Writer) - } +// SetOut overrides the destination for messages +func SetOut(out io.Writer) { + outputWriter = out +} - app := cli.NewApp() - app.Name = "trivy" - app.Version = version - app.ArgsUsage = "target" - app.Usage = "Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets" - app.EnableBashCompletion = true - app.Flags = globalFlags +// NewApp is the factory method to return Trivy CLI +func NewApp(version string) *cobra.Command { + globalFlags := flag.NewGlobalFlagGroup() + rootCmd := NewRootCommand(version, globalFlags) if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" { - app.Action = func(ctx *cli.Context) error { - return plugin.RunWithArgs(ctx.Context, runAsPlugin, ctx.Args().Slice()) + rootCmd.RunE = func(cmd *cobra.Command, args []string) error { + return plugin.RunWithArgs(cmd.Context(), runAsPlugin, args) } - app.HideVersion = true - app.HideHelp = true - app.HideHelpCommand = true - app.Flags = append(app.Flags, &cli.BoolFlag{ - Name: "help", - Aliases: []string{"h"}, - }) - return app + rootCmd.DisableFlagParsing = true + return rootCmd } - app.Commands = []*cli.Command{ - NewImageCommand(), - NewFilesystemCommand(), - NewRootfsCommand(), - NewRepositoryCommand(), - NewClientCommand(), - NewServerCommand(), - NewConfigCommand(), + rootCmd.AddCommand( + NewImageCommand(globalFlags), + NewFilesystemCommand(globalFlags), + NewRootfsCommand(globalFlags), + NewRepositoryCommand(globalFlags), + NewClientCommand(globalFlags), + NewServerCommand(globalFlags), + NewConfigCommand(globalFlags), NewPluginCommand(), - NewModuleCommand(), - NewK8sCommand(), - NewSbomCommand(), - NewVersionCommand(), - } - app.Commands = append(app.Commands, plugin.LoadCommands()...) + NewModuleCommand(globalFlags), + NewKubernetesCommand(globalFlags), + NewSBOMCommand(globalFlags), + NewVersionCommand(globalFlags), + ) + rootCmd.AddCommand(loadPluginCommands()...) - return app + return rootCmd +} + +func loadPluginCommands() []*cobra.Command { + var commands []*cobra.Command + plugins, err := plugin.LoadAll() + if err != nil { + log.Logger.Debugf("no plugins were loaded") + return nil + } + for _, p := range plugins { + p := p + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s [flags]", p.Name), + Short: p.Usage, + RunE: func(cmd *cobra.Command, args []string) error { + if err = p.Run(cmd.Context(), args); err != nil { + return xerrors.Errorf("plugin error: %w", err) + } + return nil + }, + } + commands = append(commands, cmd) + } + return commands +} + +func initConfig(configFile string) error { + // Configure environment variables + viper.SetEnvPrefix("trivy") // will be uppercased automatically + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + + // Read from config + viper.SetConfigFile(configFile) + viper.SetConfigType("yaml") + if err := viper.ReadInConfig(); err != nil { + if errors.Is(err, os.ErrNotExist) { + log.Logger.Debugf("config file %q not found", configFile) + return nil + } + return xerrors.Errorf("config file %q loading error: %s", configFile, err) + } + return nil +} + +func NewRootCommand(version string, globalFlags *flag.GlobalFlagGroup) *cobra.Command { + var versionFormat string + cmd := &cobra.Command{ + Use: "trivy [global flags] command [flags] target", + Short: "Unified security scanner", + Long: "Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets", + Example: ` # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Scan local filesystem + $ trivy fs . + + # Run in server mode + $ trivy server`, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + Args: cobra.NoArgs, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + cmd.SetOut(outputWriter) + + // Set the Trivy version here so that we can override version printer. + cmd.Version = version + + // viper.BindPFlag cannot be called in init(). + // cf. https://github.com/spf13/cobra/issues/875 + // https://github.com/spf13/viper/issues/233 + if err := globalFlags.Bind(cmd.Root()); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + + // The config path is needed for config initialization. + // It needs to be obtained before ToOptions(). + configPath := viper.GetString(flag.ConfigFileFlag.ConfigName) + + // Configure environment variables and config file + // It cannot be called in init() because it must be called after viper.BindPFlags. + if err := initConfig(configPath); err != nil { + return err + } + + globalOptions := globalFlags.ToOptions() + + // Initialize logger + if err := log.InitLogger(globalOptions.Debug, globalOptions.Quiet); err != nil { + return err + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + globalOptions := globalFlags.ToOptions() + if globalOptions.ShowVersion { + // Customize version output + showVersion(globalOptions.CacheDir, versionFormat, version, outputWriter) + } else { + return cmd.Help() + } + return nil + }, + } + + // Add version format flag, only json is supported + cmd.Flags().StringVarP(&versionFormat, flag.FormatFlag.Name, flag.FormatFlag.Shorthand, "", "version format (json)") + + globalFlags.AddFlags(cmd) + + return cmd +} + +func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + imageFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + ImageFlagGroup: flag.NewImageFlagGroup(), // container image specific + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: flag.NewScanFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "image [flags] IMAGE_NAME", + Aliases: []string{"i"}, + Short: "Scan a container image", + Example: ` # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Filter by severities + $ trivy image --severity HIGH,CRITICAL alpine:3.15 + + # Ignore unfixed/unpatched vulnerabilities + $ trivy image --ignore-unfixed alpine:3.15 + + # Scan a container image in client mode + $ trivy image --server http://127.0.0.1:4954 alpine:latest + + # Generate json result + $ trivy image --format json --output result.json alpine:3.15 + + # Generate a report in the CycloneDX format + $ trivy image --format cyclonedx --output result.cdx alpine:3.15`, + + // 'Args' cannot be used since it is called before PreRunE and viper is not configured yet. + // cmd.Args -> cannot validate args here + // cmd.PreRunE -> configure viper && validate args + // cmd.RunE -> run the command + PreRunE: func(cmd *cobra.Command, args []string) error { + // viper.BindPFlag cannot be called in init(), so it is called in PreRunE. + // cf. https://github.com/spf13/cobra/issues/875 + // https://github.com/spf13/viper/issues/233 + if err := imageFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + options, err := imageFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetContainerImage) + }, + SilenceErrors: true, + SilenceUsage: true, + } + + cmd.SetFlagErrorFunc(flagErrorFunc) + imageFlags.AddFlags(cmd) + + return cmd +} + +func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + fsFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: flag.NewScanFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "filesystem [flags] PATH", + Aliases: []string{"fs"}, + Short: "Scan local filesystem", + Example: ` # Scan a local project including language-specific files + $ trivy fs /path/to/your_project + + # Scan a single file + $ trivy fs ./trivy-ci-test/Pipfile.lock`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := fsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := fsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := fsFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetFilesystem) + }, + SilenceErrors: true, + SilenceUsage: true, + } + + cmd.SetFlagErrorFunc(flagErrorFunc) + fsFlags.AddFlags(cmd) + + return cmd +} + +func NewRootfsCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + rootfsFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: flag.NewScanFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "rootfs [flags] ROOTDIR", + Short: "Scan rootfs", + Example: ` # Scan unpacked filesystem + $ docker export $(docker create alpine:3.10.2) | tar -C /tmp/rootfs -xvf - + $ trivy rootfs /tmp/rootfs + + # Scan from inside a container + $ docker run --rm -it alpine:3.11 + / # curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + / # trivy rootfs /`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := rootfsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := rootfsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := rootfsFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetRootfs) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + rootfsFlags.AddFlags(cmd) + + return cmd +} + +func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + repoFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: flag.NewScanFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "repository [flags] REPO_URL", + Aliases: []string{"repo"}, + Short: "Scan a remote repository", + Example: ` # Scan your remote git repository + $ trivy repo https://github.com/knqyf263/trivy-ci-test`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := repoFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := repoFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := repoFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetRepository) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + repoFlags.AddFlags(cmd) + + return cmd +} + +// NewClientCommand returns the 'client' subcommand that is deprecated +func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + remoteFlags := flag.NewClientFlags() + remoteAddr := flag.Flag{ + Name: "remote", + ConfigName: "server.addr", + Shorthand: "", + Value: "http://localhost:4954", + Usage: "server address", + } + remoteFlags.ServerAddr = &remoteAddr // disable '--server' and enable '--remote' instead. + + clientFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + RemoteFlagGroup: remoteFlags, + ReportFlagGroup: flag.NewReportFlagGroup(), + ScanFlagGroup: flag.NewScanFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "client [flags] IMAGE_NAME", + Aliases: []string{"c"}, + Hidden: true, // 'client' command is deprecated + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := clientFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + log.Logger.Warn("'client' subcommand is deprecated now. See https://github.com/aquasecurity/trivy/discussions/2119") + + if err := clientFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := clientFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetContainerImage) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + clientFlags.AddFlags(cmd) + + return cmd +} + +func NewServerCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + serverFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + RemoteFlagGroup: flag.NewServerFlags(), + } + + cmd := &cobra.Command{ + Use: "server [flags]", + Aliases: []string{"s"}, + Short: "Server mode", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := serverFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := serverFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return server.Run(cmd.Context(), options) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + serverFlags.AddFlags(cmd) + + return cmd +} + +func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + scanFlags := &flag.ScanFlagGroup{ + // Enable only '--skip-dirs' and '--skip-files' and disable other flags + SkipDirs: &flag.SkipDirsFlag, + SkipFiles: &flag.SkipFilesFlag, + } + + configFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: scanFlags, + } + + cmd := &cobra.Command{ + Use: "config [flags] DIR", + Aliases: []string{"conf"}, + Short: "Scan config files for misconfigurations", + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := configFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := configFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := configFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + // Disable OS and language analyzers + options.DisabledAnalyzers = append(analyzer.TypeOSes, analyzer.TypeLanguages...) + + // Scan only for misconfigurations + options.SecurityChecks = []string{types.SecurityCheckConfig} + + return artifact.Run(cmd.Context(), options, artifact.TargetFilesystem) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + configFlags.AddFlags(cmd) + + return cmd +} + +func NewPluginCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "plugin subcommand", + Aliases: []string{"p"}, + Short: "Manage plugins", + SilenceErrors: true, + SilenceUsage: true, + } + cmd.AddCommand( + &cobra.Command{ + Use: "install URL | FILE_PATH", + Aliases: []string{"i"}, + Short: "Install a plugin", + SilenceErrors: true, + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if _, err := plugin.Install(cmd.Context(), args[0], true); err != nil { + return xerrors.Errorf("plugin install error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "Uninstall PLUGIN_NAME", + Aliases: []string{"u"}, + SilenceErrors: true, + DisableFlagsInUseLine: true, + Short: "uninstall a plugin", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if err := plugin.Uninstall(args[0]); err != nil { + return xerrors.Errorf("plugin uninstall error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "list", + Aliases: []string{"l"}, + SilenceErrors: true, + DisableFlagsInUseLine: true, + Short: "List installed plugin", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + info, err := plugin.List() + if err != nil { + return xerrors.Errorf("plugin list display error: %w", err) + } + if _, err = fmt.Fprintf(os.Stdout, info); err != nil { + return xerrors.Errorf("print error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "info PLUGIN_NAME", + Short: "Show information about the specified plugin", + SilenceErrors: true, + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + info, err := plugin.Information(args[0]) + if err != nil { + return xerrors.Errorf("plugin information display error: %w", err) + } + if _, err = fmt.Fprintf(os.Stdout, info); err != nil { + return xerrors.Errorf("print error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "run URL | FILE_PATH", + Aliases: []string{"r"}, + SilenceErrors: true, + DisableFlagsInUseLine: true, + Short: "Run a plugin on the fly", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return plugin.RunWithArgs(cmd.Context(), args[0], args[1:]) + }, + }, + &cobra.Command{ + Use: "update PLUGIN_NAME", + Short: "Update an existing plugin", + SilenceErrors: true, + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if err := plugin.Update(args[0]); err != nil { + return xerrors.Errorf("plugin update error: %w", err) + } + return nil + }, + }, + ) + cmd.SetFlagErrorFunc(flagErrorFunc) + return cmd +} + +func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + cmd := &cobra.Command{ + Use: "module subcommand", + Aliases: []string{"m"}, + Short: "Manage modules", + SilenceErrors: true, + SilenceUsage: true, + } + + // Add subcommands + cmd.AddCommand( + &cobra.Command{ + Use: "install [flags] REPOSITORY", + Aliases: []string{"i"}, + Short: "Install a module", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Help() + } + + repo := args[0] + opts := globalFlags.ToOptions() + return module.Install(cmd.Context(), repo, opts.Quiet, opts.Insecure) + }, + }, + &cobra.Command{ + Use: "uninstall [flags] REPOSITORY", + Aliases: []string{"u"}, + Short: "Uninstall a module", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Help() + } + + repo := args[0] + return module.Uninstall(cmd.Context(), repo) + }, + }, + ) + cmd.SetFlagErrorFunc(flagErrorFunc) + return cmd +} + +func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + scanFlags := flag.NewScanFlagGroup() + securityChecks := flag.SecurityChecksFlag + securityChecks.Value = fmt.Sprintf( // overwrite the default value + "%s,%s,%s,%s", + types.SecurityCheckVulnerability, + types.SecurityCheckConfig, + types.SecurityCheckSecret, types.SecurityCheckRbac, + ) + scanFlags.SecurityChecks = &securityChecks + + k8sFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + K8sFlagGroup: flag.NewK8sFlagGroup(), // kubernetes-specific flags + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + ReportFlagGroup: flag.NewReportFlagGroup(), + ScanFlagGroup: scanFlags, + } + cmd := &cobra.Command{ + Use: "kubernetes [flags] { cluster | all | specific resources like kubectl. eg: pods, pod/NAME }", + Aliases: []string{"k8s"}, + Short: "scan kubernetes cluster", + Example: ` # cluster scanning + $ trivy k8s --report summary cluster + + # namespace scanning: + $ trivy k8s -n kube-system --report summary all + + # resources scanning: + $ trivy k8s --report=summary deploy + $ trivy k8s --namespace=kube-system --report=summary deploy,configmaps + + # resource scanning: + $ trivy k8s deployment/orion +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := k8sFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := k8sFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + opts, err := k8sFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + return k8scommands.Run(cmd.Context(), args, opts) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + k8sFlags.AddFlags(cmd) + + return cmd +} + +func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --format summary + + scanFlags := flag.NewScanFlagGroup() + scanFlags.SecurityChecks = nil // disable '--security-checks' as it always scans for vulnerabilities + + sbomFlags := &flag.Flags{ + CacheFlagGroup: flag.NewCacheFlagGroup(), + DBFlagGroup: flag.NewDBFlagGroup(), + RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode + ReportFlagGroup: reportFlagGroup, + ScanFlagGroup: flag.NewScanFlagGroup(), + SBOMFlagGroup: flag.NewSBOMFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "sbom [flags] SBOM_PATH", + Short: "Scan SBOM for vulnerabilities", + Example: ` # Scan CycloneDX and show the result in tables + $ trivy sbom /path/to/report.cdx + + # Scan CycloneDX and generate a CycloneDX report + $ trivy sbom --format cyclonedx /path/to/report.cdx +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := scanFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := sbomFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := sbomFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + // Scan vulnerabilities + options.SecurityChecks = []string{types.SecurityCheckVulnerability} + + return artifact.Run(cmd.Context(), options, artifact.TargetSBOM) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + sbomFlags.AddFlags(cmd) + + return cmd +} + +func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + var versionFormat string + cmd := &cobra.Command{ + Use: "version [flags]", + Short: "Print the version", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + options := globalFlags.ToOptions() + showVersion(options.CacheDir, versionFormat, cmd.Version, outputWriter) + + return nil + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + + // Add version format flag, only json is supported + cmd.Flags().StringVarP(&versionFormat, flag.FormatFlag.Name, flag.FormatFlag.Shorthand, "", "version format (json)") + + return cmd } func showVersion(cacheDir, outputFormat, version string, outputWriter io.Writer) { @@ -468,544 +852,36 @@ func showVersion(cacheDir, outputFormat, version string, outputWriter io.Writer) } } -// NewImageCommand is the factory method to add image command -func NewImageCommand() *cli.Command { - return &cli.Command{ - Name: "image", - Aliases: []string{"i"}, - ArgsUsage: "image_name", - Usage: "scan an image", - Action: artifact.ImageRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &inputFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &downloadDBOnlyFlag, - &resetFlag, - &clearCacheFlag, - &noProgressFlag, - &ignoreUnfixedFlag, - &removedPkgsFlag, - &vulnTypeFlag, - &securityChecksFlag, - &ignoreFileFlag, - &timeoutFlag, - &lightFlag, - &ignorePolicy, - &listAllPackages, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &offlineScan, - &insecureFlag, - &dbRepositoryFlag, - &secretConfig, - &dependencyTree, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - - // for client/server - &remoteServer, - &token, - &tokenHeader, - &customHeaders, - }, +func validateArgs(cmd *cobra.Command, args []string) error { + // '--clear-cache', '--download-db-only' and '--reset' don't conduct the scan + if viper.GetBool(flag.ClearCacheFlag.ConfigName) || viper.GetBool(flag.DownloadDBOnlyFlag.ConfigName) || viper.GetBool(flag.ResetFlag.ConfigName) { + return nil } -} -// NewFilesystemCommand is the factory method to add filesystem command -func NewFilesystemCommand() *cli.Command { - return &cli.Command{ - Name: "filesystem", - Aliases: []string{"fs"}, - ArgsUsage: "path", - Usage: "scan local filesystem for language-specific dependencies and config files", - Action: artifact.FilesystemRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &skipPolicyUpdateFlag, - &insecureFlag, - &clearCacheFlag, - &ignoreUnfixedFlag, - &vulnTypeFlag, - &securityChecksFlag, - &ignoreFileFlag, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &timeoutFlag, - &noProgressFlag, - &ignorePolicy, - &listAllPackages, - &offlineScan, - &dbRepositoryFlag, - &secretConfig, - &dependencyTree, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), + if len(args) == 0 && viper.GetString(flag.InputFlag.ConfigName) == "" { + if err := cmd.Help(); err != nil { + return err + } - // for misconfiguration - stringSliceFlag(configPolicy), - stringSliceFlag(configData), - stringSliceFlag(policyNamespaces), - - // for client/server - &remoteServer, - &token, - &tokenHeader, - &customHeaders, - }, + if f := cmd.Flags().Lookup(flag.InputFlag.ConfigName); f != nil { + return xerrors.New(`Require at least 1 argument or --input option`) + } + return xerrors.New(`Require at least 1 argument`) + } else if cmd.Name() != "kubernetes" && len(args) > 1 { + if err := cmd.Help(); err != nil { + return err + } + return xerrors.New(`multiple targets cannot be specified`) } + + return nil } -// NewRootfsCommand is the factory method to add filesystem command -func NewRootfsCommand() *cli.Command { - return &cli.Command{ - Name: "rootfs", - ArgsUsage: "dir", - Usage: "scan rootfs", - Action: artifact.RootfsRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &insecureFlag, - &skipPolicyUpdateFlag, - &clearCacheFlag, - &ignoreUnfixedFlag, - &vulnTypeFlag, - &securityChecksFlag, - &ignoreFileFlag, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &timeoutFlag, - &noProgressFlag, - &ignorePolicy, - &listAllPackages, - &offlineScan, - &dbRepositoryFlag, - &secretConfig, - &dependencyTree, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - stringSliceFlag(configPolicy), - stringSliceFlag(configData), - stringSliceFlag(policyNamespaces), - }, +// show help on using the command when an invalid flag is encountered +func flagErrorFunc(command *cobra.Command, err error) error { + if err := command.Help(); err != nil { + return err } -} - -// NewRepositoryCommand is the factory method to add repository command -func NewRepositoryCommand() *cli.Command { - return &cli.Command{ - Name: "repository", - Aliases: []string{"repo"}, - ArgsUsage: "repo_url", - Usage: "scan remote repository", - Action: artifact.RepositoryRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &inputFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &skipPolicyUpdateFlag, - &clearCacheFlag, - &ignoreUnfixedFlag, - &removedPkgsFlag, - &vulnTypeFlag, - &securityChecksFlag, - &ignoreFileFlag, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &timeoutFlag, - &noProgressFlag, - &quietFlag, - &ignorePolicy, - &listAllPackages, - &offlineScan, - &insecureFlag, - &dbRepositoryFlag, - &secretConfig, - &dependencyTree, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - }, - } -} - -// NewClientCommand is the factory method to add client command -func NewClientCommand() *cli.Command { - return &cli.Command{ - Name: "client", - Aliases: []string{"c"}, - ArgsUsage: "image_name", - Usage: "[DEPRECATED] client mode", - Action: func(ctx *cli.Context) error { - log.Logger.Warn("`client` subcommand is deprecated now. See https://github.com/aquasecurity/trivy/discussions/2119") - return artifact.ImageRun(ctx) - }, - Hidden: true, // It is no longer displayed - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &inputFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &ignoreUnfixedFlag, - &removedPkgsFlag, - &vulnTypeFlag, - &securityChecksFlag, - &ignoreFileFlag, - &timeoutFlag, - &noProgressFlag, - &ignorePolicy, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - stringSliceFlag(configPolicy), - &listAllPackages, - &offlineScan, - &insecureFlag, - &secretConfig, - &dependencyTree, - - &token, - &tokenHeader, - &customHeaders, - - // original flags - &cli.StringFlag{ - Name: "remote", - Value: "http://localhost:4954", - Usage: "server address", - EnvVars: []string{"TRIVY_REMOTE"}, - }, - }, - } -} - -// NewServerCommand is the factory method to add server command -func NewServerCommand() *cli.Command { - return &cli.Command{ - Name: "server", - Aliases: []string{"s"}, - Usage: "server mode", - Action: server.Run, - Flags: []cli.Flag{ - &skipDBUpdateFlag, - &downloadDBOnlyFlag, - &insecureFlag, - &resetFlag, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &dbRepositoryFlag, - - // original flags - &token, - &tokenHeader, - &cli.StringFlag{ - Name: "listen", - Value: "localhost:4954", - Usage: "listen address", - EnvVars: []string{"TRIVY_LISTEN"}, - }, - }, - } -} - -// NewConfigCommand adds config command -func NewConfigCommand() *cli.Command { - return &cli.Command{ - Name: "config", - Aliases: []string{"conf"}, - ArgsUsage: "dir", - Usage: "scan config files", - Action: artifact.ConfigRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &severityFlag, - &outputFlag, - &exitCodeFlag, - &skipPolicyUpdateFlag, - &resetFlag, - &clearCacheFlag, - &ignoreFileFlag, - &timeoutFlag, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - stringSliceFlag(configPolicyAlias), - stringSliceFlag(configDataAlias), - stringSliceFlag(policyNamespaces), - stringSliceFlag(filePatterns), - &includeNonFailures, - &traceFlag, - }, - } -} - -// NewPluginCommand is the factory method to add plugin command -func NewPluginCommand() *cli.Command { - return &cli.Command{ - Name: "plugin", - Aliases: []string{"p"}, - ArgsUsage: "plugin_uri", - Usage: "manage plugins", - Subcommands: cli.Commands{ - { - Name: "install", - Aliases: []string{"i"}, - Usage: "install a plugin", - ArgsUsage: "URL | FILE_PATH", - Action: plugin.Install, - }, - { - Name: "uninstall", - Aliases: []string{"u"}, - Usage: "uninstall a plugin", - ArgsUsage: "PLUGIN_NAME", - Action: plugin.Uninstall, - }, - { - Name: "list", - Aliases: []string{"l"}, - Usage: "list installed plugin", - Action: plugin.List, - }, - { - Name: "info", - Usage: "information about a plugin", - ArgsUsage: "PLUGIN_NAME", - Action: plugin.Information, - }, - { - Name: "run", - Aliases: []string{"r"}, - Usage: "run a plugin on the fly", - ArgsUsage: "PLUGIN_NAME [PLUGIN_OPTIONS]", - Action: plugin.Run, - }, - { - Name: "update", - Usage: "update an existing plugin", - ArgsUsage: "PLUGIN_NAME", - Action: plugin.Update, - }, - }, - } -} - -// NewModuleCommand is the factory method to add module subcommand -func NewModuleCommand() *cli.Command { - return &cli.Command{ - Name: "module", - Aliases: []string{"m"}, - Usage: "manage modules", - Subcommands: cli.Commands{ - { - Name: "install", - Aliases: []string{"i"}, - Usage: "install a module", - ArgsUsage: "REPOSITORY", - Action: module.Install, - }, - { - Name: "uninstall", - Aliases: []string{"u"}, - Usage: "uninstall a module", - ArgsUsage: "REPOSITORY", - Action: module.Uninstall, - }, - }, - } -} - -// NewK8sCommand is the factory method to add k8s subcommand -func NewK8sCommand() *cli.Command { - k8sSecurityChecksFlag := withValue( - securityChecksFlag, - fmt.Sprintf( - "%s,%s,%s,%s", - types.SecurityCheckVulnerability, - types.SecurityCheckConfig, - types.SecurityCheckSecret, types.SecurityCheckRbac), - ) - - return &cli.Command{ - Name: "kubernetes", - Aliases: []string{"k8s"}, - Usage: "scan kubernetes vulnerabilities, secrets and misconfigurations", - UsageText: "trivy k8s [command options] ", - CustomHelpTemplate: cli.CommandHelpTemplate + `EXAMPLES: - - cluster scanning: - $ trivy k8s --report summary cluster - - namespace scanning: - $ trivy k8s -n kube-system --report summary all - - resources scanning: - $ trivy k8s --report=summary deploy - $ trivy k8s --namespace=kube-system --report=summary deploy,configmaps - - resource scanning: - $ trivy k8s deployment/orion -`, - Action: k8scommands.Run, - Flags: []cli.Flag{ - &contextFlag, - &namespaceFlag, - &reportFlag, - &formatFlag, - &outputFlag, - &severityFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &insecureFlag, - &skipPolicyUpdateFlag, - &clearCacheFlag, - &ignoreUnfixedFlag, - &vulnTypeFlag, - &k8sSecurityChecksFlag, - &ignoreFileFlag, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &timeoutFlag, - &noProgressFlag, - &ignorePolicy, - &listAllPackages, - &offlineScan, - &dbRepositoryFlag, - &secretConfig, - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - - // for misconfiguration - stringSliceFlag(configPolicy), - stringSliceFlag(configData), - stringSliceFlag(policyNamespaces), - }, - } -} - -// NewSbomCommand is the factory method to add sbom command -func NewSbomCommand() *cli.Command { - return &cli.Command{ - Name: "sbom", - ArgsUsage: "SBOM", - Usage: "scan SBOM for vulnerabilities", - CustomHelpTemplate: cli.CommandHelpTemplate + `EXAMPLES: - - Scan CycloneDX and show the result in tables: - $ trivy sbom /path/to/report.cdx - - - Scan CycloneDX and generate a CycloneDX report: - $ trivy sbom --format cyclonedx /path/to/report.cdx -`, - Action: artifact.SBOMRun, - Flags: []cli.Flag{ - &templateFlag, - &formatFlag, - &inputFlag, - &outputFlag, - &exitCodeFlag, - &skipDBUpdateFlag, - &downloadDBOnlyFlag, - &resetFlag, - &clearCacheFlag, - &noProgressFlag, - &ignoreUnfixedFlag, - &ignoreFileFlag, - &timeoutFlag, - &severityFlag, - &ignorePolicy, - &listAllPackages, - &cacheBackendFlag, - &cacheTTL, - &redisBackendCACert, - &redisBackendCert, - &redisBackendKey, - &offlineScan, - &insecureFlag, - &dbRepositoryFlag, - lo.ToPtr(withValue(securityChecksFlag, types.SecurityCheckVulnerability)), // Enable only vulnerability scanning - - stringSliceFlag(skipFiles), - stringSliceFlag(skipDirs), - - // for client/server - &remoteServer, - &token, - &tokenHeader, - &customHeaders, - - // deprecated options - &cli.StringFlag{ - Name: "artifact-type", - Aliases: []string{"type"}, - Usage: "input artifact type (image, fs, repo, archive)", - EnvVars: []string{"TRIVY_ARTIFACT_TYPE"}, - Hidden: true, - }, - &cli.StringFlag{ - Name: "sbom-format", - Usage: "SBOM format (cyclonedx, spdx, spdx-json, github)", - EnvVars: []string{"TRIVY_SBOM_FORMAT"}, - Hidden: true, - }, - }, - } -} - -// NewVersionCommand adds version command -func NewVersionCommand() *cli.Command { - return &cli.Command{ - Name: "version", - Usage: "print the version", - Action: func(ctx *cli.Context) error { - showVersion(ctx.String("cache-dir"), ctx.String("format"), ctx.App.Version, ctx.App.Writer) - return nil - }, - Flags: []cli.Flag{ - &formatFlag, - }, - } -} - -// StringSliceFlag is defined globally. When the app runs multiple times, -// the previous value will be retained and it causes unexpected results. -// The flag value is copied through this function to prevent the issue. -func stringSliceFlag(f cli.StringSliceFlag) *cli.StringSliceFlag { - return &f -} - -func withValue(s cli.StringFlag, value string) cli.StringFlag { - s.Value = value - return s + command.Println() //add empty line after list of flags + return err } diff --git a/pkg/commands/app_test.go b/pkg/commands/app_test.go index 1d63d71f48..65c1b89940 100644 --- a/pkg/commands/app_test.go +++ b/pkg/commands/app_test.go @@ -70,37 +70,36 @@ Vulnerability DB: name string arguments []string // 1st argument is path to trivy binaries want string - wantErr string }{ { name: "happy path. '-v' flag is used", - arguments: []string{"trivy", "-v", "--cache-dir", "testdata"}, + arguments: []string{"-v", "--cache-dir", "testdata"}, want: tableOutput, }, { name: "happy path. '-version' flag is used", - arguments: []string{"trivy", "-version", "--cache-dir", "testdata"}, + arguments: []string{"--version", "--cache-dir", "testdata"}, want: tableOutput, }, { name: "happy path. 'version' command is used", - arguments: []string{"trivy", "--cache-dir", "testdata", "version"}, + arguments: []string{"--cache-dir", "testdata", "version"}, want: tableOutput, }, { name: "happy path. 'version', '--format json' flags are used", - arguments: []string{"trivy", "--cache-dir", "testdata", "version", "--format", "json"}, + arguments: []string{"--cache-dir", "testdata", "version", "--format", "json"}, want: jsonOutput, }, { - name: "sad path. '-v', '--format json' flags are used", - arguments: []string{"trivy", "-v", "--format", "json"}, - wantErr: "flag provided but not defined: -format", + name: "happy path. '-v', '--format json' flags are used", + arguments: []string{"--cache-dir", "testdata", "-v", "--format", "json"}, + want: jsonOutput, }, { - name: "sad path. '-version', '--format json' flags are used", - arguments: []string{"trivy", "-version", "--format", "json"}, - wantErr: "flag provided but not defined: -format", + name: "happy path. '--version', '--format json' flags are used", + arguments: []string{"--cache-dir", "testdata", "--version", "--format", "json"}, + want: jsonOutput, }, } @@ -108,24 +107,12 @@ Vulnerability DB: t.Run(test.name, func(t *testing.T) { got := new(bytes.Buffer) app := NewApp("test") - app.Writer = got + SetOut(got) + app.SetArgs(test.arguments) - err := app.Run(test.arguments) - if test.wantErr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.wantErr) - return - } + err := app.Execute() + require.NoError(t, err) assert.Equal(t, test.want, got.String()) }) } } - -func TestNewCommands(t *testing.T) { - NewApp("test") - NewClientCommand() - NewFilesystemCommand() - NewImageCommand() - NewRepositoryCommand() - NewServerCommand() -} diff --git a/pkg/commands/artifact/config.go b/pkg/commands/artifact/config.go deleted file mode 100644 index bc970eca2f..0000000000 --- a/pkg/commands/artifact/config.go +++ /dev/null @@ -1,27 +0,0 @@ -package artifact - -import ( - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/fanal/analyzer" - "github.com/aquasecurity/trivy/pkg/types" -) - -// ConfigRun runs scan on config files -func ConfigRun(ctx *cli.Context) error { - opt, err := InitOption(ctx) - if err != nil { - return xerrors.Errorf("option error: %w", err) - } - - // Disable OS and language analyzers - opt.DisabledAnalyzers = append(analyzer.TypeOSes, analyzer.TypeLanguages...) - - // Scan only config files - opt.VulnType = nil - opt.SecurityChecks = []string{types.SecurityCheckConfig} - - // Run filesystem command internally - return run(ctx.Context, opt, TargetFilesystem) -} diff --git a/pkg/commands/artifact/fs.go b/pkg/commands/artifact/fs.go deleted file mode 100644 index 7324e0b19b..0000000000 --- a/pkg/commands/artifact/fs.go +++ /dev/null @@ -1,38 +0,0 @@ -package artifact - -import ( - "context" - - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/scanner" -) - -// filesystemStandaloneScanner initializes a filesystem scanner in standalone mode -func filesystemStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) - if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) - } - return s, cleanup, nil -} - -// filesystemRemoteScanner initializes a filesystem scanner in client/server mode -func filesystemRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeRemoteFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, conf.ArtifactOption) - if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) - } - return s, cleanup, nil -} - -// FilesystemRun runs scan on filesystem for language-specific dependencies and config files -func FilesystemRun(ctx *cli.Context) error { - return Run(ctx, TargetFilesystem) -} - -// RootfsRun runs scan on rootfs. -func RootfsRun(ctx *cli.Context) error { - return Run(ctx, TargetRootfs) -} diff --git a/pkg/commands/artifact/option.go b/pkg/commands/artifact/option.go deleted file mode 100644 index 0672f16065..0000000000 --- a/pkg/commands/artifact/option.go +++ /dev/null @@ -1,93 +0,0 @@ -package artifact - -import ( - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/fanal/analyzer" -) - -// Option holds the artifact options -type Option struct { - option.GlobalOption - option.ArtifactOption - option.DBOption - option.ImageOption - option.ReportOption - option.CacheOption - option.ConfigOption - option.RemoteOption - option.SbomOption - option.SecretOption - option.KubernetesOption - option.OtherOption - - // We don't want to allow disabled analyzers to be passed by users, - // but it differs depending on scanning modes. - DisabledAnalyzers []analyzer.Type -} - -// NewOption is the factory method to return options -func NewOption(c *cli.Context) (Option, error) { - gc, err := option.NewGlobalOption(c) - if err != nil { - return Option{}, xerrors.Errorf("failed to initialize global options: %w", err) - } - - return Option{ - GlobalOption: gc, - ArtifactOption: option.NewArtifactOption(c), - DBOption: option.NewDBOption(c), - ImageOption: option.NewImageOption(c), - ReportOption: option.NewReportOption(c), - CacheOption: option.NewCacheOption(c), - ConfigOption: option.NewConfigOption(c), - RemoteOption: option.NewRemoteOption(c), - SbomOption: option.NewSbomOption(c), - SecretOption: option.NewSecretOption(c), - KubernetesOption: option.NewKubernetesOption(c), - OtherOption: option.NewOtherOption(c), - }, nil -} - -// Init initializes the artifact options -func (c *Option) Init() error { - if err := c.initPreScanOptions(); err != nil { - return err - } - - // --clear-cache, --download-db-only and --reset don't conduct the scan - if c.skipScan() { - return nil - } - - if err := c.ArtifactOption.Init(c.Context, c.Logger); err != nil { - return err - } - return nil -} - -func (c *Option) initPreScanOptions() error { - if err := c.ReportOption.Init(c.Context.App.Writer, c.Logger); err != nil { - return err - } - if err := c.DBOption.Init(); err != nil { - return err - } - if err := c.CacheOption.Init(); err != nil { - return err - } - if err := c.SbomOption.Init(c.Context, c.Logger); err != nil { - return err - } - c.RemoteOption.Init(c.Logger) - return nil -} - -func (c *Option) skipScan() bool { - if c.ClearCache || c.DownloadDBOnly || c.Reset { - return true - } - return false -} diff --git a/pkg/commands/artifact/option_test.go b/pkg/commands/artifact/option_test.go deleted file mode 100644 index 4980809589..0000000000 --- a/pkg/commands/artifact/option_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package artifact - -import ( - "flag" - "net/http" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "go.uber.org/zap/zaptest/observer" - - dbTypes "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/report" - "github.com/aquasecurity/trivy/pkg/types" -) - -func TestOption_Init(t *testing.T) { - tests := []struct { - name string - args []string - logs []string - want Option - wantErr string - }{ - { - name: "happy path", - args: []string{"--severity", "CRITICAL", "--vuln-type", "os", "--quiet", "alpine:3.10"}, - want: Option{ - GlobalOption: option.GlobalOption{ - Quiet: true, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.10", - }, - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Output: os.Stdout, - }, - }, - }, - { - name: "config scanning", - args: []string{"--severity", "CRITICAL", "--security-checks", "config", "--quiet", "alpine:3.10"}, - want: Option{ - GlobalOption: option.GlobalOption{ - Quiet: true, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.10", - }, - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckConfig}, - Output: os.Stdout, - }, - }, - }, - { - name: "happy path with token and token header", - args: []string{"--server", "http://localhost:8080", "--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"}, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.11", - }, - RemoteOption: option.RemoteOption{ - RemoteAddr: "http://localhost:8080", - CustomHeaders: http.Header{ - "X-Trivy-Token": []string{"secret"}, - }, - }, - }, - }, - { - name: "invalid option combination: token and token header without server", - args: []string{"--token", "secret", "--token-header", "X-Trivy-Token", "alpine:3.11"}, - logs: []string{ - `"--token" can be used only with "--server"`, - }, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.11", - }, - }, - }, - { - name: "happy path with good custom headers", - args: []string{"--server", "http://localhost:8080", "--custom-headers", "foo:bar", "alpine:3.11"}, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.11", - }, - RemoteOption: option.RemoteOption{ - RemoteAddr: "http://localhost:8080", - CustomHeaders: http.Header{ - "Foo": []string{"bar"}, - }}, - }, - }, - { - name: "happy path with bad custom headers", - args: []string{"--server", "http://localhost:8080", "--custom-headers", "foobaz", "alpine:3.11"}, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.11", - }, - RemoteOption: option.RemoteOption{RemoteAddr: "http://localhost:8080", CustomHeaders: http.Header{}}, - }, - }, - { - name: "happy path: reset", - args: []string{"--reset"}, - want: Option{ - DBOption: option.DBOption{ - Reset: true, - }, - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - }, - }, - { - name: "happy path with an unknown severity", - args: []string{"--severity", "CRITICAL,INVALID", "centos:7"}, - logs: []string{ - "unknown severity option: unknown severity: INVALID", - }, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - ArtifactOption: option.ArtifactOption{ - Target: "centos:7", - }, - }, - }, - { - name: "invalid option combination: --template enabled without --format", - args: []string{"--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.", - }, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Template: "@contrib/gitlab.tpl", - }, - ArtifactOption: option.ArtifactOption{ - Target: "gitlab/gitlab-ce:12.7.2-ce.0", - }, - }, - }, - { - name: "invalid option combination: --template and --format json", - args: []string{"--format", "json", "--template", "@contrib/gitlab.tpl", "gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.", - }, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Template: "@contrib/gitlab.tpl", - Format: "json", - }, - ArtifactOption: option.ArtifactOption{ - Target: "gitlab/gitlab-ce:12.7.2-ce.0", - }, - }, - }, - { - name: "json and list all packages", - args: []string{"--format", "json", "--list-all-pkgs", "gitlab/gitlab-ce:12.7.2-ce.0"}, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Format: "json", - ListAllPkgs: true, - }, - ArtifactOption: option.ArtifactOption{ - Target: "gitlab/gitlab-ce:12.7.2-ce.0", - }, - }, - }, - { - name: "invalid option combination: --format template without --template", - args: []string{"--format", "template", "--severity", "MEDIUM", "gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.", - }, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityMedium}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Format: "template", - }, - ArtifactOption: option.ArtifactOption{ - Target: "gitlab/gitlab-ce:12.7.2-ce.0", - }, - }, - }, - { - name: "github enables list-all-pkgs", - args: []string{"--format", "github", "alpine:3.15"}, - want: Option{ - ReportOption: option.ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - Output: os.Stdout, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Format: report.FormatGitHub, - ListAllPkgs: true, - }, - ArtifactOption: option.ArtifactOption{ - Target: "alpine:3.15", - }, - }, - }, - - { - name: "sad: skip and download db", - args: []string{"--skip-db-update", "--download-db-only", "alpine:3.10"}, - wantErr: "--skip-db-update and --download-db-only options can not be specified both", - }, - { - name: "sad: multiple image names", - args: []string{"centos:7", "alpine:3.10"}, - logs: []string{ - "multiple targets cannot be specified", - }, - wantErr: "arguments error", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - core, obs := observer.New(zap.InfoLevel) - logger := zap.New(core) - - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - set.Bool("quiet", false, "") - set.Bool("no-progress", false, "") - set.Bool("reset", false, "") - set.Bool("skip-db-update", false, "") - set.Bool("download-db-only", false, "") - set.Bool("list-all-pkgs", false, "") - set.String("severity", "CRITICAL", "") - set.String("vuln-type", "os,library", "") - set.String("security-checks", "vuln", "") - set.String("template", "", "") - set.String("format", "", "") - set.String("server", "", "") - set.String("token", "", "") - set.String("token-header", option.DefaultTokenHeader, "") - set.Var(&cli.StringSlice{}, "custom-headers", "") - - ctx := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - c, err := NewOption(ctx) - require.NoError(t, err, err) - - c.GlobalOption.Logger = logger.Sugar() - err = c.Init() - - // tests log messages - var gotMessages []string - for _, entry := range obs.AllUntimed() { - gotMessages = append(gotMessages, entry.Message) - } - assert.Equal(t, tt.logs, gotMessages, tt.name) - - // test the error - switch { - case tt.wantErr != "": - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr, tt.name) - return - default: - assert.NoError(t, err, tt.name) - } - - tt.want.GlobalOption.Context = ctx - tt.want.GlobalOption.Logger = logger.Sugar() - assert.Equal(t, tt.want, c, tt.name) - }) - } -} diff --git a/pkg/commands/artifact/repository.go b/pkg/commands/artifact/repository.go deleted file mode 100644 index 8d718a17e5..0000000000 --- a/pkg/commands/artifact/repository.go +++ /dev/null @@ -1,24 +0,0 @@ -package artifact - -import ( - "context" - - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/scanner" -) - -// filesystemStandaloneScanner initializes a repository scanner in standalone mode -func repositoryStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeRepositoryScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) - if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) - } - return s, cleanup, nil -} - -// RepositoryRun runs scan on repository -func RepositoryRun(ctx *cli.Context) error { - return Run(ctx, TargetRepository) -} diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 3f3d9345ac..f7ecc4d03b 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -6,7 +6,6 @@ import ( "os" "github.com/hashicorp/go-multierror" - "github.com/urfave/cli/v2" "golang.org/x/exp/slices" "golang.org/x/xerrors" @@ -18,6 +17,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" "github.com/aquasecurity/trivy/pkg/fanal/artifact" "github.com/aquasecurity/trivy/pkg/fanal/cache" + "github.com/aquasecurity/trivy/pkg/flag" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/module" pkgReport "github.com/aquasecurity/trivy/pkg/report" @@ -65,19 +65,19 @@ type ScannerConfig struct { type Runner interface { // ScanImage scans an image - ScanImage(ctx context.Context, opt Option) (types.Report, error) + ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) // ScanFilesystem scans a filesystem - ScanFilesystem(ctx context.Context, opt Option) (types.Report, error) + ScanFilesystem(ctx context.Context, opts flag.Options) (types.Report, error) // ScanRootfs scans rootfs - ScanRootfs(ctx context.Context, opt Option) (types.Report, error) + ScanRootfs(ctx context.Context, opts flag.Options) (types.Report, error) // ScanRepository scans repository - ScanRepository(ctx context.Context, opt Option) (types.Report, error) + ScanRepository(ctx context.Context, opts flag.Options) (types.Report, error) // ScanSBOM scans SBOM - ScanSBOM(ctx context.Context, opt Option) (types.Report, error) + ScanSBOM(ctx context.Context, opts flag.Options) (types.Report, error) // Filter filter a report - Filter(ctx context.Context, opt Option, report types.Report) (types.Report, error) + Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) // Report a writes a report - Report(opt Option, report types.Report) error + Report(opts flag.Options, report types.Report) error // Close closes runner Close(ctx context.Context) error } @@ -93,6 +93,7 @@ type runner struct { type runnerOption func(*runner) // WithCacheClient takes a custom cache implementation +// It is useful when Trivy is imported as a library. func WithCacheClient(c cache.Cache) runnerOption { return func(r *runner) { r.cache = c @@ -101,23 +102,23 @@ func WithCacheClient(c cache.Cache) runnerOption { // NewRunner initializes Runner that provides scanning functionalities. // It is possible to return SkipScan and it must be handled by caller. -func NewRunner(cliOption Option, opts ...runnerOption) (Runner, error) { +func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...runnerOption) (Runner, error) { r := &runner{} for _, opt := range opts { opt(r) } - err := log.InitLogger(cliOption.Debug, cliOption.Quiet) - if err != nil { - return nil, xerrors.Errorf("logger error: %w", err) - } - - if err = r.initCache(cliOption); err != nil { + if err := r.initCache(cliOptions); err != nil { return nil, xerrors.Errorf("cache error: %w", err) } + // Update the vulnerability database if needed. + if err := r.initDB(cliOptions); err != nil { + return nil, xerrors.Errorf("DB error: %w", err) + } + // Initialize WASM modules - m, err := module.NewManager(cliOption.Context.Context) + m, err := module.NewManager(ctx) if err != nil { return nil, xerrors.Errorf("WASM module error: %w", err) } @@ -146,46 +147,46 @@ func (r *runner) Close(ctx context.Context) error { return errs } -func (r *runner) ScanImage(ctx context.Context, opt Option) (types.Report, error) { +func (r *runner) ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) { // Disable the lock file scanning - opt.DisabledAnalyzers = analyzer.TypeLockfiles + opts.DisabledAnalyzers = analyzer.TypeLockfiles var s InitializeScanner switch { - case opt.Input != "" && opt.RemoteAddr == "": + case opts.Input != "" && opts.ServerAddr == "": // Scan image tarball in standalone mode s = archiveStandaloneScanner - case opt.Input != "" && opt.RemoteAddr != "": + case opts.Input != "" && opts.ServerAddr != "": // Scan image tarball in client/server mode s = archiveRemoteScanner - case opt.Input == "" && opt.RemoteAddr == "": + case opts.Input == "" && opts.ServerAddr == "": // Scan container image in standalone mode s = imageStandaloneScanner - case opt.Input == "" && opt.RemoteAddr != "": + case opts.Input == "" && opts.ServerAddr != "": // Scan container image in client/server mode s = imageRemoteScanner } - return r.scanArtifact(ctx, opt, s) + return r.scanArtifact(ctx, opts, s) } -func (r *runner) ScanFilesystem(ctx context.Context, opt Option) (types.Report, error) { +func (r *runner) ScanFilesystem(ctx context.Context, opts flag.Options) (types.Report, error) { // Disable the individual package scanning - opt.DisabledAnalyzers = append(opt.DisabledAnalyzers, analyzer.TypeIndividualPkgs...) + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeIndividualPkgs...) - return r.scanFS(ctx, opt) + return r.scanFS(ctx, opts) } -func (r *runner) ScanRootfs(ctx context.Context, opt Option) (types.Report, error) { +func (r *runner) ScanRootfs(ctx context.Context, opts flag.Options) (types.Report, error) { // Disable the lock file scanning - opt.DisabledAnalyzers = append(opt.DisabledAnalyzers, analyzer.TypeLockfiles...) + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeLockfiles...) - return r.scanFS(ctx, opt) + return r.scanFS(ctx, opts) } -func (r *runner) scanFS(ctx context.Context, opt Option) (types.Report, error) { +func (r *runner) scanFS(ctx context.Context, opts flag.Options) (types.Report, error) { var s InitializeScanner - if opt.RemoteAddr == "" { + if opts.ServerAddr == "" { // Scan filesystem in standalone mode s = filesystemStandaloneScanner } else { @@ -193,26 +194,22 @@ func (r *runner) scanFS(ctx context.Context, opt Option) (types.Report, error) { s = filesystemRemoteScanner } - return r.scanArtifact(ctx, opt, s) + return r.scanArtifact(ctx, opts, s) } -func (r *runner) ScanRepository(ctx context.Context, opt Option) (types.Report, error) { +func (r *runner) ScanRepository(ctx context.Context, opts flag.Options) (types.Report, error) { // Do not scan OS packages - opt.VulnType = []string{types.VulnTypeLibrary} + opts.VulnType = []string{types.VulnTypeLibrary} // Disable the OS analyzers and individual package analyzers - opt.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...) + opts.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...) - return r.scanArtifact(ctx, opt, repositoryStandaloneScanner) + return r.scanArtifact(ctx, opts, repositoryStandaloneScanner) } -func (r *runner) ScanSBOM(ctx context.Context, opt Option) (types.Report, error) { - // Scan vulnerabilities - opt.ReportOption.VulnType = []string{types.VulnTypeOS, types.VulnTypeLibrary} - opt.ReportOption.SecurityChecks = []string{types.SecurityCheckVulnerability} - +func (r *runner) ScanSBOM(ctx context.Context, opts flag.Options) (types.Report, error) { var s InitializeScanner - if opt.RemoteAddr == "" { + if opts.ServerAddr == "" { // Scan cycloneDX in standalone mode s = sbomStandaloneScanner } else { @@ -220,16 +217,12 @@ func (r *runner) ScanSBOM(ctx context.Context, opt Option) (types.Report, error) s = sbomRemoteScanner } - return r.scanArtifact(ctx, opt, s) + return r.scanArtifact(ctx, opts, s) } -func (r *runner) scanArtifact(ctx context.Context, opt Option, initializeScanner InitializeScanner) (types.Report, error) { - // Update the vulnerability database if needed. - if err := r.initDB(opt); err != nil { - return types.Report{}, xerrors.Errorf("DB error: %w", err) - } +func (r *runner) scanArtifact(ctx context.Context, opts flag.Options, initializeScanner InitializeScanner) (types.Report, error) { - report, err := scan(ctx, opt, initializeScanner, r.cache) + report, err := scan(ctx, opts, initializeScanner, r.cache) if err != nil { return types.Report{}, xerrors.Errorf("scan error: %w", err) } @@ -237,13 +230,13 @@ func (r *runner) scanArtifact(ctx context.Context, opt Option, initializeScanner return report, nil } -func (r *runner) Filter(ctx context.Context, opt Option, report types.Report) (types.Report, error) { +func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) { results := report.Results // Filter results for i := range results { vulns, misconfSummary, misconfs, secrets, err := result.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations, results[i].Secrets, - opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy) + opts.Severities, opts.IgnoreUnfixed, opts.IncludeNonFailures, opts.IgnoreFile, opts.IgnorePolicy) if err != nil { return types.Report{}, xerrors.Errorf("unable to filter vulnerabilities: %w", err) } @@ -255,16 +248,16 @@ func (r *runner) Filter(ctx context.Context, opt Option, report types.Report) (t return report, nil } -func (r *runner) Report(opt Option, report types.Report) error { +func (r *runner) Report(opts flag.Options, report types.Report) error { if err := pkgReport.Write(report, pkgReport.Option{ - AppVersion: opt.GlobalOption.AppVersion, - Format: opt.Format, - Output: opt.Output, - Tree: opt.DependencyTree, - Severities: opt.Severities, - OutputTemplate: opt.Template, - IncludeNonFailures: opt.IncludeNonFailures, - Trace: opt.Trace, + AppVersion: opts.AppVersion, + Format: opts.Format, + Output: opts.Output, + Tree: opts.DependencyTree, + Severities: opts.Severities, + OutputTemplate: opts.Template, + IncludeNonFailures: opts.IncludeNonFailures, + Trace: opts.Trace, }); err != nil { return xerrors.Errorf("unable to write results: %w", err) } @@ -272,23 +265,23 @@ func (r *runner) Report(opt Option, report types.Report) error { return nil } -func (r *runner) initDB(c Option) error { +func (r *runner) initDB(opts flag.Options) error { // When scanning config files or running as client mode, it doesn't need to download the vulnerability database. - if c.RemoteAddr != "" || !slices.Contains(c.SecurityChecks, types.SecurityCheckVulnerability) { + if opts.ServerAddr != "" || !slices.Contains(opts.SecurityChecks, types.SecurityCheckVulnerability) { return nil } // download the database file - noProgress := c.Quiet || c.NoProgress - if err := operation.DownloadDB(c.AppVersion, c.CacheDir, c.DBRepository, noProgress, c.Insecure, c.SkipDBUpdate); err != nil { + noProgress := opts.Quiet || opts.NoProgress + if err := operation.DownloadDB(opts.AppVersion, opts.CacheDir, opts.DBRepository, noProgress, opts.Insecure, opts.SkipDBUpdate); err != nil { return err } - if c.DownloadDBOnly { + if opts.DownloadDBOnly { return SkipScan } - if err := db.Init(c.CacheDir); err != nil { + if err := db.Init(opts.CacheDir); err != nil { return xerrors.Errorf("error in vulnerability DB initialize: %w", err) } r.dbOpen = true @@ -296,58 +289,59 @@ func (r *runner) initDB(c Option) error { return nil } -func (r *runner) initCache(c Option) error { +func (r *runner) initCache(opts flag.Options) error { // Skip initializing cache when custom cache is passed if r.cache != nil { return nil } // client/server mode - if c.RemoteAddr != "" { - remoteCache := tcache.NewRemoteCache(c.RemoteAddr, c.CustomHeaders, c.Insecure) + if opts.ServerAddr != "" { + remoteCache := tcache.NewRemoteCache(opts.ServerAddr, opts.CustomHeaders, opts.Insecure) r.cache = tcache.NopCache(remoteCache) return nil } // standalone mode - utils.SetCacheDir(c.CacheDir) - cache, err := operation.NewCache(c.CacheOption) + utils.SetCacheDir(opts.CacheDir) + cacheClient, err := operation.NewCache(opts.CacheOptions) if err != nil { return xerrors.Errorf("unable to initialize the cache: %w", err) } log.Logger.Debugf("cache dir: %s", utils.CacheDir()) - if c.Reset { - defer cache.Close() - if err = cache.Reset(); err != nil { + if opts.Reset { + defer cacheClient.Close() + if err = cacheClient.Reset(); err != nil { return xerrors.Errorf("cache reset error: %w", err) } return SkipScan } - if c.ClearCache { - defer cache.Close() - if err = cache.ClearArtifacts(); err != nil { + if opts.ClearCache { + defer cacheClient.Close() + if err = cacheClient.ClearArtifacts(); err != nil { return xerrors.Errorf("cache clear error: %w", err) } return SkipScan } - r.cache = cache + r.cache = cacheClient return nil } // Run performs artifact scanning -func Run(cliCtx *cli.Context, targetKind TargetKind) error { - opt, err := InitOption(cliCtx) - if err != nil { - return xerrors.Errorf("InitOption: %w", err) - } +//func Run(cliCtx *cli.Context, targetKind TargetKind) error { +// opt, err := InitOption(cliCtx) +// if err != nil { +// return xerrors.Errorf("InitOption: %w", err) +// } +// +// return run(cliCtx.Context, opt, targetKind) +//} - return run(cliCtx.Context, opt, targetKind) -} - -func run(ctx context.Context, opt Option, targetKind TargetKind) (err error) { - ctx, cancel := context.WithTimeout(ctx, opt.Timeout) +// Run performs artifact scanning +func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err error) { + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() defer func() { @@ -356,7 +350,7 @@ func run(ctx context.Context, opt Option, targetKind TargetKind) (err error) { } }() - r, err := NewRunner(opt) + r, err := NewRunner(ctx, opts) if err != nil { if errors.Is(err, SkipScan) { return nil @@ -368,121 +362,107 @@ func run(ctx context.Context, opt Option, targetKind TargetKind) (err error) { var report types.Report switch targetKind { case TargetContainerImage, TargetImageArchive: - if report, err = r.ScanImage(ctx, opt); err != nil { + if report, err = r.ScanImage(ctx, opts); err != nil { return xerrors.Errorf("image scan error: %w", err) } case TargetFilesystem: - if report, err = r.ScanFilesystem(ctx, opt); err != nil { + if report, err = r.ScanFilesystem(ctx, opts); err != nil { return xerrors.Errorf("filesystem scan error: %w", err) } case TargetRootfs: - if report, err = r.ScanRootfs(ctx, opt); err != nil { + if report, err = r.ScanRootfs(ctx, opts); err != nil { return xerrors.Errorf("rootfs scan error: %w", err) } case TargetRepository: - if report, err = r.ScanRepository(ctx, opt); err != nil { + if report, err = r.ScanRepository(ctx, opts); err != nil { return xerrors.Errorf("repository scan error: %w", err) } case TargetSBOM: - if report, err = r.ScanSBOM(ctx, opt); err != nil { + if report, err = r.ScanSBOM(ctx, opts); err != nil { return xerrors.Errorf("sbom scan error: %w", err) } } - report, err = r.Filter(ctx, opt, report) + report, err = r.Filter(ctx, opts, report) if err != nil { return xerrors.Errorf("filter error: %w", err) } - if err = r.Report(opt, report); err != nil { + if err = r.Report(opts, report); err != nil { return xerrors.Errorf("report error: %w", err) } - Exit(opt, report.Results.Failed()) + Exit(opts, report.Results.Failed()) return nil } -func InitOption(ctx *cli.Context) (Option, error) { - opt, err := NewOption(ctx) - if err != nil { - return Option{}, xerrors.Errorf("option error: %w", err) - } - - // initialize options - if err = opt.Init(); err != nil { - return Option{}, xerrors.Errorf("option initialize error: %w", err) - } - - return opt, nil -} - -func disabledAnalyzers(opt Option) []analyzer.Type { +func disabledAnalyzers(opts flag.Options) []analyzer.Type { // Specified analyzers to be disabled depending on scanning modes // e.g. The 'image' subcommand should disable the lock file scanning. - analyzers := opt.DisabledAnalyzers + analyzers := opts.DisabledAnalyzers // It doesn't analyze apk commands by default. - if !opt.ScanRemovedPkgs { + if !opts.ScanRemovedPkgs { analyzers = append(analyzers, analyzer.TypeApkCommand) } // Do not analyze programming language packages when not running in 'library' mode - if !slices.Contains(opt.VulnType, types.VulnTypeLibrary) { + if !slices.Contains(opts.VulnType, types.VulnTypeLibrary) { analyzers = append(analyzers, analyzer.TypeLanguages...) } // Do not perform secret scanning when it is not specified. - if !slices.Contains(opt.SecurityChecks, types.SecurityCheckSecret) { + if !slices.Contains(opts.SecurityChecks, types.SecurityCheckSecret) { analyzers = append(analyzers, analyzer.TypeSecret) } // Do not perform misconfiguration scanning when it is not specified. - if !slices.Contains(opt.SecurityChecks, types.SecurityCheckConfig) { + if !slices.Contains(opts.SecurityChecks, types.SecurityCheckConfig) { analyzers = append(analyzers, analyzer.TypeConfigFiles...) } return analyzers } -func initScannerConfig(opt Option, cacheClient cache.Cache) (ScannerConfig, types.ScanOptions, error) { - target := opt.Target - if opt.Input != "" { - target = opt.Input +func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfig, types.ScanOptions, error) { + target := opts.Target + if opts.Input != "" { + target = opts.Input } scanOptions := types.ScanOptions{ - VulnType: opt.VulnType, - SecurityChecks: opt.SecurityChecks, - ScanRemovedPackages: opt.ScanRemovedPkgs, // this is valid only for 'image' subcommand - ListAllPackages: opt.ListAllPkgs, + VulnType: opts.VulnType, + SecurityChecks: opts.SecurityChecks, + ScanRemovedPackages: opts.ScanRemovedPkgs, // this is valid only for 'image' subcommand + ListAllPackages: opts.ListAllPkgs, } - if slices.Contains(opt.SecurityChecks, types.SecurityCheckVulnerability) { + if slices.Contains(opts.SecurityChecks, types.SecurityCheckVulnerability) { log.Logger.Info("Vulnerability scanning is enabled") log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType) } // ScannerOption is filled only when config scanning is enabled. var configScannerOptions config.ScannerOption - if slices.Contains(opt.SecurityChecks, types.SecurityCheckConfig) { + if slices.Contains(opts.SecurityChecks, types.SecurityCheckConfig) { log.Logger.Info("Misconfiguration scanning is enabled") configScannerOptions = config.ScannerOption{ - Trace: opt.Trace, - Namespaces: append(opt.PolicyNamespaces, defaultPolicyNamespaces...), - PolicyPaths: opt.PolicyPaths, - DataPaths: opt.DataPaths, - FilePatterns: opt.FilePatterns, + Trace: opts.Trace, + Namespaces: append(opts.PolicyNamespaces, defaultPolicyNamespaces...), + PolicyPaths: opts.PolicyPaths, + DataPaths: opts.DataPaths, + FilePatterns: opts.FilePatterns, } } // Do not load config file for secret scanning - if slices.Contains(opt.SecurityChecks, types.SecurityCheckSecret) { + if slices.Contains(opts.SecurityChecks, types.SecurityCheckSecret) { log.Logger.Info("Secret scanning is enabled") log.Logger.Info("If your scanning is slow, please try '--security-checks vuln' to disable secret scanning") - log.Logger.Infof("Please see also https://aquasecurity.github.io/trivy/%s/docs/secret/scanning/#recommendation for faster secret detection", opt.AppVersion) + log.Logger.Infof("Please see also https://aquasecurity.github.io/trivy/%s/docs/secret/scanning/#recommendation for faster secret detection", opts.AppVersion) } else { - opt.SecretConfigPath = "" + opts.SecretConfigPath = "" } return ScannerConfig{ @@ -490,33 +470,33 @@ func initScannerConfig(opt Option, cacheClient cache.Cache) (ScannerConfig, type ArtifactCache: cacheClient, LocalArtifactCache: cacheClient, RemoteOption: client.ScannerOption{ - RemoteURL: opt.RemoteAddr, - CustomHeaders: opt.CustomHeaders, - Insecure: opt.Insecure, + RemoteURL: opts.ServerAddr, + CustomHeaders: opts.CustomHeaders, + Insecure: opts.Insecure, }, ArtifactOption: artifact.Option{ - DisabledAnalyzers: disabledAnalyzers(opt), - SkipFiles: opt.SkipFiles, - SkipDirs: opt.SkipDirs, - InsecureSkipTLS: opt.Insecure, - Offline: opt.OfflineScan, - NoProgress: opt.NoProgress || opt.Quiet, + DisabledAnalyzers: disabledAnalyzers(opts), + SkipFiles: opts.SkipFiles, + SkipDirs: opts.SkipDirs, + InsecureSkipTLS: opts.Insecure, + Offline: opts.OfflineScan, + NoProgress: opts.NoProgress || opts.Quiet, // For misconfiguration scanning MisconfScannerOption: configScannerOptions, // For secret scanning SecretScannerOption: secret.ScannerOption{ - ConfigPath: opt.SecretConfigPath, + ConfigPath: opts.SecretConfigPath, }, }, }, scanOptions, nil } -func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, cacheClient cache.Cache) ( +func scan(ctx context.Context, opts flag.Options, initializeScanner InitializeScanner, cacheClient cache.Cache) ( types.Report, error) { - scannerConfig, scanOptions, err := initScannerConfig(opt, cacheClient) + scannerConfig, scanOptions, err := initScannerConfig(opts, cacheClient) if err != nil { return types.Report{}, err } @@ -534,8 +514,8 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, return report, nil } -func Exit(c Option, failedResults bool) { - if c.ExitCode != 0 && failedResults { - os.Exit(c.ExitCode) +func Exit(opts flag.Options, failedResults bool) { + if opts.ExitCode != 0 && failedResults { + os.Exit(opts.ExitCode) } } diff --git a/pkg/commands/artifact/sbom.go b/pkg/commands/artifact/sbom.go deleted file mode 100644 index f3efc25351..0000000000 --- a/pkg/commands/artifact/sbom.go +++ /dev/null @@ -1,31 +0,0 @@ -package artifact - -import ( - "context" - - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/scanner" -) - -// SBOMRun scans SBOM for vulnerabilities -func SBOMRun(ctx *cli.Context) error { - return Run(ctx, TargetSBOM) -} - -func sbomStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) - if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) - } - return s, cleanup, nil -} - -func sbomRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { - s, cleanup, err := initializeRemoteSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, conf.ArtifactOption) - if err != nil { - return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) - } - return s, cleanup, nil -} diff --git a/pkg/commands/artifact/image.go b/pkg/commands/artifact/scanner.go similarity index 52% rename from pkg/commands/artifact/image.go rename to pkg/commands/artifact/scanner.go index 618e41957e..4d69f96de5 100644 --- a/pkg/commands/artifact/image.go +++ b/pkg/commands/artifact/scanner.go @@ -3,7 +3,6 @@ package artifact import ( "context" - "github.com/urfave/cli/v2" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/scanner" @@ -64,7 +63,47 @@ func archiveRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scan return s, func() {}, nil } -// ImageRun runs scan on container image -func ImageRun(ctx *cli.Context) error { - return Run(ctx, TargetContainerImage) +// filesystemStandaloneScanner initializes a filesystem scanner in standalone mode +func filesystemStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) + } + return s, cleanup, nil +} + +// filesystemRemoteScanner initializes a filesystem scanner in client/server mode +func filesystemRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeRemoteFilesystemScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) + } + return s, cleanup, nil +} + +// filesystemStandaloneScanner initializes a repository scanner in standalone mode +func repositoryStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeRepositoryScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a filesystem scanner: %w", err) + } + return s, cleanup, nil +} + +// sbomStandaloneScanner initializes a SBOM scanner in standalone mode +func sbomStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) + } + return s, cleanup, nil +} + +// sbomRemoteScanner initializes a SBOM scanner in client/server mode +func sbomRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) { + s, cleanup, err := initializeRemoteSBOMScanner(ctx, conf.Target, conf.ArtifactCache, conf.RemoteOption, conf.ArtifactOption) + if err != nil { + return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a cycloneDX scanner: %w", err) + } + return s, cleanup, nil } diff --git a/pkg/commands/module/module.go b/pkg/commands/module/module.go deleted file mode 100644 index 6bd3dd623f..0000000000 --- a/pkg/commands/module/module.go +++ /dev/null @@ -1,58 +0,0 @@ -package module - -import ( - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/module" -) - -// Install installs a module -func Install(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("log initialization error: %w", err) - } - - repo := c.Args().First() - if err := module.Install(c.Context, repo, c.Bool("quiet"), c.Bool("insecure")); err != nil { - return xerrors.Errorf("module installation error: %w", err) - } - - return nil -} - -// Uninstall uninstalls a module -func Uninstall(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("log initialization error: %w", err) - } - - repo := c.Args().First() - if err := module.Uninstall(c.Context, repo); err != nil { - return xerrors.Errorf("module uninstall error: %w", err) - } - - return nil -} - -func initLogger(ctx *cli.Context) error { - conf, err := option.NewGlobalOption(ctx) - if err != nil { - return xerrors.Errorf("config error: %w", err) - } - - if err = log.InitLogger(conf.Debug, conf.Quiet); err != nil { - return xerrors.Errorf("failed to initialize a logger: %w", err) - } - return nil -} diff --git a/pkg/commands/operation/operation.go b/pkg/commands/operation/operation.go index 93ad785c45..a1ae16db3b 100644 --- a/pkg/commands/operation/operation.go +++ b/pkg/commands/operation/operation.go @@ -6,12 +6,15 @@ import ( "os" "strings" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/go-redis/redis/v8" "github.com/google/wire" "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/metadata" - "github.com/aquasecurity/trivy/pkg/commands/option" "github.com/aquasecurity/trivy/pkg/db" "github.com/aquasecurity/trivy/pkg/fanal/cache" "github.com/aquasecurity/trivy/pkg/log" @@ -31,7 +34,7 @@ type Cache struct { } // NewCache is the factory method for Cache -func NewCache(c option.CacheOption) (Cache, error) { +func NewCache(c flag.CacheOptions) (Cache, error) { if strings.HasPrefix(c.CacheBackend, "redis://") { log.Logger.Infof("Redis cache: %s", c.CacheBackendMasked()) options, err := redis.ParseURL(c.CacheBackend) @@ -39,7 +42,7 @@ func NewCache(c option.CacheOption) (Cache, error) { return Cache{}, err } - if (option.RedisOption{}) != c.RedisOption { + if !lo.IsEmpty(c.RedisOptions) { caCert, cert, err := utils.GetTLSConfig(c.RedisCACert, c.RedisCert, c.RedisKey) if err != nil { return Cache{}, err diff --git a/pkg/commands/option/artifact.go b/pkg/commands/option/artifact.go deleted file mode 100644 index 1bbf0ee8b1..0000000000 --- a/pkg/commands/option/artifact.go +++ /dev/null @@ -1,54 +0,0 @@ -package option - -import ( - "os" - "time" - - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "golang.org/x/xerrors" -) - -// ArtifactOption holds the options for an artifact scanning -type ArtifactOption struct { - Input string - Timeout time.Duration - ClearCache bool - - SkipDirs []string - SkipFiles []string - OfflineScan bool - - // this field is populated in Init() - Target string -} - -// NewArtifactOption is the factory method to return artifact option -func NewArtifactOption(c *cli.Context) ArtifactOption { - return ArtifactOption{ - Input: c.String("input"), - Timeout: c.Duration("timeout"), - ClearCache: c.Bool("clear-cache"), - SkipFiles: c.StringSlice("skip-files"), - SkipDirs: c.StringSlice("skip-dirs"), - OfflineScan: c.Bool("offline-scan"), - } -} - -// Init initialize the CLI context for artifact scanning -func (c *ArtifactOption) Init(ctx *cli.Context, logger *zap.SugaredLogger) (err error) { - if c.Input == "" && ctx.Args().Len() == 0 { - logger.Debug(`trivy requires at least 1 argument or --input option`) - _ = cli.ShowSubcommandHelp(ctx) // nolint: errcheck - os.Exit(0) - } else if ctx.Args().Len() > 1 && ctx.Command.Name != "kubernetes" { - logger.Error(`multiple targets cannot be specified`) - return xerrors.New("arguments error") - } - - if c.Input == "" { - c.Target = ctx.Args().First() - } - - return nil -} diff --git a/pkg/commands/option/artifact_test.go b/pkg/commands/option/artifact_test.go deleted file mode 100644 index e2fc02349e..0000000000 --- a/pkg/commands/option/artifact_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package option_test - -import ( - "flag" - "testing" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "go.uber.org/zap/zaptest/observer" -) - -func TestArtifactOption_Init(t *testing.T) { - tests := []struct { - name string - args []string - logs []string - want option.ArtifactOption - wantErr string - }{ - { - name: "happy path", - args: []string{"alpine:3.10"}, - want: option.ArtifactOption{ - Target: "alpine:3.10", - }, - }, - { - name: "sad: multiple image names", - args: []string{"centos:7", "alpine:3.10"}, - logs: []string{ - "multiple targets cannot be specified", - }, - wantErr: "arguments error", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - core, obs := observer.New(zap.DebugLevel) - logger := zap.New(core) - - app := cli.NewApp() - set := flag.NewFlagSet("test", 0) - ctx := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - c := option.NewArtifactOption(ctx) - - err := c.Init(ctx, logger.Sugar()) - - // tests log messages - var gotMessages []string - for _, entry := range obs.AllUntimed() { - gotMessages = append(gotMessages, entry.Message) - } - assert.Equal(t, tt.logs, gotMessages, tt.name) - - // test the error - switch { - case tt.wantErr != "": - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr, tt.name) - return - default: - assert.NoError(t, err, tt.name) - } - - assert.Equal(t, tt.want, c, tt.name) - }) - } -} diff --git a/pkg/commands/option/cache.go b/pkg/commands/option/cache.go deleted file mode 100644 index 3dc43dae29..0000000000 --- a/pkg/commands/option/cache.go +++ /dev/null @@ -1,66 +0,0 @@ -package option - -import ( - "fmt" - "strings" - "time" - - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" -) - -// CacheOption holds the options for cache -type CacheOption struct { - CacheBackend string - CacheTTL time.Duration - RedisOption -} - -// RedisOption holds the options for redis cache -type RedisOption struct { - RedisCACert string - RedisCert string - RedisKey string -} - -// NewCacheOption returns an instance of CacheOption -func NewCacheOption(c *cli.Context) CacheOption { - return CacheOption{ - CacheBackend: c.String("cache-backend"), - CacheTTL: c.Duration("cache-ttl"), - RedisOption: RedisOption{ - RedisCACert: c.String("redis-ca"), - RedisCert: c.String("redis-cert"), - RedisKey: c.String("redis-key"), - }, - } -} - -// Init initialize the CacheOption -func (c *CacheOption) Init() error { - // "redis://" or "fs" are allowed for now - // An empty value is also allowed for testability - if !strings.HasPrefix(c.CacheBackend, "redis://") && - c.CacheBackend != "fs" && c.CacheBackend != "" { - return xerrors.Errorf("unsupported cache backend: %s", c.CacheBackend) - } - // if one of redis option not nil, make sure CA, cert, and key provided - if (RedisOption{}) != c.RedisOption { - if c.RedisCACert == "" || c.RedisCert == "" || c.RedisKey == "" { - return xerrors.Errorf("you must provide CA, cert and key file path when using tls") - } - } - return nil -} - -// CacheBackendMasked returns the redis connection string masking credentials -func (c *CacheOption) CacheBackendMasked() string { - endIndex := strings.Index(c.CacheBackend, "@") - if endIndex == -1 { - return c.CacheBackend - } - - startIndex := strings.Index(c.CacheBackend, "//") - - return fmt.Sprintf("%s****%s", c.CacheBackend[:startIndex+2], c.CacheBackend[endIndex:]) -} diff --git a/pkg/commands/option/cache_test.go b/pkg/commands/option/cache_test.go deleted file mode 100644 index 63feea8f1c..0000000000 --- a/pkg/commands/option/cache_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package option_test - -import ( - "flag" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" - - "github.com/aquasecurity/trivy/pkg/commands/option" -) - -func TestNewCacheOption(t *testing.T) { - tests := []struct { - name string - args []string - want option.CacheOption - }{ - { - name: "happy path", - args: []string{"--cache-backend", "redis://localhost:6379"}, - want: option.CacheOption{ - CacheBackend: "redis://localhost:6379", - }, - }, - { - name: "default", - args: []string{}, - want: option.CacheOption{ - CacheBackend: "fs", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - set.String("cache-backend", "fs", "") - - c := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - got := option.NewCacheOption(c) - assert.Equal(t, tt.want, got, tt.name) - }) - } -} - -func TestCacheOption_Init(t *testing.T) { - type fields struct { - backend string - } - tests := []struct { - name string - fields fields - wantErr string - }{ - { - name: "fs", - fields: fields{ - backend: "fs", - }, - }, - { - name: "redis", - fields: fields{ - backend: "redis://localhost:6379", - }, - }, - { - name: "sad path", - fields: fields{ - backend: "unknown://", - }, - wantErr: "unsupported cache backend: unknown://", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &option.CacheOption{ - CacheBackend: tt.fields.backend, - } - - err := c.Init() - if tt.wantErr != "" { - assert.EqualError(t, err, tt.wantErr, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestCacheOption_CacheBackendMasked(t *testing.T) { - type fields struct { - backend string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "redis cache backend masked", - fields: fields{ - backend: "redis://root:password@localhost:6379", - }, - want: "redis://****@localhost:6379", - }, - { - name: "redis cache backend masked does nothing", - fields: fields{ - backend: "redis://localhost:6379", - }, - want: "redis://localhost:6379", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &option.CacheOption{ - CacheBackend: tt.fields.backend, - } - - assert.Equal(t, tt.want, c.CacheBackendMasked()) - }) - } -} diff --git a/pkg/commands/option/config.go b/pkg/commands/option/config.go deleted file mode 100644 index d6f27adb52..0000000000 --- a/pkg/commands/option/config.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" -) - -// ConfigOption holds the options for config scanning -type ConfigOption struct { - FilePatterns []string - IncludeNonFailures bool - SkipPolicyUpdate bool - Trace bool - - // Rego - PolicyPaths []string - DataPaths []string - PolicyNamespaces []string -} - -// NewConfigOption is the factory method to return config scanning options -func NewConfigOption(c *cli.Context) ConfigOption { - return ConfigOption{ - IncludeNonFailures: c.Bool("include-non-failures"), - SkipPolicyUpdate: c.Bool("skip-policy-update"), - Trace: c.Bool("trace"), - FilePatterns: c.StringSlice("file-patterns"), - PolicyPaths: c.StringSlice("config-policy"), - DataPaths: c.StringSlice("config-data"), - PolicyNamespaces: c.StringSlice("policy-namespaces"), - } -} diff --git a/pkg/commands/option/db.go b/pkg/commands/option/db.go deleted file mode 100644 index 902b0c9580..0000000000 --- a/pkg/commands/option/db.go +++ /dev/null @@ -1,41 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/log" -) - -// DBOption holds the options for trivy DB -type DBOption struct { - Reset bool - DownloadDBOnly bool - SkipDBUpdate bool - Light bool - NoProgress bool - DBRepository string -} - -// NewDBOption is the factory method to return the DBOption -func NewDBOption(c *cli.Context) DBOption { - return DBOption{ - Reset: c.Bool("reset"), - DownloadDBOnly: c.Bool("download-db-only"), - SkipDBUpdate: c.Bool("skip-db-update"), - Light: c.Bool("light"), - NoProgress: c.Bool("no-progress"), - DBRepository: c.String("db-repository"), - } -} - -// Init initialize the DBOption -func (c *DBOption) Init() (err error) { - if c.SkipDBUpdate && c.DownloadDBOnly { - return xerrors.New("--skip-db-update and --download-db-only options can not be specified both") - } - if c.Light { - log.Logger.Warn("'--light' option is deprecated and will be removed. See also: https://github.com/aquasecurity/trivy/discussions/1649") - } - return nil -} diff --git a/pkg/commands/option/db_test.go b/pkg/commands/option/db_test.go deleted file mode 100644 index 11e7262520..0000000000 --- a/pkg/commands/option/db_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package option_test - -import ( - "flag" - "testing" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" -) - -func TestNewDBOption(t *testing.T) { - tests := []struct { - name string - args []string - want option.DBOption - }{ - { - name: "happy path", - args: []string{"--reset", "--skip-db-update"}, - want: option.DBOption{ - Reset: true, - SkipDBUpdate: true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - set.Bool("reset", false, "") - set.Bool("skip-db-update", false, "") - - c := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - got := option.NewDBOption(c) - assert.Equal(t, tt.want, got, tt.name) - }) - } -} - -func TestDBOption_Init(t *testing.T) { - type fields struct { - Reset bool - DownloadDBOnly bool - SkipUpdate bool - Light bool - } - tests := []struct { - name string - fields fields - wantErr string - }{ - { - name: "happy path", - fields: fields{ - Light: true, - }, - }, - { - name: "sad path", - fields: fields{ - DownloadDBOnly: true, - SkipUpdate: true, - }, - wantErr: "--skip-db-update and --download-db-only options can not be specified both", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &option.DBOption{ - Reset: tt.fields.Reset, - DownloadDBOnly: tt.fields.DownloadDBOnly, - SkipDBUpdate: tt.fields.SkipUpdate, - Light: tt.fields.Light, - } - - err := c.Init() - if tt.wantErr != "" { - assert.EqualError(t, err, tt.wantErr, err) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/pkg/commands/option/global.go b/pkg/commands/option/global.go deleted file mode 100644 index f0a11b3b74..0000000000 --- a/pkg/commands/option/global.go +++ /dev/null @@ -1,40 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/log" -) - -// GlobalOption holds the global options for trivy -type GlobalOption struct { - Context *cli.Context - Logger *zap.SugaredLogger - - AppVersion string - Quiet bool - Debug bool - CacheDir string -} - -// NewGlobalOption is the factory method to return GlobalOption -func NewGlobalOption(c *cli.Context) (GlobalOption, error) { - quiet := c.Bool("quiet") - debug := c.Bool("debug") - logger, err := log.NewLogger(debug, quiet) - if err != nil { - return GlobalOption{}, xerrors.New("failed to create a logger") - } - - return GlobalOption{ - Context: c, - Logger: logger, - - AppVersion: c.App.Version, - Quiet: quiet, - Debug: debug, - CacheDir: c.String("cache-dir"), - }, nil -} diff --git a/pkg/commands/option/global_test.go b/pkg/commands/option/global_test.go deleted file mode 100644 index 3816e0ff16..0000000000 --- a/pkg/commands/option/global_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package option_test - -import ( - "flag" - "testing" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -func TestNewGlobalConfig(t *testing.T) { - tests := []struct { - name string - args []string - want option.GlobalOption - }{ - { - name: "happy path", - args: []string{"--quiet", "--debug"}, - want: option.GlobalOption{ - Quiet: true, - Debug: true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - set.Bool("debug", false, "") - set.Bool("quiet", false, "") - - c := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - got, err := option.NewGlobalOption(c) - require.NoError(t, err, err) - assert.Equal(t, tt.want.Quiet, got.Quiet, tt.name) - assert.Equal(t, tt.want.Debug, got.Debug, tt.name) - assert.Equal(t, tt.want.CacheDir, got.CacheDir, tt.name) - }) - } -} diff --git a/pkg/commands/option/image.go b/pkg/commands/option/image.go deleted file mode 100644 index d445cbbb00..0000000000 --- a/pkg/commands/option/image.go +++ /dev/null @@ -1,17 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" -) - -// ImageOption holds the options for scanning images -type ImageOption struct { - ScanRemovedPkgs bool -} - -// NewImageOption is the factory method to return ImageOption -func NewImageOption(c *cli.Context) ImageOption { - return ImageOption{ - ScanRemovedPkgs: c.Bool("removed-pkgs"), - } -} diff --git a/pkg/commands/option/kubernetes.go b/pkg/commands/option/kubernetes.go deleted file mode 100644 index fd432d04b2..0000000000 --- a/pkg/commands/option/kubernetes.go +++ /dev/null @@ -1,21 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" -) - -// KubernetesOption holds the options for Kubernetes scanning -type KubernetesOption struct { - ClusterContext string - Namespace string - ReportFormat string -} - -// NewKubernetesOption is the factory method to return Kubernetes options -func NewKubernetesOption(c *cli.Context) KubernetesOption { - return KubernetesOption{ - ClusterContext: c.String("context"), - Namespace: c.String("namespace"), - ReportFormat: c.String("report"), - } -} diff --git a/pkg/commands/option/others.go b/pkg/commands/option/others.go deleted file mode 100644 index 2343ffec7f..0000000000 --- a/pkg/commands/option/others.go +++ /dev/null @@ -1,14 +0,0 @@ -package option - -import "github.com/urfave/cli/v2" - -type OtherOption struct { - Insecure bool -} - -// NewOtherOption is the factory method to return other option -func NewOtherOption(c *cli.Context) OtherOption { - return OtherOption{ - Insecure: c.Bool("insecure"), - } -} diff --git a/pkg/commands/option/remote.go b/pkg/commands/option/remote.go deleted file mode 100644 index 134fb376b5..0000000000 --- a/pkg/commands/option/remote.go +++ /dev/null @@ -1,81 +0,0 @@ -package option - -import ( - "net/http" - "strings" - - "github.com/urfave/cli/v2" - "go.uber.org/zap" -) - -const DefaultTokenHeader = "Trivy-Token" - -// RemoteOption holds options for client/server -type RemoteOption struct { - RemoteAddr string - customHeaders []string - token string - tokenHeader string - remote string // deprecated - - // this field is populated in Init() - CustomHeaders http.Header -} - -func NewRemoteOption(c *cli.Context) RemoteOption { - r := RemoteOption{ - RemoteAddr: c.String("server"), - customHeaders: c.StringSlice("custom-headers"), - token: c.String("token"), - tokenHeader: c.String("token-header"), - remote: c.String("remote"), // deprecated - } - - return r -} - -// Init initialize the options for client/server mode -func (c *RemoteOption) Init(logger *zap.SugaredLogger) { - // for testability - defer func() { - c.token = "" - c.tokenHeader = "" - c.remote = "" - c.customHeaders = nil - }() - - // for backward compatibility, should be removed in the future - if c.remote != "" { - c.RemoteAddr = c.remote - } - - if c.RemoteAddr == "" { - switch { - case len(c.customHeaders) > 0: - logger.Warn(`"--custom-header"" can be used only with "--server"`) - case c.token != "": - logger.Warn(`"--token" can be used only with "--server"`) - case c.tokenHeader != "" && c.tokenHeader != DefaultTokenHeader: - logger.Warn(`'--token-header' can be used only with "--server"`) - } - return - } - - c.CustomHeaders = splitCustomHeaders(c.customHeaders) - if c.token != "" { - c.CustomHeaders.Set(c.tokenHeader, c.token) - } -} - -func splitCustomHeaders(headers []string) http.Header { - result := make(http.Header) - for _, header := range headers { - // e.g. x-api-token:XXX - s := strings.SplitN(header, ":", 2) - if len(s) != 2 { - continue - } - result.Set(s[0], s[1]) - } - return result -} diff --git a/pkg/commands/option/remote_test.go b/pkg/commands/option/remote_test.go deleted file mode 100644 index 2fa5c0cfa7..0000000000 --- a/pkg/commands/option/remote_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package option - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_splitCustomHeaders(t *testing.T) { - type args struct { - headers []string - } - tests := []struct { - name string - args args - want http.Header - }{ - { - name: "happy path", - args: args{ - headers: []string{"x-api-token:foo bar", "Authorization:user:password"}, - }, - want: http.Header{ - "X-Api-Token": []string{"foo bar"}, - "Authorization": []string{"user:password"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := splitCustomHeaders(tt.args.headers) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/commands/option/report.go b/pkg/commands/option/report.go deleted file mode 100644 index 6dde9deb17..0000000000 --- a/pkg/commands/option/report.go +++ /dev/null @@ -1,169 +0,0 @@ -package option - -import ( - "io" - "os" - "strings" - - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "golang.org/x/exp/slices" - "golang.org/x/xerrors" - - dbTypes "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/types" -) - -// ReportOption holds the options for reporting scan results -type ReportOption struct { - Format string - Template string - DependencyTree bool - - IgnoreFile string - IgnoreUnfixed bool - ExitCode int - IgnorePolicy string - - // these variables are not exported - vulnType string - securityChecks string - output string - severities string - - // these variables are populated by Init() - VulnType []string - SecurityChecks []string - Output io.Writer - Severities []dbTypes.Severity - ListAllPkgs bool -} - -// NewReportOption is the factory method to return ReportOption -func NewReportOption(c *cli.Context) ReportOption { - return ReportOption{ - output: c.String("output"), - Format: c.String("format"), - DependencyTree: c.Bool("dependency-tree"), - Template: c.String("template"), - IgnorePolicy: c.String("ignore-policy"), - - vulnType: c.String("vuln-type"), - securityChecks: c.String("security-checks"), - severities: c.String("severity"), - IgnoreFile: c.String("ignorefile"), - IgnoreUnfixed: c.Bool("ignore-unfixed"), - ExitCode: c.Int("exit-code"), - ListAllPkgs: c.Bool("list-all-pkgs"), - } -} - -// Init initializes the ReportOption -func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error { - if c.Template != "" { - if c.Format == "" { - logger.Warn("'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.") - } else if c.Format != "template" { - logger.Warnf("'--template' is ignored because '--format %s' is specified. Use '--template' option with '--format template' option.", c.Format) - } - } else { - if c.Format == "template" { - logger.Warn("'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.") - } - } - - // "--list-all-pkgs" option is unavailable with "--format table". - // If user specifies "--list-all-pkgs" with "--format table", we should warn it. - if c.ListAllPkgs && c.Format == "table" { - logger.Warn(`"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`) - } - - // "--dependency-tree" option is available only with "--format table". - if c.DependencyTree && c.Format != "table" { - logger.Warn(`"--dependency-tree" can be used only with "--format table".`) - } - - if c.forceListAllPkgs(logger) { - c.ListAllPkgs = true - } - - c.Severities = splitSeverity(logger, c.severities) - - if err := c.populateVulnTypes(); err != nil { - return xerrors.Errorf("vuln type: %w", err) - } - - if err := c.populateSecurityChecks(); err != nil { - return xerrors.Errorf("security checks: %w", err) - } - - // for testability - c.severities = "" - c.vulnType = "" - c.securityChecks = "" - - // The output is os.Stdout by default - if c.output != "" { - var err error - if output, err = os.Create(c.output); err != nil { - return xerrors.Errorf("failed to create an output file: %w", err) - } - } - - c.Output = output - - return nil -} - -func (c *ReportOption) populateVulnTypes() error { - if c.vulnType == "" { - return nil - } - - for _, v := range strings.Split(c.vulnType, ",") { - if !slices.Contains(types.VulnTypes, v) { - return xerrors.Errorf("unknown vulnerability type (%s)", v) - } - c.VulnType = append(c.VulnType, v) - } - return nil -} - -func (c *ReportOption) populateSecurityChecks() error { - if c.securityChecks == "" { - return nil - } - - for _, v := range strings.Split(c.securityChecks, ",") { - if !slices.Contains(types.SecurityChecks, v) { - return xerrors.Errorf("unknown security check (%s)", v) - } - c.SecurityChecks = append(c.SecurityChecks, v) - } - return nil -} - -func (c *ReportOption) forceListAllPkgs(logger *zap.SugaredLogger) bool { - if slices.Contains(supportedSbomFormats, c.Format) && !c.ListAllPkgs { - logger.Debugf("'github', 'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.") - return true - } - if c.DependencyTree { - logger.Debugf("'--dependency-tree' enables '--list-all-pkgs'.") - return true - } - return false -} - -func splitSeverity(logger *zap.SugaredLogger, severity string) []dbTypes.Severity { - logger.Debugf("Severities: %s", severity) - var severities []dbTypes.Severity - for _, s := range strings.Split(severity, ",") { - severity, err := dbTypes.NewSeverity(s) - if err != nil { - logger.Warnf("unknown severity option: %s", err) - } - severities = append(severities, severity) - } - return severities -} diff --git a/pkg/commands/option/report_test.go b/pkg/commands/option/report_test.go deleted file mode 100644 index 553a159d02..0000000000 --- a/pkg/commands/option/report_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package option - -import ( - "flag" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "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/aquasecurity/trivy/pkg/report" - "github.com/aquasecurity/trivy/pkg/types" -) - -func TestReportReportConfig_Init(t *testing.T) { - type fields struct { - output string - Format string - Template string - vulnType string - securityChecks string - severities string - IgnoreFile string - IgnoreUnfixed bool - listAllPksgs bool - ExitCode int - VulnType []string - Output *os.File - Severities []dbTypes.Severity - debug bool - } - tests := []struct { - name string - fields fields - args []string - logs []string - want ReportOption - wantErr string - }{ - { - name: "happy path", - fields: fields{ - severities: "CRITICAL", - vulnType: "os", - securityChecks: "vuln", - }, - args: []string{"alpine:3.10"}, - want: ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Output: os.Stdout, - }, - }, - { - name: "happy path with an unknown severity", - fields: fields{ - severities: "CRITICAL,INVALID", - vulnType: "os,library", - securityChecks: "config", - }, - args: []string{"centos:7"}, - logs: []string{ - "unknown severity option: unknown severity: INVALID", - }, - want: ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityUnknown}, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckConfig}, - Output: os.Stdout, - }, - }, - { - name: "happy path with an cyclonedx", - fields: fields{ - severities: "CRITICAL", - vulnType: "os,library", - securityChecks: "vuln", - Format: report.FormatCycloneDX, - listAllPksgs: true, - }, - args: []string{"centos:7"}, - want: ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Format: report.FormatCycloneDX, - Output: os.Stdout, - ListAllPkgs: true, - }, - }, - { - name: "happy path with an cyclonedx option list-all-pkgs is false", - fields: fields{ - severities: "CRITICAL", - vulnType: "os,library", - securityChecks: "vuln", - Format: "cyclonedx", - listAllPksgs: false, - debug: true, - }, - args: []string{"centos:7"}, - logs: []string{ - "'github', 'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.", - "Severities: CRITICAL", - }, - want: ReportOption{ - Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, - VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - Format: "cyclonedx", - Output: os.Stdout, - ListAllPkgs: true, - }, - }, - { - name: "invalid option combination: --template enabled without --format", - fields: fields{ - Template: "@contrib/gitlab.tpl", - severities: "LOW", - vulnType: "os", - securityChecks: "vuln", - }, - args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.", - }, - want: ReportOption{ - Output: os.Stdout, - Severities: []dbTypes.Severity{dbTypes.SeverityLow}, - Template: "@contrib/gitlab.tpl", - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - }, - { - name: "invalid option combination: --template and --format json", - fields: fields{ - Format: "json", - Template: "@contrib/gitlab.tpl", - severities: "LOW", - vulnType: "os", - securityChecks: "config", - }, - args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.", - }, - want: ReportOption{ - Format: "json", - Output: os.Stdout, - Severities: []dbTypes.Severity{dbTypes.SeverityLow}, - Template: "@contrib/gitlab.tpl", - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckConfig}, - }, - }, - { - name: "invalid option combination: --format template without --template", - fields: fields{ - Format: "template", - severities: "LOW", - vulnType: "os", - securityChecks: "vuln", - }, - args: []string{"gitlab/gitlab-ce:12.7.2-ce.0"}, - logs: []string{ - "'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.", - }, - want: ReportOption{ - Format: "template", - Output: os.Stdout, - Severities: []dbTypes.Severity{dbTypes.SeverityLow}, - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - }, - }, - { - name: "invalid option combination: --list-all-pkgs with --format table", - fields: fields{ - Format: "table", - severities: "LOW", - vulnType: "os", - securityChecks: "vuln", - listAllPksgs: true, - }, - args: []string{"centos:7"}, - logs: []string{ - `"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`, - }, - want: ReportOption{ - Format: "table", - Output: os.Stdout, - Severities: []dbTypes.Severity{dbTypes.SeverityLow}, - VulnType: []string{types.VulnTypeOS}, - SecurityChecks: []string{types.SecurityCheckVulnerability}, - ListAllPkgs: true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - level := zap.InfoLevel - if tt.fields.debug { - level = zap.DebugLevel - } - - core, obs := observer.New(level) - logger := zap.New(core) - - set := flag.NewFlagSet("test", 0) - _ = set.Parse(tt.args) - - c := &ReportOption{ - output: tt.fields.output, - Format: tt.fields.Format, - Template: tt.fields.Template, - vulnType: tt.fields.vulnType, - securityChecks: tt.fields.securityChecks, - severities: tt.fields.severities, - IgnoreFile: tt.fields.IgnoreFile, - IgnoreUnfixed: tt.fields.IgnoreUnfixed, - ExitCode: tt.fields.ExitCode, - ListAllPkgs: tt.fields.listAllPksgs, - Output: tt.fields.Output, - } - err := c.Init(os.Stdout, logger.Sugar()) - - // tests log messages - var gotMessages []string - for _, entry := range obs.AllUntimed() { - gotMessages = append(gotMessages, entry.Message) - } - assert.Equal(t, tt.logs, gotMessages, tt.name) - - // test the error - switch { - case tt.wantErr != "": - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr, tt.name) - return - } - - assert.NoError(t, err, tt.name) - assert.Equal(t, &tt.want, c, tt.name) - }) - } -} diff --git a/pkg/commands/option/sbom.go b/pkg/commands/option/sbom.go deleted file mode 100644 index f515954c33..0000000000 --- a/pkg/commands/option/sbom.go +++ /dev/null @@ -1,41 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" - "go.uber.org/zap" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/report" -) - -var supportedSbomFormats = []string{report.FormatCycloneDX, report.FormatSPDX, report.FormatSPDXJSON, - report.FormatGitHub} - -// SbomOption holds the options for SBOM generation -type SbomOption struct { - ArtifactType string // deprecated - SbomFormat string // deprecated -} - -// NewSbomOption is the factory method to return SBOM options -func NewSbomOption(c *cli.Context) SbomOption { - return SbomOption{ - ArtifactType: c.String("artifact-type"), - SbomFormat: c.String("sbom-format"), - } -} - -// Init initialize the CLI context for SBOM generation -func (c *SbomOption) Init(ctx *cli.Context, logger *zap.SugaredLogger) error { - if ctx.Command.Name != "sbom" { - return nil - } - - if c.ArtifactType != "" || c.SbomFormat != "" { - logger.Error("'trivy sbom' is now for scanning SBOM. " + - "See https://github.com/aquasecurity/trivy/discussions/2407 for the detail") - return xerrors.New("'--artifact-type' and '--sbom-format' are no longer available") - } - - return nil -} diff --git a/pkg/commands/option/secret.go b/pkg/commands/option/secret.go deleted file mode 100644 index e29ecea66e..0000000000 --- a/pkg/commands/option/secret.go +++ /dev/null @@ -1,17 +0,0 @@ -package option - -import ( - "github.com/urfave/cli/v2" -) - -// SecretOption holds the options for secret scanning -type SecretOption struct { - SecretConfigPath string -} - -// NewSecretOption is the factory method to return secret options -func NewSecretOption(c *cli.Context) SecretOption { - return SecretOption{ - SecretConfigPath: c.String("secret-config"), - } -} diff --git a/pkg/commands/plugin/plugin.go b/pkg/commands/plugin/plugin.go deleted file mode 100644 index e3471faffa..0000000000 --- a/pkg/commands/plugin/plugin.go +++ /dev/null @@ -1,179 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "os" - - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/plugin" -) - -// Install installs a plugin -func Install(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - url := c.Args().First() - if _, err := plugin.Install(c.Context, url, true); err != nil { - return xerrors.Errorf("plugin install error: %w", err) - } - - return nil -} - -// Uninstall uninstalls the plugin -func Uninstall(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - pluginName := c.Args().First() - if err := plugin.Uninstall(pluginName); err != nil { - return xerrors.Errorf("plugin uninstall error: %w", err) - } - - return nil -} - -// Information displays information about the plugin -func Information(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize logger error: %w", err) - } - - pluginName := c.Args().First() - info, err := plugin.Information(pluginName) - if err != nil { - return xerrors.Errorf("plugin information display error: %w", err) - } - - if _, err = fmt.Fprintf(os.Stdout, info); err != nil { - return xerrors.Errorf("print error: %w", err) - } - - return nil -} - -// List displays a list of all of installed plugins -func List(c *cli.Context) error { - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - info, err := plugin.List() - if err != nil { - return xerrors.Errorf("plugin list display error: %w", err) - } - - if _, err = fmt.Fprintf(os.Stdout, info); err != nil { - return xerrors.Errorf("print error: %w", err) - } - - return nil -} - -// Update updates an existing plugin -func Update(c *cli.Context) error { - if c.NArg() != 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - pluginName := c.Args().First() - if err := plugin.Update(pluginName); err != nil { - return xerrors.Errorf("plugin update error: %w", err) - } - - return nil -} - -// Run runs the plugin -func Run(c *cli.Context) error { - if c.NArg() < 1 { - cli.ShowSubcommandHelpAndExit(c, 1) - } - - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - url := c.Args().First() - args := c.Args().Tail() - return RunWithArgs(c.Context, url, args) -} - -// RunWithArgs runs the plugin with arguments -func RunWithArgs(ctx context.Context, url string, args []string) error { - pl, err := plugin.Install(ctx, url, false) - if err != nil { - return xerrors.Errorf("plugin install error: %w", err) - } - - if err = pl.Run(ctx, args); err != nil { - return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err) - } - return nil -} - -// LoadCommands loads plugins as subcommands -func LoadCommands() cli.Commands { - var commands cli.Commands - plugins, err := plugin.LoadAll() - if err != nil { - log.Logger.Debugf("no plugins were loaded") - return nil - } - for _, p := range plugins { - p := p - cmd := &cli.Command{ - Name: p.Name, - Usage: p.Usage, - Action: func(c *cli.Context) error { - if err := initLogger(c); err != nil { - return xerrors.Errorf("initialize error: %w", err) - } - - if err := p.Run(c.Context, c.Args().Slice()); err != nil { - return xerrors.Errorf("plugin error: %w", err) - } - return nil - }, - SkipFlagParsing: true, - } - commands = append(commands, cmd) - } - return commands -} - -func initLogger(ctx *cli.Context) error { - conf, err := option.NewGlobalOption(ctx) - if err != nil { - return xerrors.Errorf("config error: %w", err) - } - - if err = log.InitLogger(conf.Debug, conf.Quiet); err != nil { - return xerrors.Errorf("failed to initialize a logger: %w", err) - } - return nil -} diff --git a/pkg/commands/server/option.go b/pkg/commands/server/option.go deleted file mode 100644 index 9878e8730c..0000000000 --- a/pkg/commands/server/option.go +++ /dev/null @@ -1,47 +0,0 @@ -package server - -import ( - "github.com/urfave/cli/v2" - - "github.com/aquasecurity/trivy/pkg/commands/option" -) - -// Option holds the Trivy config -type Option struct { - option.GlobalOption - option.DBOption - option.CacheOption - option.OtherOption - - Listen string - Token string - TokenHeader string -} - -// NewOption is the factory method to return config -func NewOption(c *cli.Context) Option { - // the error is ignored because logger is unnecessary - gc, _ := option.NewGlobalOption(c) // nolint: errcheck - return Option{ - GlobalOption: gc, - DBOption: option.NewDBOption(c), - CacheOption: option.NewCacheOption(c), - OtherOption: option.NewOtherOption(c), - - Listen: c.String("listen"), - Token: c.String("token"), - TokenHeader: c.String("token-header"), - } -} - -// Init initializes the config -func (c *Option) Init() (err error) { - if err := c.DBOption.Init(); err != nil { - return err - } - if err := c.CacheOption.Init(); err != nil { - return err - } - - return nil -} diff --git a/pkg/commands/server/option_test.go b/pkg/commands/server/option_test.go deleted file mode 100644 index 92df369335..0000000000 --- a/pkg/commands/server/option_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package server_test - -import ( - "flag" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/aquasecurity/trivy/pkg/commands/server" -) - -func TestNew(t *testing.T) { - tests := []struct { - name string - args []string - want server.Option - }{ - { - name: "happy path", - args: []string{"-quiet", "--no-progress", "--reset", "--skip-db-update", "--listen", "localhost:8080"}, - want: server.Option{ - GlobalOption: option.GlobalOption{ - Quiet: true, - }, - DBOption: option.DBOption{ - Reset: true, - SkipDBUpdate: true, - NoProgress: true, - }, - Listen: "localhost:8080", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - set.Bool("quiet", false, "") - set.Bool("no-progress", false, "") - set.Bool("reset", false, "") - set.Bool("skip-db-update", false, "") - set.String("listen", "", "") - - ctx := cli.NewContext(app, set, nil) - _ = set.Parse(tt.args) - - tt.want.GlobalOption.Context = ctx - - got := server.NewOption(ctx) - assert.Equal(t, tt.want.GlobalOption.Quiet, got.Quiet, tt.name) - assert.Equal(t, tt.want.DBOption, got.DBOption, tt.name) - assert.Equal(t, tt.want.Listen, got.Listen, tt.name) - }) - } -} - -func TestConfig_Init(t *testing.T) { - tests := []struct { - name string - globalConfig option.GlobalOption - dbConfig option.DBOption - args []string - wantErr string - }{ - { - name: "happy path", - args: []string{"alpine:3.10"}, - }, - { - name: "happy path: reset", - dbConfig: option.DBOption{ - Reset: true, - }, - args: []string{"alpine:3.10"}, - }, - { - name: "sad: skip and download db", - dbConfig: option.DBOption{ - SkipDBUpdate: true, - DownloadDBOnly: true, - }, - args: []string{"alpine:3.10"}, - wantErr: "--skip-db-update and --download-db-only options can not be specified both", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &server.Option{ - DBOption: tt.dbConfig, - } - - err := c.Init() - - // test the error - switch { - case tt.wantErr != "": - require.NotNil(t, err, tt.name) - assert.Contains(t, err.Error(), tt.wantErr, tt.name) - return - default: - assert.NoError(t, err, tt.name) - } - }) - } -} diff --git a/pkg/commands/server/run.go b/pkg/commands/server/run.go index 1a8ed82530..9abbf4973a 100644 --- a/pkg/commands/server/run.go +++ b/pkg/commands/server/run.go @@ -1,11 +1,13 @@ package server import ( - "github.com/urfave/cli/v2" + "context" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/flag" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/module" rpcServer "github.com/aquasecurity/trivy/pkg/rpc/server" @@ -13,53 +15,45 @@ import ( ) // Run runs the scan -func Run(ctx *cli.Context) error { - return run(NewOption(ctx)) -} - -func run(c Option) (err error) { - if err = log.InitLogger(c.Debug, c.Quiet); err != nil { +func Run(ctx context.Context, opts flag.Options) (err error) { + if err = log.InitLogger(opts.Debug, opts.Quiet); err != nil { return xerrors.Errorf("failed to initialize a logger: %w", err) } - // initialize config - if err = c.Init(); err != nil { - return xerrors.Errorf("failed to initialize options: %w", err) - } - // configure cache dir - utils.SetCacheDir(c.CacheDir) - cache, err := operation.NewCache(c.CacheOption) + utils.SetCacheDir(opts.CacheDir) + cache, err := operation.NewCache(opts.CacheOptions) if err != nil { return xerrors.Errorf("server cache error: %w", err) } defer cache.Close() log.Logger.Debugf("cache dir: %s", utils.CacheDir()) - if c.Reset { + if opts.Reset { return cache.ClearDB() } // download the database file - if err = operation.DownloadDB(c.AppVersion, c.CacheDir, c.DBRepository, true, c.Insecure, c.SkipDBUpdate); err != nil { + if err = operation.DownloadDB(opts.AppVersion, opts.CacheDir, opts.DBRepository, + true, opts.Insecure, opts.SkipDBUpdate); err != nil { return err } - if c.DownloadDBOnly { + if opts.DownloadDBOnly { return nil } - if err = db.Init(c.CacheDir); err != nil { + if err = db.Init(opts.CacheDir); err != nil { return xerrors.Errorf("error in vulnerability DB initialize: %w", err) } // Initialize WASM modules - m, err := module.NewManager(c.Context.Context) + m, err := module.NewManager(ctx) if err != nil { return xerrors.Errorf("WASM module error: %w", err) } m.Register() - server := rpcServer.NewServer(c.AppVersion, c.Listen, c.CacheDir, c.Token, c.TokenHeader, c.DBRepository) - return server.ListenAndServe(cache, c.Insecure) + server := rpcServer.NewServer(opts.AppVersion, opts.Listen, opts.CacheDir, opts.Token, opts.TokenHeader, opts.DBRepository) + return server.ListenAndServe(cache, opts.Insecure) } diff --git a/pkg/flag/cache_flags.go b/pkg/flag/cache_flags.go new file mode 100644 index 0000000000..df7309c906 --- /dev/null +++ b/pkg/flag/cache_flags.go @@ -0,0 +1,155 @@ +package flag + +import ( + "fmt" + "strings" + "time" + + "github.com/samber/lo" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +// e.g. config yaml +// cache: +// clear: true +// backend: "redis://localhost:6379" +// redis: +// ca: ca-cert.pem +// cert: cert.pem +// key: key.pem +var ( + ClearCacheFlag = Flag{ + Name: "clear-cache", + ConfigName: "cache.clear", + Value: false, + Usage: "clear image caches without scanning", + } + CacheBackendFlag = Flag{ + Name: "cache-backend", + ConfigName: "cache.backend", + Value: "fs", + Usage: "cache backend (e.g. redis://localhost:6379)", + } + CacheTTLFlag = Flag{ + Name: "cache-ttl", + ConfigName: "cache.ttl", + Value: time.Duration(0), + Usage: "cache TTL when using redis as cache backend", + } + RedisCACertFlag = Flag{ + Name: "redis-ca", + ConfigName: "cache.redis.ca", + Value: "", + Usage: "redis ca file location, if using redis as cache backend", + } + RedisCertFlag = Flag{ + Name: "redis-cert", + ConfigName: "cache.redis.cert", + Value: "", + Usage: "redis certificate file location, if using redis as cache backend", + } + RedisKeyFlag = Flag{ + Name: "redis-key", + ConfigName: "cache.redis.key", + Value: "", + Usage: "redis key file location, if using redis as cache backend", + } +) + +// CacheFlagGroup composes common printer flag structs used for commands requiring cache logic. +type CacheFlagGroup struct { + ClearCache *Flag + CacheBackend *Flag + CacheTTL *Flag + + RedisCACert *Flag + RedisCert *Flag + RedisKey *Flag +} + +type CacheOptions struct { + ClearCache bool + CacheBackend string + CacheTTL time.Duration + RedisOptions +} + +// RedisOptions holds the options for redis cache +type RedisOptions struct { + RedisCACert string + RedisCert string + RedisKey string +} + +// NewCacheFlagGroup returns a default CacheFlagGroup +func NewCacheFlagGroup() *CacheFlagGroup { + return &CacheFlagGroup{ + ClearCache: lo.ToPtr(ClearCacheFlag), + CacheBackend: lo.ToPtr(CacheBackendFlag), + CacheTTL: lo.ToPtr(CacheTTLFlag), + RedisCACert: lo.ToPtr(RedisCACertFlag), + RedisCert: lo.ToPtr(RedisCertFlag), + RedisKey: lo.ToPtr(RedisKeyFlag), + } +} + +func (f *CacheFlagGroup) flags() []*Flag { + return []*Flag{f.ClearCache, f.CacheBackend, f.CacheTTL, f.RedisCACert, f.RedisCert, f.RedisKey} +} + +func (f *CacheFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *CacheFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *CacheFlagGroup) ToOptions() (CacheOptions, error) { + cacheBackend := getString(f.CacheBackend) + redisOptions := RedisOptions{ + RedisCACert: getString(f.RedisCACert), + RedisCert: getString(f.RedisCert), + RedisKey: getString(f.RedisKey), + } + + // "redis://" or "fs" are allowed for now + // An empty value is also allowed for testability + if !strings.HasPrefix(cacheBackend, "redis://") && + cacheBackend != "fs" && cacheBackend != "" { + return CacheOptions{}, xerrors.Errorf("unsupported cache backend: %s", cacheBackend) + } + // if one of redis option not nil, make sure CA, cert, and key provided + if !lo.IsEmpty(redisOptions) { + if redisOptions.RedisCACert == "" || redisOptions.RedisCert == "" || redisOptions.RedisKey == "" { + return CacheOptions{}, xerrors.Errorf("you must provide Redis CA, cert and key file path when using TLS") + } + } + + return CacheOptions{ + ClearCache: getBool(f.ClearCache), + CacheBackend: cacheBackend, + CacheTTL: getDuration(f.CacheTTL), + RedisOptions: redisOptions, + }, nil +} + +// CacheBackendMasked returns the redis connection string masking credentials +func (o *CacheOptions) CacheBackendMasked() string { + endIndex := strings.Index(o.CacheBackend, "@") + if endIndex == -1 { + return o.CacheBackend + } + + startIndex := strings.Index(o.CacheBackend, "//") + + return fmt.Sprintf("%s****%s", o.CacheBackend[:startIndex+2], o.CacheBackend[endIndex:]) +} diff --git a/pkg/flag/cache_flags_test.go b/pkg/flag/cache_flags_test.go new file mode 100644 index 0000000000..e86cb41a4b --- /dev/null +++ b/pkg/flag/cache_flags_test.go @@ -0,0 +1,145 @@ +package flag_test + +import ( + "testing" + "time" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" +) + +func TestCacheFlagGroup_ToOptions(t *testing.T) { + type fields struct { + ClearCache bool + CacheBackend string + CacheTTL time.Duration + RedisCACert string + RedisCert string + RedisKey string + } + tests := []struct { + name string + fields fields + want flag.CacheOptions + assertion require.ErrorAssertionFunc + }{ + { + name: "fs", + fields: fields{ + CacheBackend: "fs", + }, + want: flag.CacheOptions{ + CacheBackend: "fs", + }, + assertion: require.NoError, + }, + { + name: "redis", + fields: fields{ + CacheBackend: "redis://localhost:6379", + }, + want: flag.CacheOptions{ + CacheBackend: "redis://localhost:6379", + }, + assertion: require.NoError, + }, + { + name: "redis tls", + fields: fields{ + CacheBackend: "redis://localhost:6379", + RedisCACert: "ca-cert.pem", + RedisCert: "cert.pem", + RedisKey: "key.pem", + }, + want: flag.CacheOptions{ + CacheBackend: "redis://localhost:6379", + RedisOptions: flag.RedisOptions{ + RedisCACert: "ca-cert.pem", + RedisCert: "cert.pem", + RedisKey: "key.pem", + }, + }, + assertion: require.NoError, + }, + { + name: "unknown backend", + fields: fields{ + CacheBackend: "unknown", + }, + assertion: func(t require.TestingT, err error, msgs ...interface{}) { + require.ErrorContains(t, err, "unsupported cache backend") + }, + }, + { + name: "sad redis tls", + fields: fields{ + CacheBackend: "redis://localhost:6379", + RedisCACert: "ca-cert.pem", + }, + assertion: func(t require.TestingT, err error, msgs ...interface{}) { + require.ErrorContains(t, err, "you must provide Redis CA") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Set(flag.ClearCacheFlag.ConfigName, tt.fields.ClearCache) + viper.Set(flag.CacheBackendFlag.ConfigName, tt.fields.CacheBackend) + viper.Set(flag.CacheTTLFlag.ConfigName, tt.fields.CacheTTL) + viper.Set(flag.RedisCACertFlag.ConfigName, tt.fields.RedisCACert) + viper.Set(flag.RedisCertFlag.ConfigName, tt.fields.RedisCert) + viper.Set(flag.RedisKeyFlag.ConfigName, tt.fields.RedisKey) + + f := &flag.CacheFlagGroup{ + ClearCache: &flag.ClearCacheFlag, + CacheBackend: &flag.CacheBackendFlag, + CacheTTL: &flag.CacheTTLFlag, + RedisCACert: &flag.RedisCACertFlag, + RedisCert: &flag.RedisCertFlag, + RedisKey: &flag.RedisKeyFlag, + } + + got, err := f.ToOptions() + tt.assertion(t, err) + assert.Equalf(t, tt.want, got, "ToOptions()") + }) + } +} + +func TestCacheOptions_CacheBackendMasked(t *testing.T) { + type fields struct { + backend string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "redis cache backend masked", + fields: fields{ + backend: "redis://root:password@localhost:6379", + }, + want: "redis://****@localhost:6379", + }, + { + name: "redis cache backend masked does nothing", + fields: fields{ + backend: "redis://localhost:6379", + }, + want: "redis://localhost:6379", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &flag.CacheOptions{ + CacheBackend: tt.fields.backend, + } + + assert.Equal(t, tt.want, c.CacheBackendMasked()) + }) + } +} diff --git a/pkg/flag/db_flags.go b/pkg/flag/db_flags.go new file mode 100644 index 0000000000..7280ee1fb4 --- /dev/null +++ b/pkg/flag/db_flags.go @@ -0,0 +1,122 @@ +package flag + +import ( + "github.com/samber/lo" + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const defaultDBRepository = "ghcr.io/aquasecurity/trivy-db" + +var ( + ResetFlag = Flag{ + Name: "reset", + ConfigName: "reset", + Value: false, + Usage: "remove all caches and database", + } + DownloadDBOnlyFlag = Flag{ + Name: "download-db-only", + ConfigName: "db.download-only", + Value: false, + Usage: "download/update vulnerability database but don't run a scan", + } + SkipDBUpdateFlag = Flag{ + Name: "skip-db-update", + ConfigName: "db.skip-update", + Value: false, + Usage: "skip updating vulnerability database", + } + NoProgressFlag = Flag{ + Name: "no-progress", + ConfigName: "db.no-progress", + Value: false, + Usage: "suppress progress bar", + } + DBRepositoryFlag = Flag{ + Name: "db-repository", + ConfigName: "db.repository", + Value: defaultDBRepository, + Usage: "OCI repository to retrieve trivy-db from\"", + } + LightFlag = Flag{ + Name: "light", + ConfigName: "db.light", + Value: false, + Usage: "deprecated", + } +) + +// DBFlagGroup composes common printer flag structs used for commands requiring DB logic. +type DBFlagGroup struct { + Reset *Flag + DownloadDBOnly *Flag + SkipDBUpdate *Flag + NoProgress *Flag + DBRepository *Flag + Light *Flag // deprecated +} + +type DBOptions struct { + Reset bool + DownloadDBOnly bool + SkipDBUpdate bool + NoProgress bool + DBRepository string + Light bool // deprecated +} + +// NewDBFlagGroup returns a default DBFlagGroup +func NewDBFlagGroup() *DBFlagGroup { + return &DBFlagGroup{ + Reset: lo.ToPtr(ResetFlag), + DownloadDBOnly: lo.ToPtr(DownloadDBOnlyFlag), + SkipDBUpdate: lo.ToPtr(SkipDBUpdateFlag), + Light: lo.ToPtr(LightFlag), + NoProgress: lo.ToPtr(NoProgressFlag), + DBRepository: lo.ToPtr(DBRepositoryFlag), + } +} + +func (f *DBFlagGroup) flags() []*Flag { + return []*Flag{f.Reset, f.DownloadDBOnly, f.SkipDBUpdate, f.NoProgress, f.DBRepository, f.Light} +} + +func (f *DBFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *DBFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *DBFlagGroup) ToOptions() (DBOptions, error) { + skipDBUpdate := getBool(f.SkipDBUpdate) + downloadDBOnly := getBool(f.DownloadDBOnly) + light := getBool(f.Light) + + if downloadDBOnly && skipDBUpdate { + return DBOptions{}, xerrors.New("--skip-db-update and --download-db-only options can not be specified both") + } + if light { + log.Logger.Warn("'--light' option is deprecated and will be removed. See also: https://github.com/aquasecurity/trivy/discussions/1649") + } + + return DBOptions{ + Reset: getBool(f.Reset), + DownloadDBOnly: downloadDBOnly, + SkipDBUpdate: skipDBUpdate, + Light: light, + NoProgress: getBool(f.NoProgress), + DBRepository: getString(f.DBRepository), + }, nil +} diff --git a/pkg/flag/db_flags_test.go b/pkg/flag/db_flags_test.go new file mode 100644 index 0000000000..50e203307f --- /dev/null +++ b/pkg/flag/db_flags_test.go @@ -0,0 +1,94 @@ +package flag_test + +import ( + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestDBFlagGroup_ToOptions(t *testing.T) { + type fields struct { + SkipDBUpdate bool + DownloadDBOnly bool + Light bool + } + tests := []struct { + name string + fields fields + want flag.DBOptions + wantLogs []string + assertion require.ErrorAssertionFunc + }{ + { + name: "happy", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: false, + }, + want: flag.DBOptions{ + SkipDBUpdate: true, + DownloadDBOnly: false, + }, + assertion: require.NoError, + }, + { + name: "light", + fields: fields{ + Light: true, + }, + want: flag.DBOptions{ + Light: true, + }, + wantLogs: []string{ + "'--light' option is deprecated and will be removed. See also: https://github.com/aquasecurity/trivy/discussions/1649", + }, + assertion: require.NoError, + }, + { + name: "sad", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: true, + }, + assertion: func(t require.TestingT, err error, msgs ...interface{}) { + require.ErrorContains(t, err, "--skip-db-update and --download-db-only options can not be specified both") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := zap.WarnLevel + core, obs := observer.New(level) + log.Logger = zap.New(core).Sugar() + + viper.Set(flag.SkipDBUpdateFlag.ConfigName, tt.fields.SkipDBUpdate) + viper.Set(flag.DownloadDBOnlyFlag.ConfigName, tt.fields.DownloadDBOnly) + viper.Set(flag.LightFlag.ConfigName, tt.fields.Light) + + // Assert options + f := &flag.DBFlagGroup{ + DownloadDBOnly: &flag.DownloadDBOnlyFlag, + SkipDBUpdate: &flag.SkipDBUpdateFlag, + Light: &flag.LightFlag, + } + got, err := f.ToOptions() + tt.assertion(t, err) + assert.Equalf(t, tt.want, got, "ToOptions()") + + // Assert log messages + var gotMessages []string + for _, entry := range obs.AllUntimed() { + gotMessages = append(gotMessages, entry.Message) + } + assert.Equal(t, tt.wantLogs, gotMessages, tt.name) + }) + } +} diff --git a/pkg/flag/global_flags.go b/pkg/flag/global_flags.go new file mode 100644 index 0000000000..e63d61a9e1 --- /dev/null +++ b/pkg/flag/global_flags.go @@ -0,0 +1,131 @@ +package flag + +import ( + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/aquasecurity/trivy/pkg/utils" +) + +var ( + ConfigFileFlag = Flag{ + Name: "config", + ConfigName: "config", + Shorthand: "c", + Value: "trivy.yaml", + Usage: "config path", + Persistent: true, + } + ShowVersionFlag = Flag{ + Name: "version", + ConfigName: "version", + Shorthand: "v", + Value: false, + Usage: "show version", + Persistent: true, + } + QuietFlag = Flag{ + Name: "quiet", + ConfigName: "quiet", + Shorthand: "q", + Value: false, + Usage: "suppress progress bar and log output", + Persistent: true, + } + DebugFlag = Flag{ + Name: "debug", + ConfigName: "debug", + Shorthand: "d", + Value: false, + Usage: "debug mode", + Persistent: true, + } + InsecureFlag = Flag{ + Name: "insecure", + ConfigName: "insecure", + Value: false, + Usage: "allow insecure server connections when using TLS", + Persistent: true, + } + TimeoutFlag = Flag{ + Name: "timeout", + ConfigName: "timeout", + Value: time.Second * 300, // 5 mins + Usage: "timeout", + Persistent: true, + } + CacheDirFlag = Flag{ + Name: "cache-dir", + ConfigName: "cache.dir", + Value: utils.DefaultCacheDir(), + Usage: "cache directory", + Persistent: true, + } +) + +// GlobalFlagGroup composes global flags +type GlobalFlagGroup struct { + ConfigFile *Flag + ShowVersion *Flag // spf13/cobra can't override the logic of version printing like VersionPrinter in urfave/cli. -v needs to be defined ourselves. + Quiet *Flag + Debug *Flag + Insecure *Flag + Timeout *Flag + CacheDir *Flag +} + +// GlobalOptions defines flags and other configuration parameters for all the subcommands +type GlobalOptions struct { + ConfigFile string + ShowVersion bool + Quiet bool + Debug bool + Insecure bool + Timeout time.Duration + CacheDir string +} + +func NewGlobalFlagGroup() *GlobalFlagGroup { + return &GlobalFlagGroup{ + ConfigFile: &ConfigFileFlag, + ShowVersion: &ShowVersionFlag, + Quiet: &QuietFlag, + Debug: &DebugFlag, + Insecure: &InsecureFlag, + Timeout: &TimeoutFlag, + CacheDir: &CacheDirFlag, + } +} + +func (f *GlobalFlagGroup) flags() []*Flag { + return []*Flag{f.ConfigFile, f.ShowVersion, f.Quiet, f.Debug, f.Insecure, f.Timeout, f.CacheDir} +} + +func (f *GlobalFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *GlobalFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *GlobalFlagGroup) ToOptions() GlobalOptions { + return GlobalOptions{ + ConfigFile: getString(f.ConfigFile), + ShowVersion: getBool(f.ShowVersion), + Quiet: getBool(f.Quiet), + Debug: getBool(f.Debug), + Insecure: getBool(f.Insecure), + Timeout: viper.GetDuration(f.Timeout.ConfigName), + CacheDir: getString(f.CacheDir), + } +} diff --git a/pkg/flag/image_flags.go b/pkg/flag/image_flags.go new file mode 100644 index 0000000000..7758b59001 --- /dev/null +++ b/pkg/flag/image_flags.go @@ -0,0 +1,68 @@ +package flag + +import ( + "github.com/spf13/cobra" +) + +// e.g. config yaml +// image: +// removed-pkgs: true +// input: "/path/to/alpine" + +var ( + ScanRemovedPkgsFlag = Flag{ + Name: "removed-pkgs", + ConfigName: "image.removed-pkgs", + Value: false, + Usage: "detect vulnerabilities of removed packages (only for Alpine)", + } + InputFlag = Flag{ + Name: "input", + ConfigName: "image.input", + Value: "", + Usage: "input file path instead of image name", + } +) + +type ImageFlagGroup struct { + Input *Flag // local image archive + ScanRemovedPkgs *Flag +} + +type ImageOptions struct { + Input string + ScanRemovedPkgs bool +} + +func NewImageFlagGroup() *ImageFlagGroup { + return &ImageFlagGroup{ + Input: &InputFlag, + ScanRemovedPkgs: &ScanRemovedPkgsFlag, + } +} + +func (f *ImageFlagGroup) flags() []*Flag { + return []*Flag{f.Input, f.ScanRemovedPkgs} +} + +func (f *ImageFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *ImageFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *ImageFlagGroup) ToOptions() ImageOptions { + return ImageOptions{ + Input: getString(f.Input), + ScanRemovedPkgs: getBool(f.ScanRemovedPkgs), + } +} diff --git a/pkg/flag/kubernetes_flags.go b/pkg/flag/kubernetes_flags.go new file mode 100644 index 0000000000..0d9031f137 --- /dev/null +++ b/pkg/flag/kubernetes_flags.go @@ -0,0 +1,64 @@ +package flag + +import ( + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +var ( + ClusterContextFlag = Flag{ + Name: "context", + ConfigName: "kubernetes.context", + Value: "", + Usage: "specify a context to scan", + } + K8sNamespaceFlag = Flag{ + Name: "namespace", + ConfigName: "kubernetes.namespace", + Value: "", + Usage: "specify a namespace to sca", + } +) + +type K8sFlagGroup struct { + ClusterContext *Flag + Namespace *Flag +} + +type K8sOptions struct { + ClusterContext string + Namespace string +} + +func NewK8sFlagGroup() *K8sFlagGroup { + return &K8sFlagGroup{ + ClusterContext: lo.ToPtr(ClusterContextFlag), + Namespace: lo.ToPtr(K8sNamespaceFlag), + } +} + +func (f *K8sFlagGroup) flags() []*Flag { + return []*Flag{f.ClusterContext, f.Namespace} +} + +func (f *K8sFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *K8sFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *K8sFlagGroup) ToOptions() K8sOptions { + return K8sOptions{ + ClusterContext: getString(f.ClusterContext), + Namespace: getString(f.Namespace), + } +} diff --git a/pkg/flag/misconf_flags.go b/pkg/flag/misconf_flags.go new file mode 100644 index 0000000000..9e701fe413 --- /dev/null +++ b/pkg/flag/misconf_flags.go @@ -0,0 +1,130 @@ +package flag + +import ( + "github.com/samber/lo" + "github.com/spf13/cobra" + + "github.com/aquasecurity/trivy/pkg/log" +) + +// e.g. config yaml +// misconfiguration: +// trace: true +// config-policy: "custom-policy/policy" +// policy-namespaces: "user" +var ( + FilePatternsFlag = Flag{ + Name: "file-patterns", + ConfigName: "misconfiguration.file-patterns", + Value: []string{}, + Usage: "specify config file patterns, available with '--security-checks config'", + } + IncludeNonFailuresFlag = Flag{ + Name: "include-non-failures", + ConfigName: "misconfiguration.include-non-failures", + Value: false, + Usage: "include successes and exceptions, available with '--security-checks config'", + } + SkipPolicyUpdateFlag = Flag{ + Name: "skip-policy-update", + ConfigName: "misconfiguration.skip-policy-update", + Value: false, + Usage: "deprecated", + } + TraceFlag = Flag{ + Name: "trace", + ConfigName: "misconfiguration.trace", + Value: false, + Usage: "enable more verbose trace output for custom queries", + } + ConfigPolicyFlag = Flag{ + Name: "config-policy", + ConfigName: "misconfiguration.config-policy", + Value: []string{}, + Usage: "specify paths to the Rego policy files directory, applying config files", + } + ConfigDataFlag = Flag{ + Name: "config-data", + ConfigName: "misconfiguration.config-data", + Value: []string{}, + Usage: "specify paths from which data for the Rego policies will be recursively loaded", + } + PolicyNamespaceFlag = Flag{ + Name: "policy-namespaces", + ConfigName: "misconfiguration.policy-namespaces", + Value: []string{}, + Usage: "Rego namespaces", + } +) + +// MisconfFlagGroup composes common printer flag structs used for commands providing misconfinguration scanning. +type MisconfFlagGroup struct { + FilePatterns *Flag + IncludeNonFailures *Flag + SkipPolicyUpdate *Flag // deprecated + Trace *Flag + + // Rego + PolicyPaths *Flag + DataPaths *Flag + PolicyNamespaces *Flag +} + +type MisconfOptions struct { + FilePatterns []string + IncludeNonFailures bool + SkipPolicyUpdate bool // deprecated + Trace bool + + // Rego + PolicyPaths []string + DataPaths []string + PolicyNamespaces []string +} + +func NewMisconfFlagGroup() *MisconfFlagGroup { + return &MisconfFlagGroup{ + FilePatterns: lo.ToPtr(FilePatternsFlag), + IncludeNonFailures: lo.ToPtr(IncludeNonFailuresFlag), + SkipPolicyUpdate: lo.ToPtr(SkipPolicyUpdateFlag), + Trace: lo.ToPtr(TraceFlag), + PolicyPaths: lo.ToPtr(ConfigPolicyFlag), + DataPaths: lo.ToPtr(ConfigDataFlag), + PolicyNamespaces: lo.ToPtr(PolicyNamespaceFlag), + } +} + +func (f *MisconfFlagGroup) flags() []*Flag { + return []*Flag{f.FilePatterns, f.IncludeNonFailures, f.SkipPolicyUpdate, f.Trace, f.PolicyPaths, f.DataPaths, f.PolicyNamespaces} +} + +func (f *MisconfFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *MisconfFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *MisconfFlagGroup) ToOptions() (MisconfOptions, error) { + skipPolicyUpdateFlag := getBool(f.SkipPolicyUpdate) + if skipPolicyUpdateFlag { + log.Logger.Warn("'--skip-policy-update' is no longer necessary as the built-in policies are embedded into the binary") + } + return MisconfOptions{ + FilePatterns: getStringSlice(f.FilePatterns), + IncludeNonFailures: getBool(f.IncludeNonFailures), + Trace: getBool(f.Trace), + + PolicyPaths: getStringSlice(f.PolicyPaths), + DataPaths: getStringSlice(f.DataPaths), + PolicyNamespaces: getStringSlice(f.PolicyNamespaces), + }, nil +} diff --git a/pkg/flag/options.go b/pkg/flag/options.go new file mode 100644 index 0000000000..33955e783a --- /dev/null +++ b/pkg/flag/options.go @@ -0,0 +1,297 @@ +package flag + +import ( + "io" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +type Flag struct { + // Name is for CLI flag and environment variable. + // If this field is empty, it will be available only in config file. + Name string + + // ConfigName is a key in config file. It is also used as a key of viper. + ConfigName string + + // Shorthand is a shorthand letter. + Shorthand string + + // Value is the default value. It must be filled to determine the flag type. + Value interface{} + + // Usage explains how to use the flag. + Usage string + + // Persistent represents if the flag is persistent + Persistent bool +} + +type FlagGroup interface { + AddFlags(cmd *cobra.Command) + Bind(cmd *cobra.Command) error +} + +type Flags struct { + CacheFlagGroup *CacheFlagGroup + DBFlagGroup *DBFlagGroup + ImageFlagGroup *ImageFlagGroup + K8sFlagGroup *K8sFlagGroup + MisconfFlagGroup *MisconfFlagGroup + RemoteFlagGroup *RemoteFlagGroup + ReportFlagGroup *ReportFlagGroup + SBOMFlagGroup *SBOMFlagGroup + ScanFlagGroup *ScanFlagGroup +} + +// Options holds all the runtime configuration +type Options struct { + GlobalOptions + CacheOptions + DBOptions + ImageOptions + K8sOptions + MisconfOptions + RemoteOptions + ReportOptions + SBOMOptions + ScanOptions + + // Trivy's version, not populated via CLI flags + AppVersion string + + // We don't want to allow disabled analyzers to be passed by users, but it is necessary for internal use. + DisabledAnalyzers []analyzer.Type +} + +func addFlag(cmd *cobra.Command, flag *Flag) { + if flag == nil || flag.Name == "" { + return + } + switch v := flag.Value.(type) { + case int: + if flag.Persistent { + cmd.PersistentFlags().IntP(flag.Name, flag.Shorthand, v, flag.Usage) + } else { + cmd.Flags().IntP(flag.Name, flag.Shorthand, v, flag.Usage) + } + case string: + if flag.Persistent { + cmd.PersistentFlags().StringP(flag.Name, flag.Shorthand, v, flag.Usage) + } else { + cmd.Flags().StringP(flag.Name, flag.Shorthand, v, flag.Usage) + } + case []string: + if flag.Persistent { + cmd.PersistentFlags().StringSliceP(flag.Name, flag.Shorthand, v, flag.Usage) + } else { + cmd.Flags().StringSliceP(flag.Name, flag.Shorthand, v, flag.Usage) + } + case bool: + if flag.Persistent { + cmd.PersistentFlags().BoolP(flag.Name, flag.Shorthand, v, flag.Usage) + } else { + cmd.Flags().BoolP(flag.Name, flag.Shorthand, v, flag.Usage) + } + case time.Duration: + if flag.Persistent { + cmd.PersistentFlags().DurationP(flag.Name, flag.Shorthand, v, flag.Usage) + } else { + cmd.PersistentFlags().DurationP(flag.Name, flag.Shorthand, v, flag.Usage) + } + } +} + +func bind(cmd *cobra.Command, flag *Flag) error { + if flag == nil || flag.Name == "" { + return nil + } + if flag.Persistent { + if err := viper.BindPFlag(flag.ConfigName, cmd.PersistentFlags().Lookup(flag.Name)); err != nil { + return err + } + } else { + if err := viper.BindPFlag(flag.ConfigName, cmd.Flags().Lookup(flag.Name)); err != nil { + return err + } + } + // We don't use viper.AutomaticEnv, so we need to add a prefix manually here. + if err := viper.BindEnv(flag.ConfigName, strings.ToUpper("trivy_"+flag.Name)); err != nil { + return err + } + return nil +} + +func getString(flag *Flag) string { + if flag == nil { + return "" + } + return viper.GetString(flag.ConfigName) +} + +func getStringSlice(flag *Flag) []string { + if flag == nil { + return nil + } + return viper.GetStringSlice(flag.ConfigName) +} + +func getInt(flag *Flag) int { + if flag == nil { + return 0 + } + return viper.GetInt(flag.ConfigName) +} + +func getBool(flag *Flag) bool { + if flag == nil { + return false + } + return viper.GetBool(flag.ConfigName) +} + +func getDuration(flag *Flag) time.Duration { + if flag == nil { + return 0 + } + return viper.GetDuration(flag.ConfigName) +} + +func (f *Flags) groups() []FlagGroup { + var groups []FlagGroup + if f.CacheFlagGroup != nil { + groups = append(groups, f.CacheFlagGroup) + } + if f.DBFlagGroup != nil { + groups = append(groups, f.DBFlagGroup) + } + if f.ImageFlagGroup != nil { + groups = append(groups, f.ImageFlagGroup) + } + if f.K8sFlagGroup != nil { + groups = append(groups, f.K8sFlagGroup) + } + if f.MisconfFlagGroup != nil { + groups = append(groups, f.MisconfFlagGroup) + } + if f.RemoteFlagGroup != nil { + groups = append(groups, f.RemoteFlagGroup) + } + if f.ReportFlagGroup != nil { + groups = append(groups, f.ReportFlagGroup) + } + if f.SBOMFlagGroup != nil { + groups = append(groups, f.SBOMFlagGroup) + } + if f.ScanFlagGroup != nil { + groups = append(groups, f.ScanFlagGroup) + } + return groups +} + +func (f *Flags) AddFlags(cmd *cobra.Command) { + for _, group := range f.groups() { + if group == nil { + continue + } + group.AddFlags(cmd) + } + + cmd.Flags().SetNormalizeFunc(flagNameNormalize) +} + +func (f *Flags) Bind(cmd *cobra.Command) error { + for _, group := range f.groups() { + if group == nil { + continue + } + if err := group.Bind(cmd); err != nil { + return xerrors.Errorf("flag groups: %w", err) + } + } + return nil +} + +func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalFlagGroup, output io.Writer) (Options, error) { + var err error + opts := Options{ + AppVersion: appVersion, + GlobalOptions: globalFlags.ToOptions(), + } + + if f.CacheFlagGroup != nil { + opts.CacheOptions, err = f.CacheFlagGroup.ToOptions() + if err != nil { + return Options{}, xerrors.Errorf("cache flag error: %w", err) + } + } + + if f.DBFlagGroup != nil { + opts.DBOptions, err = f.DBFlagGroup.ToOptions() + if err != nil { + return Options{}, xerrors.Errorf("flag error: %w", err) + } + } + + if f.ImageFlagGroup != nil { + opts.ImageOptions = f.ImageFlagGroup.ToOptions() + } + + if f.K8sFlagGroup != nil { + opts.K8sOptions = f.K8sFlagGroup.ToOptions() + } + + if f.MisconfFlagGroup != nil { + opts.MisconfOptions, err = f.MisconfFlagGroup.ToOptions() + if err != nil { + return Options{}, xerrors.Errorf("misconfiguration flag error: %w", err) + } + } + + if f.RemoteFlagGroup != nil { + opts.RemoteOptions = f.RemoteFlagGroup.ToOptions() + } + + if f.ReportFlagGroup != nil { + opts.ReportOptions, err = f.ReportFlagGroup.ToOptions(output) + if err != nil { + return Options{}, xerrors.Errorf("report flag error: %w", err) + } + } + + if f.SBOMFlagGroup != nil { + opts.SBOMOptions, err = f.SBOMFlagGroup.ToOptions() + if err != nil { + return Options{}, xerrors.Errorf("sbom flag error: %w", err) + } + } + + if f.ScanFlagGroup != nil { + opts.ScanOptions = f.ScanFlagGroup.ToOptions(args) + } + + return opts, nil +} + +func flagNameNormalize(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "skip-update": + name = SkipDBUpdateFlag.Name + case "policy": + name = ConfigPolicyFlag.Name + case "data": + name = ConfigDataFlag.Name + case "namespaces": + name = PolicyNamespaceFlag.Name + case "ctx": + name = ClusterContextFlag.Name + } + return pflag.NormalizedName(name) +} diff --git a/pkg/flag/remote_flags.go b/pkg/flag/remote_flags.go new file mode 100644 index 0000000000..f96d5af55d --- /dev/null +++ b/pkg/flag/remote_flags.go @@ -0,0 +1,155 @@ +package flag + +import ( + "net/http" + "strings" + + "github.com/spf13/cobra" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + DefaultTokenHeader = "Trivy-Token" +) + +var ( + ServerTokenFlag = Flag{ + Name: "token", + ConfigName: "server.token", + Value: "", + Usage: "for authentication in client/server mode", + } + ServerTokenHeaderFlag = Flag{ + Name: "token-header", + ConfigName: "server.token-header", + Value: DefaultTokenHeader, + Usage: "specify a header name for token in client/server mode", + } + ServerAddrFlag = Flag{ + Name: "server", + ConfigName: "server.addr", + Value: "", + Usage: "server address in client mode", + } + ServerCustomHeadersFlag = Flag{ + Name: "custom-headers", + ConfigName: "server.custom-headers", + Value: []string{}, + Usage: "custom headers in client mode", + } + ServerListenFlag = Flag{ + Name: "listen", + ConfigName: "server.listen", + Value: "localhost:4954", + Usage: "listen address in server mode", + } +) + +// RemoteFlagGroup composes common printer flag structs +// used for commands requiring reporting logic. +type RemoteFlagGroup struct { + // for client/server + Token *Flag + TokenHeader *Flag + + // for client + ServerAddr *Flag + CustomHeaders *Flag + + // for server + Listen *Flag +} + +type RemoteOptions struct { + Token string + TokenHeader string + + ServerAddr string + Listen string + CustomHeaders http.Header +} + +func NewClientFlags() *RemoteFlagGroup { + return &RemoteFlagGroup{ + Token: &ServerTokenFlag, + TokenHeader: &ServerTokenHeaderFlag, + ServerAddr: &ServerAddrFlag, + CustomHeaders: &ServerCustomHeadersFlag, + } +} + +func NewServerFlags() *RemoteFlagGroup { + return &RemoteFlagGroup{ + Token: &ServerTokenFlag, + TokenHeader: &ServerTokenHeaderFlag, + Listen: &ServerListenFlag, + } +} + +func (f *RemoteFlagGroup) flags() []*Flag { + return []*Flag{f.Token, f.TokenHeader, f.ServerAddr, f.CustomHeaders, f.Listen} +} + +func (f *RemoteFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *RemoteFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *RemoteFlagGroup) ToOptions() RemoteOptions { + serverAddr := getString(f.ServerAddr) + customHeaders := splitCustomHeaders(getStringSlice(f.CustomHeaders)) + listen := getString(f.Listen) + token := getString(f.Token) + tokenHeader := getString(f.TokenHeader) + + if serverAddr == "" && listen == "" { + switch { + case len(customHeaders) > 0: + log.Logger.Warn(`"--custom-header" can be used only with "--server"`) + case token != "": + log.Logger.Warn(`"--token" can be used only with "--server"`) + case tokenHeader != "" && tokenHeader != DefaultTokenHeader: + log.Logger.Warn(`"--token-header" can be used only with "--server"`) + } + } + + if token == "" && tokenHeader != DefaultTokenHeader { + log.Logger.Warn(`"--token-header" should be used with "--token"`) + } + + if token != "" && tokenHeader != "" { + customHeaders.Set(tokenHeader, token) + } + + return RemoteOptions{ + Token: token, + TokenHeader: tokenHeader, + ServerAddr: serverAddr, + CustomHeaders: customHeaders, + Listen: listen, + } +} + +func splitCustomHeaders(headers []string) http.Header { + result := make(http.Header) + for _, header := range headers { + // e.g. x-api-token:XXX + s := strings.SplitN(header, ":", 2) + if len(s) != 2 { + continue + } + result.Set(s[0], s[1]) + } + return result +} diff --git a/pkg/flag/remote_flags_test.go b/pkg/flag/remote_flags_test.go new file mode 100644 index 0000000000..5c2eea6b76 --- /dev/null +++ b/pkg/flag/remote_flags_test.go @@ -0,0 +1,127 @@ +package flag_test + +import ( + "net/http" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestRemoteFlagGroup_ToOptions(t *testing.T) { + type fields struct { + Server string + CustomHeaders []string + Token string + TokenHeader string + } + tests := []struct { + name string + fields fields + want flag.RemoteOptions + wantLogs []string + }{ + { + name: "happy", + fields: fields{ + Server: "http://localhost:4954", + CustomHeaders: []string{ + "x-api-token:foo bar", + "Authorization:user:password", + }, + Token: "token", + TokenHeader: "Trivy-Token", + }, + want: flag.RemoteOptions{ + ServerAddr: "http://localhost:4954", + CustomHeaders: http.Header{ + "X-Api-Token": []string{"foo bar"}, + "Authorization": []string{"user:password"}, + "Trivy-Token": []string{"token"}, + }, + Token: "token", + TokenHeader: "Trivy-Token", + }, + }, + { + name: "custom headers and no server", + fields: fields{ + CustomHeaders: []string{ + "Authorization:user:password", + }, + TokenHeader: "Trivy-Token", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{ + "Authorization": []string{"user:password"}, + }, + TokenHeader: "Trivy-Token", + }, + wantLogs: []string{ + `"--custom-header" can be used only with "--server"`, + }, + }, + { + name: "token and no server", + fields: fields{ + Token: "token", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{}, + Token: "token", + }, + wantLogs: []string{ + `"--token" can be used only with "--server"`, + }, + }, + { + name: "token header and no token", + fields: fields{ + Server: "http://localhost:4954", + TokenHeader: "Non-Default", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{}, + ServerAddr: "http://localhost:4954", + TokenHeader: "Non-Default", + }, + wantLogs: []string{ + `"--token-header" should be used with "--token"`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := zap.WarnLevel + core, obs := observer.New(level) + log.Logger = zap.New(core).Sugar() + + viper.Set(flag.ServerAddrFlag.ConfigName, tt.fields.Server) + viper.Set(flag.ServerCustomHeadersFlag.ConfigName, tt.fields.CustomHeaders) + viper.Set(flag.ServerTokenFlag.ConfigName, tt.fields.Token) + viper.Set(flag.ServerTokenHeaderFlag.ConfigName, tt.fields.TokenHeader) + + // Assert options + f := &flag.RemoteFlagGroup{ + ServerAddr: &flag.ServerAddrFlag, + CustomHeaders: &flag.ServerCustomHeadersFlag, + Token: &flag.ServerTokenFlag, + TokenHeader: &flag.ServerTokenHeaderFlag, + } + got := f.ToOptions() + assert.Equalf(t, tt.want, got, "ToOptions()") + + // Assert log messages + var gotMessages []string + for _, entry := range obs.AllUntimed() { + gotMessages = append(gotMessages, entry.Message) + } + assert.Equal(t, tt.wantLogs, gotMessages, tt.name) + }) + } +} diff --git a/pkg/flag/report_flags.go b/pkg/flag/report_flags.go new file mode 100644 index 0000000000..aaf1a5857e --- /dev/null +++ b/pkg/flag/report_flags.go @@ -0,0 +1,251 @@ +package flag + +import ( + "io" + "os" + "strings" + + "github.com/samber/lo" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/result" +) + +// e.g. config yaml +// report: +// format: table +// dependency-tree: true +// exit-code: 1 +// severity: HIGH,CRITICAL +var ( + FormatFlag = Flag{ + Name: "format", + ConfigName: "format", + Shorthand: "f", + Value: report.FormatTable, + Usage: "format (table, json, sarif, template, cyclonedx, spdx, spdx-json, github)", + } + ReportFormatFlag = Flag{ + Name: "report", + ConfigName: "report", + Value: "all", + Usage: "specify a report format for the output. (all,summary)", + } + TemplateFlag = Flag{ + Name: "template", + ConfigName: "template", + Shorthand: "t", + Value: "", + Usage: "output template", + } + DependencyTreeFlag = Flag{ + Name: "dependency-tree", + ConfigName: "dependency-tree", + Value: false, + Usage: "show dependency origin tree (EXPERIMENTAL)", + } + ListAllPkgsFlag = Flag{ + Name: "list-all-pkgs", + ConfigName: "list-all-pkgs", + Value: false, + Usage: "enabling the option will output all packages regardless of vulnerability", + } + IgnoreFileFlag = Flag{ + Name: "ignorefile", + ConfigName: "ignorefile", + Value: result.DefaultIgnoreFile, + Usage: "specify .trivyignore file", + } + IgnorePolicyFlag = Flag{ + Name: "ignore-policy", + ConfigName: "ignore-policy", + Value: "", + Usage: "specify the Rego file path to evaluate each vulnerability", + } + ExitCodeFlag = Flag{ + Name: "exit-code", + ConfigName: "exit-code", + Value: 0, + Usage: "specify exit code when any security issues are found", + } + OutputFlag = Flag{ + Name: "output", + ConfigName: "output", + Shorthand: "o", + Value: "", + Usage: "output file name", + } + SeverityFlag = Flag{ + Name: "severity", + ConfigName: "severity", + Shorthand: "s", + Value: strings.Join(dbTypes.SeverityNames, ","), + Usage: "severities of security issues to be displayed (comma separated)", + } + + // Vulnerabilities + IgnoreUnfixedFlag = Flag{ + Name: "ignore-unfixed", + ConfigName: "vulnerability.ignore-unfixed", + Value: false, + Usage: "display only fixed vulnerabilities", + } +) + +// ReportFlagGroup composes common printer flag structs +// used for commands requiring reporting logic. +type ReportFlagGroup struct { + Format *Flag + ReportFormat *Flag + Template *Flag + DependencyTree *Flag + ListAllPkgs *Flag + IgnoreUnfixed *Flag + IgnoreFile *Flag + IgnorePolicy *Flag + ExitCode *Flag + Output *Flag + Severity *Flag +} + +type ReportOptions struct { + Format string + ReportFormat string + Template string + DependencyTree bool + ListAllPkgs bool + IgnoreUnfixed bool + IgnoreFile string + ExitCode int + IgnorePolicy string + Output io.Writer + Severities []dbTypes.Severity +} + +func NewReportFlagGroup() *ReportFlagGroup { + return &ReportFlagGroup{ + Format: lo.ToPtr(FormatFlag), + ReportFormat: lo.ToPtr(ReportFormatFlag), + Template: lo.ToPtr(TemplateFlag), + DependencyTree: lo.ToPtr(DependencyTreeFlag), + ListAllPkgs: lo.ToPtr(ListAllPkgsFlag), + IgnoreUnfixed: lo.ToPtr(IgnoreUnfixedFlag), + IgnoreFile: lo.ToPtr(IgnoreFileFlag), + IgnorePolicy: lo.ToPtr(IgnorePolicyFlag), + ExitCode: lo.ToPtr(ExitCodeFlag), + Output: lo.ToPtr(OutputFlag), + Severity: lo.ToPtr(SeverityFlag), + } +} + +func (f *ReportFlagGroup) flags() []*Flag { + return []*Flag{f.Format, f.ReportFormat, f.Template, f.DependencyTree, f.ListAllPkgs, f.IgnoreUnfixed, f.IgnoreFile, f.IgnorePolicy, + f.ExitCode, f.Output, f.Severity} +} + +func (f *ReportFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *ReportFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *ReportFlagGroup) ToOptions(out io.Writer) (ReportOptions, error) { + format := getString(f.Format) + template := getString(f.Template) + dependencyTree := getBool(f.DependencyTree) + listAllPkgs := getBool(f.ListAllPkgs) + output := getString(f.Output) + + if template != "" { + if format == "" { + log.Logger.Warn("'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.") + } else if format != "template" { + log.Logger.Warnf("'--template' is ignored because '--format %s' is specified. Use '--template' option with '--format template' option.", format) + } + } else { + if format == report.FormatTemplate { + log.Logger.Warn("'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.") + } + } + + // "--list-all-pkgs" option is unavailable with "--format table". + // If user specifies "--list-all-pkgs" with "--format table", we should warn it. + if listAllPkgs && format == report.FormatTable { + log.Logger.Warn(`"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`) + } + + // "--dependency-tree" option is available only with "--format table". + if dependencyTree && format != report.FormatTable { + log.Logger.Warn(`"--dependency-tree" can be used only with "--format table".`) + } + + // Enable '--list-all-pkgs' if needed + if f.forceListAllPkgs(format, listAllPkgs, dependencyTree) { + listAllPkgs = true + } + + if output != "" { + var err error + if out, err = os.Create(output); err != nil { + return ReportOptions{}, xerrors.Errorf("failed to create an output file: %w", err) + } + } + + return ReportOptions{ + Format: format, + ReportFormat: getString(f.ReportFormat), + Template: template, + DependencyTree: dependencyTree, + ListAllPkgs: listAllPkgs, + IgnoreUnfixed: getBool(f.IgnoreUnfixed), + IgnoreFile: getString(f.IgnoreFile), + ExitCode: getInt(f.ExitCode), + IgnorePolicy: getString(f.IgnorePolicy), + Output: out, + Severities: splitSeverity(getString(f.Severity)), + }, nil +} + +func (f *ReportFlagGroup) forceListAllPkgs(format string, listAllPkgs, dependencyTree bool) bool { + if slices.Contains(report.SupportedSBOMFormats, format) && !listAllPkgs { + log.Logger.Debugf("%q automatically enables '--list-all-pkgs'.", report.SupportedSBOMFormats) + return true + } + if dependencyTree && !listAllPkgs { + log.Logger.Debugf("'--dependency-tree' enables '--list-all-pkgs'.") + return true + } + return false +} + +func splitSeverity(severity string) []dbTypes.Severity { + if severity == "" { + return nil + } + + var severities []dbTypes.Severity + for _, s := range strings.Split(severity, ",") { + sev, err := dbTypes.NewSeverity(s) + if err != nil { + log.Logger.Warnf("unknown severity option: %s", err) + continue + } + severities = append(severities, sev) + } + log.Logger.Debugf("Severities: %q", severities) + return severities +} diff --git a/pkg/flag/report_flags_test.go b/pkg/flag/report_flags_test.go new file mode 100644 index 0000000000..13ffbf6fcf --- /dev/null +++ b/pkg/flag/report_flags_test.go @@ -0,0 +1,208 @@ +package flag_test + +import ( + "os" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report" +) + +func TestReportFlagGroup_ToOptions(t *testing.T) { + type fields struct { + format string + template string + dependencyTree bool + listAllPkgs bool + ignoreUnfixed bool + ignoreFile string + exitCode int + ignorePolicy string + output string + severities string + + debug bool + } + tests := []struct { + name string + fields fields + want flag.ReportOptions + wantLogs []string + }{ + { + name: "happy default (without flags)", + fields: fields{}, + want: flag.ReportOptions{ + Output: os.Stdout, + }, + }, + { + name: "happy path with an unknown severity", + fields: fields{ + severities: "CRITICAL,INVALID", + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + }, + }, + wantLogs: []string{ + "unknown severity option: unknown severity: INVALID", + }, + }, + { + name: "happy path with an cyclonedx", + fields: fields{ + severities: "CRITICAL", + format: "cyclonedx", + listAllPkgs: true, + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, + Format: report.FormatCycloneDX, + ListAllPkgs: true, + }, + }, + { + name: "happy path with an cyclonedx option list-all-pkgs is false", + fields: fields{ + severities: "CRITICAL", + format: "cyclonedx", + listAllPkgs: false, + + debug: true, + }, + wantLogs: []string{ + `["cyclonedx" "spdx" "spdx-json" "github"] automatically enables '--list-all-pkgs'.`, + `Severities: ["CRITICAL"]`, + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + }, + Format: report.FormatCycloneDX, + ListAllPkgs: true, + }, + }, + { + name: "invalid option combination: --template enabled without --format", + fields: fields{ + template: "@contrib/gitlab.tpl", + severities: "LOW", + }, + wantLogs: []string{ + "'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.", + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + Template: "@contrib/gitlab.tpl", + }, + }, + { + name: "invalid option combination: --template and --format json", + fields: fields{ + format: "json", + template: "@contrib/gitlab.tpl", + severities: "LOW", + }, + wantLogs: []string{ + "'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.", + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Format: "json", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + Template: "@contrib/gitlab.tpl", + }, + }, + { + name: "invalid option combination: --format template without --template", + fields: fields{ + format: "template", + severities: "LOW", + }, + wantLogs: []string{ + "'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.", + }, + want: flag.ReportOptions{ + Output: os.Stdout, + Format: "template", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + }, + }, + { + name: "invalid option combination: --list-all-pkgs with --format table", + fields: fields{ + format: "table", + severities: "LOW", + listAllPkgs: true, + }, + wantLogs: []string{ + `"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`, + }, + want: flag.ReportOptions{ + Format: "table", + Output: os.Stdout, + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + ListAllPkgs: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := zap.WarnLevel + if tt.fields.debug { + level = zap.DebugLevel + } + core, obs := observer.New(level) + log.Logger = zap.New(core).Sugar() + + viper.Set(flag.FormatFlag.ConfigName, tt.fields.format) + viper.Set(flag.TemplateFlag.ConfigName, tt.fields.template) + viper.Set(flag.DependencyTreeFlag.ConfigName, tt.fields.dependencyTree) + viper.Set(flag.ListAllPkgsFlag.ConfigName, tt.fields.listAllPkgs) + viper.Set(flag.IgnoreFileFlag.ConfigName, tt.fields.ignoreFile) + viper.Set(flag.IgnoreUnfixedFlag.ConfigName, tt.fields.ignoreUnfixed) + viper.Set(flag.IgnorePolicyFlag.ConfigName, tt.fields.ignorePolicy) + viper.Set(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode) + viper.Set(flag.OutputFlag.ConfigName, tt.fields.output) + viper.Set(flag.SeverityFlag.ConfigName, tt.fields.severities) + + // Assert options + f := &flag.ReportFlagGroup{ + Format: &flag.FormatFlag, + Template: &flag.TemplateFlag, + DependencyTree: &flag.DependencyTreeFlag, + ListAllPkgs: &flag.ListAllPkgsFlag, + IgnoreFile: &flag.IgnoreFileFlag, + IgnoreUnfixed: &flag.IgnoreUnfixedFlag, + IgnorePolicy: &flag.IgnorePolicyFlag, + ExitCode: &flag.ExitCodeFlag, + Output: &flag.OutputFlag, + Severity: &flag.SeverityFlag, + } + + got, err := f.ToOptions(os.Stdout) + assert.NoError(t, err) + assert.Equalf(t, tt.want, got, "ToOptions()") + + // Assert log messages + var gotMessages []string + for _, entry := range obs.AllUntimed() { + gotMessages = append(gotMessages, entry.Message) + } + assert.Equal(t, tt.wantLogs, gotMessages, tt.name) + }) + } +} diff --git a/pkg/flag/sbom_flags.go b/pkg/flag/sbom_flags.go new file mode 100644 index 0000000000..b8df102692 --- /dev/null +++ b/pkg/flag/sbom_flags.go @@ -0,0 +1,67 @@ +package flag + +import ( + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + ArtifactTypeFlag = Flag{ + Name: "artifact-type", + Value: "", + Usage: "deprecated", + } + SBOMFormatFlag = Flag{ + Name: "sbom-format", + Value: "", + Usage: "deprecated", + } +) + +type SBOMFlagGroup struct { + ArtifactType *Flag // deprecated + SBOMFormat *Flag // deprecated +} + +type SBOMOptions struct { + ArtifactType string // deprecated + SBOMFormat string // deprecated +} + +func NewSBOMFlagGroup() *SBOMFlagGroup { + return &SBOMFlagGroup{ + ArtifactType: &ArtifactTypeFlag, + SBOMFormat: &SBOMFormatFlag, + } +} + +func (f *SBOMFlagGroup) AddFlags(cmd *cobra.Command) { + if f.ArtifactType != nil { + cmd.Flags().String(ArtifactTypeFlag.Name, "", "deprecated") + cmd.Flags().MarkHidden(ArtifactTypeFlag.Name) // nolint: gosec + } + if f.SBOMFormat != nil { + cmd.Flags().String(SBOMFormatFlag.Name, "", "deprecated") + cmd.Flags().MarkHidden(SBOMFormatFlag.Name) // nolint: gosec + } +} + +func (f *SBOMFlagGroup) Bind(cmd *cobra.Command) error { + // All the flags are deprecated + return nil +} + +func (f *SBOMFlagGroup) ToOptions() (SBOMOptions, error) { + artifactType := getString(f.ArtifactType) + sbomFormat := getString(f.SBOMFormat) + + if artifactType != "" || sbomFormat != "" { + log.Logger.Error("'trivy sbom' is now for scanning SBOM. " + + "See https://github.com/aquasecurity/trivy/discussions/2407 for the detail") + return SBOMOptions{}, xerrors.New("'--artifact-type' and '--sbom-format' are no longer available") + } + + return SBOMOptions{}, nil +} diff --git a/pkg/flag/scan_flags.go b/pkg/flag/scan_flags.go new file mode 100644 index 0000000000..1574ec5092 --- /dev/null +++ b/pkg/flag/scan_flags.go @@ -0,0 +1,161 @@ +package flag + +import ( + "fmt" + "strings" + + "github.com/samber/lo" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + SkipDirsFlag = Flag{ + Name: "skip-dirs", + ConfigName: "scan.skip-dirs", + Value: "", + Usage: "specify the directories where the traversal is skipped", + } + SkipFilesFlag = Flag{ + Name: "skip-files", + ConfigName: "scan.skip-files", + Value: "", + Usage: "specify the file paths to skip traversal", + } + OfflineScanFlag = Flag{ + Name: "offline-scan", + ConfigName: "scan.offline", + Value: false, + Usage: "do not issue API requests to identify dependencies", + } + SecurityChecksFlag = Flag{ + Name: "security-checks", + ConfigName: "scan.security-checks", + Value: fmt.Sprintf("%s,%s", types.SecurityCheckVulnerability, types.SecurityCheckSecret), + Usage: "comma-separated list of what security issues to detect (vuln,config,secret)", + } + VulnTypeFlag = Flag{ + Name: "vuln-type", + ConfigName: "vulnerability.type", + Value: strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), + Usage: "comma-separated list of vulnerability types (os,library)", + } + SecretConfigFlag = Flag{ + Name: "secret-config", + ConfigName: "secret.config", + Value: "trivy-secret.yaml", + Usage: "specify a path to config file for secret scanning", + } +) + +type ScanFlagGroup struct { + SkipDirs *Flag + SkipFiles *Flag + OfflineScan *Flag + SecurityChecks *Flag + + VulnType *Flag + SecretConfig *Flag +} + +type ScanOptions struct { + Target string + SkipDirs []string + SkipFiles []string + OfflineScan bool + SecurityChecks []string + + // Vulnerabilities + VulnType []string + + // Secrets + SecretConfigPath string +} + +func NewScanFlagGroup() *ScanFlagGroup { + return &ScanFlagGroup{ + SkipDirs: lo.ToPtr(SkipDirsFlag), + SkipFiles: lo.ToPtr(SkipFilesFlag), + OfflineScan: lo.ToPtr(OfflineScanFlag), + SecurityChecks: lo.ToPtr(SecurityChecksFlag), + VulnType: lo.ToPtr(VulnTypeFlag), + SecretConfig: lo.ToPtr(SecretConfigFlag), + } +} + +func (f *ScanFlagGroup) flags() []*Flag { + return []*Flag{f.SkipDirs, f.SkipFiles, f.OfflineScan, f.SecurityChecks, f.VulnType, f.SecretConfig} +} + +func (f *ScanFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.flags() { + if err := bind(cmd, flag); err != nil { + return err + } + } + return nil +} + +func (f *ScanFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.flags() { + addFlag(cmd, flag) + } +} + +func (f *ScanFlagGroup) ToOptions(args []string) ScanOptions { + var target string + if len(args) == 1 { + target = args[0] + } + + return ScanOptions{ + Target: target, + SkipDirs: getStringSlice(f.SkipDirs), + SkipFiles: getStringSlice(f.SkipFiles), + OfflineScan: getBool(f.OfflineScan), + VulnType: parseVulnType(getStringSlice(f.VulnType)), + SecurityChecks: parseSecurityCheck(getStringSlice(f.SecurityChecks)), + SecretConfigPath: getString(f.SecretConfig), + } +} + +func parseVulnType(vulnType []string) []string { + switch { + case len(vulnType) == 0: // no types + return nil + case len(vulnType) == 1 && strings.Contains(vulnType[0], ","): // get checks from flag + vulnType = strings.Split(vulnType[0], ",") + } + + var vulnTypes []string + for _, v := range vulnType { + if !slices.Contains(types.VulnTypes, v) { + log.Logger.Warnf("unknown vulnerability type: %s", v) + continue + } + vulnTypes = append(vulnTypes, v) + } + return vulnTypes +} + +func parseSecurityCheck(securityCheck []string) []string { + switch { + case len(securityCheck) == 0: // no checks + return nil + case len(securityCheck) == 1 && strings.Contains(securityCheck[0], ","): // get checks from flag + securityCheck = strings.Split(securityCheck[0], ",") + } + + var securityChecks []string + for _, v := range securityCheck { + if !slices.Contains(types.SecurityChecks, v) { + log.Logger.Warnf("unknown security check: %s", v) + continue + } + securityChecks = append(securityChecks, v) + } + return securityChecks +} diff --git a/pkg/flag/scan_flags_test.go b/pkg/flag/scan_flags_test.go new file mode 100644 index 0000000000..17081efdaf --- /dev/null +++ b/pkg/flag/scan_flags_test.go @@ -0,0 +1,175 @@ +package flag_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanFlagGroup_ToOptions(t *testing.T) { + type fields struct { + skipDirs []string + skipFiles []string + offlineScan bool + vulnType string + securityChecks string + } + tests := []struct { + name string + args []string + fields fields + want flag.ScanOptions + wantLogs []string + }{ + { + name: "happy path", + args: []string{"alpine:latest"}, + fields: fields{}, + want: flag.ScanOptions{ + Target: "alpine:latest", + }, + }, + { + name: "happy path for OS vulnerabilities", + args: []string{"alpine:latest"}, + fields: fields{ + vulnType: "os", + securityChecks: "vuln", + }, + want: flag.ScanOptions{ + Target: "alpine:latest", + VulnType: []string{types.VulnTypeOS}, + SecurityChecks: []string{types.SecurityCheckVulnerability}, + }, + }, + { + name: "happy path for library vulnerabilities", + args: []string{"alpine:latest"}, + fields: fields{ + vulnType: "library", + securityChecks: "vuln", + }, + want: flag.ScanOptions{ + Target: "alpine:latest", + VulnType: []string{types.VulnTypeLibrary}, + SecurityChecks: []string{types.SecurityCheckVulnerability}, + }, + }, + { + name: "happy path for configs", + args: []string{"alpine:latest"}, + fields: fields{ + securityChecks: "config", + }, + want: flag.ScanOptions{ + Target: "alpine:latest", + SecurityChecks: []string{types.SecurityCheckConfig}, + }, + }, + { + name: "with wrong security check", + fields: fields{ + securityChecks: "vuln,WRONG-CHECK", + }, + want: flag.ScanOptions{ + SecurityChecks: []string{types.SecurityCheckVulnerability}, + }, + wantLogs: []string{ + `unknown security check: WRONG-CHECK`, + }, + }, + { + name: "with wrong vuln type", + fields: fields{ + vulnType: "os,nonevuln", + }, + want: flag.ScanOptions{ + VulnType: []string{types.VulnTypeOS}, + }, + wantLogs: []string{ + `unknown vulnerability type: nonevuln`, + }, + }, + { + name: "without target (args)", + args: []string{}, + fields: fields{}, + want: flag.ScanOptions{}, + }, + { + name: "with two or more targets (args)", + args: []string{"alpine:latest", "nginx:latest"}, + fields: fields{}, + want: flag.ScanOptions{}, + }, + { + name: "skip two files", + fields: fields{ + skipFiles: []string{"file1", "file2"}, + }, + want: flag.ScanOptions{ + SkipFiles: []string{"file1", "file2"}, + }, + }, + { + name: "skip two folders", + fields: fields{ + skipDirs: []string{"dir1", "dir2"}, + }, + want: flag.ScanOptions{ + SkipDirs: []string{"dir1", "dir2"}, + }, + }, + { + name: "offline scan", + fields: fields{ + offlineScan: true, + }, + want: flag.ScanOptions{ + OfflineScan: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := zap.WarnLevel + + core, obs := observer.New(level) + log.Logger = zap.New(core).Sugar() + + viper.Set(flag.SkipDirsFlag.ConfigName, tt.fields.skipDirs) + viper.Set(flag.SkipFilesFlag.ConfigName, tt.fields.skipFiles) + viper.Set(flag.OfflineScanFlag.ConfigName, tt.fields.offlineScan) + viper.Set(flag.VulnTypeFlag.ConfigName, tt.fields.vulnType) + viper.Set(flag.SecurityChecksFlag.ConfigName, tt.fields.securityChecks) + + // Assert options + f := &flag.ScanFlagGroup{ + SkipDirs: &flag.SkipDirsFlag, + SkipFiles: &flag.SkipFilesFlag, + OfflineScan: &flag.OfflineScanFlag, + VulnType: &flag.VulnTypeFlag, + SecurityChecks: &flag.SecurityChecksFlag, + } + + got := f.ToOptions(tt.args) + assert.Equalf(t, tt.want, got, "ToOptions()") + + // Assert log messages + var gotMessages []string + for _, entry := range obs.AllUntimed() { + gotMessages = append(gotMessages, entry.Message) + } + assert.Equal(t, tt.wantLogs, gotMessages, tt.name) + }) + + } +} diff --git a/pkg/k8s/commands/cluster.go b/pkg/k8s/commands/cluster.go index c3bc1780dc..06bc143efa 100644 --- a/pkg/k8s/commands/cluster.go +++ b/pkg/k8s/commands/cluster.go @@ -1,25 +1,26 @@ package commands import ( + "context" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" "github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s" - cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/flag" "github.com/aquasecurity/trivy/pkg/log" - "github.com/urfave/cli/v2" "golang.org/x/xerrors" ) // clusterRun runs scan on kubernetes cluster -func clusterRun(cliCtx *cli.Context, opt cmd.Option, cluster k8s.Cluster) error { - if err := validateReportArguments(cliCtx); err != nil { +func clusterRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) error { + if err := validateReportArguments(opts); err != nil { return err } - artifacts, err := trivyk8s.New(cluster, log.Logger).ListArtifacts(cliCtx.Context) + artifacts, err := trivyk8s.New(cluster, log.Logger).ListArtifacts(ctx) if err != nil { return xerrors.Errorf("get k8s artifacts error: %w", err) } - return run(cliCtx.Context, opt, cluster.GetCurrentContext(), artifacts) + return run(ctx, opts, cluster.GetCurrentContext(), artifacts) } diff --git a/pkg/k8s/commands/namespace.go b/pkg/k8s/commands/namespace.go index d7a36ae5c3..a967dc1261 100644 --- a/pkg/k8s/commands/namespace.go +++ b/pkg/k8s/commands/namespace.go @@ -1,35 +1,35 @@ package commands import ( - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" + "context" - cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" - "github.com/aquasecurity/trivy/pkg/log" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" "github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" ) // namespaceRun runs scan on kubernetes cluster -func namespaceRun(cliCtx *cli.Context, opt cmd.Option, cluster k8s.Cluster) error { - if err := validateReportArguments(cliCtx); err != nil { +func namespaceRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) error { + if err := validateReportArguments(opts); err != nil { return err } - trivyk8s := trivyk8s.New(cluster, log.Logger).Namespace(getNamespace(opt, cluster.GetCurrentNamespace())) + trivyk8s := trivyk8s.New(cluster, log.Logger).Namespace(getNamespace(opts, cluster.GetCurrentNamespace())) - artifacts, err := trivyk8s.ListArtifacts(cliCtx.Context) + artifacts, err := trivyk8s.ListArtifacts(ctx) if err != nil { return xerrors.Errorf("get k8s artifacts error: %w", err) } - return run(cliCtx.Context, opt, cluster.GetCurrentContext(), artifacts) + return run(ctx, opts, cluster.GetCurrentContext(), artifacts) } -func getNamespace(opt cmd.Option, currentNamespace string) string { - if len(opt.KubernetesOption.Namespace) > 0 { - return opt.KubernetesOption.Namespace +func getNamespace(opts flag.Options, currentNamespace string) string { + if len(opts.K8sOptions.Namespace) > 0 { + return opts.K8sOptions.Namespace } return currentNamespace diff --git a/pkg/k8s/commands/namespace_test.go b/pkg/k8s/commands/namespace_test.go index bd40b5b156..661360d700 100644 --- a/pkg/k8s/commands/namespace_test.go +++ b/pkg/k8s/commands/namespace_test.go @@ -3,9 +3,9 @@ package commands import ( "testing" - cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" - "github.com/aquasecurity/trivy/pkg/commands/option" - "gotest.tools/assert" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/flag" ) func Test_getNamespace(t *testing.T) { @@ -13,27 +13,35 @@ func Test_getNamespace(t *testing.T) { tests := []struct { name string currentNamespace string - opt cmd.Option - expected string + opts flag.Options + want string }{ { name: "--namespace=custom", currentNamespace: "default", - opt: cmd.Option{KubernetesOption: option.KubernetesOption{Namespace: "custom"}}, - expected: "custom", + opts: flag.Options{ + K8sOptions: flag.K8sOptions{ + Namespace: "custom", + }, + }, + want: "custom", }, { name: "no namespaces passed", currentNamespace: "default", - opt: cmd.Option{KubernetesOption: option.KubernetesOption{Namespace: ""}}, - expected: "default", + opts: flag.Options{ + K8sOptions: flag.K8sOptions{ + Namespace: "", + }, + }, + want: "default", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := getNamespace(test.opt, test.currentNamespace) - assert.Equal(t, test.expected, got) + got := getNamespace(test.opts, test.currentNamespace) + assert.Equal(t, test.want, got) }) } } diff --git a/pkg/k8s/commands/resource.go b/pkg/k8s/commands/resource.go index fd5d5f45bd..c727e7b3d4 100644 --- a/pkg/k8s/commands/resource.go +++ b/pkg/k8s/commands/resource.go @@ -1,47 +1,48 @@ package commands import ( + "context" "strings" - "github.com/urfave/cli/v2" + "github.com/aquasecurity/trivy/pkg/flag" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" "github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s" - cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/log" ) // resourceRun runs scan on kubernetes cluster -func resourceRun(cliCtx *cli.Context, opt cmd.Option, cluster k8s.Cluster) error { - kind, name, err := extractKindAndName(cliCtx.Args().Slice()) +func resourceRun(ctx context.Context, args []string, opts flag.Options, cluster k8s.Cluster) error { + kind, name, err := extractKindAndName(args) if err != nil { return err } - trivyk8s := trivyk8s.New(cluster, log.Logger).Namespace(getNamespace(opt, cluster.GetCurrentNamespace())) + trivyk8s := trivyk8s.New(cluster, log.Logger).Namespace(getNamespace(opts, cluster.GetCurrentNamespace())) if len(name) == 0 { // pods or configmaps etc - if err := validateReportArguments(cliCtx); err != nil { + if err = validateReportArguments(opts); err != nil { return err } - targets, err := trivyk8s.Resources(kind).ListArtifacts(cliCtx.Context) + targets, err := trivyk8s.Resources(kind).ListArtifacts(ctx) if err != nil { return err } - return run(cliCtx.Context, opt, cluster.GetCurrentContext(), targets) + return run(ctx, opts, cluster.GetCurrentContext(), targets) } // pod/NAME or pod NAME etc - artifact, err := trivyk8s.GetArtifact(cliCtx.Context, kind, name) + artifact, err := trivyk8s.GetArtifact(ctx, kind, name) if err != nil { return err } - return run(cliCtx.Context, opt, cluster.GetCurrentContext(), []*artifacts.Artifact{artifact}) + return run(ctx, opts, cluster.GetCurrentContext(), []*artifacts.Artifact{artifact}) } func extractKindAndName(args []string) (string, string, error) { diff --git a/pkg/k8s/commands/run.go b/pkg/k8s/commands/run.go index 01426a7e0e..49f92c8d7f 100644 --- a/pkg/k8s/commands/run.go +++ b/pkg/k8s/commands/run.go @@ -4,7 +4,10 @@ import ( "context" "errors" - "github.com/urfave/cli/v2" + "github.com/spf13/viper" + + "github.com/aquasecurity/trivy/pkg/flag" + "golang.org/x/xerrors" cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" @@ -22,29 +25,24 @@ const ( ) // Run runs a k8s scan -func Run(cliCtx *cli.Context) error { - opt, err := cmd.InitOption(cliCtx) - if err != nil { - return xerrors.Errorf("option error: %w", err) - } - - cluster, err := k8s.GetCluster(opt.KubernetesOption.ClusterContext) +func Run(ctx context.Context, args []string, opts flag.Options) error { + cluster, err := k8s.GetCluster(opts.K8sOptions.ClusterContext) if err != nil { return xerrors.Errorf("failed getting k8s cluster: %w", err) } - switch cliCtx.Args().Get(0) { + switch args[0] { case clusterArtifact: - return clusterRun(cliCtx, opt, cluster) + return clusterRun(ctx, opts, cluster) case allArtifact: - return namespaceRun(cliCtx, opt, cluster) + return namespaceRun(ctx, opts, cluster) default: // resourceArtifact - return resourceRun(cliCtx, opt, cluster) + return resourceRun(ctx, args, opts, cluster) } } -func run(ctx context.Context, opt cmd.Option, cluster string, artifacts []*artifacts.Artifact) error { - ctx, cancel := context.WithTimeout(ctx, opt.Timeout) +func run(ctx context.Context, opts flag.Options, cluster string, artifacts []*artifacts.Artifact) error { + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() var err error @@ -54,7 +52,7 @@ func run(ctx context.Context, opt cmd.Option, cluster string, artifacts []*artif } }() - runner, err := cmd.NewRunner(opt) + runner, err := cmd.NewRunner(ctx, opts) if err != nil { if errors.Is(err, cmd.SkipScan) { return nil @@ -67,22 +65,22 @@ func run(ctx context.Context, opt cmd.Option, cluster string, artifacts []*artif } }() - s := scanner.NewScanner(cluster, runner, opt) + s := scanner.NewScanner(cluster, runner, opts) r, err := s.Scan(ctx, artifacts) if err != nil { return xerrors.Errorf("k8s scan error: %w", err) } if err := report.Write(r, report.Option{ - Format: opt.Format, - Report: opt.KubernetesOption.ReportFormat, - Output: opt.Output, - Severities: opt.Severities, - }, opt.ReportOption.SecurityChecks); err != nil { + Format: opts.Format, + Report: opts.ReportFormat, + Output: opts.Output, + Severities: opts.Severities, + }, opts.ScanOptions.SecurityChecks); err != nil { return xerrors.Errorf("unable to write results: %w", err) } - cmd.Exit(opt, r.Failed()) + cmd.Exit(opts, r.Failed()) return nil } @@ -101,10 +99,10 @@ func run(ctx context.Context, opt cmd.Option, cluster string, artifacts []*artif // Single resource scanning is allowed with implicit "--report all". // // e.g. $ trivy k8s pod myapp -func validateReportArguments(cliCtx *cli.Context) error { - if cliCtx.String("report") == "all" && - !cliCtx.IsSet("report") && - cliCtx.String("format") == "table" { +func validateReportArguments(opts flag.Options) error { + if opts.ReportFormat == "all" && + !viper.IsSet("report") && + opts.Format == "table" { m := "All the results in the table format can mess up your terminal. Use \"--report all\" to tell Trivy to output it to your terminal anyway, or consider \"--report summary\" to show the summary output." diff --git a/pkg/k8s/report/report_test.go b/pkg/k8s/report/report_test.go index 15841872c2..08a13f3df4 100644 --- a/pkg/k8s/report/report_test.go +++ b/pkg/k8s/report/report_test.go @@ -3,10 +3,9 @@ package report import ( "testing" - "github.com/aquasecurity/trivy/pkg/commands/option" - "github.com/stretchr/testify/assert" + "github.com/aquasecurity/trivy/pkg/flag" "github.com/aquasecurity/trivy/pkg/types" ) @@ -214,42 +213,42 @@ func Test_separateMisConfigRoleAssessment(t *testing.T) { tests := []struct { name string k8sReport Report - rp option.ReportOption + opts flag.ScanOptions wantRbacReport Report wantMisConfigReport Report }{ { name: "Role and Deployment Reports", k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}}, - rp: option.ReportOption{SecurityChecks: []string{"config", "rbac"}}, + opts: flag.ScanOptions{SecurityChecks: []string{"config", "rbac"}}, wantRbacReport: Report{Misconfigurations: []Resource{{Kind: "Role"}}}, wantMisConfigReport: Report{Misconfigurations: []Resource{{Kind: "Deployment"}}}, }, { name: "Role Report Only", k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}}, - rp: option.ReportOption{SecurityChecks: []string{"rbac"}}, + opts: flag.ScanOptions{SecurityChecks: []string{"rbac"}}, wantRbacReport: Report{Misconfigurations: []Resource{{Kind: "Role"}}}, wantMisConfigReport: Report{Misconfigurations: []Resource{}}, }, { name: "Deployment Report Only", k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}}, - rp: option.ReportOption{SecurityChecks: []string{"config"}}, + opts: flag.ScanOptions{SecurityChecks: []string{"config"}}, wantRbacReport: Report{Misconfigurations: []Resource{}}, wantMisConfigReport: Report{Misconfigurations: []Resource{{Kind: "Deployment"}}}, }, { name: "No Deployment & No Role Reports", k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}}, - rp: option.ReportOption{SecurityChecks: []string{"vuln"}}, + opts: flag.ScanOptions{SecurityChecks: []string{"vuln"}}, wantRbacReport: Report{Misconfigurations: []Resource{}}, wantMisConfigReport: Report{Misconfigurations: []Resource{}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - misConfig, rbac := separateMisConfigRoleAssessment(tt.k8sReport, tt.rp.SecurityChecks) + misConfig, rbac := separateMisConfigRoleAssessment(tt.k8sReport, tt.opts.SecurityChecks) assert.Equal(t, len(tt.wantMisConfigReport.Misconfigurations), len(misConfig.Misconfigurations)) assert.Equal(t, len(tt.wantRbacReport.Misconfigurations), len(rbac.Misconfigurations)) }) diff --git a/pkg/k8s/report/summary_test.go b/pkg/k8s/report/summary_test.go index 911144bdca..41ecc24cf5 100644 --- a/pkg/k8s/report/summary_test.go +++ b/pkg/k8s/report/summary_test.go @@ -6,43 +6,46 @@ import ( "github.com/stretchr/testify/assert" - "github.com/aquasecurity/trivy/pkg/commands/option" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" ) func TestReport_ColumnHeading(t *testing.T) { tests := []struct { name string - rp option.ReportOption + opts flag.ScanOptions availableColumns []string want []string }{ { - name: "all workload columns", - rp: option.ReportOption{SecurityChecks: []string{"vuln", "config", "secret", "rbac"}}, + name: "all workload columns", + opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability, + types.SecurityCheckConfig, types.SecurityCheckSecret, types.SecurityCheckRbac}}, availableColumns: WorkloadColumns(), want: []string{NamespaceColumn, ResourceColumn, VulnerabilitiesColumn, MisconfigurationsColumn, SecretsColumn}, }, { - name: "all rbac columns", - rp: option.ReportOption{SecurityChecks: []string{"vuln", "config", "secret", "rbac"}}, + name: "all rbac columns", + opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability, + types.SecurityCheckConfig, types.SecurityCheckSecret, types.SecurityCheckRbac}}, availableColumns: RoleColumns(), want: []string{NamespaceColumn, ResourceColumn, RbacAssessmentColumn}, }, { name: "config column only", - rp: option.ReportOption{SecurityChecks: []string{"config"}}, + opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckConfig}}, availableColumns: WorkloadColumns(), want: []string{NamespaceColumn, ResourceColumn, MisconfigurationsColumn}, }, { name: "secret column only", - rp: option.ReportOption{SecurityChecks: []string{"secret"}}, + opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckSecret}}, availableColumns: WorkloadColumns(), want: []string{NamespaceColumn, ResourceColumn, SecretsColumn}, }, { name: "vuln column only", - rp: option.ReportOption{SecurityChecks: []string{"vuln"}}, + opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability}}, availableColumns: WorkloadColumns(), want: []string{NamespaceColumn, ResourceColumn, VulnerabilitiesColumn}, }, @@ -50,7 +53,7 @@ func TestReport_ColumnHeading(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - column := ColumnHeading(tt.rp.SecurityChecks, tt.availableColumns) + column := ColumnHeading(tt.opts.SecurityChecks, tt.availableColumns) if !assert.Equal(t, column, tt.want) { t.Error(fmt.Errorf("TestReport_ColumnHeading want %v got %v", tt.want, column)) } diff --git a/pkg/k8s/scanner/scanner.go b/pkg/k8s/scanner/scanner.go index 31246b99a9..c73a482043 100644 --- a/pkg/k8s/scanner/scanner.go +++ b/pkg/k8s/scanner/scanner.go @@ -8,28 +8,28 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/flag" "github.com/aquasecurity/trivy/pkg/k8s/report" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/types" - - "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" ) type Scanner struct { cluster string runner cmd.Runner - opt cmd.Option + opts flag.Options } -func NewScanner(cluster string, runner cmd.Runner, opt cmd.Option) *Scanner { - return &Scanner{cluster, runner, opt} +func NewScanner(cluster string, runner cmd.Runner, opts flag.Options) *Scanner { + return &Scanner{cluster, runner, opts} } func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (report.Report, error) { // progress bar bar := pb.StartNew(len(artifacts)) - if s.opt.NoProgress { + if s.opts.NoProgress { bar.SetWriter(io.Discard) } defer bar.Finish() @@ -37,7 +37,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re var vulns, misconfigs []report.Resource // disable logs before scanning - err := log.InitLogger(s.opt.Debug, true) + err := log.InitLogger(s.opts.Debug, true) if err != nil { return report.Report{}, xerrors.Errorf("logger error: %w", err) } @@ -46,7 +46,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re // to enable logs even when the function returns earlier // due to an error defer func() { - err = log.InitLogger(s.opt.Debug, false) + err = log.InitLogger(s.opts.Debug, false) if err != nil { // we use log.Fatal here because the error was to enable the logger log.Fatal(xerrors.Errorf("can't enable logger error: %w", err)) @@ -58,7 +58,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re for _, artifact := range artifacts { bar.Increment() - if slices.Contains(s.opt.SecurityChecks, types.SecurityCheckVulnerability) { + if slices.Contains(s.opts.SecurityChecks, types.SecurityCheckVulnerability) { resources, err := s.scanVulns(ctx, artifact) if err != nil { return report.Report{}, xerrors.Errorf("scanning vulnerabilities error: %w", err) @@ -66,7 +66,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re vulns = append(vulns, resources...) } - if s.shouldScanMisconfig(s.opt.SecurityChecks) { + if s.shouldScanMisconfig(s.opts.SecurityChecks) { resource, err := s.scanMisconfigs(ctx, artifact) if err != nil { return report.Report{}, xerrors.Errorf("scanning misconfigurations error: %w", err) @@ -92,9 +92,9 @@ func (s *Scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact) ( for _, image := range artifact.Images { - s.opt.Target = image + s.opts.Target = image - imageReport, err := s.runner.ScanImage(ctx, s.opt) + imageReport, err := s.runner.ScanImage(ctx, s.opts) if err != nil { log.Logger.Debugf("failed to scan image %s: %s", image, err) @@ -119,9 +119,9 @@ func (s *Scanner) scanMisconfigs(ctx context.Context, artifact *artifacts.Artifa return report.Resource{}, xerrors.Errorf("scan error: %w", err) } - s.opt.Target = configFile + s.opts.Target = configFile - configReport, err := s.runner.ScanFilesystem(ctx, s.opt) + configReport, err := s.runner.ScanFilesystem(ctx, s.opts) //remove config file after scanning removeFile(configFile) if err != nil { @@ -133,7 +133,7 @@ func (s *Scanner) scanMisconfigs(ctx context.Context, artifact *artifacts.Artifa } func (s *Scanner) filter(ctx context.Context, r types.Report, artifact *artifacts.Artifact) (report.Resource, error) { - r, err := s.runner.Filter(ctx, s.opt, r) + r, err := s.runner.Filter(ctx, s.opts, r) if err != nil { return report.Resource{}, xerrors.Errorf("filter error: %w", err) } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 058f9e98de..015d7926d6 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -280,6 +280,19 @@ func LoadAll() ([]Plugin, error) { return plugins, nil } +// RunWithArgs runs the plugin with arguments +func RunWithArgs(ctx context.Context, url string, args []string) error { + pl, err := Install(ctx, url, false) + if err != nil { + return xerrors.Errorf("plugin install error: %w", err) + } + + if err = pl.Run(ctx, args); err != nil { + return xerrors.Errorf("unable to run %s plugin: %w", pl.Name, err) + } + return nil +} + func loadMetadata(dir string) (Plugin, error) { filePath := filepath.Join(dir, configFile) f, err := os.Open(filePath) diff --git a/pkg/report/writer.go b/pkg/report/writer.go index 092a4023a3..ef4c5f8871 100644 --- a/pkg/report/writer.go +++ b/pkg/report/writer.go @@ -28,6 +28,10 @@ const ( FormatGitHub = "github" ) +var ( + SupportedSBOMFormats = []string{FormatCycloneDX, FormatSPDX, FormatSPDXJSON, FormatGitHub} +) + type Option struct { AppVersion string diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index 6aad2d1a62..a03971a7c6 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -97,12 +97,12 @@ func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { if bom.Metadata != nil { metadata.Timestamp = bom.Metadata.Timestamp if bom.Metadata.Component != nil { - metadata.Component = toTrivyCdxComponent(fromPtr(bom.Metadata.Component)) + metadata.Component = toTrivyCdxComponent(lo.FromPtr(bom.Metadata.Component)) } } var components []ftypes.Component - for _, c := range fromPtr(bom.Components) { + for _, c := range lo.FromPtr(bom.Components) { components = append(components, toTrivyCdxComponent(c)) } @@ -196,7 +196,7 @@ func (u *Unmarshaler) walkDependencies(rootRef string) []cdx.Component { func componentMap(metadata *cdx.Metadata, components *[]cdx.Component) map[string]cdx.Component { cmap := make(map[string]cdx.Component) - for _, component := range fromPtr(components) { + for _, component := range lo.FromPtr(components) { cmap[component.BOMRef] = component } if metadata != nil { @@ -208,13 +208,13 @@ func componentMap(metadata *cdx.Metadata, components *[]cdx.Component) map[strin func dependencyMap(deps *[]cdx.Dependency) map[string][]string { depMap := make(map[string][]string) - for _, dep := range fromPtr(deps) { + for _, dep := range lo.FromPtr(deps) { if _, ok := depMap[dep.Ref]; ok { continue } var refs []string - for _, d := range fromPtr(dep.Dependencies) { + for _, d := range lo.FromPtr(dep.Dependencies) { refs = append(refs, d.Ref) } @@ -270,11 +270,11 @@ func toPackage(component cdx.Component) (string, *ftypes.Package, error) { pkg := p.Package() pkg.Ref = component.BOMRef - for _, license := range fromPtr(component.Licenses) { + for _, license := range lo.FromPtr(component.Licenses) { pkg.Licenses = append(pkg.Licenses, license.Expression) } - for _, prop := range fromPtr(component.Properties) { + for _, prop := range lo.FromPtr(component.Properties) { if strings.HasPrefix(prop.Name, Namespace) { switch strings.TrimPrefix(prop.Name, Namespace) { case PropertySrcName: @@ -311,18 +311,10 @@ func toTrivyCdxComponent(component cdx.Component) ftypes.Component { } func lookupProperty(properties *[]cdx.Property, key string) string { - for _, p := range fromPtr(properties) { + for _, p := range lo.FromPtr(properties) { if p.Name == Namespace+key { return p.Value } } return "" } - -func fromPtr[T any](ptr *T) T { - if ptr == nil { - var t T - return t - } - return *ptr -}