diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e35c1f3175a..e6023593ce86 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,8 +7,11 @@ helm/trivy/ @krol3 # Misconfiguration scanning examples/misconf/ @owenrumney @liamg @knqyf263 docs/docs/misconfiguration @owenrumney @liamg @knqyf263 +docs/docs/cloud @owenrumney @liamg @knqyf263 pkg/fanal/analyzer/config @owenrumney @liamg @knqyf263 pkg/fanal/handler/misconf @owenrumney @liamg @knqyf263 +pkg/cloud @owenrumney @liamg @knqyf263 +pkg/flag @owenrumney @liamg @knqyf263 # Kubernetes scanning pkg/k8s/ @josedonizetti @chen-keinan @knqyf263 diff --git a/docs/docs/cloud/aws/scanning.md b/docs/docs/cloud/aws/scanning.md new file mode 100644 index 000000000000..8d178cb22cf1 --- /dev/null +++ b/docs/docs/cloud/aws/scanning.md @@ -0,0 +1,55 @@ +# Amazon Web Services + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +The Trivy AWS CLI allows you to scan your AWS account for misconfigurations. You can either run the CLI locally or integrate it into your CI/CD pipeline. + +Whilst you can already scan the infrastructure-as-code that defines your AWS resources with `trivy config`, you can now scan your live AWS account(s) directly too. + +The included checks cover all of the aspects of the [AWS CIS 1.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-cis.html) automated benchmarks. + +Trivy uses the same [authentication methods](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) as the AWS CLI to configure and authenticate your access to the AWS platform. + +You will need permissions configured to read all AWS resources - we recommend using a group/role with the `ReadOnlyAccess` and `SecurityAudit` policies attached. + +Once you've scanned your account, you can run additional commands to filter the results without having to run the entire scan again - results are cached locally per AWS account/region. + +## CLI Commands + +Scan a full AWS account (all supported services): + +```shell +trivy aws --region us-east-1 +``` + +You can allow Trivy to determine the AWS region etc. by using the standard AWS configuration files and environment variables. The `--region` flag overrides these. + +![AWS Summary Report](../../../imgs/trivy-aws.png) + +The summary view is the default when scanning multiple services. + +Scan a specific service: + +```shell +trivy aws --service s3 +``` + +Scan multiple services: + +```shell +# --service s3,ec2 works too +trivy aws --service s3 --service ec2 +``` + +Show results for a specific AWS resource: + +```shell +trivy aws --service s3 --arn arn:aws:s3:::example-bucket +``` + +All ARNs with detected issues will be displayed when showing results for their associated service. + +## Cached Results + +By default, Trivy will cache results for each service for 24 hours. This means you can filter and view results for a service without having to wait for the scan to run again. If you want to force the cache to be refreshed with the latest data, you can use `--update-cache`. Or if you'd like to use cached data for a different timeframe, you can specify `--max-cache-age` (e.g. `--max-cache-age 2h`.) diff --git a/docs/docs/references/customization/config-file.md b/docs/docs/references/customization/config-file.md index febd9fcd6502..e8943f899a64 100644 --- a/docs/docs/references/customization/config-file.md +++ b/docs/docs/references/customization/config-file.md @@ -6,7 +6,7 @@ An example is [here][example]. ## Global Options -``` +```yaml # Same as '--quiet' # Default is false quiet: false @@ -30,7 +30,7 @@ cache-dir: $HOME/.cache/trivy ## Report Options -``` +```yaml # Same as '--format' # Default is 'table' format: table @@ -80,7 +80,7 @@ severity: ## Scan Options Available in client/server mode -``` +```yaml scan: # Same as '--skip-dirs' # Default is empty @@ -107,7 +107,7 @@ scan: ## Cache Options -``` +```yaml cache: # Same as '--cache-backend' # Default is 'fs' @@ -134,7 +134,7 @@ cache: ## DB Options -``` +```yaml db: # Same as '--skip-db-update' # Default is false @@ -152,7 +152,7 @@ db: ## Image Options Available with container image scanning -``` +```yaml image: # Same as '--input' (available with 'trivy image') # Default is empty @@ -166,7 +166,7 @@ image: ## Vulnerability Options Available with vulnerability scanning -``` +```yaml vulnerability: # Same as '--vuln-type' # Default is 'os,library' @@ -182,7 +182,7 @@ vulnerability: ## Secret Options Available with secret scanning -``` +```yaml secret: # Same as '--secret-config' # Default is 'trivy-secret.yaml' @@ -193,7 +193,7 @@ secret: ## Misconfiguration Options Available with misconfiguration scanning -``` +```yaml misconfiguration: # Same as '--file-patterns' # Default is empty @@ -256,7 +256,7 @@ misconfiguration: ## Kubernetes Options Available with Kubernetes scanning -``` +```yaml kubernetes: # Same as '--context' # Default is empty @@ -270,7 +270,7 @@ kubernetes: ## Repository Options Available with git repository scanning (`trivy repo`) -``` +```yaml repository: # Same as '--branch' # Default is empty @@ -288,7 +288,7 @@ repository: ## Client/Server Options Available in client/server mode -``` +```yaml server: # Same as '--server' (available in client mode) # Default is empty @@ -313,4 +313,28 @@ server: listen: 0.0.0.0:10000 ``` +## Cloud Options + +Available for cloud scanning (currently only `trivy aws`) + +```yaml +cloud: + # whether to force a cache update for every scan + update-cache: false + + # how old cached results can be before being invalidated + max-cache-age: 24h + + # aws-specific cloud settings + aws: + # the aws region to use + region: us-east-1 + + # the aws endpoint to use (not required for general use) + endpoint: https://my.custom.aws.endpoint + + # the aws account to use (this will be determined from your environment when not set) + account: 123456789012 +``` + [example]: https://github.com/aquasecurity/trivy/tree/{{ git.tag }}/examples/trivy-conf/trivy.yaml diff --git a/docs/imgs/trivy-aws.png b/docs/imgs/trivy-aws.png new file mode 100644 index 000000000000..5e748fea5757 Binary files /dev/null and b/docs/imgs/trivy-aws.png differ diff --git a/go.mod b/go.mod index 62d2d59f6e31..0d736857e3ee 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,9 @@ require ( github.com/aquasecurity/testdocker v0.0.0-20210911155206-e1e85f5a1516 github.com/aquasecurity/trivy-db v0.0.0-20220627104749-930461748b63 github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220727123250-2cfd49c5b6c3 + github.com/aws/aws-sdk-go-v2 v1.16.8 + github.com/aws/aws-sdk-go-v2/config v1.15.15 + github.com/aws/aws-sdk-go-v2/service/sts v1.16.10 github.com/caarlos0/env/v6 v6.9.3 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cheggaaa/pb/v3 v3.1.0 @@ -38,6 +41,7 @@ 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/loading v0.0.4 github.com/liamg/memoryfs v1.4.2 github.com/liamg/tml v0.6.0 github.com/mailru/easyjson v0.7.7 @@ -67,8 +71,58 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.12.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.11 // indirect + github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.9 // indirect + github.com/aws/aws-sdk-go-v2/service/athena v1.18.1 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.5 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.15.11 // indirect + github.com/aws/aws-sdk-go-v2/service/codebuild v1.19.9 // indirect + github.com/aws/aws-sdk-go-v2/service/docdb v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.10 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.17.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/efs v1.17.7 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticache v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.9 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.16.1 // indirect + github.com/aws/aws-sdk-go-v2/service/emr v1.20.2 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.18.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/kafka v1.17.10 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.15.10 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.18.1 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.23.5 // indirect + github.com/aws/aws-sdk-go-v2/service/mq v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/neptune v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.23.2 // indirect + github.com/aws/aws-sdk-go-v2/service/redshift v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.17.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.11.13 // indirect + github.com/aws/aws-sdk-go-v2/service/workspaces v1.22.0 // indirect + github.com/aws/smithy-go v1.12.0 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect + gonum.org/v1/gonum v0.7.0 // indirect ) require ( @@ -92,7 +146,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/squirrel v1.5.2 // indirect + github.com/Masterminds/squirrel v1.5.3 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/hcsshim v0.9.3 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect @@ -107,7 +161,7 @@ require ( github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/aquasecurity/defsec v0.70.0 + github.com/aquasecurity/defsec v0.71.5 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/aws/aws-sdk-go v1.44.66 github.com/beorn7/perks v1.0.1 // indirect @@ -173,7 +227,6 @@ 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.13.0 // indirect github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b // indirect github.com/huandu/xstrings v1.3.2 // indirect @@ -182,7 +235,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jdkato/prose v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jmoiron/sqlx v1.3.4 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect @@ -194,7 +247,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/liamg/iamgo v0.0.9 // indirect github.com/liamg/jfather v0.0.7 // indirect - github.com/lib/pq v1.10.4 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect @@ -241,7 +294,6 @@ 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/shogo82148/go-shuffle v0.0.0-20170808115208-59829097ff3b // indirect @@ -274,11 +326,10 @@ require ( 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-20220715151400-c0bba94af5f8 // indirect - golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 golang.org/x/text v0.3.7 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect - gonum.org/v1/gonum v0.7.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 @@ -292,11 +343,11 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.2.0 // indirect - helm.sh/helm/v3 v3.9.0 // indirect + helm.sh/helm/v3 v3.9.2 // indirect k8s.io/api v0.25.0-alpha.2 // indirect - k8s.io/apiextensions-apiserver v0.24.0 // indirect + k8s.io/apiextensions-apiserver v0.24.2 // indirect k8s.io/apimachinery v0.25.0-alpha.2 // indirect - k8s.io/apiserver v0.24.1 // indirect + k8s.io/apiserver v0.24.2 // indirect k8s.io/cli-runtime v0.24.3 // indirect k8s.io/client-go v0.25.0-alpha.2 // indirect k8s.io/component-base v0.24.3 // indirect diff --git a/go.sum b/go.sum index a492a50cad1b..4c41dfe2901e 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE= -github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -204,12 +204,13 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM= github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= -github.com/aquasecurity/defsec v0.70.0 h1:tzmKrnR/OssRC/0RwnmmPwnoWOCOY7rPc+3ZaqLumg0= -github.com/aquasecurity/defsec v0.70.0/go.mod h1:ZMuvHCXmvdL6EM3ckt/qY/qIJ6WEr5GNeGhNDFgVrcw= +github.com/aquasecurity/defsec v0.71.5 h1:HOao1TaP74lhbsLUmYaNgHx1afdYImDicB8b/f54FIM= +github.com/aquasecurity/defsec v0.71.5/go.mod h1:+ouYrROGLz3lGutl+K+ilXX5V41S76JIi+L8aXPBsAQ= github.com/aquasecurity/go-dep-parser v0.0.0-20220807122629-b5a21d267b03 h1:Axx5KwV0c83IlPLIIsi/Ht6sGsSJBzABUngXjFHFg4I= github.com/aquasecurity/go-dep-parser v0.0.0-20220807122629-b5a21d267b03/go.mod h1:SONYN1M+sYu6VIJsZnltmVfcGOCvp09HWbhpnHDn3aY= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= +github.com/aquasecurity/go-mock-aws v0.0.0-20220726154943-99847deb62b0 h1:tihCUjLWkF0b1SAjAKcFltUs3SpsqGrLtI+Frye0D10= github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 h1:eveqE9ivrt30CJ7dOajOfBavhZ4zPqHcZe/4tKp0alc= github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798/go.mod h1:hxbJZtKlO4P8sZ9nztizR6XLoE33O+BkPmuYQ4ACyz0= github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4= @@ -239,6 +240,106 @@ github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3A github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.66 h1:xdH4EvHyUnkm4I8d536ui7yMQKYzrkbSDQ2LvRRHqsg= github.com/aws/aws-sdk-go v1.44.66/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.16.8 h1:gOe9UPR98XSf7oEJCcojYg+N2/jCRm4DdeIsP85pIyQ= +github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y= +github.com/aws/aws-sdk-go-v2/config v1.15.15 h1:yBV+J7Au5KZwOIrIYhYkTGJbifZPCkAnCFSvGsF3ui8= +github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E= +github.com/aws/aws-sdk-go-v2/credentials v1.12.10 h1:7gGcMQePejwiKoDWjB9cWnpfVdnz/e5JwJFuT6OrroI= +github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9 h1:hz8tc+OW17YqxyFFPSkvfSikbqWcyyHRyPVSTzC0+aI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15 h1:bx5F2mr6H6FC7zNIQoDoUr8wEKnvmwRncujT3FYRtic= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9 h1:5sbyznZC2TeFpa4fvtpvpcGbzeXEEs1l1Jo51ynUNsQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9/go.mod h1:08tUpeSGN33QKSO7fwxXczNfiwCpbj+GxK6XKwqWVv0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16 h1:f0ySVcmQhwmzn7zQozd8wBM3yuGBfzdpsOaKQ0/Epzw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16/go.mod h1:CYmI+7x03jjJih8kBEEFKRQc40UjUokT0k7GbvrhhTc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6 h1:3L8pcjvgaSOs0zzZcMKzxDSkYKEpwJ2dNVDdxm68jAY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6/go.mod h1:O7Oc4peGZDEKlddivslfYFvAbgzvl/GH3J8j3JIGBXc= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.11 h1:dLu3dF3ruiSZsG+in4ZzZWL3F7w4TeOX/F257qE2mT0= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.11/go.mod h1:Hb+D/fjqxVd1jAkIjTZF8Cg540F3E4YK5Uu4unA3rS0= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.9 h1:MkWoCyvIqAhaMO+LTSFag8s0wd6zV6Pd+X0urDKn2I8= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.9/go.mod h1:Va1mvuuqN0pejuszzc1nMPAsqGbIqIxBowdXzPYR9Gw= +github.com/aws/aws-sdk-go-v2/service/athena v1.18.1 h1:RzNtlZanMLTYe3dcq7cZEEv40YvHY6hYylHz32jwEbk= +github.com/aws/aws-sdk-go-v2/service/athena v1.18.1/go.mod h1:JBXnq5zXBUeQo+bbMrsg1Fx3+7+vxxwYLB+EDJiLP94= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.5 h1:MWmwy+Py1HXLNILagezUP9JPEV4CS33tU8xTJR65vMY= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.5/go.mod h1:xi7heuDU7iKWmWhvGCpsEvBko0NylAm4cmiJoxJKv9w= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.5 h1:dxrJ5ki6GuqZB9AqbE6HsqT8mrLcI2E+POgYt98YWTs= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.5/go.mod h1:55qJ5OVAwXAGgoBu9bPqoFlUj0iExM6UgvxiCqrHgYU= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.19.1 h1:JvOaYDuqyFn5JYggztv688+7eRMVtNp81vQ+F6OrBIw= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.19.1/go.mod h1:ZmYbhXLOStOS1+PItLyb9BNm8QtAQWkT5Nbd/tT19c4= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.15.11 h1:d9d/Vg1zkmo4OY0tWDywu5je9fXS4KXL5bW2T8wJ1cU= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.15.11/go.mod h1:0vT2mfhUL63/UT1RvYF/1wuqvvuvY0e+CiLB1paT+qI= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.19.9 h1:SBw4owb6Y9cKOmY0Z8PnY75PeceVYxnIgXNkuT3XGRU= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.19.9/go.mod h1:cQpAzFHSPsL/an19DbTTRb7kvuzMq8EcCX3WGO3+P0I= +github.com/aws/aws-sdk-go-v2/service/docdb v1.19.1 h1:5EL1Sx9cwNXiX5z3gC6lbm/YyleuCwcssiOMi4zg7PI= +github.com/aws/aws-sdk-go-v2/service/docdb v1.19.1/go.mod h1:gBnPk1RQP1qnmscOIiezJRsaQDrT6SDG3OwUmx6IA6c= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.10 h1:GLklbtMUQCToju09LyT+AjbwTQ0KCQudNLTA0H2xbBk= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.10/go.mod h1:zM5dQf0mZfcW4s8OsJFXvzedbY5n1rO581X4xei6XcA= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1 h1:y88XFO3AJWDVJ3HjcYc+Oo38fB948armdg6ulfphkUM= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1/go.mod h1:bKs78Qpk4syfUFXKhA0hIqT3X0sxmvIAPlEHV4qVbP0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.17.9 h1:9nU17hDiQCBptGMuCnx6UbN/RUGEDV+YOM+6W8i8zII= +github.com/aws/aws-sdk-go-v2/service/ecr v1.17.9/go.mod h1:fkIc4qe3SfQhPt/HAmDG7DJMjMBHElHV44axRyUSojA= +github.com/aws/aws-sdk-go-v2/service/ecs v1.18.12 h1:PWpVksq9WWpOM7SiWD4gaiPDwUm8K/rn4nxQkdkYRtw= +github.com/aws/aws-sdk-go-v2/service/ecs v1.18.12/go.mod h1:h1UvIIC+fPNj4PkuQ/o9QyRH0/vC+qlHRNGefwwYzv8= +github.com/aws/aws-sdk-go-v2/service/efs v1.17.7 h1:FfmUBdGQ5tuFIIIwjmvy/DeGvvW0myQVFToQjPjjtEQ= +github.com/aws/aws-sdk-go-v2/service/efs v1.17.7/go.mod h1:cCrmFuFfPmhBtdw5YD3IzqtrpytrOYDDNhIMwuNrXTU= +github.com/aws/aws-sdk-go-v2/service/eks v1.21.5 h1:miWUBz+htptzay+IZl70zYkTlO1FD7JIypv1D+8+rm0= +github.com/aws/aws-sdk-go-v2/service/eks v1.21.5/go.mod h1:t2jyBeR+NLVCfPHpqT/1aygIu9yrW29JZREUJjgxnWg= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.22.1 h1:ctpT3Cl9LCSnzfDsulH5kECwXLL0jMXAnjukWeIdSZ4= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.22.1/go.mod h1:1Yuus60M9YJNgRxEYkfcAZs8NIyK2QAutQX2uYFbA+s= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.9 h1:ce76ovZsRsjqBEUHw/6sK1u3lMzrCi253ba1vaqBujQ= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.9/go.mod h1:HCDI4POpmQJpQK4UaQMDEHd3FsqfdzV8YGCwpznWhak= +github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.16.1 h1:x6/McT+Lxlr1hcADHu3dFzG2jRZope4BeBNTaCF2kYM= +github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.16.1/go.mod h1:A4rBOsc7JmoqJI0QlhMVmaeBA1gY504A9Pt/Z1vVDPc= +github.com/aws/aws-sdk-go-v2/service/emr v1.20.2 h1:G66jwQlixBxtbxUh5AxRfeNFrA9FvjtbvxyGl9xY8gw= +github.com/aws/aws-sdk-go-v2/service/emr v1.20.2/go.mod h1:FFLSJvJVSw9px5ZHi5KRq/JNOBu1d9n95V40SD/QWfs= +github.com/aws/aws-sdk-go-v2/service/iam v1.18.10 h1:lB6TiFIJR0sZNWC2rGZ9+7OMtGpUEh/u/wYAn6HfbKk= +github.com/aws/aws-sdk-go-v2/service/iam v1.18.10/go.mod h1:fhDORN+qPbMYyu98/RaDDiV60LXb9gvJ5UNZXY2hBNs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10 h1:7LJcuRalaLw+GYQTMGmVUl4opg2HrDZkvn/L3KvIQfw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10/go.mod h1:Qks+dxK3O+Z2deAhNo6cJ8ls1bam3tUGUAcgxQP1c70= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.9 h1:COsLtfmOSgPGnKUreE99/5pIgtmGLzmLtVrQa12QzU4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.9/go.mod h1:IixPDVckNk0HhYDQwUmTonTAfQlfABg9E72whAbq5k0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9 h1:sHfDuhbOuuWSIAEDd3pma6p0JgUcR2iePxtCE8gfCxQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9/go.mod h1:yQowTpvdZkFVuHrLBXmczat4W+WJKg/PafBZnGBLga0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9 h1:sJdKvydGYDML9LTFcp6qq6Z5fIjN0Rdq2Gvw1hUg8tc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9/go.mod h1:Rc5+wn2k8gFSi3V1Ch4mhxOzjMh+bYSXVFfVaqowQOY= +github.com/aws/aws-sdk-go-v2/service/kafka v1.17.10 h1:ZsFXMWeNEkUjLEuVZY0jZb1uvAcDIYX67BI16ISG8LE= +github.com/aws/aws-sdk-go-v2/service/kafka v1.17.10/go.mod h1:j3dSazeOhP6nWt7C3FAnYAwEGhYeLfneaapKIFJSlPk= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.15.10 h1:MKiqeOllGwLLP3PawduTfkQqPavNtGrSG9J9gahaSwA= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.15.10/go.mod h1:0Nz7L2pwh2bOumoDyt5oWFaC+qqw7BCzM46wxwR68O4= +github.com/aws/aws-sdk-go-v2/service/kms v1.18.1 h1:y07kzPdcjuuyDVYWf1CCsQQ6kcAWMbFy+yIJ71xQBS0= +github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM= +github.com/aws/aws-sdk-go-v2/service/lambda v1.23.5 h1:/tq5WZODNF3juZkpTIIMfzeJx6c8kLk73SjTTvOAphY= +github.com/aws/aws-sdk-go-v2/service/lambda v1.23.5/go.mod h1:7YjiELsNgxpiMMG2KapRbAnOF1O+e1UnoLwARPNHKYc= +github.com/aws/aws-sdk-go-v2/service/mq v1.13.5 h1:ztNwJLLJxGWc140Ixh+5316UxJd2N4sSCViA6lT1UUk= +github.com/aws/aws-sdk-go-v2/service/mq v1.13.5/go.mod h1:Ap0H9UgOdD2eP1CEFGA50iIQFpJ/qxXogr4UDSozjTA= +github.com/aws/aws-sdk-go-v2/service/neptune v1.17.3 h1:w3a/x4gSzMcHcS/ZiflrX+PygI9xr7T8po4uU3jPcGQ= +github.com/aws/aws-sdk-go-v2/service/neptune v1.17.3/go.mod h1:yIMXrISmxkkek9J7e61+c1gP2PwJk2hFjyxBQ+mgaG4= +github.com/aws/aws-sdk-go-v2/service/rds v1.23.2 h1:PiW9+dKNwnRCfpln8UukyBBOHhOGfS4NV0qkZQg+uPM= +github.com/aws/aws-sdk-go-v2/service/rds v1.23.2/go.mod h1:OiFKbn0c0/8hLpOLFg4P8Pw9bofLnuweWWqZPY7chBM= +github.com/aws/aws-sdk-go-v2/service/redshift v1.26.1 h1:PXlUX4ErwlY1u7lZoMt3fuWSWebdSLMxsBDd0DqnpiA= +github.com/aws/aws-sdk-go-v2/service/redshift v1.26.1/go.mod h1:XTvP5x9LIIgImxvUtXUHXdi3R56P+8BsSI7UeXCPz2U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2 h1:NvzGue25jKnuAsh6yQ+TZ4ResMcnp49AWgWGm2L4b5o= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14 h1:dvvIB9OYsOH10RUNAY7yiCq5fQwGebXx1auBOkBTUlg= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.10 h1:ZZuqucIwjbUEJqxxR++VDZX9BcMbX5ZcQaKoWul/ELk= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM= +github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1 h1:HaQD4g8eumwEW218TgQzhnwTXmq77ZogA67SxBnGyPc= +github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.13 h1:DQpf+al+aWozOEmVEdml67qkVZ6vdtGUi71BZZWw40k= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.10 h1:7tquJrhjYz2EsCBvA9VTl+sBAAh1bv7h/sGASdZOGGo= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E= +github.com/aws/aws-sdk-go-v2/service/workspaces v1.22.0 h1:6CPEYECdt2tRdtGObCxYN+NXFc46vC0tYpwY4mf2tS4= +github.com/aws/aws-sdk-go-v2/service/workspaces v1.22.0/go.mod h1:ziCHySWl+3sgDxO+9lXeXZOmKtiUqXf1RPqcbYDlsb8= +github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0= +github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -502,8 +603,8 @@ github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2 h1:lx1ZQgST/imDh github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2/go.mod h1:hgHYKsoIw7S/hlWtP7wD1wZ7SX1jPTtKko5X9jrOgPQ= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DBZ2sN7CK6dgvHVpQsQj4sRMCbWTmd17l+5SUCjnQSY= github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= @@ -940,8 +1041,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= -github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -1019,14 +1120,16 @@ github.com/liamg/iamgo v0.0.9 h1:tADGm3xVotyRJmuKKaH4+zsBn7LOcvgdpuF3WsSKW3c= github.com/liamg/iamgo v0.0.9/go.mod h1:Kk6ZxBF/GQqG9nnaUjIi6jf+WXNpeOTyhwc6gnguaZQ= github.com/liamg/jfather v0.0.7 h1:Xf78zS263yfT+xr2VSo6+kyAy4ROlCacRqJG7s5jt4k= github.com/liamg/jfather v0.0.7/go.mod h1:xXBGiBoiZ6tmHhfy5Jzw8sugzajwYdi6VosIpB3/cPM= +github.com/liamg/loading v0.0.4 h1:i3+8cxqCbwVnz6RLqRZG4zHPKnY31T6NfM0h48mucvg= +github.com/liamg/loading v0.0.4/go.mod h1:MpUOigKhyrByiW/te5JtMB9/f2MbZ4ZDk4wjorOwlpI= github.com/liamg/memoryfs v1.4.2 h1:6T9Oy1DdWxGCzIY89p0Ykeya5H0uAlzG2xHEGcvo6MU= github.com/liamg/memoryfs v1.4.2/go.mod h1:z7mfqXFQS8eSeBBsFjYLlxYRMRyiPktytvYCYTb3BSk= github.com/liamg/tml v0.6.0 h1:yOC/Q9p9Io3J11U9LdYVIwpRTnTE1GPMNFLrygkmE2Y= github.com/liamg/tml v0.6.0/go.mod h1:0h4EAV/zBOsqI91EWONedjRpO8O0itjGJVd+wG5eC+E= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= @@ -1264,8 +1367,8 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko 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= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -2290,8 +2393,8 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -helm.sh/helm/v3 v3.9.0 h1:qDSWViuF6SzZX5s5AB/NVRGWmdao7T5j4S4ebIkMGag= -helm.sh/helm/v3 v3.9.0/go.mod h1:fzZfyslcPAWwSdkXrXlpKexFeE2Dei8N27FFQWt+PN0= +helm.sh/helm/v3 v3.9.2 h1:bx7kdhr5VAhYoWv9bIdT1C6qWR+/7SIoPCwLx22l78g= +helm.sh/helm/v3 v3.9.2/go.mod h1:y/dJc/0Lzcn40jgd85KQXnufhFF7sr4v6L/vYMLRaRM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2303,20 +2406,18 @@ k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= -k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I= -k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= +k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= k8s.io/api v0.24.3/go.mod h1:elGR/XSZrS7z7cSZPzVWaycpJuGIw57j9b95/1PdJNI= k8s.io/api v0.25.0-alpha.2 h1:azwXduCht76Ecuv80QzZkCDzcFcLotKPXiE9/+jx5Qk= k8s.io/api v0.25.0-alpha.2/go.mod h1:wOntqHYj8WveLW2sh6q4tkE2vMZTtxe0MrFyVwO8JCM= -k8s.io/apiextensions-apiserver v0.24.0 h1:JfgFqbA8gKJ/uDT++feAqk9jBIwNnL9YGdQvaI9DLtY= -k8s.io/apiextensions-apiserver v0.24.0/go.mod h1:iuVe4aEpe6827lvO6yWQVxiPSpPoSKVjkq+MIdg84cM= +k8s.io/apiextensions-apiserver v0.24.2 h1:/4NEQHKlEz1MlaK/wHT5KMKC9UKYz6NZz6JE6ov4G6k= +k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= -k8s.io/apimachinery v0.24.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= -k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apimachinery v0.25.0-alpha.2 h1:y6uTWaiqsPTPRewnXJ15IFyGmBo2qPt6enm4zszG8Z0= k8s.io/apimachinery v0.25.0-alpha.2/go.mod h1:h34FtK3eCxige6ZIACdBSYExtDaKAUxoc7hVe2LOxzw= @@ -2324,29 +2425,26 @@ k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= -k8s.io/apiserver v0.24.0/go.mod h1:WFx2yiOMawnogNToVvUYT9nn1jaIkMKj41ZYCVycsBA= -k8s.io/apiserver v0.24.1 h1:LAA5UpPOeaREEtFAQRUQOI3eE5So/j5J3zeQJjeLdz4= -k8s.io/apiserver v0.24.1/go.mod h1:dQWNMx15S8NqJMp0gpYfssyvhYnkilc1LpExd/dkLh0= +k8s.io/apiserver v0.24.2 h1:orxipm5elPJSkkFNlwH9ClqaKEDJJA3yR2cAAlCnyj4= +k8s.io/apiserver v0.24.2/go.mod h1:pSuKzr3zV+L+MWqsEo0kHHYwCo77AT5qXbFXP2jbvFI= k8s.io/cli-runtime v0.24.3 h1:O9YvUHrDSCQUPlsqVmaqDrueqjpJ7IO6Yas9B6xGSoo= k8s.io/cli-runtime v0.24.3/go.mod h1:In84wauoMOqa7JDvDSXGbf8lTNlr70fOGpYlYfJtSqA= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= -k8s.io/client-go v0.24.0/go.mod h1:VFPQET+cAFpYxh6Bq6f4xyMY80G6jKKktU6G0m00VDw= -k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8= +k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw= k8s.io/client-go v0.25.0-alpha.2 h1:kXlDl2L/CmdubzbRTPOCXj9JDPv9U0MuEjRXSCltQ00= k8s.io/client-go v0.25.0-alpha.2/go.mod h1:AN5W2BkXTu2lNm2BANn5lC6VnGlv6AM5HNPQLsriBOA= k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= -k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= +k8s.io/code-generator v0.24.2/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/code-generator v0.24.3/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= -k8s.io/component-base v0.24.0/go.mod h1:Dgazgon0i7KYUsS8krG8muGiMVtUZxG037l1MKyXgrA= -k8s.io/component-base v0.24.1/go.mod h1:DW5vQGYVCog8WYpNob3PMmmsY8A3L9QZNg4j/dV3s38= +k8s.io/component-base v0.24.2/go.mod h1:ucHwW76dajvQ9B7+zecZAP3BVqvrHoOxm8olHEg0nmM= k8s.io/component-base v0.24.3 h1:u99WjuHYCRJjS1xeLOx72DdRaghuDnuMgueiGMFy1ec= k8s.io/component-base v0.24.3/go.mod h1:bqom2IWN9Lj+vwAkPNOv2TflsP1PeVDIwIN0lRthxYY= k8s.io/component-helpers v0.24.3/go.mod h1:/1WNW8TfBOijQ1ED2uCHb4wtXYWDVNMqUll8h36iNVo= diff --git a/mkdocs.yml b/mkdocs.yml index 3138d18ed7e7..f9352cf47fdd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,8 @@ nav: - Scanning: docs/kubernetes/cli/scanning.md - Operator: - Overview: docs/kubernetes/operator/index.md + - Cloud: + - AWS: docs/cloud/aws/scanning.md - SBOM: - Overview: docs/sbom/index.md - CycloneDX: docs/sbom/cyclonedx.md diff --git a/pkg/cloud/aws/commands/run.go b/pkg/cloud/aws/commands/run.go new file mode 100644 index 000000000000..09abc97e839e --- /dev/null +++ b/pkg/cloud/aws/commands/run.go @@ -0,0 +1,187 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/aquasecurity/defsec/pkg/errs" + + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + + "github.com/aquasecurity/trivy/pkg/cloud" + + "github.com/aquasecurity/trivy/pkg/cloud/cache" + + "github.com/aquasecurity/trivy/pkg/flag" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" + + "github.com/aquasecurity/trivy/pkg/cloud/aws/scanner" + "github.com/aquasecurity/trivy/pkg/cloud/report" + + "github.com/aquasecurity/trivy/pkg/log" + + awsScanner "github.com/aquasecurity/defsec/pkg/scanners/cloud/aws" +) + +func getAccountIDAndRegion(ctx context.Context, region string) (string, string, error) { + log.Logger.Debug("Looking for AWS credentials provider...") + + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return "", "", err + } + if region != "" { + cfg.Region = region + } + + svc := sts.NewFromConfig(cfg) + + log.Logger.Debug("Looking up AWS caller identity...") + result, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return "", "", fmt.Errorf("failed to discover AWS caller identity: %w", err) + } + if result.Account == nil { + return "", "", fmt.Errorf("missing account id for aws account") + } + log.Logger.Debugf("Verified AWS credentials for account %s!", *result.Account) + return *result.Account, cfg.Region, nil +} + +func processOptions(ctx context.Context, opt *flag.Options) error { + // support comma separated services too + var splitServices []string + for _, service := range opt.Services { + splitServices = append(splitServices, strings.Split(service, ",")...) + } + opt.Services = splitServices + + if len(opt.Services) != 1 && opt.ARN != "" { + return fmt.Errorf("you must specify the single --service which the --arn relates to") + } + + if opt.Account == "" || opt.Region == "" { + var err error + opt.Account, opt.Region, err = getAccountIDAndRegion(ctx, opt.Region) + if err != nil { + return err + } + } + + if len(opt.Services) == 0 { + log.Logger.Debug("No service(s) specified, scanning all services...") + opt.Services = awsScanner.AllSupportedServices() + } else { + log.Logger.Debugf("Specific services were requested: [%s]...", strings.Join(opt.Services, ", ")) + for _, service := range opt.Services { + var found bool + supported := awsScanner.AllSupportedServices() + for _, allowed := range supported { + if allowed == service { + found = true + break + } + } + if !found { + return fmt.Errorf("service '%s' is not currently supported - supported services are: %s", service, strings.Join(supported, ", ")) + } + } + } + + return nil +} + +func Run(ctx context.Context, opt flag.Options) error { + + ctx, cancel := context.WithTimeout(ctx, opt.GlobalOptions.Timeout) + defer cancel() + + if err := log.InitLogger(opt.Debug, false); err != nil { + return fmt.Errorf("logger error: %w", err) + } + + var err error + defer func() { + if errors.Is(err, context.DeadlineExceeded) { + log.Logger.Warn("Increase --timeout value") + } + }() + + if err := processOptions(ctx, &opt); err != nil { + return err + } + + cached := cache.New(opt.CacheDir, opt.MaxCacheAge, cloud.ProviderAWS, opt.Account, opt.Region) + servicesInCache := cached.ListAvailableServices(false) + var servicesToLoadFromCache []string + var servicesToScan []string + for _, service := range opt.Services { + if cached != nil { + var inCache bool + for _, cacheSvc := range servicesInCache { + if cacheSvc == service { + log.Logger.Debugf("Results for service '%s' found in cache.", service) + inCache = true + break + } + } + if inCache && !opt.UpdateCache { + servicesToLoadFromCache = append(servicesToLoadFromCache, service) + continue + } + } + servicesToScan = append(servicesToScan, service) + } + + var r *report.Report + + // if there is anything we need that wasn't in the cache, scan it now + if len(servicesToScan) > 0 { + log.Logger.Debugf("Scanning the following services using the AWS API: [%s]...", strings.Join(servicesToScan, ", ")) + opt.Services = servicesToScan + results, err := scanner.NewScanner().Scan(ctx, opt) + if err != nil { + var aerr errs.AdapterError + if errors.As(err, &aerr) { + for _, e := range aerr.Errors() { + log.Logger.Warnf("Adapter error: %s", e) + } + } + return fmt.Errorf("aws scan error: %w", err) + } + r = report.New(cloud.ProviderAWS, opt.Account, opt.Region, results.GetFailed(), opt.Services) + } else { + log.Logger.Debug("No more services to scan - everything was found in the cache.") + r = report.New(cloud.ProviderAWS, opt.Account, opt.Region, nil, opt.Services) + } + if len(servicesToLoadFromCache) > 0 { + log.Logger.Debug("Loading cached results...") + cachedReport, err := cached.LoadReport(servicesToLoadFromCache...) + if err != nil { + return err + } + for service, results := range cachedReport.Results { + log.Logger.Debugf("Adding cached results for '%s'...", service) + r.AddResultsForService(service, results.Results, results.CreationTime) + } + } + + if len(servicesToScan) > 0 { // don't write cache if we didn't scan anything new + log.Logger.Debugf("Writing results to cache for services [%s]...", strings.Join(r.ServicesInScope, ", ")) + if err := cached.Save(r); err != nil { + return err + } + } + + log.Logger.Debug("Writing report to output...") + if err := report.Write(r, opt, len(servicesToLoadFromCache) > 0); err != nil { + return fmt.Errorf("unable to write results: %w", err) + } + + cmd.Exit(opt, r.Failed()) + return nil +} diff --git a/pkg/cloud/aws/scanner/progress.go b/pkg/cloud/aws/scanner/progress.go new file mode 100644 index 000000000000..57bffa8c3e34 --- /dev/null +++ b/pkg/cloud/aws/scanner/progress.go @@ -0,0 +1,79 @@ +package scanner + +import ( + "fmt" + "os" + + "github.com/liamg/loading/pkg/bar" +) + +type progressTracker struct { + serviceBar *bar.Bar + serviceTotal int + serviceCurrent int + isTTY bool +} + +func newProgressTracker() *progressTracker { + var isTTY bool + if stat, err := os.Stdout.Stat(); err == nil { + isTTY = stat.Mode()&os.ModeCharDevice == os.ModeCharDevice + } + return &progressTracker{ + isTTY: isTTY, + } +} + +func (m *progressTracker) Finish() { + if !m.isTTY || m.serviceBar == nil { + return + } + m.serviceBar.Finish() +} + +func (m *progressTracker) IncrementResource() { + if !m.isTTY { + return + } + m.serviceBar.Increment() +} + +func (m *progressTracker) SetTotalResources(i int) { + if !m.isTTY { + return + } + m.serviceBar.SetTotal(i) +} + +func (m *progressTracker) SetTotalServices(i int) { + m.serviceTotal = i +} + +func (m *progressTracker) SetServiceLabel(label string) { + if !m.isTTY { + return + } + m.serviceBar.SetLabel("└╴" + label) + m.serviceBar.SetCurrent(0) +} + +func (m *progressTracker) FinishService() { + if !m.isTTY { + return + } + m.serviceCurrent++ + m.serviceBar.Finish() +} + +func (m *progressTracker) StartService(name string) { + if !m.isTTY { + return + } + fmt.Printf("[%d/%d] Scanning %s...\n", m.serviceCurrent+1, m.serviceTotal, name) + m.serviceBar = bar.New( + bar.OptionHideOnFinish(true), + bar.OptionWithAutoComplete(false), + bar.OptionWithRenderFunc(bar.RenderColoured(0xff, 0x66, 0x00)), + ) + m.SetServiceLabel("Initializing...") +} diff --git a/pkg/cloud/aws/scanner/scanner.go b/pkg/cloud/aws/scanner/scanner.go new file mode 100644 index 000000000000..b67f90d068e7 --- /dev/null +++ b/pkg/cloud/aws/scanner/scanner.go @@ -0,0 +1,74 @@ +package scanner + +import ( + "context" + "strings" + + "github.com/aquasecurity/defsec/pkg/framework" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + + "github.com/aquasecurity/defsec/pkg/scan" + "github.com/aquasecurity/defsec/pkg/scanners/cloud/aws" + "github.com/aquasecurity/defsec/pkg/scanners/options" +) + +type AWSScanner struct { +} + +func NewScanner() *AWSScanner { + return &AWSScanner{} +} + +func (s *AWSScanner) Scan(ctx context.Context, option flag.Options) (scan.Results, error) { + + var scannerOpts []options.ScannerOption + if !option.NoProgress { + tracker := newProgressTracker() + defer tracker.Finish() + scannerOpts = append(scannerOpts, aws.ScannerWithProgressTracker(tracker)) + } + + if len(option.Services) > 0 { + scannerOpts = append(scannerOpts, aws.ScannerWithAWSServices(option.Services...)) + } + + if option.Debug { + scannerOpts = append(scannerOpts, options.ScannerWithDebug(&defsecLogger{})) + } + + if option.Region != "" { + scannerOpts = append( + scannerOpts, + aws.ScannerWithAWSRegion(option.Region), + ) + } + + if option.Endpoint != "" { + scannerOpts = append( + scannerOpts, + aws.ScannerWithAWSEndpoint(option.Endpoint), + ) + } + + scannerOpts = append(scannerOpts, options.ScannerWithFrameworks( + framework.Default, + framework.CIS_AWS_1_2, + )) + + defsecResults, err := aws.New(scannerOpts...).Scan(ctx) + if err != nil { + return nil, err + } + + return defsecResults, nil +} + +type defsecLogger struct { +} + +func (d *defsecLogger) Write(p []byte) (n int, err error) { + log.Logger.Debug("[defsec] " + strings.TrimSpace(string(p))) + return len(p), nil +} diff --git a/pkg/cloud/cache/cache.go b/pkg/cloud/cache/cache.go new file mode 100644 index 000000000000..22ddd8ccaaa5 --- /dev/null +++ b/pkg/cloud/cache/cache.go @@ -0,0 +1,65 @@ +package cache + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "time" +) + +const ( + metadataFilename = "metadata.json" + cacheFilename = "cache.json" + dataDirName = "data" + cacheSubDir = "cloud" +) + +var ErrCacheNotFound = fmt.Errorf("cache record not found") + +type Cache struct { + path string + provider string + accountID string + region string + maxAge time.Duration +} + +func New(basePath string, maxAge time.Duration, provider string, accountID string, region string) *Cache { + return &Cache{ + path: path.Join(basePath, cacheSubDir, strings.ToLower(provider), accountID, strings.ToLower(region)), + provider: provider, + accountID: accountID, + region: region, + maxAge: maxAge, + } +} + +func (c *Cache) ListAvailableServices(includeExpired bool) []string { + metadata, err := c.loadMetadata() + if err != nil { + return nil + } + r, err := c.LoadReport(metadata.ServicesInScope...) + if err != nil { + return nil + } + var available []string + for _, service := range metadata.ServicesInScope { + if entry, ok := r.Results[service]; ok { + if includeExpired || entry.CreationTime.Add(c.maxAge).After(time.Now()) { + available = append(available, service) + } + } + } + return available +} + +func (c *Cache) getServicePath(service string) string { + service = strings.NewReplacer(" ", "_", ".", "_").Replace(service) + return filepath.Join(c.path, dataDirName, service, cacheFilename) +} + +func (c *Cache) getMetadataPath() string { + return filepath.Join(c.path, metadataFilename) +} diff --git a/pkg/cloud/cache/cache_test.go b/pkg/cloud/cache/cache_test.go new file mode 100644 index 000000000000..c3f8cc73877b --- /dev/null +++ b/pkg/cloud/cache/cache_test.go @@ -0,0 +1,166 @@ +package cache + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cloud/report" +) + +func TestCache(t *testing.T) { + + tests := []struct { + name string + input report.Report + services []string + }{ + { + name: "no services", + input: report.Report{ + Provider: "AWS", + AccountID: "1234567890", + Region: "us-east-1", + Results: make(map[string]report.ResultsAtTime), + ServicesInScope: nil, + }, + services: nil, + }, + { + name: "all services", + input: report.Report{ + Provider: "AWS", + AccountID: "1234567890", + Region: "us-east-1", + Results: map[string]report.ResultsAtTime{ + "s3": { + Results: nil, + CreationTime: time.Now(), + }, + "ec2": { + Results: nil, + CreationTime: time.Now(), + }, + }, + ServicesInScope: []string{"ec2", "s3"}, + }, + services: []string{"ec2", "s3"}, + }, + { + name: "partial services", + input: report.Report{ + Provider: "AWS", + AccountID: "1234567890", + Region: "us-east-1", + Results: map[string]report.ResultsAtTime{ + "s3": { + Results: nil, + CreationTime: time.Now(), + }, + "ec2": { + Results: nil, + CreationTime: time.Now(), + }, + }, + ServicesInScope: []string{"ec2", "s3"}, + }, + services: []string{"ec2"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + baseDir := t.TempDir() + + // ensure saving doesn't error + cache := New(baseDir, time.Hour, test.input.Provider, test.input.AccountID, test.input.Region) + require.NoError(t, cache.Save(&test.input)) + + // ensure all scoped services were cached + available := cache.ListAvailableServices(false) + assert.Equal(t, test.input.ServicesInScope, available) + + // ensure all cached services are really available + fullReport, err := cache.LoadReport(available...) + require.NoError(t, err) + assert.Equal(t, available, fullReport.ServicesInScope) + + // ensure loading restores all (specified) data + loaded, err := cache.LoadReport(test.services...) + require.NoError(t, err) + + assert.Equal(t, test.input.Provider, loaded.Provider) + assert.Equal(t, test.input.AccountID, loaded.AccountID) + assert.Equal(t, test.input.Region, loaded.Region) + assert.ElementsMatch(t, test.services, loaded.ServicesInScope) + + var actualServices []string + for service := range loaded.Results { + actualServices = append(actualServices, service) + } + assert.ElementsMatch(t, test.services, actualServices) + + for _, service := range test.services { + assert.Equal(t, test.input.Results[service].CreationTime.Format(time.RFC3339), loaded.Results[service].CreationTime.Format(time.RFC3339)) + assert.Equal(t, test.input.Results[service].Results, loaded.Results[service].Results) + } + }) + } +} + +func TestPartialCacheOverwrite(t *testing.T) { + baseDir := t.TempDir() + + r1 := report.Report{ + Provider: "AWS", + AccountID: "1234567890", + Region: "us-east-1", + Results: map[string]report.ResultsAtTime{ + "a": { + Results: nil, + CreationTime: time.Now(), + }, + "b": { + Results: nil, + CreationTime: time.Now(), + }, + "c": { + Results: nil, + CreationTime: time.Now(), + }, + "d": { + Results: nil, + CreationTime: time.Now(), + }, + }, + ServicesInScope: []string{"a", "b", "c", "d"}, + } + + // ensure saving doesn't error + cache := New(baseDir, time.Hour, "AWS", "1234567890", "us-east-1") + require.NoError(t, cache.Save(&r1)) + + r2 := report.Report{ + Provider: "AWS", + AccountID: "1234567890", + Region: "us-east-1", + Results: map[string]report.ResultsAtTime{ + "a": { + Results: nil, + CreationTime: time.Now(), + }, + "b": { + Results: nil, + CreationTime: time.Now(), + }, + }, + ServicesInScope: []string{"a", "b"}, + } + require.NoError(t, cache.Save(&r2)) + + assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, cache.ListAvailableServices(false)) +} diff --git a/pkg/cloud/cache/load.go b/pkg/cloud/cache/load.go new file mode 100644 index 000000000000..b1593e1a368d --- /dev/null +++ b/pkg/cloud/cache/load.go @@ -0,0 +1,59 @@ +package cache + +import ( + "encoding/json" + "os" + + "github.com/aquasecurity/trivy/pkg/cloud/report" +) + +func (c *Cache) loadMetadata() (*Metadata, error) { + metadataFile := c.getMetadataPath() + m, err := os.Open(metadataFile) + if err != nil { + return nil, ErrCacheNotFound + } + + var metadata Metadata + if err := json.NewDecoder(m).Decode(&metadata); err != nil { + return nil, err + } + return &metadata, nil +} + +func (c *Cache) LoadReport(services ...string) (*report.Report, error) { + + metadata, err := c.loadMetadata() + if err != nil { + return nil, err + } + + base := report.New(c.provider, c.accountID, c.region, nil, nil) + + for _, service := range services { + if !contains(metadata.ServicesInScope, service) { + continue + } + serviceFile := c.getServicePath(service) + s, err := os.Open(serviceFile) + if err != nil { + return nil, err + } + var serviceRecord Record + if err := json.NewDecoder(s).Decode(&serviceRecord); err != nil { + return nil, err + } + base.AddResultsForService(service, serviceRecord.Results, serviceRecord.CreationTime) + } + + return base, nil +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/pkg/cloud/cache/save.go b/pkg/cloud/cache/save.go new file mode 100644 index 000000000000..f8f55c5679c6 --- /dev/null +++ b/pkg/cloud/cache/save.go @@ -0,0 +1,77 @@ +package cache + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/cloud/report" +) + +func (c *Cache) Save(r *report.Report) error { + + existingServices := c.ListAvailableServices(true) + + if err := os.MkdirAll( + filepath.Dir(c.getMetadataPath()), + 0700, + ); err != nil { // only the current user is allowed to see this report + return err + } + + var retainedServices []string + for _, existing := range existingServices { + var found bool + for _, service := range r.ServicesInScope { + if service == existing { + found = true + break + } + } + if found { + continue + } + retainedServices = append(retainedServices, existing) + } + + for _, service := range r.ServicesInScope { + serviceFile := c.getServicePath(service) + if err := os.MkdirAll( + filepath.Dir(serviceFile), + 0700, + ); err != nil { + return err + } + resultSet, err := r.GetResultsForService(service) + if err != nil { + return err + } + s, err := os.Create(serviceFile) + if err != nil { + return err + } + record := Record{ + SchemaVersion: SchemaVersion, + Service: service, + Results: resultSet.Results, + CreationTime: resultSet.CreationTime, + } + if err := json.NewEncoder(s).Encode(record); err != nil { + return err + } + } + + metadataFile := c.getMetadataPath() + metadata := Metadata{ + SchemaVersion: SchemaVersion, + Provider: c.provider, + AccountID: c.accountID, + Region: c.region, + ServicesInScope: append(r.ServicesInScope, retainedServices...), + } + m, err := os.Create(metadataFile) + if err != nil { + return err + } + return json.NewEncoder(m).Encode(metadata) +} diff --git a/pkg/cloud/cache/schema.go b/pkg/cloud/cache/schema.go new file mode 100644 index 000000000000..5966b314a192 --- /dev/null +++ b/pkg/cloud/cache/schema.go @@ -0,0 +1,24 @@ +package cache + +import ( + "time" + + "github.com/aquasecurity/trivy/pkg/types" +) + +const SchemaVersion = 1 + +type Metadata struct { + SchemaVersion int `json:"schema_version"` + Provider string `json:"provider"` + AccountID string `json:"account_id"` + Region string `json:"region"` + ServicesInScope []string `json:"services"` +} + +type Record struct { + SchemaVersion int `json:"schema_version"` + Service string `json:"service"` + Results types.Results `json:"results"` + CreationTime time.Time `json:"creation_time"` +} diff --git a/pkg/cloud/provider.go b/pkg/cloud/provider.go new file mode 100644 index 000000000000..f495e15e2095 --- /dev/null +++ b/pkg/cloud/provider.go @@ -0,0 +1,5 @@ +package cloud + +const ( + ProviderAWS = "AWS" +) diff --git a/pkg/cloud/report/convert.go b/pkg/cloud/report/convert.go new file mode 100644 index 000000000000..b9b1dedc8487 --- /dev/null +++ b/pkg/cloud/report/convert.go @@ -0,0 +1,95 @@ +package report + +import ( + "fmt" + "strings" + "time" + + "github.com/aquasecurity/defsec/pkg/scan" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func convertResults(results scan.Results, provider string, scoped []string) map[string]ResultsAtTime { + convertedResults := make(map[string]ResultsAtTime) + resultsByServiceAndARN := make(map[string]map[string]scan.Results) + for _, result := range results { + existingService, ok := resultsByServiceAndARN[result.Rule().Service] + if !ok { + existingService = make(map[string]scan.Results) + } + resource := result.Flatten().Resource + + existingService[resource] = append(existingService[resource], result) + resultsByServiceAndARN[result.Rule().Service] = existingService + } + // ensure we have entries for all scoped services, even if there are no results + for _, service := range scoped { + if _, ok := resultsByServiceAndARN[service]; !ok { + resultsByServiceAndARN[service] = nil + } + } + for service, arnResults := range resultsByServiceAndARN { + + var convertedArnResults []types.Result + + for arn, serviceResults := range arnResults { + + arnResult := types.Result{ + Target: arn, + Class: types.ClassConfig, + Type: ftypes.Cloud, + } + + for _, result := range serviceResults { + + var primaryURL string + + // empty namespace implies a go rule from defsec, "builtin" refers to a built-in rego rule + // this ensures we don't generate bad links for custom policies + if result.RegoNamespace() == "" || strings.HasPrefix(result.RegoNamespace(), "builtin.") { + primaryURL = fmt.Sprintf("https://avd.aquasec.com/misconfig/%s", strings.ToLower(result.Rule().AVDID)) + } + + status := types.StatusFailure + switch result.Status() { + case scan.StatusPassed: + status = types.StatusPassed + case scan.StatusIgnored: + status = types.StatusException + } + + flat := result.Flatten() + + arnResult.Misconfigurations = append(arnResult.Misconfigurations, types.DetectedMisconfiguration{ + Type: provider, + ID: result.Rule().AVDID, + Title: result.Rule().Summary, + Description: strings.TrimSpace(result.Rule().Explanation), + Message: strings.TrimSpace(result.Description()), + Namespace: result.RegoNamespace(), + Query: result.RegoRule(), + Resolution: result.Rule().Resolution, + Severity: string(result.Severity()), + PrimaryURL: primaryURL, + References: []string{primaryURL}, + Status: status, + CauseMetadata: ftypes.CauseMetadata{ + Resource: flat.Resource, + Provider: string(flat.RuleProvider), + Service: flat.RuleService, + StartLine: flat.Location.StartLine, + EndLine: flat.Location.EndLine, + }, + }) + } + + convertedArnResults = append(convertedArnResults, arnResult) + } + convertedResults[service] = ResultsAtTime{ + Results: convertedArnResults, + CreationTime: time.Now(), + } + } + return convertedResults +} diff --git a/pkg/cloud/report/convert_test.go b/pkg/cloud/report/convert_test.go new file mode 100644 index 000000000000..6700ec6ac00b --- /dev/null +++ b/pkg/cloud/report/convert_test.go @@ -0,0 +1,241 @@ +package report + +import ( + "sort" + "testing" + + fanaltypes "github.com/aquasecurity/trivy/pkg/fanal/types" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + + defsecTypes "github.com/aquasecurity/defsec/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/defsec/pkg/scan" +) + +func Test_ResultConversion(t *testing.T) { + + tests := []struct { + name string + results scan.Results + provider string + scoped []string + expected map[string]ResultsAtTime + }{ + { + name: "no results", + results: scan.Results{}, + provider: "AWS", + expected: make(map[string]ResultsAtTime), + }, + { + name: "no results, multiple scoped services", + results: scan.Results{}, + provider: "AWS", + scoped: []string{"s3", "ec2"}, + expected: map[string]ResultsAtTime{ + "s3": {}, + "ec2": {}, + }, + }, + { + name: "multiple results", + results: func() scan.Results { + + baseRule := scan.Rule{ + AVDID: "AVD-AWS-9999", + Aliases: []string{"AWS999"}, + ShortCode: "no-bad-stuff", + Summary: "Do not use bad stuff", + Explanation: "Bad stuff is... bad", + Impact: "Bad things", + Resolution: "Remove bad stuff", + Provider: "AWS", + Severity: "HIGH", + } + + var s3Results scan.Results + s3Results.Add( + "something failed", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket1", + }).String()), + ) + s3Results.Add( + "something else failed", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket2", + }).String()), + ) + s3Results.Add( + "something else failed again", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket2", + }).String()), + ) + baseRule.Service = "s3" + s3Results.SetRule(baseRule) + var ec2Results scan.Results + ec2Results.Add( + "instance is bad", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "ec2", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "instance1", + }).String()), + ) + baseRule.Service = "ec2" + ec2Results.SetRule(baseRule) + return append(s3Results, ec2Results...) + }(), + provider: "AWS", + expected: map[string]ResultsAtTime{ + "s3": { + Results: types.Results{ + { + Target: "arn:aws:s3:us-east-1:1234567890:bucket1", + Class: "config", + Type: "cloud", + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "AWS", + ID: "AVD-AWS-9999", + Title: "Do not use bad stuff", + Description: "Bad stuff is... bad", + Message: "something failed", + Resolution: "Remove bad stuff", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-9999", + References: []string{ + "https://avd.aquasec.com/misconfig/avd-aws-9999", + }, + Status: "FAIL", + CauseMetadata: fanaltypes.CauseMetadata{ + Resource: "arn:aws:s3:us-east-1:1234567890:bucket1", + Provider: "AWS", + Service: "s3", + StartLine: 0, + EndLine: 0, + Code: fanaltypes.Code{}, + }, + }, + }, + }, + { + Target: "arn:aws:s3:us-east-1:1234567890:bucket2", + Class: "config", + Type: "cloud", + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "AWS", + ID: "AVD-AWS-9999", + Title: "Do not use bad stuff", + Description: "Bad stuff is... bad", + Message: "something else failed", + Resolution: "Remove bad stuff", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-9999", + References: []string{ + "https://avd.aquasec.com/misconfig/avd-aws-9999", + }, + Status: "FAIL", + CauseMetadata: fanaltypes.CauseMetadata{ + Resource: "arn:aws:s3:us-east-1:1234567890:bucket2", + Provider: "AWS", + Service: "s3", + }, + }, + { + Type: "AWS", + ID: "AVD-AWS-9999", + Title: "Do not use bad stuff", + Description: "Bad stuff is... bad", + Message: "something else failed again", + Resolution: "Remove bad stuff", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-9999", + References: []string{ + "https://avd.aquasec.com/misconfig/avd-aws-9999", + }, + Status: "FAIL", + CauseMetadata: fanaltypes.CauseMetadata{ + Resource: "arn:aws:s3:us-east-1:1234567890:bucket2", + Provider: "AWS", + Service: "s3", + }, + }, + }, + }, + }, + }, + "ec2": { + Results: types.Results{ + { + Target: "arn:aws:ec2:us-east-1:1234567890:instance1", + Class: "config", + Type: "cloud", + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "AWS", + ID: "AVD-AWS-9999", + Title: "Do not use bad stuff", + Description: "Bad stuff is... bad", + Message: "instance is bad", + Resolution: "Remove bad stuff", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-9999", + References: []string{ + "https://avd.aquasec.com/misconfig/avd-aws-9999", + }, + Status: "FAIL", + CauseMetadata: fanaltypes.CauseMetadata{ + Resource: "arn:aws:ec2:us-east-1:1234567890:instance1", + Provider: "AWS", + Service: "ec2", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + converted := convertResults(test.results, test.provider, test.scoped) + assertConvertedResultsMatch(t, test.expected, converted) + }) + } + +} + +func assertConvertedResultsMatch(t *testing.T, expected, actual map[string]ResultsAtTime) { + assert.Equal(t, len(expected), len(actual)) + for service, resultsAtTime := range expected { + _, ok := actual[service] + assert.True(t, ok) + sort.Slice(actual[service].Results, func(i, j int) bool { + return actual[service].Results[i].Target < actual[service].Results[j].Target + }) + assert.ElementsMatch(t, resultsAtTime.Results, actual[service].Results) + } +} diff --git a/pkg/cloud/report/report.go b/pkg/cloud/report/report.go new file mode 100644 index 000000000000..efdbe04c6579 --- /dev/null +++ b/pkg/cloud/report/report.go @@ -0,0 +1,175 @@ +package report + +import ( + "context" + "fmt" + "os" + "sort" + "time" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + + "github.com/liamg/tml" + + "github.com/aquasecurity/trivy/pkg/flag" + + "github.com/aquasecurity/trivy/pkg/report" + + "github.com/aquasecurity/trivy/pkg/result" + + "github.com/aquasecurity/defsec/pkg/scan" + pkgReport "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + tableFormat = "table" +) + +// Report represents a kubernetes scan report +type Report struct { + Provider string + AccountID string + Region string + Results map[string]ResultsAtTime + ServicesInScope []string +} + +type ResultsAtTime struct { + Results types.Results + CreationTime time.Time +} + +func New(provider, accountID, region string, defsecResults scan.Results, scopedServices []string) *Report { + return &Report{ + Provider: provider, + AccountID: accountID, + Results: convertResults(defsecResults, provider, scopedServices), + ServicesInScope: scopedServices, + Region: region, + } +} + +// Failed returns whether the aws report includes any "failed" results +func (r *Report) Failed() bool { + for _, set := range r.Results { + if set.Results.Failed() { + return true + } + } + return false +} + +// Write writes the results in the give format +func Write(rep *Report, opt flag.Options, fromCache bool) error { + + var filtered []types.Result + + ctx := context.Background() + + // filter results + for _, resultsAtTime := range rep.Results { + for _, res := range resultsAtTime.Results { + resCopy := res + if err := result.Filter( + ctx, + &resCopy, + opt.Severities, + false, + false, + "", + "", + nil, + ); err != nil { + return err + } + sort.Slice(resCopy.Misconfigurations, func(i, j int) bool { + return resCopy.Misconfigurations[i].CauseMetadata.Resource < resCopy.Misconfigurations[i].CauseMetadata.Resource + }) + filtered = append(filtered, resCopy) + } + } + sort.Slice(filtered, func(i, j int) bool { + return filtered[i].Target < filtered[j].Target + }) + + base := types.Report{ + ArtifactName: rep.AccountID, + ArtifactType: ftypes.ArtifactAWSAccount, + Results: filtered, + } + + switch opt.Format { + case tableFormat: + + // ensure color/formatting is disabled for pipes/non-pty + var useANSI bool + if opt.Output == os.Stdout { + if o, err := os.Stdout.Stat(); err == nil { + useANSI = (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice + } + } + if !useANSI { + tml.DisableFormatting() + } + + switch { + case len(opt.Services) == 1 && opt.ARN == "": + if err := writeResourceTable(rep, filtered, opt.Output, opt.Services[0]); err != nil { + return err + } + case len(opt.Services) == 1 && opt.ARN != "": + if err := writeResultsForARN(rep, filtered, opt.Output, opt.Services[0], opt.ARN, opt.Severities); err != nil { + return err + } + default: + if err := writeServiceTable(rep, filtered, opt.Output); err != nil { + return err + } + } + + // render cache info + if fromCache { + _ = tml.Fprintf(opt.Output, "\nThis scan report was loaded from cached results. If you'd like to run a fresh scan, use --update-cache.\n") + } + + return nil + default: + return report.Write(base, pkgReport.Option{ + Format: opt.Format, + Output: opt.Output, + Severities: opt.Severities, + OutputTemplate: opt.Template, + IncludeNonFailures: opt.IncludeNonFailures, + Trace: opt.Trace, + }) + } +} + +func (r *Report) GetResultsForService(service string) (*ResultsAtTime, error) { + if set, ok := r.Results[service]; ok { + return &set, nil + } + for _, scoped := range r.ServicesInScope { + if scoped == service { + return &ResultsAtTime{ + Results: nil, + CreationTime: time.Now(), + }, nil + } + } + return nil, fmt.Errorf("service %q not found", service) +} + +func (r *Report) AddResultsForService(service string, results types.Results, creation time.Time) { + r.Results[service] = ResultsAtTime{ + Results: results, + CreationTime: creation, + } + for _, exists := range r.ServicesInScope { + if exists == service { + return + } + } + r.ServicesInScope = append(r.ServicesInScope, service) +} diff --git a/pkg/cloud/report/resource.go b/pkg/cloud/report/resource.go new file mode 100644 index 000000000000..27dd83b1117c --- /dev/null +++ b/pkg/cloud/report/resource.go @@ -0,0 +1,89 @@ +package report + +import ( + "fmt" + "io" + "sort" + "strconv" + + "github.com/liamg/tml" + + "golang.org/x/term" + + "github.com/aquasecurity/table" + pkgReport "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +type sortableRow struct { + name string + counts map[string]int +} + +func writeResourceTable(report *Report, results types.Results, output io.Writer, service string) error { + + termWidth, _, err := term.GetSize(0) + if err != nil { + termWidth = 80 + } + maxWidth := termWidth - 48 + if maxWidth < 20 { + maxWidth = 20 + } + + t := table.New(output) + t.SetColumnMaxWidth(maxWidth) + t.SetHeaders("Resource", "Misconfigurations") + t.AddHeaders("Resource", "Critical", "High", "Medium", "Low", "Unknown") + t.SetHeaderVerticalAlignment(table.AlignBottom) + t.SetHeaderAlignment(table.AlignLeft, table.AlignCenter, table.AlignCenter, table.AlignCenter, table.AlignCenter, table.AlignCenter) + t.SetAlignment(table.AlignLeft, table.AlignRight, table.AlignRight, table.AlignRight, table.AlignRight, table.AlignRight) + t.SetRowLines(false) + t.SetAutoMergeHeaders(true) + t.SetHeaderColSpans(0, 1, 5) + + // map resource -> severity -> count + grouped := make(map[string]map[string]int) + for _, result := range results { + for _, misconfiguration := range result.Misconfigurations { + if misconfiguration.CauseMetadata.Service != service { + continue + } + if _, ok := grouped[misconfiguration.CauseMetadata.Resource]; !ok { + grouped[misconfiguration.CauseMetadata.Resource] = make(map[string]int) + } + grouped[misconfiguration.CauseMetadata.Resource][misconfiguration.Severity]++ + } + } + + var sortable []sortableRow + for resource, severityCounts := range grouped { + sortable = append(sortable, sortableRow{ + name: resource, + counts: severityCounts, + }) + } + sort.Slice(sortable, func(i, j int) bool { return sortable[i].name < sortable[j].name }) + for _, row := range sortable { + t.AddRow( + row.name, + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["CRITICAL"]), "CRITICAL"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["HIGH"]), "HIGH"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["MEDIUM"]), "MEDIUM"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["LOW"]), "LOW"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["UNKNOWN"]), "UNKNOWN"), + ) + } + + // render scan title + _ = tml.Fprintf(output, "\nResource Summary for Service '%s' (%s Account %s)\n", service, report.Provider, report.AccountID) + + // render table + if len(sortable) > 0 { + t.Render() + } else { + _, _ = fmt.Fprint(output, "\nNo problems detected.\n") + } + + return nil +} diff --git a/pkg/cloud/report/resource_test.go b/pkg/cloud/report/resource_test.go new file mode 100644 index 000000000000..dbe070cff93b --- /dev/null +++ b/pkg/cloud/report/resource_test.go @@ -0,0 +1,123 @@ +package report + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/flag" +) + +func Test_ResourceReport(t *testing.T) { + tests := []struct { + name string + options flag.Options + fromCache bool + expected string + }{ + { + name: "simple table output", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"s3"}, + }, + }, + fromCache: false, + expected: ` +Resource Summary for Service 's3' (AWS Account ) +┌─────────────────────────────────────────┬──────────────────────────────────────────┐ +│ │ Misconfigurations │ +│ ├──────────┬──────┬────────┬─────┬─────────┤ +│ Resource │ Critical │ High │ Medium │ Low │ Unknown │ +├─────────────────────────────────────────┼──────────┼──────┼────────┼─────┼─────────┤ +│ arn:aws:s3:us-east-1:1234567890:bucket1 │ 0 │ 1 │ 0 │ 0 │ 0 │ +│ arn:aws:s3:us-east-1:1234567890:bucket2 │ 0 │ 2 │ 0 │ 0 │ 0 │ +└─────────────────────────────────────────┴──────────┴──────┴────────┴─────┴─────────┘ +`, + }, + { + name: "results from cache", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"s3"}, + }, + }, + fromCache: true, + expected: ` +Resource Summary for Service 's3' (AWS Account ) +┌─────────────────────────────────────────┬──────────────────────────────────────────┐ +│ │ Misconfigurations │ +│ ├──────────┬──────┬────────┬─────┬─────────┤ +│ Resource │ Critical │ High │ Medium │ Low │ Unknown │ +├─────────────────────────────────────────┼──────────┼──────┼────────┼─────┼─────────┤ +│ arn:aws:s3:us-east-1:1234567890:bucket1 │ 0 │ 1 │ 0 │ 0 │ 0 │ +│ arn:aws:s3:us-east-1:1234567890:bucket2 │ 0 │ 2 │ 0 │ 0 │ 0 │ +└─────────────────────────────────────────┴──────────┴──────┴────────┴─────┴─────────┘ + +This scan report was loaded from cached results. If you'd like to run a fresh scan, use --update-cache. +`, + }, + { + name: "no problems", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"s3"}, + }, + }, + fromCache: false, + expected: ` +Resource Summary for Service 's3' (AWS Account ) + +No problems detected. +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + report := New( + "AWS", + tt.options.AWSOptions.Account, + tt.options.AWSOptions.Region, + createTestResults(), + tt.options.AWSOptions.Services, + ) + + buffer := bytes.NewBuffer([]byte{}) + tt.options.Output = buffer + require.NoError(t, Write(report, tt.options, tt.fromCache)) + + assert.Equal(t, "AWS", report.Provider) + assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID) + assert.Equal(t, tt.options.AWSOptions.Region, report.Region) + assert.ElementsMatch(t, tt.options.AWSOptions.Services, report.ServicesInScope) + assert.Equal(t, tt.expected, buffer.String()) + }) + } +} diff --git a/pkg/cloud/report/result.go b/pkg/cloud/report/result.go new file mode 100644 index 000000000000..e7ff103845a4 --- /dev/null +++ b/pkg/cloud/report/result.go @@ -0,0 +1,37 @@ +package report + +import ( + "fmt" + "io" + + "github.com/liamg/tml" + + renderer "github.com/aquasecurity/trivy/pkg/report/table" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func writeResultsForARN(report *Report, results types.Results, output io.Writer, service, arn string, severities []dbTypes.Severity) error { + + // render scan title + _ = tml.Fprintf(output, "\nResults for '%s' (%s Account %s)\n\n", arn, report.Provider, report.AccountID) + + for _, result := range results { + var filtered []types.DetectedMisconfiguration + for _, misconfiguration := range result.Misconfigurations { + if arn != "" && misconfiguration.CauseMetadata.Resource != arn { + continue + } + if service != "" && misconfiguration.CauseMetadata.Service != service { + continue + } + filtered = append(filtered, misconfiguration) + } + if len(filtered) > 0 { + _, _ = fmt.Fprint(output, renderer.NewMisconfigRenderer(result, severities, false, false, true).Render()) + } + } + + return nil +} diff --git a/pkg/cloud/report/result_test.go b/pkg/cloud/report/result_test.go new file mode 100644 index 000000000000..e12f63b19fd8 --- /dev/null +++ b/pkg/cloud/report/result_test.go @@ -0,0 +1,82 @@ +package report + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/flag" +) + +func Test_ARNReport(t *testing.T) { + tests := []struct { + name string + options flag.Options + fromCache bool + expected string + }{ + { + name: "simple output", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"s3"}, + ARN: "arn:aws:s3:us-east-1:1234567890:bucket1", + Account: "1234567890", + }, + }, + fromCache: false, + expected: ` +Results for 'arn:aws:s3:us-east-1:1234567890:bucket1' (AWS Account 1234567890) + + +arn:aws:s3:us-east-1:1234567890:bucket1 (cloud) + +Tests: 1 (SUCCESSES: 0, FAILURES: 1, EXCEPTIONS: 0) +Failures: 1 (LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +HIGH: something failed +════════════════════════════════════════ +Bad stuff is... bad + +See https://avd.aquasec.com/misconfig/avd-aws-9999 +──────────────────────────────────────── + + +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + report := New( + "AWS", + tt.options.AWSOptions.Account, + tt.options.AWSOptions.Region, + createTestResults(), + tt.options.AWSOptions.Services, + ) + + buffer := bytes.NewBuffer([]byte{}) + tt.options.Output = buffer + require.NoError(t, Write(report, tt.options, tt.fromCache)) + + assert.Equal(t, "AWS", report.Provider) + assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID) + assert.Equal(t, tt.options.AWSOptions.Region, report.Region) + assert.ElementsMatch(t, tt.options.AWSOptions.Services, report.ServicesInScope) + assert.Equal(t, tt.expected, strings.ReplaceAll(buffer.String(), "\r\n", "\n")) + }) + } +} diff --git a/pkg/cloud/report/service.go b/pkg/cloud/report/service.go new file mode 100644 index 000000000000..f334418a5afe --- /dev/null +++ b/pkg/cloud/report/service.go @@ -0,0 +1,86 @@ +package report + +import ( + "fmt" + "io" + "sort" + "strconv" + "time" + + "github.com/liamg/tml" + + "github.com/aquasecurity/table" + pkgReport "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +func writeServiceTable(report *Report, results types.Results, output io.Writer) error { + + t := table.New(output) + + t.SetHeaders("Service", "Misconfigurations", "Last Scanned") + t.AddHeaders("Service", "Critical", "High", "Medium", "Low", "Unknown", "Last Scanned") + t.SetRowLines(false) + t.SetHeaderVerticalAlignment(table.AlignBottom) + t.SetHeaderAlignment(table.AlignLeft, table.AlignCenter, table.AlignCenter, table.AlignCenter, table.AlignCenter, table.AlignCenter, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignRight, table.AlignRight, table.AlignRight, table.AlignRight, table.AlignRight, table.AlignLeft) + t.SetAutoMergeHeaders(true) + t.SetHeaderColSpans(0, 1, 5, 1) + + // map service -> severity -> count + grouped := make(map[string]map[string]int) + // set zero counts for all services + for _, service := range report.ServicesInScope { + grouped[service] = make(map[string]int) + } + for _, result := range results { + for _, misconfiguration := range result.Misconfigurations { + service := misconfiguration.CauseMetadata.Service + if _, ok := grouped[service]; !ok { + grouped[service] = make(map[string]int) + } + grouped[service][misconfiguration.Severity]++ + } + } + + var sortable []sortableRow + for service, severityCounts := range grouped { + sortable = append(sortable, sortableRow{ + name: service, + counts: severityCounts, + }) + } + sort.Slice(sortable, func(i, j int) bool { return sortable[i].name < sortable[j].name }) + for _, row := range sortable { + var lastScanned string + scanAgo := time.Since(report.Results[row.name].CreationTime).Truncate(time.Minute) + switch { + case scanAgo.Hours() >= 48: + lastScanned = fmt.Sprintf("%d days ago", int(scanAgo.Hours()/24)) + case scanAgo.Hours() > 1: + lastScanned = fmt.Sprintf("%d hours ago", int(scanAgo.Hours())) + case scanAgo.Minutes() > 1: + lastScanned = fmt.Sprintf("%d minutes ago", int(scanAgo.Minutes())) + default: + lastScanned = "just now" + } + + t.AddRow( + row.name, + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["CRITICAL"]), "CRITICAL"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["HIGH"]), "HIGH"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["MEDIUM"]), "MEDIUM"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["LOW"]), "LOW"), + pkgReport.ColorizeSeverity(strconv.Itoa(row.counts["UNKNOWN"]), "UNKNOWN"), + lastScanned, + ) + } + + // render scan title + _ = tml.Fprintf(output, "\nScan Overview for %s Account %s\n", report.Provider, report.AccountID) + + // render table + t.Render() + + return nil +} diff --git a/pkg/cloud/report/service_test.go b/pkg/cloud/report/service_test.go new file mode 100644 index 000000000000..d357d4262a48 --- /dev/null +++ b/pkg/cloud/report/service_test.go @@ -0,0 +1,407 @@ +package report + +import ( + "bytes" + "testing" + + "github.com/aquasecurity/trivy-db/pkg/types" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" + + "github.com/stretchr/testify/assert" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + + "github.com/aquasecurity/defsec/pkg/scan" + defsecTypes "github.com/aquasecurity/defsec/pkg/types" +) + +func Test_ServiceReport(t *testing.T) { + tests := []struct { + name string + options flag.Options + fromCache bool + expected string + }{ + { + name: "simple table output", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + }, + fromCache: false, + expected: ` +Scan Overview for AWS Account +┌─────────┬──────────────────────────────────────────────────┬──────────────┐ +│ │ Misconfigurations │ │ +│ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ +│ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │ +├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼──────────────┤ +│ ec2 │ 0 │ 1 │ 0 │ 0 │ 0 │ just now │ +│ s3 │ 0 │ 3 │ 0 │ 0 │ 0 │ just now │ +└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴──────────────┘ +`, + }, + { + name: "results from cache", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + }, + fromCache: true, + expected: ` +Scan Overview for AWS Account +┌─────────┬──────────────────────────────────────────────────┬──────────────┐ +│ │ Misconfigurations │ │ +│ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ +│ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │ +├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼──────────────┤ +│ ec2 │ 0 │ 1 │ 0 │ 0 │ 0 │ just now │ +│ s3 │ 0 │ 3 │ 0 │ 0 │ 0 │ just now │ +└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴──────────────┘ + +This scan report was loaded from cached results. If you'd like to run a fresh scan, use --update-cache. +`, + }, + { + name: "filter severities", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityMedium, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"s3", "ec2"}, + }, + }, + fromCache: false, + expected: ` +Scan Overview for AWS Account +┌─────────┬──────────────────────────────────────────────────┬──────────────┐ +│ │ Misconfigurations │ │ +│ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ +│ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │ +├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼──────────────┤ +│ ec2 │ 0 │ 0 │ 0 │ 0 │ 0 │ just now │ +│ s3 │ 0 │ 0 │ 0 │ 0 │ 0 │ just now │ +└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴──────────────┘ +`, + }, + { + name: "scoped services without results", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: tableFormat, + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + AWSOptions: flag.AWSOptions{ + Services: []string{"ec2", "s3", "iam"}, + }, + }, + fromCache: false, + expected: ` +Scan Overview for AWS Account +┌─────────┬──────────────────────────────────────────────────┬──────────────┐ +│ │ Misconfigurations │ │ +│ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ +│ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │ +├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼──────────────┤ +│ ec2 │ 0 │ 1 │ 0 │ 0 │ 0 │ just now │ +│ iam │ 0 │ 0 │ 0 │ 0 │ 0 │ just now │ +│ s3 │ 0 │ 3 │ 0 │ 0 │ 0 │ just now │ +└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴──────────────┘ +`, + }, + { + name: "json output", + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: "json", + Severities: []types.Severity{ + types.SeverityLow, + types.SeverityMedium, + types.SeverityHigh, + types.SeverityCritical, + }, + }, + }, + fromCache: false, + expected: `{ + "ArtifactType": "aws_account", + "Metadata": { + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "arn:aws:ec2:us-east-1:1234567890:instance1", + "Class": "config", + "Type": "cloud", + "MisconfSummary": { + "Successes": 0, + "Failures": 1, + "Exceptions": 0 + }, + "Misconfigurations": [ + { + "Type": "AWS", + "ID": "AVD-AWS-9999", + "Title": "Do not use bad stuff", + "Description": "Bad stuff is... bad", + "Message": "instance is bad", + "Resolution": "Remove bad stuff", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/avd-aws-9999", + "References": [ + "https://avd.aquasec.com/misconfig/avd-aws-9999" + ], + "Status": "FAIL", + "Layer": {}, + "CauseMetadata": { + "Resource": "arn:aws:ec2:us-east-1:1234567890:instance1", + "Provider": "AWS", + "Service": "ec2", + "Code": { + "Lines": null + } + } + } + ] + }, + { + "Target": "arn:aws:s3:us-east-1:1234567890:bucket1", + "Class": "config", + "Type": "cloud", + "MisconfSummary": { + "Successes": 0, + "Failures": 1, + "Exceptions": 0 + }, + "Misconfigurations": [ + { + "Type": "AWS", + "ID": "AVD-AWS-9999", + "Title": "Do not use bad stuff", + "Description": "Bad stuff is... bad", + "Message": "something failed", + "Resolution": "Remove bad stuff", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/avd-aws-9999", + "References": [ + "https://avd.aquasec.com/misconfig/avd-aws-9999" + ], + "Status": "FAIL", + "Layer": {}, + "CauseMetadata": { + "Resource": "arn:aws:s3:us-east-1:1234567890:bucket1", + "Provider": "AWS", + "Service": "s3", + "Code": { + "Lines": null + } + } + } + ] + }, + { + "Target": "arn:aws:s3:us-east-1:1234567890:bucket2", + "Class": "config", + "Type": "cloud", + "MisconfSummary": { + "Successes": 0, + "Failures": 2, + "Exceptions": 0 + }, + "Misconfigurations": [ + { + "Type": "AWS", + "ID": "AVD-AWS-9999", + "Title": "Do not use bad stuff", + "Description": "Bad stuff is... bad", + "Message": "something else failed", + "Resolution": "Remove bad stuff", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/avd-aws-9999", + "References": [ + "https://avd.aquasec.com/misconfig/avd-aws-9999" + ], + "Status": "FAIL", + "Layer": {}, + "CauseMetadata": { + "Resource": "arn:aws:s3:us-east-1:1234567890:bucket2", + "Provider": "AWS", + "Service": "s3", + "Code": { + "Lines": null + } + } + }, + { + "Type": "AWS", + "ID": "AVD-AWS-9999", + "Title": "Do not use bad stuff", + "Description": "Bad stuff is... bad", + "Message": "something else failed again", + "Resolution": "Remove bad stuff", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/avd-aws-9999", + "References": [ + "https://avd.aquasec.com/misconfig/avd-aws-9999" + ], + "Status": "FAIL", + "Layer": {}, + "CauseMetadata": { + "Resource": "arn:aws:s3:us-east-1:1234567890:bucket2", + "Provider": "AWS", + "Service": "s3", + "Code": { + "Lines": null + } + } + } + ] + }, + { + "Target": "arn:aws:s3:us-east-1:1234567890:bucket3", + "Class": "config", + "Type": "cloud", + "MisconfSummary": { + "Successes": 1, + "Failures": 0, + "Exceptions": 0 + } + } + ] +}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + report := New( + "AWS", + tt.options.AWSOptions.Account, + tt.options.AWSOptions.Region, + createTestResults(), + tt.options.AWSOptions.Services, + ) + + buffer := bytes.NewBuffer([]byte{}) + tt.options.Output = buffer + require.NoError(t, Write(report, tt.options, tt.fromCache)) + + assert.Equal(t, "AWS", report.Provider) + assert.Equal(t, tt.options.AWSOptions.Account, report.AccountID) + assert.Equal(t, tt.options.AWSOptions.Region, report.Region) + assert.ElementsMatch(t, tt.options.AWSOptions.Services, report.ServicesInScope) + if tt.options.Format == "json" { + // json output can be formatted/ordered differently - we just care that the data matches + assert.JSONEq(t, tt.expected, buffer.String()) + } else { + assert.Equal(t, tt.expected, buffer.String()) + } + }) + } +} + +func createTestResults() scan.Results { + + baseRule := scan.Rule{ + AVDID: "AVD-AWS-9999", + Aliases: []string{"AWS999"}, + ShortCode: "no-bad-stuff", + Summary: "Do not use bad stuff", + Explanation: "Bad stuff is... bad", + Impact: "Bad things", + Resolution: "Remove bad stuff", + Provider: "AWS", + Severity: "HIGH", + } + + var s3Results scan.Results + s3Results.Add( + "something failed", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket1", + }).String()), + ) + s3Results.Add( + "something else failed", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket2", + }).String()), + ) + s3Results.Add( + "something else failed again", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket2", + }).String()), + ) + s3Results.AddPassed( + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "s3", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "bucket3", + }).String()), + ) + baseRule.Service = "s3" + s3Results.SetRule(baseRule) + var ec2Results scan.Results + ec2Results.Add( + "instance is bad", + defsecTypes.NewRemoteMetadata((arn.ARN{ + Partition: "aws", + Service: "ec2", + Region: "us-east-1", + AccountID: "1234567890", + Resource: "instance1", + }).String()), + ) + baseRule.Service = "ec2" + ec2Results.SetRule(baseRule) + return append(s3Results, ec2Results...) +} diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 89251033faf2..4994e108aeb9 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -6,12 +6,17 @@ import ( "fmt" "io" "os" + "strings" + "time" + + awsScanner "github.com/aquasecurity/defsec/pkg/scanners/cloud/aws" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/metadata" + awscommands "github.com/aquasecurity/trivy/pkg/cloud/aws/commands" "github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/commands/server" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" @@ -81,6 +86,7 @@ func NewApp(version string) *cobra.Command { NewKubernetesCommand(globalFlags), NewSBOMCommand(globalFlags), NewVersionCommand(globalFlags), + NewAWSCommand(globalFlags), ) rootCmd.AddCommand(loadPluginCommands()...) @@ -787,6 +793,66 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { return cmd } +func NewAWSCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + + awsFlags := &flag.Flags{ + AWSFlagGroup: flag.NewAWSFlagGroup(), + CloudFlagGroup: flag.NewCloudFlagGroup(), + MisconfFlagGroup: flag.NewMisconfFlagGroup(), + ReportFlagGroup: flag.NewReportFlagGroup(), + } + + services := awsScanner.AllSupportedServices() + + cmd := &cobra.Command{ + Use: "aws [flags]", + Aliases: []string{}, + Args: cobra.ExactArgs(0), + Short: "scan aws account", + Long: fmt.Sprintf(`Scan an AWS account for misconfigurations. Trivy uses the same authentication methods as the AWS CLI. See https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html + +The following services are supported: +- %s +`, strings.Join(services, "\n- ")), + Example: ` # basic scanning + $ trivy aws --region us-east-1 + + # limit scan to a single service: + $ trivy aws --region us-east-1 --service s3 + + # limit scan to multiple services: + $ trivy aws --region us-east-1 --service s3 --service ec2 + + # force refresh of cache for fresh results + $ trivy aws --region us-east-1 --update-cache +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := awsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts, err := awsFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + if opts.Timeout < time.Hour { + opts.Timeout = time.Hour + log.Logger.Debug("Timeout is set to less than 1 hour - upgrading to 1 hour for this command.") + } + return awscommands.Run(cmd.Context(), opts) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + awsFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, awsFlags.Usages(cmd))) + + return cmd +} + func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { reportFlagGroup := flag.NewReportFlagGroup() reportFlagGroup.DependencyTree = nil // disable '--dependency-tree' diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index f73c4ef9ccab..52bb0e08898a 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -533,7 +533,7 @@ func scan(ctx context.Context, opts flag.Options, initializeScanner InitializeSc report, err := s.ScanArtifact(ctx, scanOptions) if err != nil { - return types.Report{}, xerrors.Errorf("image scan failed: %w", err) + return types.Report{}, xerrors.Errorf("scan failed: %w", err) } return report, nil } diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go index a424f8e5f71e..c2699a014dc2 100644 --- a/pkg/fanal/types/artifact.go +++ b/pkg/fanal/types/artifact.go @@ -95,6 +95,7 @@ const ( ArtifactFilesystem ArtifactType = "filesystem" ArtifactRemoteRepository ArtifactType = "repository" ArtifactCycloneDX ArtifactType = "cyclonedx" + ArtifactAWSAccount ArtifactType = "aws_account" ) // ArtifactReference represents a reference of container image, local filesystem and repository diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index 5b1291097f7e..6594fd936fcb 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -38,6 +38,7 @@ const ( Ansible = "ansible" Helm = "helm" Rbac = "rbac" + Cloud = "cloud" // Licensing License = "license" diff --git a/pkg/flag/aws_flags.go b/pkg/flag/aws_flags.go new file mode 100644 index 000000000000..2ccb742a52cc --- /dev/null +++ b/pkg/flag/aws_flags.go @@ -0,0 +1,78 @@ +package flag + +var ( + awsRegionFlag = Flag{ + Name: "region", + ConfigName: "cloud.aws.region", + Value: "", + Usage: "AWS Region to scan", + } + awsEndpointFlag = Flag{ + Name: "endpoint", + ConfigName: "cloud.aws.endpoint", + Value: "", + Usage: "AWS Endpoint override", + } + awsServiceFlag = Flag{ + Name: "service", + ConfigName: "cloud.aws.service", + Value: []string{}, + Usage: "Only scan AWS Service(s) specified with this flag. Can specify multiple services using --service A --service B etc.", + } + awsAccountFlag = Flag{ + Name: "account", + ConfigName: "cloud.aws.account", + Value: "", + Usage: "The AWS account to scan. It's useful to specify this when reviewing cached results for multipel accounts.", + } + awsARNFlag = Flag{ + Name: "arn", + ConfigName: "cloud.aws.arn", + Value: "", + Usage: "The AWS ARN to show results for. Useful to filter results once a scan is cached.", + } +) + +type AWSFlagGroup struct { + Region *Flag + Endpoint *Flag + Services *Flag + Account *Flag + ARN *Flag +} + +type AWSOptions struct { + Region string + Endpoint string + Services []string + Account string + ARN string +} + +func NewAWSFlagGroup() *AWSFlagGroup { + return &AWSFlagGroup{ + Region: &awsRegionFlag, + Endpoint: &awsEndpointFlag, + Services: &awsServiceFlag, + Account: &awsAccountFlag, + ARN: &awsARNFlag, + } +} + +func (f *AWSFlagGroup) Name() string { + return "AWS" +} + +func (f *AWSFlagGroup) Flags() []*Flag { + return []*Flag{f.Region, f.Endpoint, f.Services, f.Account, f.ARN} +} + +func (f *AWSFlagGroup) ToOptions() AWSOptions { + return AWSOptions{ + Region: getString(f.Region), + Endpoint: getString(f.Endpoint), + Services: getStringSlice(f.Services), + Account: getString(f.Account), + ARN: getString(f.ARN), + } +} diff --git a/pkg/flag/cloud_flags.go b/pkg/flag/cloud_flags.go new file mode 100644 index 000000000000..4be13e9a81ec --- /dev/null +++ b/pkg/flag/cloud_flags.go @@ -0,0 +1,50 @@ +package flag + +import "time" + +var ( + cloudUpdateCacheFlag = Flag{ + Name: "update-cache", + ConfigName: "cloud.update-cache", + Value: false, + Usage: "Update the cache for the applicable cloud provider instead of using cached results.", + } + cloudMaxCacheAgeFlag = Flag{ + Name: "max-cache-age", + ConfigName: "cloud.max-cache-age", + Value: time.Hour * 24, + Usage: "The maximum age of the cloud cache. Cached data will be requeried from the cloud provider if it is older than this.", + } +) + +type CloudFlagGroup struct { + UpdateCache *Flag + MaxCacheAge *Flag +} + +type CloudOptions struct { + MaxCacheAge time.Duration + UpdateCache bool +} + +func NewCloudFlagGroup() *CloudFlagGroup { + return &CloudFlagGroup{ + UpdateCache: &cloudUpdateCacheFlag, + MaxCacheAge: &cloudMaxCacheAgeFlag, + } +} + +func (f *CloudFlagGroup) Name() string { + return "Cloud" +} + +func (f *CloudFlagGroup) Flags() []*Flag { + return []*Flag{f.UpdateCache, f.MaxCacheAge} +} + +func (f *CloudFlagGroup) ToOptions() CloudOptions { + return CloudOptions{ + UpdateCache: getBool(f.UpdateCache), + MaxCacheAge: getDuration(f.MaxCacheAge), + } +} diff --git a/pkg/flag/options.go b/pkg/flag/options.go index 9bd842c21859..a05ca131cd15 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -46,7 +46,9 @@ type FlagGroup interface { } type Flags struct { + AWSFlagGroup *AWSFlagGroup CacheFlagGroup *CacheFlagGroup + CloudFlagGroup *CloudFlagGroup DBFlagGroup *DBFlagGroup ImageFlagGroup *ImageFlagGroup K8sFlagGroup *K8sFlagGroup @@ -64,7 +66,9 @@ type Flags struct { // Options holds all the runtime configuration type Options struct { GlobalOptions + AWSOptions CacheOptions + CloudOptions DBOptions ImageOptions K8sOptions @@ -221,6 +225,12 @@ func (f *Flags) groups() []FlagGroup { if f.LicenseFlagGroup != nil { groups = append(groups, f.LicenseFlagGroup) } + if f.CloudFlagGroup != nil { + groups = append(groups, f.CloudFlagGroup) + } + if f.AWSFlagGroup != nil { + groups = append(groups, f.AWSFlagGroup) + } if f.K8sFlagGroup != nil { groups = append(groups, f.K8sFlagGroup) } @@ -279,6 +289,7 @@ func (f *Flags) Bind(cmd *cobra.Command) error { return nil } +//nolint: gocyclo func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalFlagGroup, output io.Writer) (Options, error) { var err error opts := Options{ @@ -286,6 +297,14 @@ func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalF GlobalOptions: globalFlags.ToOptions(), } + if f.AWSFlagGroup != nil { + opts.AWSOptions = f.AWSFlagGroup.ToOptions() + } + + if f.CloudFlagGroup != nil { + opts.CloudOptions = f.CloudFlagGroup.ToOptions() + } + if f.CacheFlagGroup != nil { opts.CacheOptions, err = f.CacheFlagGroup.ToOptions() if err != nil {