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 {