feat: Add AWS Cloud scanning (#2493)

* feat: Added AWS Cloud scanning

Co-authored-by: Owen Rumney <owen.rumney@aquasec.com>
This commit is contained in:
Liam Galvin
2022-08-11 14:59:32 +01:00
committed by GitHub
parent f8edda8479
commit b259b25ce4
32 changed files with 2569 additions and 50 deletions

3
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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`.)

View File

@@ -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

BIN
docs/imgs/trivy-aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

73
go.mod
View File

@@ -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

150
go.sum
View File

@@ -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=

View File

@@ -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

View File

@@ -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
}

View File

@@ -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...")
}

View File

@@ -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
}

65
pkg/cloud/cache/cache.go vendored Normal file
View File

@@ -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)
}

166
pkg/cloud/cache/cache_test.go vendored Normal file
View File

@@ -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))
}

59
pkg/cloud/cache/load.go vendored Normal file
View File

@@ -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
}

77
pkg/cloud/cache/save.go vendored Normal file
View File

@@ -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)
}

24
pkg/cloud/cache/schema.go vendored Normal file
View File

@@ -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"`
}

5
pkg/cloud/provider.go Normal file
View File

@@ -0,0 +1,5 @@
package cloud
const (
ProviderAWS = "AWS"
)

View File

@@ -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
}

View File

@@ -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)
}
}

175
pkg/cloud/report/report.go Normal file
View File

@@ -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, "\n<blue>This scan report was loaded from cached results. If you'd like to run a fresh scan, use --update-cache.</blue>\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)
}

View File

@@ -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, "\n<bold>Resource Summary for Service '%s' (%s Account %s)</bold>\n", service, report.Provider, report.AccountID)
// render table
if len(sortable) > 0 {
t.Render()
} else {
_, _ = fmt.Fprint(output, "\nNo problems detected.\n")
}
return nil
}

View File

@@ -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())
})
}
}

View File

@@ -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, "\n<bold>Results for '%s' (%s Account %s)</bold>\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
}

View File

@@ -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"))
})
}
}

View File

@@ -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, "\n<bold>Scan Overview for %s Account %s</bold>\n", report.Provider, report.AccountID)
// render table
t.Render()
return nil
}

View File

@@ -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...)
}

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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

View File

@@ -38,6 +38,7 @@ const (
Ansible = "ansible"
Helm = "helm"
Rbac = "rbac"
Cloud = "cloud"
// Licensing
License = "license"

78
pkg/flag/aws_flags.go Normal file
View File

@@ -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),
}
}

50
pkg/flag/cloud_flags.go Normal file
View File

@@ -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),
}
}

View File

@@ -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 {