├── .github ├── release.yml └── workflows │ ├── ci.yml │ └── tagpr.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .octocov.yml ├── .tagpr ├── CHANGELOG.md ├── CREDITS ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── apply.go ├── export.go ├── notify.go ├── plan.go └── root.go ├── go.mod ├── go.sum ├── main.go ├── sechub ├── apply.go ├── fetch.go ├── finding.go ├── finding_test.go ├── notify.go ├── notify_test.go ├── plan.go ├── sechub.go ├── sechub_test.go ├── testdata │ ├── change_header.golden │ ├── change_message.golden │ ├── notify_critical.golden │ ├── use_custom_template.golden │ └── use_default_template.golden ├── yaml.go └── yaml_test.go └── version └── version.go /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - tagpr 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | job-test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out source code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: go.mod 21 | 22 | - name: Run test 23 | run: make ci 24 | 25 | - name: Run octocov 26 | uses: k1LoW/octocov-action@v1 27 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yml: -------------------------------------------------------------------------------- 1 | name: tagpr 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | tagpr: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | tagpr-tag: ${{ steps.run-tagpr.outputs.tag }} 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | steps: 15 | - name: Check out source code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: go.mod 22 | 23 | - id: run-tagpr 24 | name: Run tagpr 25 | uses: Songmu/tagpr@v1 26 | 27 | release: 28 | needs: tagpr 29 | if: needs.tagpr.outputs.tagpr-tag != '' 30 | runs-on: macos-latest 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | steps: 34 | - name: Check out source code 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version-file: go.mod 43 | 44 | - name: Setup 45 | run: brew install goreleaser 46 | 47 | - name: Release 48 | run: make release 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage.out 3 | control-controls 4 | *.yml 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | fast: false 3 | enable: 4 | - gosec 5 | linters-settings: 6 | staticcheck: 7 | go: 1.17 8 | issues: 9 | exclude: 10 | - SA3000 11 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod download 5 | - go mod tidy 6 | builds: 7 | - 8 | id: control-controls-darwin 9 | ldflags: 10 | - -s -w -X github.com/pepabo/control-controls.version={{.Version}} -X github.com/pepabo/control-controls.commit={{.FullCommit}} -X github.com/pepabo/control-controls.date={{.Date}} -X github.com/pepabo/control-controls/version.Version={{.Version}} 11 | goos: 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | - 17 | id: control-controls-linux 18 | ldflags: 19 | - -s -w -X github.com/pepabo/control-controls.version={{.Version}} -X github.com/pepabo/control-controls.commit={{.FullCommit}} -X github.com/pepabo/control-controls.date={{.Date}} -X github.com/pepabo/control-controls/version.Version={{.Version}} 20 | goos: 21 | - linux 22 | goarch: 23 | - amd64 24 | - arm64 25 | archives: 26 | - 27 | id: control-controls-archive 28 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 29 | format_overrides: 30 | - goos: darwin 31 | format: zip 32 | files: 33 | - CREDITS 34 | - README.md 35 | - CHANGELOG.md 36 | checksum: 37 | name_template: 'checksums.txt' 38 | snapshot: 39 | name_template: "{{ .Version }}-next" 40 | changelog: 41 | sort: asc 42 | filters: 43 | exclude: 44 | - '^docs:' 45 | - '^test:' 46 | nfpms: 47 | - 48 | id: control-controls-nfpms 49 | file_name_template: "{{ .ProjectName }}_{{ .Version }}-1_{{ .Arch }}" 50 | builds: 51 | - control-controls-linux 52 | homepage: https://github.com/pepabo/control-controls 53 | maintainer: 'GMO Pepabo, inc.' 54 | description: control-controls control controls of AWS Security Hub across all regions. 55 | license: MIT 56 | formats: 57 | - deb 58 | - rpm 59 | - apk 60 | bindir: /usr/bin 61 | epoch: 1 62 | -------------------------------------------------------------------------------- /.octocov.yml: -------------------------------------------------------------------------------- 1 | # generated by octocov init 2 | coverage: 3 | if: true 4 | codeToTestRatio: 5 | code: 6 | - '**/*.go' 7 | - '!**/*_test.go' 8 | test: 9 | - '**/*_test.go' 10 | testExecutionTime: 11 | if: true 12 | diff: 13 | datastores: 14 | - artifact://${GITHUB_REPOSITORY} 15 | comment: 16 | if: is_pull_request 17 | report: 18 | if: is_default_branch 19 | datastores: 20 | - artifact://${GITHUB_REPOSITORY} 21 | -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The pcpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.tmplate (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | [tagpr] 33 | vPrefix = true 34 | releaseBranch = main 35 | versionFile = version/version.go 36 | command = "make prerelease_for_tagpr" 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [v0.8.4](https://github.com/pepabo/control-controls/compare/v0.8.3...v0.8.4) - 2025-04-16 2 | - introduce the --dryrun option to the notify command by @hiboma in https://github.com/pepabo/control-controls/pull/43 3 | 4 | ## [v0.8.3](https://github.com/pepabo/control-controls/compare/v0.8.2...v0.8.3) - 2024-10-02 5 | 6 | ## [v0.8.2](https://github.com/pepabo/control-controls/compare/v0.8.1...v0.8.2) - 2024-10-02 7 | - Fix nil pointer dereference in ctrl.DisabledReason by @k1LoW in https://github.com/pepabo/control-controls/pull/40 8 | 9 | ## [v0.8.1](https://github.com/pepabo/control-controls/compare/v0.8.0...v0.8.1) - 2023-05-12 10 | 11 | ## [v0.8.0](https://github.com/pepabo/control-controls/compare/v0.7.0...v0.8.0) - 2023-05-12 12 | - (ref #34) feat: Add improve handling of ControlFindingGenerator by @htnosm in https://github.com/pepabo/control-controls/pull/35 13 | 14 | ## [v0.7.0](https://github.com/pepabo/control-controls/compare/v0.6.6...v0.7.0) - 2023-01-12 15 | - Update packages by @k1LoW in https://github.com/pepabo/control-controls/pull/31 16 | - Target only findings whose RecordState is ACTIVE. by @k1LoW in https://github.com/pepabo/control-controls/pull/33 17 | 18 | ## [v0.6.6](https://github.com/pepabo/control-controls/compare/v0.6.5...v0.6.6) - 2022-10-07 19 | - Add params for time condition by @k1LoW in https://github.com/pepabo/control-controls/pull/29 20 | 21 | ## [v0.6.5](https://github.com/pepabo/control-controls/compare/v0.6.4...v0.6.5) - 2022-10-07 22 | - Fix defaultTemplate and Add `message:` by @k1LoW in https://github.com/pepabo/control-controls/pull/27 23 | 24 | ## [v0.6.4](https://github.com/pepabo/control-controls/compare/v0.6.3...v0.6.4) - 2022-10-07 25 | - Change `cond:` to `if:` by @k1LoW in https://github.com/pepabo/control-controls/pull/23 26 | - Add `header:` to customize header only by @k1LoW in https://github.com/pepabo/control-controls/pull/25 27 | - Fix field name by @k1LoW in https://github.com/pepabo/control-controls/pull/26 28 | 29 | ## [v0.6.3](https://github.com/pepabo/control-controls/compare/v0.6.2...v0.6.3) - 2022-10-06 30 | - Expand env when load YAML by @k1LoW in https://github.com/pepabo/control-controls/pull/20 31 | - Fix defaultTemplate by @k1LoW in https://github.com/pepabo/control-controls/pull/22 32 | 33 | ## [v0.6.2](https://github.com/pepabo/control-controls/compare/v0.6.1...v0.6.2) - 2022-10-05 34 | - Remove homebrew-tap setting because updates in the homebrew-tap repository by @k1LoW in https://github.com/pepabo/control-controls/pull/15 35 | - Use tagpr by @k1LoW in https://github.com/pepabo/control-controls/pull/16 36 | - Support notification by @k1LoW in https://github.com/pepabo/control-controls/pull/18 37 | - Bump up go version by @k1LoW in https://github.com/pepabo/control-controls/pull/19 38 | 39 | ## [v0.6.1](https://github.com/pepabo/control-controls/compare/v0.6.0...v0.6.1) (2022-06-17) 40 | 41 | * Fix handling non region arn (eg. `arn:aws:s3:::` ) [#14](https://github.com/pepabo/control-controls/pull/14) ([k1LoW](https://github.com/k1LoW)) 42 | 43 | ## [v0.6.0](https://github.com/pepabo/control-controls/compare/v0.5.0...v0.6.0) (2022-06-16) 44 | 45 | * Support workflow status (and note) management [#13](https://github.com/pepabo/control-controls/pull/13) ([k1LoW](https://github.com/k1LoW)) 46 | 47 | ## [v0.5.0](https://github.com/pepabo/control-controls/compare/v0.4.0...v0.5.0) (2022-06-09) 48 | 49 | * Add Validate() [#12](https://github.com/pepabo/control-controls/pull/12) ([k1LoW](https://github.com/k1LoW)) 50 | 51 | ## [v0.4.0](https://github.com/pepabo/control-controls/compare/v0.3.0...v0.4.0) (2022-06-08) 52 | 53 | * Add `--overlay` option for patch [#11](https://github.com/pepabo/control-controls/pull/11) ([k1LoW](https://github.com/k1LoW)) 54 | 55 | ## [v0.3.0](https://github.com/pepabo/control-controls/compare/v0.2.1...v0.3.0) (2022-06-07) 56 | 57 | * Fix flag [#10](https://github.com/pepabo/control-controls/pull/10) ([k1LoW](https://github.com/k1LoW)) 58 | * Add reason of disabled in the configuration file. [#9](https://github.com/pepabo/control-controls/pull/9) ([k1LoW](https://github.com/k1LoW)) 59 | 60 | ## [v0.2.1](https://github.com/pepabo/control-controls/compare/v0.2.0...v0.2.1) (2022-04-18) 61 | 62 | * Fix nil pointer dereference [#8](https://github.com/pepabo/control-controls/pull/8) ([k1LoW](https://github.com/k1LoW)) 63 | 64 | ## [v0.2.0](https://github.com/pepabo/control-controls/compare/v0.1.2...v0.2.0) (2022-04-18) 65 | 66 | * exit status 2 when plan diff is not empty [#7](https://github.com/pepabo/control-controls/pull/7) ([k1LoW](https://github.com/k1LoW)) 67 | 68 | ## [v0.1.2](https://github.com/pepabo/control-controls/compare/v0.1.1...v0.1.2) (2022-04-15) 69 | 70 | * Fix contextcopy bug [#6](https://github.com/pepabo/control-controls/pull/6) ([k1LoW](https://github.com/k1LoW)) 71 | 72 | ## [v0.1.1](https://github.com/pepabo/control-controls/compare/v0.1.0...v0.1.1) (2022-04-15) 73 | 74 | * Fix sechub.Override behavior [#5](https://github.com/pepabo/control-controls/pull/5) ([k1LoW](https://github.com/k1LoW)) 75 | 76 | ## [v0.1.0](https://github.com/pepabo/control-controls/compare/60006830255c...v0.1.0) (2022-04-14) 77 | 78 | * Add option `--disabled-reason` [#4](https://github.com/pepabo/control-controls/pull/4) ([k1LoW](https://github.com/k1LoW)) 79 | * Add command `plan` [#3](https://github.com/pepabo/control-controls/pull/3) ([k1LoW](https://github.com/k1LoW)) 80 | * Fix apply [#2](https://github.com/pepabo/control-controls/pull/2) ([k1LoW](https://github.com/k1LoW)) 81 | * Add command `apply` [#1](https://github.com/pepabo/control-controls/pull/1) ([k1LoW](https://github.com/k1LoW)) 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 GMO Pepabo, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG = github.com/pepabo/control-controls 2 | COMMIT = $$(git describe --tags --always) 3 | OSNAME=${shell uname -s} 4 | ifeq ($(OSNAME),Darwin) 5 | DATE = $$(gdate --utc '+%Y-%m-%d_%H:%M:%S') 6 | else 7 | DATE = $$(date --utc '+%Y-%m-%d_%H:%M:%S') 8 | endif 9 | 10 | export GO111MODULE=on 11 | 12 | BUILD_LDFLAGS = -X $(PKG).commit=$(COMMIT) -X $(PKG).date=$(DATE) 13 | 14 | default: test 15 | 16 | ci: depsdev test sec 17 | 18 | test: 19 | go test ./... -coverprofile=coverage.out -covermode=count 20 | 21 | sec: 22 | gosec ./... 23 | 24 | lint: 25 | golangci-lint run ./... 26 | 27 | build: 28 | go build -ldflags="$(BUILD_LDFLAGS)" 29 | 30 | depsdev: 31 | go install github.com/Songmu/ghch/cmd/ghch@v0.10.2 32 | go install github.com/Songmu/gocredits/cmd/gocredits@v0.2.0 33 | go install github.com/securego/gosec/v2/cmd/gosec@latest 34 | 35 | prerelease: 36 | git pull origin main --tag 37 | go mod tidy 38 | ghch -w -N ${VER} 39 | gocredits -w 40 | git add CHANGELOG.md CREDITS go.mod go.sum 41 | git commit -m'Bump up version number' 42 | git tag ${VER} 43 | 44 | prerelease_for_tagpr: 45 | gocredits -w . 46 | git add CHANGELOG.md CREDITS go.mod go.sum 47 | 48 | release: 49 | git push origin main --tag 50 | goreleaser --clean 51 | 52 | .PHONY: default test 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # control-controls 2 | 3 | control-controls control controls of AWS Security Hub across all regions. 4 | 5 | ## Usage 6 | 7 | Export current security standards controls as a controls.yml. 8 | 9 | ``` console 10 | $ control-controls export > controls.yml 11 | 2022-04-14T15:08:59+09:00 INF Fetching controls from eu-north-1 12 | 2022-04-14T15:09:04+09:00 INF Fetching controls from ap-south-1 13 | 2022-04-14T15:09:07+09:00 INF Fetching controls from eu-west-3 14 | 2022-04-14T15:09:12+09:00 INF Fetching controls from eu-west-2 15 | 2022-04-14T15:09:16+09:00 INF Fetching controls from eu-west-1 16 | 2022-04-14T15:09:21+09:00 INF Fetching controls from ap-northeast-3 17 | 2022-04-14T15:09:22+09:00 INF Fetching controls from ap-northeast-2 18 | 2022-04-14T15:09:24+09:00 INF Fetching controls from ap-northeast-1 19 | 2022-04-14T15:09:25+09:00 INF Fetching controls from sa-east-1 20 | 2022-04-14T15:09:30+09:00 INF Fetching controls from ca-central-1 21 | 2022-04-14T15:09:34+09:00 INF Fetching controls from ap-southeast-1 22 | 2022-04-14T15:09:36+09:00 INF Fetching controls from ap-southeast-2 23 | 2022-04-14T15:09:39+09:00 INF Fetching controls from eu-central-1 24 | 2022-04-14T15:09:43+09:00 INF Fetching controls from us-east-1 25 | 2022-04-14T15:09:47+09:00 INF Fetching controls from us-east-2 26 | 2022-04-14T15:09:50+09:00 INF Fetching controls from us-west-1 27 | 2022-04-14T15:09:53+09:00 INF Fetching controls from us-west-2 28 | $ 29 | ``` 30 | 31 |
32 | 33 | exported controls.yml is here 34 | 35 | ``` yaml 36 | autoEnable: true 37 | standards: 38 | aws-foundational-security-best-practices/v/1.0.0: 39 | enable: true 40 | controls: 41 | enable: [APIGateway.5, AutoScaling.1, AutoScaling.2, CloudTrail.1, CloudTrail.2, CloudTrail.4, CloudTrail.5, Config.1, DynamoDB.1, EC2.19, EC2.2, EC2.21, EC2.6, ECR.3, ELB.10, ELB.5, ELB.7, ES.4, ES.5, ES.6, ES.7, ES.8, IAM.1, IAM.2, IAM.3, IAM.5, IAM.6, IAM.7, IAM.8, NetworkFirewall.6, RDS.11, RDS.17, RDS.18, RDS.19, RDS.2, RDS.20, RDS.21, RDS.22, RDS.23, RDS.25, RDS.3, RDS.5, Redshift.4, Redshift.6, Redshift.8, S3.1, S3.10, S3.11, S3.12, S3.2, S3.3, S3.4, S3.5, S3.6, S3.9, SQS.1, SSM.1, SSM.4] 42 | cis-aws-foundations-benchmark/v/1.2.0: 43 | enable: true 44 | controls: 45 | enable: [CIS.1.1, CIS.1.10, CIS.1.11, CIS.1.13, CIS.1.14, CIS.1.16, CIS.1.2, CIS.1.22, CIS.1.3, CIS.1.4, CIS.1.5, CIS.1.6, CIS.1.7, CIS.1.8, CIS.1.9, CIS.2.1, CIS.2.2, CIS.2.3, CIS.2.4, CIS.2.5, CIS.2.6, CIS.2.7, CIS.2.8, CIS.2.9, CIS.3.1, CIS.3.10, CIS.3.11, CIS.3.12, CIS.3.13, CIS.3.14, CIS.3.2, CIS.3.3, CIS.3.4, CIS.3.5, CIS.3.6, CIS.3.7, CIS.3.8, CIS.3.9, CIS.4.3] 46 | pci-dss/v/3.2.1: 47 | enable: false 48 | regions: 49 | ap-northeast-1: 50 | standards: 51 | aws-foundational-security-best-practices/v/1.0.0: 52 | controls: 53 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 54 | cis-aws-foundations-benchmark/v/1.2.0: 55 | controls: 56 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 57 | ap-northeast-2: 58 | standards: 59 | aws-foundational-security-best-practices/v/1.0.0: 60 | controls: 61 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 62 | cis-aws-foundations-benchmark/v/1.2.0: 63 | controls: 64 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 65 | ap-northeast-3: 66 | standards: 67 | aws-foundational-security-best-practices/v/1.0.0: 68 | controls: 69 | enable: [RDS.16, RDS.24] 70 | ap-south-1: 71 | standards: 72 | aws-foundational-security-best-practices/v/1.0.0: 73 | controls: 74 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 75 | cis-aws-foundations-benchmark/v/1.2.0: 76 | controls: 77 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 78 | ap-southeast-1: 79 | standards: 80 | aws-foundational-security-best-practices/v/1.0.0: 81 | controls: 82 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 83 | cis-aws-foundations-benchmark/v/1.2.0: 84 | controls: 85 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 86 | ap-southeast-2: 87 | standards: 88 | aws-foundational-security-best-practices/v/1.0.0: 89 | controls: 90 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 91 | cis-aws-foundations-benchmark/v/1.2.0: 92 | controls: 93 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 94 | ca-central-1: 95 | standards: 96 | aws-foundational-security-best-practices/v/1.0.0: 97 | controls: 98 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 99 | cis-aws-foundations-benchmark/v/1.2.0: 100 | controls: 101 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 102 | eu-central-1: 103 | standards: 104 | aws-foundational-security-best-practices/v/1.0.0: 105 | controls: 106 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 107 | cis-aws-foundations-benchmark/v/1.2.0: 108 | controls: 109 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 110 | eu-north-1: 111 | standards: 112 | aws-foundational-security-best-practices/v/1.0.0: 113 | controls: 114 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 115 | cis-aws-foundations-benchmark/v/1.2.0: 116 | controls: 117 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 118 | eu-west-1: 119 | standards: 120 | aws-foundational-security-best-practices/v/1.0.0: 121 | controls: 122 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 123 | cis-aws-foundations-benchmark/v/1.2.0: 124 | controls: 125 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 126 | eu-west-2: 127 | standards: 128 | aws-foundational-security-best-practices/v/1.0.0: 129 | controls: 130 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 131 | cis-aws-foundations-benchmark/v/1.2.0: 132 | controls: 133 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 134 | eu-west-3: 135 | standards: 136 | aws-foundational-security-best-practices/v/1.0.0: 137 | controls: 138 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 139 | cis-aws-foundations-benchmark/v/1.2.0: 140 | controls: 141 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 142 | sa-east-1: 143 | standards: 144 | aws-foundational-security-best-practices/v/1.0.0: 145 | controls: 146 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.13, RDS.4, RDS.6, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 147 | cis-aws-foundations-benchmark/v/1.2.0: 148 | controls: 149 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 150 | us-east-1: 151 | standards: 152 | aws-foundational-security-best-practices/v/1.0.0: 153 | controls: 154 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CloudFront.1, CloudFront.2, CloudFront.3, CloudFront.4, CloudFront.5, CloudFront.6, CloudFront.7, CloudFront.8, CloudFront.9, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4, WAF.1] 155 | cis-aws-foundations-benchmark/v/1.2.0: 156 | controls: 157 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 158 | us-east-2: 159 | standards: 160 | aws-foundational-security-best-practices/v/1.0.0: 161 | controls: 162 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 163 | cis-aws-foundations-benchmark/v/1.2.0: 164 | controls: 165 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 166 | us-west-1: 167 | standards: 168 | aws-foundational-security-best-practices/v/1.0.0: 169 | controls: 170 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 171 | cis-aws-foundations-benchmark/v/1.2.0: 172 | controls: 173 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 174 | us-west-2: 175 | standards: 176 | aws-foundational-security-best-practices/v/1.0.0: 177 | controls: 178 | enable: [ACM.1, APIGateway.1, APIGateway.2, APIGateway.3, APIGateway.4, Autoscaling.5, CodeBuild.1, CodeBuild.2, CodeBuild.4, CodeBuild.5, DMS.1, DynamoDB.2, DynamoDB.3, EC2.1, EC2.10, EC2.15, EC2.16, EC2.17, EC2.18, EC2.20, EC2.22, EC2.3, EC2.4, EC2.7, EC2.8, EC2.9, ECS.1, ECS.2, EFS.1, EFS.2, ELB.2, ELB.3, ELB.4, ELB.6, ELB.8, ELB.9, ELBv2.1, EMR.1, ES.1, ES.2, ES.3, ElasticBeanstalk.1, ElasticBeanstalk.2, GuardDuty.1, IAM.21, IAM.4, KMS.1, KMS.2, KMS.3, Lambda.1, Lambda.2, Lambda.5, Opensearch.1, Opensearch.2, Opensearch.3, Opensearch.4, Opensearch.5, Opensearch.6, Opensearch.8, RDS.1, RDS.10, RDS.12, RDS.13, RDS.14, RDS.15, RDS.16, RDS.24, RDS.4, RDS.6, RDS.7, RDS.8, RDS.9, Redshift.1, Redshift.2, Redshift.3, Redshift.7, S3.8, SNS.1, SSM.2, SSM.3, SageMaker.1, SecretsManager.1, SecretsManager.2, SecretsManager.3, SecretsManager.4] 179 | cis-aws-foundations-benchmark/v/1.2.0: 180 | controls: 181 | enable: [CIS.1.12, CIS.1.20, CIS.4.1, CIS.4.2] 182 | ``` 183 | 184 |
185 | 186 | For example, disable controls (Redshift.4, Redshift.6, Redshift.8). 187 | 188 | ``` yaml 189 | autoEnable: true 190 | standards: 191 | aws-foundational-security-best-practices/v/1.0.0: 192 | enable: true 193 | controls: 194 | enable: [APIGateway.5, AutoScaling.1, AutoScaling.2, CloudTrail.1, CloudTrail.2, CloudTrail.4, CloudTrail.5, Config.1, DynamoDB.1, EC2.19, EC2.2, EC2.21, EC2.6, ECR.3, ELB.10, ELB.5, ELB.7, ES.4, ES.5, ES.6, ES.7, ES.8, IAM.1, IAM.2, IAM.3, IAM.5, IAM.6, IAM.7, IAM.8, NetworkFirewall.6, RDS.11, RDS.17, RDS.18, RDS.19, RDS.2, RDS.20, RDS.21, RDS.22, RDS.23, RDS.25, RDS.3, RDS.5, S3.1, S3.10, S3.11, S3.12, S3.2, S3.3, S3.4, S3.5, S3.6, S3.9, SQS.1, SSM.1, SSM.4] 195 | disable: 196 | Redshift.4: Redshift is not running. 197 | Redshift.6: Redshift is not running. 198 | Redshift.8: Redshift is not running. 199 | [...] 200 | ``` 201 | 202 | Dry run. 203 | 204 | ``` console 205 | $ control-controls plan controls.yml 206 | 2022-04-14T15:16:54+09:00 INF Checking eu-north-1 207 | 2022-04-14T15:17:02+09:00 INF Checking ap-south-1 208 | 2022-04-14T15:17:08+09:00 INF Checking eu-west-3 209 | 2022-04-14T15:17:15+09:00 INF Checking eu-west-2 210 | 2022-04-14T15:17:23+09:00 INF Checking eu-west-1 211 | 2022-04-14T15:17:31+09:00 INF Checking ap-northeast-3 212 | 2022-04-14T15:17:34+09:00 INF Checking ap-northeast-2 213 | 2022-04-14T15:17:37+09:00 INF Checking ap-northeast-1 214 | 2022-04-14T15:17:40+09:00 INF Checking sa-east-1 215 | 2022-04-14T15:17:49+09:00 INF Checking ca-central-1 216 | 2022-04-14T15:17:55+09:00 INF Checking ap-southeast-1 217 | 2022-04-14T15:17:59+09:00 INF Checking ap-southeast-2 218 | 2022-04-14T15:18:05+09:00 INF Checking eu-central-1 219 | 2022-04-14T15:18:13+09:00 INF Checking us-east-1 220 | 2022-04-14T15:18:19+09:00 INF Checking us-east-2 221 | 2022-04-14T15:18:25+09:00 INF Checking us-west-1 222 | 2022-04-14T15:18:31+09:00 INF Checking us-west-2 223 | - eu-north-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.4 (disabled reason: Redshift is not running.) 224 | - eu-north-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.6 (disabled reason: Redshift is not running.) 225 | - eu-north-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.8 (disabled reason: Redshift is not running.) 226 | - ap-south-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.4 (disabled reason: Redshift is not running.) 227 | - ap-south-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.6 (disabled reason: Redshift is not running.) 228 | [...] 229 | - us-west-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.6 (disabled reason: Redshift is not running.) 230 | - us-west-1::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.8 (disabled reason: Redshift is not running.) 231 | - us-west-2::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.4 (disabled reason: Redshift is not running.) 232 | - us-west-2::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.6 (disabled reason: Redshift is not running.) 233 | - us-west-2::standards::aws-foundational-security-best-practices/v/1.0.0::controls::Redshift.8 (disabled reason: Redshift is not running.) 234 | 235 | Plan: 0 to enable, 51 to disable 236 | ``` 237 | 238 | Apply changes. 239 | 240 | ``` console 241 | $ control-controls apply controls.yml 242 | 2022-04-14T15:43:37+09:00 INF Applying to eu-north-1 243 | 2022-04-14T15:43:46+09:00 INF Disable control Control=Redshift.4 Reason="Redshift is not running." Region=eu-north-1 Standard=aws-foundational-security-best-practice 244 | s/v/1.0.0 245 | 2022-04-14T15:43:47+09:00 INF Disable control Control=Redshift.6 Reason="Redshift is not running." Region=eu-north-1 Standard=aws-foundational-security-best-practice 246 | s/v/1.0.0 247 | 2022-04-14T15:43:49+09:00 INF Disable control Control=Redshift.8 Reason="Redshift is not running." Region=eu-north-1 Standard=aws-foundational-security-best-practice 248 | s/v/1.0.0 249 | 2022-04-14T15:43:51+09:00 INF Applying to ap-south-1 250 | 2022-04-14T15:43:56+09:00 INF Disable control Control=Redshift.4 Reason="Redshift is not running." Region=ap-south-1 Standard=aws-foundational-security-best-practice 251 | s/v/1.0.0 252 | 2022-04-14T15:43:57+09:00 INF Disable control Control=Redshift.6 Reason="Redshift is not running." Region=ap-south-1 Standard=aws-foundational-security-best-practice 253 | s/v/1.0.0 254 | [...] 255 | 2022-04-14T15:46:18+09:00 INF Disable control Control=Redshift.6 Reason="Redshift is not running." Region=us-west-1 Standard=aws-foundational-security-best-practices 256 | /v/1.0.0 257 | 2022-04-14T15:46:19+09:00 INF Disable control Control=Redshift.8 Reason="Redshift is not running." Region=us-west-1 Standard=aws-foundational-security-best-practices 258 | /v/1.0.0 259 | 2022-04-14T15:46:20+09:00 INF Applying to us-west-2 260 | 2022-04-14T15:46:26+09:00 INF Disable control Control=Redshift.4 Reason="Redshift is not running." Region=us-west-2 Standard=aws-foundational-security-best-practices 261 | /v/1.0.0 262 | 2022-04-14T15:46:27+09:00 INF Disable control Control=Redshift.6 Reason="Redshift is not running." Region=us-west-2 Standard=aws-foundational-security-best-practices 263 | /v/1.0.0 264 | 2022-04-14T15:46:29+09:00 INF Disable control Control=Redshift.8 Reason="Redshift is not running." Region=us-west-2 Standard=aws-foundational-security-best-practices 265 | /v/1.0.0 266 | 267 | Apply complete 268 | ``` 269 | 270 | ## Configuration 271 | 272 | ### `autoEnable` 273 | 274 | Automatically enabling new controls across all regions. 275 | 276 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/controls-auto-enable.html 277 | 278 | ``` yaml 279 | autoEnable: true 280 | ``` 281 | 282 | ### `standards..enable` 283 | 284 | Enabling a security standard across all regions. 285 | 286 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable.html 287 | 288 | ``` yaml 289 | standards: 290 | aws-foundational-security-best-practices/v/1.0.0: 291 | enable: true 292 | cis-aws-foundations-benchmark/v/1.2.0: 293 | enable: true 294 | pci-dss/v/3.2.1: 295 | enable: false 296 | ``` 297 | 298 | ### `standards..controls.enable` 299 | 300 | Enabling individual controls across all regions. 301 | 302 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable-controls.html 303 | 304 | ``` yaml 305 | standards: 306 | aws-foundational-security-best-practices/v/1.0.0: 307 | enable: true 308 | controls: 309 | enable: [APIGateway.5, AutoScaling.1, AutoScaling.2, CloudTrail.1, CloudTrail.2, CloudTrail.4, CloudTrail.5, Config.1, DynamoDB.1, EC2.19, EC2.2, EC2.21, EC2.6, ECR.3, ELB.10, ELB.5, ELB.7, ES.4, ES.5, ES.6, ES.7, ES.8, IAM.1, IAM.2, IAM.3, IAM.5, IAM.6, IAM.7, IAM.8, NetworkFirewall.6, RDS.11, RDS.17, RDS.18, RDS.19, RDS.2, RDS.20, RDS.21, RDS.22, RDS.23, RDS.25, RDS.3, RDS.5, Redshift.4, Redshift.6, Redshift.8, S3.1, S3.10, S3.11, S3.12, S3.2, S3.3, S3.4, S3.5, S3.6, S3.9, SQS.1, SSM.1, SSM.4] 310 | ``` 311 | 312 | ### `standards..controls.disable` 313 | 314 | Disabling individual controls across all regions. 315 | 316 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable-controls.html 317 | 318 | ``` yaml 319 | standards: 320 | aws-foundational-security-best-practices/v/1.0.0: 321 | enable: true 322 | controls: 323 | disable: 324 | Redshift.4: Redshift is not running. 325 | Redshift.6: Redshift is not running. 326 | Redshift.8: Redshift is not running. 327 | ``` 328 | 329 | ### `standards..findings...status` 330 | 331 | Set workflow status to individual findings across all regions. 332 | 333 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/finding-workflow-status.html 334 | 335 | ``` yaml 336 | standards: 337 | aws-foundational-security-best-practices/v/1.0.0: 338 | findings: 339 | S3.2: 340 | arn:aws:s3:::static.example.com: 341 | status: SUPPRESSED 342 | note: Use as simple web hosting 343 | ``` 344 | 345 | ### `standards..findings...note` 346 | 347 | Set note to individual findings across all regions. 348 | 349 | ref: https://docs.aws.amazon.com/securityhub/latest/userguide/asff-note.html 350 | 351 | ### `regions..standards.*` 352 | 353 | Set override settings for each region. 354 | 355 | ## Overlay 356 | 357 | It is possible to override the settings with `--overlay` option. 358 | 359 | ``` console 360 | $ control-controls plan base.yml --overlay custom.yml 361 | [...] 362 | $ control-controls apply base.yml --overlay custom.yml 363 | [...] 364 | ``` 365 | 366 | ## Required permissions 367 | 368 | - `ec2:DescribeRegions` 369 | - `securityhub:*` 370 | 371 | ## Install 372 | 373 | **homebrew tap:** 374 | 375 | ```console 376 | $ brew install pepabo/tap/control-controls 377 | ``` 378 | 379 | **manually:** 380 | 381 | Download binany from [releases page](https://github.com/pepabo/control-controls/releases) 382 | 383 | **go install:** 384 | 385 | ```console 386 | $ go install github.com/pepabo/control-controls@latest 387 | ``` 388 | -------------------------------------------------------------------------------- /cmd/apply.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | 28 | "github.com/aws/aws-sdk-go-v2/config" 29 | "github.com/pepabo/control-controls/sechub" 30 | "github.com/rs/zerolog/log" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | var applyCmd = &cobra.Command{ 35 | Use: "apply [CONFIG_FILE]", 36 | Short: "apply", 37 | Long: `apply.`, 38 | Args: cobra.ExactArgs(1), 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | ctx := context.Background() 41 | cfg, err := config.LoadDefaultConfig(ctx) 42 | if err != nil { 43 | return err 44 | } 45 | hub, err := sechub.Load(args[0]) 46 | if err != nil { 47 | return err 48 | } 49 | for _, o := range overlays { 50 | oo, err := sechub.Load(o) 51 | if err != nil { 52 | return err 53 | } 54 | hub.Overlay(oo) 55 | } 56 | regions, err := regions(ctx, cfg) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | for _, r := range regions { 62 | cfg.Region = r 63 | log.Info().Msg(fmt.Sprintf("Applying to %s", r)) 64 | if err := hub.Apply(ctx, cfg, disabledReason); err != nil { 65 | return err 66 | } 67 | } 68 | 69 | cmd.Println("") 70 | cmd.Println("Apply complete") 71 | return nil 72 | }, 73 | } 74 | 75 | func init() { 76 | rootCmd.AddCommand(applyCmd) 77 | applyCmd.Flags().StringVarP(&disabledReason, "disabled-reason", "", "", "A description of the reason why you are disabling a security standard control.") 78 | applyCmd.Flags().StringSliceVarP(&overlays, "overlay", "", []string{}, "patch file or directory for overlaying") 79 | } 80 | -------------------------------------------------------------------------------- /cmd/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | 28 | "github.com/aws/aws-sdk-go-v2/config" 29 | "github.com/goccy/go-yaml" 30 | "github.com/pepabo/control-controls/sechub" 31 | "github.com/rs/zerolog/log" 32 | "github.com/spf13/cobra" 33 | ) 34 | 35 | var exportCmd = &cobra.Command{ 36 | Use: "export", 37 | Short: "export", 38 | Long: `export.`, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | ctx := context.Background() 41 | cfg, err := config.LoadDefaultConfig(ctx) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | regions, err := regions(ctx, cfg) 47 | if err != nil { 48 | return err 49 | } 50 | var base *sechub.SecHub 51 | hubs := []*sechub.SecHub{} 52 | for _, r := range regions { 53 | cfg.Region = r 54 | log.Info().Msg(fmt.Sprintf("Fetching controls from %s", r)) 55 | sh := sechub.New(r) 56 | if err := sh.Fetch(ctx, cfg); err != nil { 57 | return err 58 | } 59 | if base == nil { 60 | base = sh 61 | } else { 62 | base = sechub.Intersect(base, sh) 63 | } 64 | hubs = append(hubs, sh) 65 | } 66 | 67 | for _, h := range hubs { 68 | d, err := sechub.Diff(base, h) 69 | if err != nil { 70 | return err 71 | } 72 | if d != nil { 73 | base.Regions = append(base.Regions, d) 74 | } 75 | } 76 | 77 | b, err := yaml.Marshal(base) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | cmd.Println(string(b)) 83 | return nil 84 | }, 85 | } 86 | 87 | func init() { 88 | rootCmd.AddCommand(exportCmd) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/notify.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "context" 26 | 27 | "github.com/aws/aws-sdk-go-v2/aws" 28 | "github.com/aws/aws-sdk-go-v2/aws/arn" 29 | "github.com/aws/aws-sdk-go-v2/config" 30 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 31 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 32 | "github.com/pepabo/control-controls/sechub" 33 | "github.com/spf13/cobra" 34 | ) 35 | 36 | var dryrun bool 37 | 38 | var notifyCmd = &cobra.Command{ 39 | Use: "notify [CONFIG_FILE]", 40 | Short: "notify", 41 | Long: `notify.`, 42 | Args: cobra.ExactArgs(1), 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | ctx := context.Background() 45 | cfg, err := config.LoadDefaultConfig(ctx) 46 | if err != nil { 47 | return err 48 | } 49 | hub, err := sechub.Load(args[0]) 50 | if err != nil { 51 | return err 52 | } 53 | for _, o := range overlays { 54 | oo, err := sechub.Load(o) 55 | if err != nil { 56 | return err 57 | } 58 | hub.Overlay(oo) 59 | } 60 | if len(hub.Notifications) == 0 { 61 | cmd.Println("no notifications") 62 | return nil 63 | } 64 | region, err := detectAggregationRegion(ctx, cfg) 65 | if err != nil { 66 | return err 67 | } 68 | cfg.Region = region 69 | findings, err := collectActiveFindings(ctx, cfg) 70 | if err != nil { 71 | return err 72 | } 73 | if err := hub.Notify(ctx, cfg, findings, dryrun); err != nil { 74 | return err 75 | } 76 | return nil 77 | }, 78 | } 79 | 80 | func init() { 81 | rootCmd.AddCommand(notifyCmd) 82 | notifyCmd.Flags().StringSliceVarP(&overlays, "overlay", "", []string{}, "patch file or directory for overlaying") 83 | notifyCmd.Flags().BoolVarP(&dryrun, "dryrun", "s", false, "output notifications to stdout") 84 | 85 | } 86 | 87 | func collectActiveFindings(ctx context.Context, cfg aws.Config) ([]sechub.NotifyFinding, error) { 88 | c := securityhub.NewFromConfig(cfg) 89 | hub, err := c.DescribeHub(ctx, &securityhub.DescribeHubInput{}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | a, err := arn.Parse(*hub.HubArn) 94 | if err != nil { 95 | return nil, err 96 | } 97 | var ( 98 | nt *string 99 | findings []types.AwsSecurityFinding 100 | ) 101 | for { 102 | o, err := c.GetFindings(ctx, &securityhub.GetFindingsInput{ 103 | Filters: &types.AwsSecurityFindingFilters{ 104 | AwsAccountId: []types.StringFilter{{Comparison: types.StringFilterComparisonEquals, Value: aws.String(a.AccountID)}}, 105 | ProductName: []types.StringFilter{{Comparison: types.StringFilterComparisonEquals, Value: aws.String("Security Hub")}}, 106 | RecordState: []types.StringFilter{{Comparison: types.StringFilterComparisonEquals, Value: aws.String("ACTIVE")}}, 107 | SeverityLabel: []types.StringFilter{{Comparison: types.StringFilterComparisonNotEquals, Value: aws.String("INFORMATIONAL")}}, 108 | }, 109 | MaxResults: int32(100), 110 | NextToken: nt, 111 | }) 112 | if err != nil { 113 | return nil, err 114 | } 115 | findings = append(findings, o.Findings...) 116 | if o.NextToken == nil { 117 | break 118 | } 119 | nt = o.NextToken 120 | } 121 | nf := []sechub.NotifyFinding{} 122 | for _, f := range findings { 123 | nf = append(nf, sechub.NotifyFinding{ 124 | SeverityLabel: f.Severity.Label, 125 | WorkflowStatus: f.Workflow.Status, 126 | }) 127 | } 128 | return nf, nil 129 | } 130 | -------------------------------------------------------------------------------- /cmd/plan.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | "os" 28 | 29 | "github.com/aws/aws-sdk-go-v2/config" 30 | "github.com/fatih/color" 31 | "github.com/pepabo/control-controls/sechub" 32 | "github.com/rs/zerolog/log" 33 | "github.com/spf13/cobra" 34 | ) 35 | 36 | var planCmd = &cobra.Command{ 37 | Use: "plan [CONFIG_FILE]", 38 | Short: "plan", 39 | Long: `plan.`, 40 | Args: cobra.ExactArgs(1), 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | ctx := context.Background() 43 | cfg, err := config.LoadDefaultConfig(ctx) 44 | if err != nil { 45 | return err 46 | } 47 | hub, err := sechub.Load(args[0]) 48 | if err != nil { 49 | return err 50 | } 51 | for _, o := range overlays { 52 | oo, err := sechub.Load(o) 53 | if err != nil { 54 | return err 55 | } 56 | hub.Overlay(oo) 57 | } 58 | regions, err := regions(ctx, cfg) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | changes := []*sechub.Change{} 64 | for _, r := range regions { 65 | cfg.Region = r 66 | log.Info().Msg(fmt.Sprintf("Checking %s", r)) 67 | c, err := hub.Plan(ctx, cfg, disabledReason) 68 | if err != nil { 69 | return err 70 | } 71 | changes = append(changes, c...) 72 | } 73 | 74 | cmd.Println("") 75 | 76 | if len(changes) == 0 { 77 | cmd.Println("No changes. Controls are up-to-date.") 78 | } else { 79 | green := color.New(color.FgGreen).PrintfFunc() 80 | red := color.New(color.FgRed).PrintfFunc() 81 | yellow := color.New(color.FgYellow).PrintfFunc() 82 | enable := 0 83 | disable := 0 84 | change := 0 85 | for _, c := range changes { 86 | switch c.ChangeType { 87 | case sechub.ENABLE: 88 | enable += 1 89 | green("%s %s\n", c.ChangeType, c.Key) 90 | case sechub.DISABLE: 91 | disable += 1 92 | if c.DisabledReason == "" { 93 | red("%s %s\n", c.ChangeType, c.Key) 94 | } else { 95 | red("%s %s (disabled reason: %s)\n", c.ChangeType, c.Key, c.DisabledReason) 96 | } 97 | case sechub.CHANGE: 98 | change += 1 99 | yellow("%s %s %s\n", c.ChangeType, c.Key, c.Changed) 100 | } 101 | } 102 | cmd.Println("") 103 | cmd.Printf("Plan: %d to enable, %d to change, %d to disable\n", enable, change, disable) 104 | os.Exit(2) 105 | } 106 | 107 | return nil 108 | }, 109 | } 110 | 111 | func init() { 112 | rootCmd.AddCommand(planCmd) 113 | planCmd.Flags().StringVarP(&disabledReason, "disabled-reason", "", "", "A description of the reason why you are disabling a security standard control.") 114 | planCmd.Flags().StringSliceVarP(&overlays, "overlay", "", []string{}, "patch file or directory for overlaying") 115 | } 116 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "context" 26 | "errors" 27 | "os" 28 | "time" 29 | 30 | "github.com/aws/aws-sdk-go-v2/aws" 31 | "github.com/aws/aws-sdk-go-v2/service/ec2" 32 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 33 | "github.com/pepabo/control-controls/version" 34 | "github.com/rs/zerolog" 35 | "github.com/rs/zerolog/log" 36 | "github.com/spf13/cobra" 37 | ) 38 | 39 | var ( 40 | disabledReason string 41 | overlays []string 42 | ) 43 | 44 | var rootCmd = &cobra.Command{ 45 | Use: "control-controls", 46 | Short: "control-controls control controls of AWS Security Hub across all regions", 47 | Long: `control-controls control controls of AWS Security Hub across all regions.`, 48 | SilenceUsage: true, 49 | Version: version.Version, 50 | } 51 | 52 | func Execute() { 53 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}) 54 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 55 | if os.Getenv("DEBUG") != "" && os.Getenv("DEBUG") != "0" { 56 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 57 | } 58 | 59 | rootCmd.SetOut(os.Stdout) 60 | rootCmd.SetErr(os.Stderr) 61 | if err := rootCmd.Execute(); err != nil { 62 | os.Exit(1) 63 | } 64 | } 65 | 66 | func regions(ctx context.Context, cfg aws.Config) ([]string, error) { 67 | ec2s := ec2.NewFromConfig(cfg) 68 | rs, err := ec2s.DescribeRegions(ctx, &ec2.DescribeRegionsInput{AllRegions: aws.Bool(false)}) 69 | if err != nil { 70 | return nil, err 71 | } 72 | regions := []string{} 73 | for _, r := range rs.Regions { 74 | regions = append(regions, *r.RegionName) 75 | } 76 | return regions, nil 77 | } 78 | 79 | func detectAggregationRegion(ctx context.Context, cfg aws.Config) (string, error) { 80 | rs, err := regions(ctx, cfg) 81 | if err != nil { 82 | return "", err 83 | } 84 | for _, r := range rs { 85 | cfg.Region = r 86 | c := securityhub.NewFromConfig(cfg) 87 | as, err := c.ListFindingAggregators(ctx, &securityhub.ListFindingAggregatorsInput{}) 88 | if err != nil { 89 | return "", err 90 | } 91 | for _, a := range as.FindingAggregators { 92 | aa, err := c.GetFindingAggregator(ctx, &securityhub.GetFindingAggregatorInput{ 93 | FindingAggregatorArn: a.FindingAggregatorArn, 94 | }) 95 | if err != nil { 96 | return "", err 97 | } 98 | if aa.RegionLinkingMode != nil && aa.FindingAggregationRegion != nil { 99 | return *aa.FindingAggregationRegion, nil 100 | } 101 | } 102 | } 103 | return "", errors.New("no aggregation region") 104 | } 105 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pepabo/control-controls 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/antonmedv/expr v1.9.0 7 | github.com/aws/aws-sdk-go-v2 v1.17.7 8 | github.com/aws/aws-sdk-go-v2/config v1.18.8 9 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0 10 | github.com/aws/aws-sdk-go-v2/service/securityhub v1.29.3 11 | github.com/fatih/color v1.13.0 12 | github.com/goccy/go-yaml v1.9.8 13 | github.com/google/go-cmp v0.5.9 14 | github.com/k1LoW/expand v0.5.5 15 | github.com/k1LoW/httpstub v0.3.2 16 | github.com/rs/zerolog v1.28.0 17 | github.com/spf13/cobra v1.6.1 18 | github.com/tenntenn/golden v0.2.0 19 | ) 20 | 21 | require ( 22 | github.com/aws/aws-sdk-go-v2/credentials v1.13.8 // indirect 23 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 // indirect 31 | github.com/aws/smithy-go v1.13.5 // indirect 32 | github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251 // indirect 33 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 // indirect 36 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca // indirect 37 | github.com/mattn/go-colorable v0.1.13 // indirect 38 | github.com/mattn/go-isatty v0.0.17 // indirect 39 | github.com/spf13/pflag v1.0.5 // indirect 40 | golang.org/x/sys v0.4.0 // indirect 41 | golang.org/x/tools v0.1.7 // indirect 42 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 2 | github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= 3 | github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= 4 | github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 5 | github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= 6 | github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 7 | github.com/aws/aws-sdk-go-v2/config v1.18.8 h1:lDpy0WM8AHsywOnVrOHaSMfpaiV2igOw8D7svkFkXVA= 8 | github.com/aws/aws-sdk-go-v2/config v1.18.8/go.mod h1:5XCmmyutmzzgkpk/6NYTjeWb6lgo9N170m1j6pQkIBs= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.13.8 h1:vTrwTvv5qAwjWIGhZDSBH/oQHuIQjGmD232k01FUh6A= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.13.8/go.mod h1:lVa4OHbvgjVot4gmh1uouF1ubgexSCN92P6CJQpT0t8= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg= 13 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ= 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c= 21 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0 h1:m6HYlpZlTWb9vHuuRHpWRieqPHWlS0mvQ90OJNrG/Nk= 22 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0/go.mod h1:mV0E7631M1eXdB+tlGFIw6JxfsC7Pz7+7Aw15oLVhZw= 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM= 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k= 25 | github.com/aws/aws-sdk-go-v2/service/securityhub v1.29.3 h1:+5S1gzriktBaFXPGgi+KeN5OoTX4CIajWn3UbBoFoOU= 26 | github.com/aws/aws-sdk-go-v2/service/securityhub v1.29.3/go.mod h1:s/xuTeRC/O8Ww3AIVz5aOi4NKwSOUH0uMgqH+cHa/j0= 27 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 h1:/2gzjhQowRLarkkBOGPXSRnb8sQ2RVsjdG1C/UliK/c= 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.0/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A= 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 h1:Jfly6mRxk2ZOSlbCvZfKNS7TukSx1mIzhSsqZ/IGSZI= 30 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8= 31 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 h1:kOO++CYo50RcTFISESluhWEi5Prhg+gaSs4whWabiZU= 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.0/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I= 33 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 34 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 35 | github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251 h1:k6UDF1uPYOs0iy1HPeotNa155qXRWrzKnqAaGXHLZCE= 36 | github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251/go.mod h1:gbPR1gPu9dB96mucYIR7T3B7p/78hRVSOuzIWLHK2Y4= 37 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 38 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 39 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 41 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 43 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 44 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 45 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 46 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 47 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 48 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 49 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 50 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 51 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 52 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 53 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 54 | github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ= 55 | github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= 56 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 57 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 58 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 61 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 62 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 63 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 64 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 65 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 66 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 67 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 h1:c+ctPFdISggaSNCfU1IueNBAsqetJSvMcpQlT+0OVdY= 68 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6/go.mod h1:Rv/momJI8DgrWnBZip+SgagpcgORIZQE5SERlxNb8LY= 69 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca h1:a8xeK4GsWLE4LYo5VI4u1Cn7ZvT1NtXouXR3DdKLB8Q= 70 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca/go.mod h1:UbC32ft9G/jG+sZI8wLbIBNIrYr7vp/yqMDa9SxVBNA= 71 | github.com/k1LoW/expand v0.5.5 h1:XC+BYzAfJvn7jXVMaAfEvqKJHm6EcjXs2VR+7YJjATg= 72 | github.com/k1LoW/expand v0.5.5/go.mod h1:HyNqvB5984hIVR+HMPGBZ511XH+49hJa9DGSBkO3AP4= 73 | github.com/k1LoW/httpstub v0.3.2 h1:mBQT6qqP9ETDzQ5AGS6s+ED6Rmz9n+ZrmjChFh4Q9PE= 74 | github.com/k1LoW/httpstub v0.3.2/go.mod h1:+DdiOxIHHtRxNc4oHk997R2jYEytpj5s3E2+qR2526A= 75 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 76 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 77 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 78 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 79 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 80 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 81 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 82 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 83 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 84 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 85 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 86 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 87 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 88 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 89 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 90 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 91 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 92 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 93 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 94 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 95 | github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= 96 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 97 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 98 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 99 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 100 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 101 | github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= 102 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 103 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 104 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 105 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 108 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 109 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 110 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 111 | github.com/tenntenn/golden v0.2.0 h1:ENbHNS5P2Bcnh2QWQcwtNPDYnIvFGuK4lKVDkCq4AHs= 112 | github.com/tenntenn/golden v0.2.0/go.mod h1:OB8A7xwUZ9xE19KXoOMPl223hhcH4uD8oeQS9fLTiEE= 113 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 114 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 115 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 117 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 118 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 119 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 120 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 121 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 122 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 137 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 139 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 140 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 141 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 143 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 144 | golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= 145 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 146 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 147 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 150 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 154 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 155 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 156 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 GMO Pepabo, inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import "github.com/pepabo/control-controls/cmd" 25 | 26 | func main() { 27 | cmd.Execute() 28 | } 29 | -------------------------------------------------------------------------------- /sechub/apply.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/aws/arn" 10 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 11 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | const noteUpdateBy = "control-controls" 16 | 17 | func (sh *SecHub) Apply(ctx context.Context, cfg aws.Config, reason string) error { 18 | region := cfg.Region 19 | c := securityhub.NewFromConfig(cfg) 20 | d := sh.Regions.findByRegionName(region) 21 | a, err := contextcopy(sh) 22 | if err != nil { 23 | return err 24 | } 25 | if d != nil { 26 | a, err = Override(sh, d) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | current := New(region) 32 | if err := current.Fetch(ctx, cfg); err != nil { 33 | return err 34 | } 35 | if !current.enabled { 36 | log.Info().Str("Region", region).Msg("Skip because Security Hub is not enabled") 37 | return nil 38 | } 39 | diff, err := Diff(current, a) 40 | if err != nil { 41 | return err 42 | } 43 | if diff == nil { 44 | log.Info().Str("Region", region).Msg("No changes") 45 | return nil 46 | } 47 | update := false 48 | 49 | // AutoEnable 50 | if diff.AutoEnable != nil { 51 | if *diff.AutoEnable { 52 | log.Info().Str("Region", region).Msg("Enable auto-enable-controls") 53 | } else { 54 | log.Info().Str("Region", region).Msg("Disable auto-enable-controls") 55 | } 56 | if _, err := c.UpdateSecurityHubConfiguration(ctx, &securityhub.UpdateSecurityHubConfigurationInput{ 57 | AutoEnableControls: *diff.AutoEnable, 58 | }); err != nil { 59 | return err 60 | } 61 | } 62 | 63 | // Standards 64 | stds, err := standards(ctx, c) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | for _, std := range diff.Standards { 70 | key := std.Key 71 | s := stds.findByKey(key) 72 | a, err := arn.Parse(*s.subscriptionArn) 73 | if err != nil { 74 | return err 75 | } 76 | if s == nil { 77 | return fmt.Errorf("could not find standard on %s: %s", region, key) 78 | } 79 | 80 | // Standards.Enable 81 | if std.Enable != nil { 82 | update = true 83 | switch *std.Enable { 84 | case true: 85 | log.Info().Str("Region", region).Str("Standard", key).Msg("Enable standard") 86 | o, err := c.BatchEnableStandards(ctx, &securityhub.BatchEnableStandardsInput{ 87 | StandardsSubscriptionRequests: []types.StandardsSubscriptionRequest{ 88 | types.StandardsSubscriptionRequest{ 89 | StandardsArn: s.arn, 90 | }, 91 | }, 92 | }) 93 | if err != nil { 94 | return err 95 | } 96 | s.subscriptionArn = o.StandardsSubscriptions[0].StandardsSubscriptionArn 97 | 98 | // ref: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/securityhub@v1.20.0#pkg-overview 99 | // * BatchEnableStandards - RateLimit of 1 request per second, BurstLimit of 1 request per second. 100 | time.Sleep(1 * time.Second) 101 | case false: 102 | log.Info().Str("Region", region).Str("Standard", key).Msg("Disable standard") 103 | if _, err := c.BatchDisableStandards(ctx, &securityhub.BatchDisableStandardsInput{ 104 | StandardsSubscriptionArns: []string{*s.subscriptionArn}, 105 | }); err != nil { 106 | return err 107 | } 108 | continue 109 | } 110 | } 111 | 112 | if std.Controls == nil && std.Findings == nil { 113 | log.Debug().Str("Region", region).Str("Standard", key).Msg("Skip controls as there is no difference") 114 | continue 115 | } 116 | 117 | // Standards.Controls 118 | if std.Controls != nil { 119 | cs, err := ctrls(ctx, c, s.subscriptionArn) 120 | if err != nil { 121 | return err 122 | } 123 | for _, id := range std.Controls.Enable { 124 | arn, ok := cs.arns[id] 125 | if !ok { 126 | log.Debug().Str("Region", region).Str("Standard", key).Str("Control", id).Msg("Skip control") 127 | continue 128 | } 129 | if contains(cs.Enable, id) { 130 | continue 131 | } 132 | update = true 133 | log.Info().Str("Region", region).Str("Standard", key).Str("Control", id).Msg("Enable control") 134 | if _, err := c.UpdateStandardsControl(ctx, &securityhub.UpdateStandardsControlInput{ 135 | StandardsControlArn: arn, 136 | ControlStatus: types.ControlStatusEnabled, 137 | }); err != nil { 138 | return err 139 | } 140 | // ref: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/securityhub@v1.20.0#pkg-overview 141 | // * UpdateStandardsControl - RateLimit of 1 request per second, BurstLimit of 5 requests per second. 142 | time.Sleep(1 * time.Second) 143 | } 144 | for _, d := range std.Controls.Disable { 145 | id := d.Key.(string) 146 | if d.Value.(string) != "" { 147 | reason = d.Value.(string) 148 | } 149 | arn, ok := cs.arns[id] 150 | if !ok { 151 | log.Debug().Str("Region", region).Str("Standard", key).Str("Control", id).Msg("Skip control") 152 | continue 153 | } 154 | if containsMapSlice(cs.Disable, id, reason) { 155 | continue 156 | } 157 | update = true 158 | log.Info().Str("Region", region).Str("Standard", key).Str("Control", id).Str("Reason", reason).Msg("Disable control") 159 | if _, err := c.UpdateStandardsControl(ctx, &securityhub.UpdateStandardsControlInput{ 160 | StandardsControlArn: arn, 161 | ControlStatus: types.ControlStatusDisabled, 162 | DisabledReason: &reason, 163 | }); err != nil { 164 | return err 165 | } 166 | // ref: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/securityhub@v1.20.0#pkg-overview 167 | // * UpdateStandardsControl - RateLimit of 1 request per second, BurstLimit of 5 requests per second. 168 | time.Sleep(1 * time.Second) 169 | } 170 | } 171 | 172 | // ControlFindingGenerator 173 | hub, err := c.DescribeHub(ctx, &securityhub.DescribeHubInput{}) 174 | if err != nil { 175 | return err 176 | } 177 | ctrlfg := hub.ControlFindingGenerator 178 | 179 | // Standards.Findings 180 | if std.Findings != nil { 181 | cs, err := ctrls(ctx, c, s.subscriptionArn) 182 | if err != nil { 183 | return err 184 | } 185 | for _, fg := range std.Findings { 186 | for _, r := range fg.Resources { 187 | aa, err := arn.Parse(r.Arn) 188 | if err != nil { 189 | return err 190 | } 191 | if region != "" && aa.Region != "" && aa.Region != region { 192 | continue 193 | } 194 | cArn, ok := cs.arns[fg.ControlID] 195 | if !ok { 196 | return fmt.Errorf("not found: %s", fg.ControlID) 197 | } 198 | 199 | findingFilters := &types.AwsSecurityFindingFilters{ 200 | AwsAccountId: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(a.AccountID)}}, 201 | ResourceId: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(r.Arn)}}, 202 | ProductName: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String("Security Hub")}}, 203 | RecordState: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String("ACTIVE")}}, 204 | } 205 | switch ctrlfg { 206 | case types.ControlFindingGeneratorSecurityControl: 207 | findingFilters.ComplianceSecurityControlId = []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(fg.ControlID)}} 208 | findingFilters.ComplianceAssociatedStandardsId = []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(fmt.Sprintf("standards/%s", key))}} 209 | case types.ControlFindingGeneratorStandardControl: 210 | findingFilters.ProductFields = []types.MapFilter{types.MapFilter{Comparison: types.MapFilterComparisonEquals, Key: aws.String("StandardsControlArn"), Value: cArn}} 211 | default: 212 | return fmt.Errorf("unsupported ControlFindingGenerator: %v", ctrlfg) 213 | } 214 | got, err := c.GetFindings(ctx, &securityhub.GetFindingsInput{ 215 | Filters: findingFilters, 216 | }) 217 | if err != nil { 218 | return err 219 | } 220 | if len(got.Findings) != 1 { 221 | if len(got.Findings) == 0 && aa.Region == "" { 222 | // eg. arn:aws:s3::: 223 | continue 224 | } 225 | return fmt.Errorf("not found: %s", r.Arn) 226 | } 227 | gotFg := got.Findings[0] 228 | status := string(gotFg.Workflow.Status) 229 | note := "" 230 | if gotFg.Note != nil && gotFg.Note.Text != nil { 231 | note = *gotFg.Note.Text 232 | } 233 | if r.Status != status || r.Note != note { 234 | log.Info().Str("Region", region).Str("Standard", key).Str("Control", fg.ControlID).Str("Resource ID", r.Arn).Str("Status", r.Status).Str("Note", r.Note).Msg("Change workfow status") 235 | 236 | input := &securityhub.BatchUpdateFindingsInput{ 237 | FindingIdentifiers: []types.AwsSecurityFindingIdentifier{{ 238 | Id: gotFg.Id, 239 | ProductArn: gotFg.ProductArn, 240 | }}, 241 | Workflow: &types.WorkflowUpdate{ 242 | Status: types.WorkflowStatus(r.Status), 243 | }, 244 | } 245 | if r.Note != "" { 246 | input.Note = &types.NoteUpdate{ 247 | Text: aws.String(r.Note), 248 | UpdatedBy: aws.String(noteUpdateBy), 249 | } 250 | } 251 | if _, err := c.BatchUpdateFindings(ctx, input); err != nil { 252 | return err 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | } 260 | 261 | if !update { 262 | log.Info().Str("Region", region).Msg("No changes") 263 | } 264 | 265 | return nil 266 | } 267 | -------------------------------------------------------------------------------- /sechub/fetch.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 8 | ) 9 | 10 | func (sh *SecHub) Fetch(ctx context.Context, cfg aws.Config) error { 11 | c := securityhub.NewFromConfig(cfg) 12 | stds, err := standards(ctx, c) 13 | if err != nil { 14 | return err 15 | } 16 | hub, err := c.DescribeHub(ctx, &securityhub.DescribeHubInput{}) 17 | if err != nil { 18 | return err 19 | } 20 | if hub.SubscribedAt != nil { 21 | sh.enabled = true 22 | } 23 | sh.AutoEnable = aws.Bool(hub.AutoEnableControls) 24 | for _, std := range stds { 25 | if std.Enable == nil || !*std.Enable { 26 | continue 27 | } 28 | cs, err := ctrls(ctx, c, std.subscriptionArn) 29 | if err != nil { 30 | return err 31 | } 32 | std.Controls = cs 33 | } 34 | sh.Standards = stds 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /sechub/finding.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import "fmt" 4 | 5 | type FindingGroup struct { 6 | ControlID string 7 | Resources FindingResources 8 | } 9 | 10 | type FindingGroups []*FindingGroup 11 | 12 | func (fgs FindingGroups) ControlIDs() []string { 13 | ids := []string{} 14 | for _, fg := range fgs { 15 | ids = append(ids, fg.ControlID) 16 | } 17 | return ids 18 | } 19 | 20 | func (fgs FindingGroups) FindByControlID(id string) (*FindingGroup, error) { 21 | for _, fg := range fgs { 22 | if fg.ControlID == id { 23 | return fg, nil 24 | } 25 | } 26 | return nil, fmt.Errorf("not found: %s", id) 27 | } 28 | 29 | type FindingResource struct { 30 | Arn string 31 | Status string 32 | Note string 33 | } 34 | 35 | type FindingResources []*FindingResource 36 | 37 | func (frs FindingResources) Arns() []string { 38 | arns := []string{} 39 | for _, fr := range frs { 40 | arns = append(arns, fr.Arn) 41 | } 42 | return arns 43 | } 44 | 45 | func (frs FindingResources) FindByArn(arn string) (*FindingResource, error) { 46 | for _, fr := range frs { 47 | if fr.Arn == arn { 48 | return fr, nil 49 | } 50 | } 51 | return nil, fmt.Errorf("not found: %s", arn) 52 | } 53 | 54 | func intersectFindingGroups(a, b FindingGroups) FindingGroups { 55 | fgs := FindingGroups{} 56 | ids := intersect(a.ControlIDs(), b.ControlIDs()) 57 | for _, id := range ids { 58 | fg := &FindingGroup{ControlID: id} 59 | afg, _ := a.FindByControlID(id) 60 | bfg, _ := b.FindByControlID(id) 61 | if afg == nil || bfg == nil { 62 | continue 63 | } 64 | arns := intersect(afg.Resources.Arns(), bfg.Resources.Arns()) 65 | for _, arn := range arns { 66 | ar, _ := afg.Resources.FindByArn(arn) 67 | br, _ := bfg.Resources.FindByArn(arn) 68 | if ar == nil || br == nil { 69 | continue 70 | } 71 | if ar.Status == br.Status && ar.Note == br.Note { 72 | fg.Resources = append(fg.Resources, &FindingResource{ 73 | Arn: arn, 74 | Status: ar.Status, 75 | Note: ar.Note, 76 | }) 77 | } 78 | } 79 | if len(fg.Resources) > 0 { 80 | fgs = append(fgs, fg) 81 | } 82 | } 83 | return fgs 84 | } 85 | 86 | func diffFindingGroups(base, a FindingGroups) FindingGroups { 87 | fgs := FindingGroups{} 88 | ids := unique(append(base.ControlIDs(), a.ControlIDs()...)) 89 | for _, id := range ids { 90 | fg := &FindingGroup{ControlID: id} 91 | basefg, _ := base.FindByControlID(id) 92 | afg, _ := a.FindByControlID(id) 93 | switch { 94 | case afg == nil: 95 | // do nothing 96 | case basefg == nil: 97 | fg.Resources = afg.Resources 98 | case basefg != nil && afg != nil: 99 | arns := unique(append(basefg.Resources.Arns(), afg.Resources.Arns()...)) 100 | for _, arn := range arns { 101 | baser, _ := basefg.Resources.FindByArn(arn) 102 | ar, _ := afg.Resources.FindByArn(arn) 103 | switch { 104 | case ar == nil: 105 | // do noting 106 | case baser == nil: 107 | fg.Resources = append(fg.Resources, ar) 108 | case baser != nil && ar != nil: 109 | if baser.Status != ar.Status || baser.Note != ar.Note { 110 | fg.Resources = append(fg.Resources, &FindingResource{ 111 | Arn: arn, 112 | Status: ar.Status, 113 | Note: ar.Note, 114 | }) 115 | } 116 | } 117 | } 118 | } 119 | if len(fg.Resources) > 0 { 120 | fgs = append(fgs, fg) 121 | } 122 | } 123 | return fgs 124 | } 125 | 126 | func overlayFindingGroups(base, overlay FindingGroups) FindingGroups { 127 | for _, ofg := range overlay { 128 | basefg, _ := base.FindByControlID(ofg.ControlID) 129 | if basefg == nil { 130 | base = append(base, ofg) 131 | continue 132 | } 133 | for _, r := range ofg.Resources { 134 | baser, _ := basefg.Resources.FindByArn(r.Arn) 135 | if baser == nil { 136 | basefg.Resources = append(basefg.Resources, r) 137 | continue 138 | } 139 | baser.Status = r.Status 140 | baser.Note = r.Note 141 | } 142 | } 143 | return base 144 | } 145 | -------------------------------------------------------------------------------- /sechub/finding_test.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestIntersectFindingGroups(t *testing.T) { 10 | tests := []struct { 11 | a FindingGroups 12 | b FindingGroups 13 | want FindingGroups 14 | }{ 15 | {nil, nil, FindingGroups{}}, 16 | { 17 | FindingGroups{ 18 | &FindingGroup{ 19 | ControlID: "IAM.1", 20 | Resources: FindingResources{ 21 | &FindingResource{ 22 | Arn: "arn:aws:iam::1234567890:user/user-a", 23 | Status: "SUPPRESSED", 24 | Note: "This is suppressed", 25 | }, 26 | &FindingResource{ 27 | Arn: "arn:aws:iam::1234567890:user/user-b", 28 | Status: "RESOLVED", 29 | Note: "This is resolved", 30 | }, 31 | }, 32 | }, 33 | }, 34 | FindingGroups{ 35 | &FindingGroup{ 36 | ControlID: "IAM.1", 37 | Resources: FindingResources{ 38 | &FindingResource{ 39 | Arn: "arn:aws:iam::1234567890:user/user-a", 40 | Status: "SUPPRESSED", 41 | Note: "This is suppressed", 42 | }, 43 | }, 44 | }, 45 | &FindingGroup{ 46 | ControlID: "IAM.2", 47 | Resources: FindingResources{ 48 | &FindingResource{ 49 | Arn: "arn:aws:iam::1234567890:user/user-a", 50 | Status: "SUPPRESSED", 51 | Note: "This is suppressed", 52 | }, 53 | }, 54 | }, 55 | }, 56 | FindingGroups{ 57 | &FindingGroup{ 58 | ControlID: "IAM.1", 59 | Resources: FindingResources{ 60 | &FindingResource{ 61 | Arn: "arn:aws:iam::1234567890:user/user-a", 62 | Status: "SUPPRESSED", 63 | Note: "This is suppressed", 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | got := intersectFindingGroups(tt.a, tt.b) 72 | if diff := cmp.Diff(got, tt.want, nil); diff != "" { 73 | t.Errorf("%s", diff) 74 | } 75 | } 76 | } 77 | 78 | func TestDiffFindingGroups(t *testing.T) { 79 | tests := []struct { 80 | base FindingGroups 81 | a FindingGroups 82 | want FindingGroups 83 | }{ 84 | {nil, nil, FindingGroups{}}, 85 | { 86 | FindingGroups{ 87 | &FindingGroup{ 88 | ControlID: "IAM.1", 89 | Resources: FindingResources{ 90 | &FindingResource{ 91 | Arn: "arn:aws:iam::1234567890:user/user-a", 92 | Status: "SUPPRESSED", 93 | Note: "This is suppressed", 94 | }, 95 | &FindingResource{ 96 | Arn: "arn:aws:iam::1234567890:user/user-b", 97 | Status: "RESOLVED", 98 | Note: "This is resolved", 99 | }, 100 | }, 101 | }, 102 | }, 103 | FindingGroups{ 104 | &FindingGroup{ 105 | ControlID: "IAM.1", 106 | Resources: FindingResources{ 107 | &FindingResource{ 108 | Arn: "arn:aws:iam::1234567890:user/user-a", 109 | Status: "SUPPRESSED", 110 | Note: "This is suppressed", 111 | }, 112 | }, 113 | }, 114 | &FindingGroup{ 115 | ControlID: "IAM.2", 116 | Resources: FindingResources{ 117 | &FindingResource{ 118 | Arn: "arn:aws:iam::1234567890:user/user-a", 119 | Status: "SUPPRESSED", 120 | Note: "This is suppressed", 121 | }, 122 | }, 123 | }, 124 | }, 125 | FindingGroups{ 126 | &FindingGroup{ 127 | ControlID: "IAM.2", 128 | Resources: FindingResources{ 129 | &FindingResource{ 130 | Arn: "arn:aws:iam::1234567890:user/user-a", 131 | Status: "SUPPRESSED", 132 | Note: "This is suppressed", 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | } 139 | for _, tt := range tests { 140 | got := diffFindingGroups(tt.base, tt.a) 141 | if diff := cmp.Diff(got, tt.want, nil); diff != "" { 142 | t.Errorf("%s", diff) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /sechub/notify.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | "github.com/antonmedv/expr" 14 | "github.com/aws/aws-sdk-go-v2/aws" 15 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 16 | "github.com/goccy/go-yaml" 17 | "github.com/k1LoW/expand" 18 | ) 19 | 20 | const ( 21 | defaultHeader = "'*AWS Security Hub Notification*'" 22 | defaultMessageTmpl = "Notified because condition *'%s'* is met." 23 | defaultConsoleURL = "https://ap-northeast-1.console.aws.amazon.com/securityhub/home?region=ap-northeast-1#/findings?search=RecordState%3D%255Coperator%255C%253AEQUALS%255C%253AACTIVE%26WorkflowStatus%3D%255Coperator%255C%253AEQUALS%255C%253ANEW%26WorkflowStatus%3D%255Coperator%255C%253AEQUALS%255C%253ANOTIFIED" 24 | ) 25 | 26 | var defaultTemplate = map[string]interface{}{ 27 | "blocks": []interface{}{ 28 | map[string]interface{}{ 29 | "type": "section", 30 | "text": map[string]interface{}{ 31 | "type": "mrkdwn", 32 | "text": "{{ header }}", 33 | }, 34 | }, 35 | map[string]interface{}{ 36 | "type": "section", 37 | "text": map[string]interface{}{ 38 | "type": "mrkdwn", 39 | "text": "{{ message }}", 40 | }, 41 | }, 42 | map[string]interface{}{ 43 | "type": "section", 44 | "fields": []interface{}{ 45 | map[string]interface{}{ 46 | "type": "mrkdwn", 47 | "text": "*CRITICAL:*\n{{ critical - critical_resolved - critical_suppressed }}", 48 | }, 49 | map[string]interface{}{ 50 | "type": "mrkdwn", 51 | "text": "*HIGH:*\n{{ high - high_resolved - high_suppressed }}", 52 | }, 53 | }, 54 | }, 55 | map[string]interface{}{ 56 | "type": "section", 57 | "fields": []interface{}{ 58 | map[string]interface{}{ 59 | "type": "mrkdwn", 60 | "text": "*MEDIUM:*\n{{ medium - medium_resolved - medium_suppressed }}", 61 | }, 62 | map[string]interface{}{ 63 | "type": "mrkdwn", 64 | "text": "*LOW:*\n{{ low - low_resolved - low_suppressed }}", 65 | }, 66 | }, 67 | }, 68 | map[string]interface{}{ 69 | "type": "section", 70 | "text": map[string]interface{}{ 71 | "type": "mrkdwn", 72 | "text": "<{{ consoleURL }}|View findings>", 73 | }, 74 | }, 75 | }, 76 | } 77 | 78 | type NotifyFinding struct { 79 | SeverityLabel types.SeverityLabel 80 | WorkflowStatus types.WorkflowStatus 81 | } 82 | 83 | func (sh *SecHub) Notify(ctx context.Context, cfg aws.Config, findings []NotifyFinding, dryrun bool) error { 84 | urep := strings.NewReplacer("ap-northeast-1", cfg.Region) 85 | now := time.Now() 86 | env := map[string]interface{}{ 87 | "region": sh.region, 88 | "consoleURL": urep.Replace(defaultConsoleURL), 89 | "month": int(now.Month()), 90 | "day": now.Day(), 91 | "hour": now.Hour(), 92 | "weekday": int(now.Weekday()), 93 | } 94 | for _, sl := range types.SeverityLabelCritical.Values() { 95 | slkey := strings.ToLower(string(sl)) 96 | env[slkey] = 0 97 | for _, ws := range types.WorkflowStatusNew.Values() { 98 | wskey := strings.ToLower(string(ws)) 99 | env[wskey] = 0 100 | key := fmt.Sprintf("%s_%s", slkey, wskey) 101 | env[key] = 0 102 | } 103 | } 104 | for _, f := range findings { 105 | slkey := strings.ToLower(string(f.SeverityLabel)) 106 | wskey := strings.ToLower(string(f.WorkflowStatus)) 107 | key := fmt.Sprintf("%s_%s", slkey, wskey) 108 | env[slkey] = env[slkey].(int) + 1 109 | env[wskey] = env[wskey].(int) + 1 110 | env[key] = env[key].(int) + 1 111 | } 112 | for _, n := range sh.Notifications { 113 | if n.Header == "" { 114 | n.Header = defaultHeader 115 | } 116 | if n.Message == "" { 117 | n.Message = fmt.Sprintf(defaultMessageTmpl, n.If) 118 | } 119 | if n.If == "" { 120 | return errors.New("no cond") 121 | } 122 | if !dryrun && n.WebhookURL == "" { 123 | return errors.New("no webhookURL") 124 | } 125 | env["header"] = n.Header 126 | env["cond"] = n.If 127 | env["message"] = n.Message 128 | tf, err := expr.Eval(fmt.Sprintf("(%s) == true", n.If), env) 129 | if err != nil { 130 | return err 131 | } 132 | if !tf.(bool) { 133 | continue 134 | } 135 | if n.Template == nil { 136 | n.Template = defaultTemplate 137 | } 138 | b, err := expandBody(n.Template, env) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | if dryrun { 144 | var out bytes.Buffer 145 | if err := json.Indent(&out, b, "", " "); err != nil { 146 | return err 147 | } 148 | fmt.Println(out.String()) 149 | continue 150 | } 151 | 152 | req, err := http.NewRequest( 153 | http.MethodPost, 154 | n.WebhookURL, 155 | bytes.NewBuffer(b), 156 | ) 157 | if err != nil { 158 | return err 159 | } 160 | req.Header.Set("Content-Type", "application/json") 161 | client := &http.Client{} 162 | resp, err := client.Do(req) 163 | if err != nil { 164 | return err 165 | } 166 | if err := resp.Body.Close(); err != nil { 167 | return err 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | func expandBody(tmpl, env interface{}) ([]byte, error) { 174 | const ( 175 | delimStart = "{{" 176 | delimEnd = "}}" 177 | ) 178 | b, err := yaml.Marshal(tmpl) 179 | if err != nil { 180 | return nil, err 181 | } 182 | e, err := expand.ReplaceYAML(string(b), expand.ExprRepFn(delimStart, delimEnd, env), false) 183 | if err != nil { 184 | return nil, err 185 | } 186 | var ee interface{} 187 | if err := yaml.Unmarshal([]byte(e), &ee); err != nil { 188 | return nil, err 189 | } 190 | buf := new(bytes.Buffer) 191 | enc := json.NewEncoder(buf) 192 | enc.SetEscapeHTML(false) 193 | if err := enc.Encode(ee); err != nil { 194 | return nil, err 195 | } 196 | return buf.Bytes(), nil 197 | } 198 | -------------------------------------------------------------------------------- /sechub/notify_test.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 12 | "github.com/k1LoW/httpstub" 13 | "github.com/tenntenn/golden" 14 | ) 15 | 16 | func TestNotify(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | notification *Notification 20 | findings []NotifyFinding 21 | notify bool 22 | }{ 23 | { 24 | "use default template", 25 | &Notification{ 26 | If: "true", 27 | }, 28 | []NotifyFinding{ 29 | { 30 | SeverityLabel: types.SeverityLabelCritical, 31 | WorkflowStatus: types.WorkflowStatusNew, 32 | }, 33 | }, 34 | true, 35 | }, 36 | { 37 | "cond false", 38 | &Notification{ 39 | If: "false", 40 | }, 41 | []NotifyFinding{ 42 | { 43 | SeverityLabel: types.SeverityLabelCritical, 44 | WorkflowStatus: types.WorkflowStatusNew, 45 | }, 46 | }, 47 | false, 48 | }, 49 | { 50 | "notify critical", 51 | &Notification{ 52 | If: "critical > 0", 53 | }, 54 | []NotifyFinding{ 55 | { 56 | SeverityLabel: types.SeverityLabelCritical, 57 | WorkflowStatus: types.WorkflowStatusNew, 58 | }, 59 | }, 60 | true, 61 | }, 62 | { 63 | "not notify critical", 64 | &Notification{ 65 | If: "critical > 0", 66 | }, 67 | []NotifyFinding{ 68 | { 69 | SeverityLabel: types.SeverityLabelHigh, 70 | WorkflowStatus: types.WorkflowStatusNew, 71 | }, 72 | }, 73 | false, 74 | }, 75 | { 76 | "use custom template", 77 | &Notification{ 78 | If: "true", 79 | Template: map[string]interface{}{ 80 | "critical": "CRITICAL: {{ critical }}", 81 | }, 82 | }, 83 | []NotifyFinding{ 84 | { 85 | SeverityLabel: types.SeverityLabelCritical, 86 | WorkflowStatus: types.WorkflowStatusNew, 87 | }, 88 | }, 89 | true, 90 | }, 91 | { 92 | "change header", 93 | &Notification{ 94 | Header: "Notification!!", 95 | If: "true", 96 | }, 97 | []NotifyFinding{ 98 | { 99 | SeverityLabel: types.SeverityLabelCritical, 100 | WorkflowStatus: types.WorkflowStatusNew, 101 | }, 102 | }, 103 | true, 104 | }, 105 | { 106 | "change message", 107 | &Notification{ 108 | Message: "Notice!!", 109 | If: "true", 110 | }, 111 | []NotifyFinding{ 112 | { 113 | SeverityLabel: types.SeverityLabelCritical, 114 | WorkflowStatus: types.WorkflowStatusNew, 115 | }, 116 | }, 117 | true, 118 | }, 119 | { 120 | "dryrun true", 121 | &Notification{ 122 | If: "true", 123 | }, 124 | []NotifyFinding{ 125 | { 126 | SeverityLabel: types.SeverityLabelCritical, 127 | WorkflowStatus: types.WorkflowStatusNew, 128 | }, 129 | }, 130 | false, 131 | }, 132 | } 133 | ctx := context.Background() 134 | cfg, err := config.LoadDefaultConfig(ctx) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | region := "dummy-ap-1" 139 | cfg.Region = region 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | r := httpstub.NewRouter(t) 143 | r.Method(http.MethodPost).Header("Content-Type", "application/json").ResponseString(http.StatusOK, ``) 144 | ts := r.Server() 145 | t.Cleanup(func() { 146 | ts.Close() 147 | }) 148 | tt.notification.WebhookURL = ts.URL 149 | sh := New(region) 150 | sh.Notifications = append(sh.Notifications, tt.notification) 151 | dryrun := tt.name == "dryrun true" 152 | if err := sh.Notify(ctx, cfg, tt.findings, dryrun); err != nil { 153 | t.Error(err) 154 | } 155 | if len(r.Requests()) == 0 { 156 | if tt.notify { 157 | t.Error("want notify") 158 | } 159 | return 160 | } 161 | got := r.Requests()[0].Body 162 | t.Cleanup(func() { 163 | if err := r.Requests()[0].Body.Close(); err != nil { 164 | t.Error(err) 165 | } 166 | }) 167 | key := strings.Replace(tt.name, " ", "_", -1) 168 | if os.Getenv("UPDATE_GOLDEN") != "" { 169 | golden.Update(t, "testdata", key, got) 170 | return 171 | } 172 | if diff := golden.Diff(t, "testdata", key, got); diff != "" { 173 | t.Error(diff) 174 | } 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /sechub/plan.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/aws/arn" 9 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 10 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | type ChangeType string 15 | 16 | const ( 17 | ENABLE ChangeType = "+" 18 | DISABLE ChangeType = "-" 19 | CHANGE ChangeType = "~" 20 | ) 21 | 22 | type Change struct { 23 | Key string 24 | ChangeType ChangeType 25 | DisabledReason string 26 | Changed interface{} 27 | } 28 | 29 | func (sh *SecHub) Plan(ctx context.Context, cfg aws.Config, reason string) ([]*Change, error) { 30 | changes := []*Change{} 31 | region := cfg.Region 32 | c := securityhub.NewFromConfig(cfg) 33 | d := sh.Regions.findByRegionName(region) 34 | a, err := contextcopy(sh) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if d != nil { 39 | a, err = Override(sh, d) 40 | if err != nil { 41 | return nil, err 42 | } 43 | } 44 | current := New(region) 45 | if err := current.Fetch(ctx, cfg); err != nil { 46 | return nil, err 47 | } 48 | if !current.enabled { 49 | log.Info().Str("Region", region).Msg("Skip because Security Hub is not enabled") 50 | return changes, nil 51 | } 52 | diff, err := Diff(current, a) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if diff == nil { 58 | return changes, nil 59 | } 60 | 61 | // AutoEnable 62 | if diff.AutoEnable != nil { 63 | if *diff.AutoEnable { 64 | changes = append(changes, &Change{ 65 | Key: fmt.Sprintf("%s::%s", region, "auto-enable-controls"), 66 | ChangeType: ENABLE, 67 | }) 68 | } else { 69 | changes = append(changes, &Change{ 70 | Key: fmt.Sprintf("%s::%s", region, "auto-enable-controls"), 71 | ChangeType: DISABLE, 72 | }) 73 | } 74 | } 75 | 76 | // Standards 77 | stds, err := standards(ctx, c) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | for _, std := range diff.Standards { 83 | key := std.Key 84 | s := stds.findByKey(key) 85 | a, err := arn.Parse(*s.subscriptionArn) 86 | if err != nil { 87 | return nil, err 88 | } 89 | if s == nil { 90 | return nil, fmt.Errorf("could not find standard on %s: %s", region, key) 91 | } 92 | 93 | // Standards.Enable 94 | if std.Enable != nil { 95 | switch *std.Enable { 96 | case true: 97 | changes = append(changes, &Change{ 98 | Key: fmt.Sprintf("%s::standards::%s", region, key), 99 | ChangeType: ENABLE, 100 | }) 101 | case false: 102 | changes = append(changes, &Change{ 103 | Key: fmt.Sprintf("%s::standards::%s", region, key), 104 | ChangeType: DISABLE, 105 | }) 106 | } 107 | continue 108 | } 109 | 110 | // Standards.Controls 111 | if std.Controls != nil { 112 | cs, err := ctrls(ctx, c, s.subscriptionArn) 113 | if err != nil { 114 | return nil, err 115 | } 116 | for _, id := range std.Controls.Enable { 117 | _, ok := cs.arns[id] 118 | if !ok { 119 | continue 120 | } 121 | changes = append(changes, &Change{ 122 | Key: fmt.Sprintf("%s::standards::%s::controls::%s", region, key, id), 123 | ChangeType: ENABLE, 124 | DisabledReason: "", 125 | }) 126 | } 127 | for _, d := range std.Controls.Disable { 128 | id := d.Key.(string) 129 | if d.Value.(string) != "" { 130 | reason = d.Value.(string) 131 | } 132 | _, ok := cs.arns[id] 133 | if !ok { 134 | continue 135 | } 136 | changes = append(changes, &Change{ 137 | Key: fmt.Sprintf("%s::standards::%s::controls::%s", region, key, id), 138 | ChangeType: DISABLE, 139 | DisabledReason: reason, 140 | }) 141 | } 142 | } 143 | 144 | // ControlFindingGenerator 145 | hub, err := c.DescribeHub(ctx, &securityhub.DescribeHubInput{}) 146 | if err != nil { 147 | return nil, err 148 | } 149 | ctrlfg := hub.ControlFindingGenerator 150 | 151 | // Standards.Findings 152 | if std.Findings != nil { 153 | cs, err := ctrls(ctx, c, s.subscriptionArn) 154 | if err != nil { 155 | return nil, err 156 | } 157 | for _, fg := range std.Findings { 158 | for _, r := range fg.Resources { 159 | aa, err := arn.Parse(r.Arn) 160 | if err != nil { 161 | return nil, err 162 | } 163 | if region != "" && aa.Region != "" && aa.Region != region { 164 | continue 165 | } 166 | cArn, ok := cs.arns[fg.ControlID] 167 | if !ok { 168 | return nil, fmt.Errorf("not found: %s", fg.ControlID) 169 | } 170 | 171 | findingFilters := &types.AwsSecurityFindingFilters{ 172 | AwsAccountId: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(a.AccountID)}}, 173 | ResourceId: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(r.Arn)}}, 174 | ProductName: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String("Security Hub")}}, 175 | RecordState: []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String("ACTIVE")}}, 176 | } 177 | switch ctrlfg { 178 | case types.ControlFindingGeneratorSecurityControl: 179 | findingFilters.ComplianceSecurityControlId = []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(fg.ControlID)}} 180 | findingFilters.ComplianceAssociatedStandardsId = []types.StringFilter{types.StringFilter{Comparison: types.StringFilterComparisonEquals, Value: aws.String(fmt.Sprintf("standards/%s", key))}} 181 | case types.ControlFindingGeneratorStandardControl: 182 | findingFilters.ProductFields = []types.MapFilter{types.MapFilter{Comparison: types.MapFilterComparisonEquals, Key: aws.String("StandardsControlArn"), Value: cArn}} 183 | default: 184 | return nil, fmt.Errorf("unsupported ControlFindingGenerator: %v", ctrlfg) 185 | } 186 | got, err := c.GetFindings(ctx, &securityhub.GetFindingsInput{ 187 | Filters: findingFilters, 188 | }) 189 | if err != nil { 190 | return nil, err 191 | } 192 | if len(got.Findings) != 1 { 193 | if len(got.Findings) == 0 && aa.Region == "" { 194 | // eg. arn:aws:s3::: 195 | continue 196 | } 197 | return nil, fmt.Errorf("not found: %s", r.Arn) 198 | } 199 | status := string(got.Findings[0].Workflow.Status) 200 | note := "" 201 | if got.Findings[0].Note != nil && got.Findings[0].Note.Text != nil { 202 | note = *got.Findings[0].Note.Text 203 | } 204 | if r.Status != status || r.Note != note { 205 | changed := fmt.Sprintf("%s -> %s (note: %s)", status, r.Status, r.Note) 206 | if r.Note == "" { 207 | changed = fmt.Sprintf("%s -> %s", status, r.Status) 208 | } 209 | changes = append(changes, &Change{ 210 | Key: *cArn, 211 | ChangeType: CHANGE, 212 | Changed: changed, 213 | }) 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | return changes, nil 221 | } 222 | -------------------------------------------------------------------------------- /sechub/sechub.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/securityhub" 12 | "github.com/aws/aws-sdk-go-v2/service/securityhub/types" 13 | "github.com/goccy/go-yaml" 14 | "github.com/k1LoW/expand" 15 | ) 16 | 17 | type Controls struct { 18 | Enable []string `yaml:"enable,flow,omitempty"` 19 | Disable yaml.MapSlice `yaml:"disable,omitempty"` 20 | arns map[string]*string 21 | } 22 | 23 | type Standard struct { 24 | Key string `yaml:"key,omitempty"` 25 | Enable *bool `yaml:"enable,omitempty"` 26 | Controls *Controls `yaml:"controls,omitempty"` 27 | Findings FindingGroups `yaml:"-"` 28 | 29 | arn *string 30 | subscriptionArn *string 31 | enabledByDefault bool 32 | } 33 | 34 | type Notification struct { 35 | If string `yaml:"if"` 36 | Header string `yaml:"header,omitempty"` 37 | Message string `yaml:"message,omitempty"` 38 | WebhookURL string `yaml:"webhookURL"` 39 | Template interface{} 40 | } 41 | 42 | type Standards []*Standard 43 | 44 | type Regions []*SecHub 45 | 46 | type Notifications []*Notification 47 | 48 | type SecHub struct { 49 | AutoEnable *bool `yaml:"autoEnable,omitempty"` 50 | Standards Standards 51 | Regions Regions 52 | Notifications Notifications `yaml:"notifications,omitempty"` 53 | region string // current region 54 | enabled bool // whether Security Hub is enabled in current region 55 | } 56 | 57 | func New(r string) *SecHub { 58 | return &SecHub{ 59 | region: r, 60 | } 61 | } 62 | 63 | func Load(p string) (*SecHub, error) { 64 | b, err := os.ReadFile(filepath.Clean(p)) 65 | if err != nil { 66 | return nil, err 67 | } 68 | hub := &SecHub{} 69 | if err := yaml.Unmarshal(expand.ExpandenvYAMLBytes(b), hub); err != nil { 70 | return nil, err 71 | } 72 | if err := hub.Validate(); err != nil { 73 | return nil, fmt.Errorf("validation error: %s: %w", p, err) 74 | } 75 | return hub, err 76 | } 77 | 78 | func Intersect(a, b *SecHub) *SecHub { 79 | i := &SecHub{} 80 | // AutoEnable 81 | if a.AutoEnable != nil && b.AutoEnable != nil && *a.AutoEnable == *b.AutoEnable { 82 | i.AutoEnable = a.AutoEnable 83 | } else { 84 | // default: true 85 | i.AutoEnable = aws.Bool(true) 86 | } 87 | 88 | // Standards 89 | i.Standards = Standards{} 90 | ikeys := intersect(a.Standards.keys(), b.Standards.keys()) 91 | for _, k := range ikeys { 92 | is := &Standard{ 93 | Key: k, 94 | } 95 | as := a.Standards.findByKey(k) 96 | bs := b.Standards.findByKey(k) 97 | // Standards.Enable 98 | if as.Enable != nil && bs.Enable != nil && *as.Enable == *bs.Enable { 99 | is.Enable = as.Enable 100 | } else { 101 | is.Enable = aws.Bool(as.enabledByDefault) 102 | } 103 | // Standards.Controls 104 | if as.Controls != nil && bs.Controls != nil { 105 | is.Controls = &Controls{} 106 | is.Controls.Enable = intersect(as.Controls.Enable, bs.Controls.Enable) 107 | is.Controls.Disable = intersectMapSlice(as.Controls.Disable, bs.Controls.Disable) 108 | } 109 | 110 | // Standards.Findngs 111 | is.Findings = intersectFindingGroups(as.Findings, bs.Findings) 112 | 113 | i.Standards = append(i.Standards, is) 114 | } 115 | 116 | return i 117 | } 118 | 119 | func Diff(base, a *SecHub) (*SecHub, error) { 120 | b, err := contextcopy(base) 121 | if err != nil { 122 | return nil, err 123 | } 124 | d := New(a.region) 125 | // AutoEnable 126 | if b.AutoEnable != nil && a.AutoEnable != nil && *b.AutoEnable == *a.AutoEnable { 127 | d.AutoEnable = nil 128 | } else { 129 | d.AutoEnable = a.AutoEnable 130 | } 131 | // Standards 132 | d.Standards = Standards{} 133 | for _, std := range a.Standards { 134 | bstd := b.Standards.findByKey(std.Key) 135 | if bstd == nil { 136 | d.Standards = append(d.Standards, std) 137 | continue 138 | } 139 | dstd := &Standard{Key: std.Key} 140 | // Standards.Enable 141 | if bstd.Enable != nil && std.Enable != nil && *bstd.Enable == *std.Enable { 142 | dstd.Enable = nil 143 | } else { 144 | dstd.Enable = std.Enable 145 | } 146 | 147 | if dstd.Enable == nil && bstd.Enable != nil && !*bstd.Enable { 148 | continue 149 | } 150 | if dstd.Enable != nil && !*dstd.Enable { 151 | d.Standards = append(d.Standards, dstd) 152 | continue 153 | } 154 | 155 | // Standards.Controls 156 | if bstd.Controls != nil && std.Controls != nil { 157 | dstd.Controls = &Controls{} 158 | if len(bstd.Controls.Enable) == len(std.Controls.Enable) && len(intersect(bstd.Controls.Enable, std.Controls.Enable)) == len(std.Controls.Enable) { 159 | dstd.Controls.Enable = nil 160 | } else { 161 | dstd.Controls.Enable = diff(bstd.Controls.Enable, std.Controls.Enable) 162 | } 163 | if len(bstd.Controls.Disable) == len(std.Controls.Disable) && len(intersectMapSlice(bstd.Controls.Disable, std.Controls.Disable)) == len(std.Controls.Disable) { 164 | dstd.Controls.Disable = nil 165 | } else { 166 | dstd.Controls.Disable = diffMapSlice(bstd.Controls.Disable, std.Controls.Disable) 167 | } 168 | } else { 169 | dstd.Controls = std.Controls 170 | } 171 | 172 | if dstd.Enable == nil && dstd.Controls == nil { 173 | continue 174 | } 175 | 176 | // Standards.Findings 177 | dstd.Findings = diffFindingGroups(bstd.Findings, std.Findings) 178 | 179 | if dstd.Enable == nil && dstd.Controls != nil && len(dstd.Controls.Enable) == 0 && len(dstd.Controls.Disable) == 0 && len(dstd.Findings) == 0 { 180 | continue 181 | } 182 | 183 | d.Standards = append(d.Standards, dstd) 184 | 185 | } 186 | 187 | if d.AutoEnable == nil && len(d.Standards) == 0 { 188 | return nil, nil 189 | } 190 | 191 | return d, nil 192 | } 193 | 194 | func Override(base, a *SecHub) (*SecHub, error) { 195 | o, err := contextcopy(base) 196 | if err != nil { 197 | return nil, err 198 | } 199 | o.overlay(a) 200 | return o, nil 201 | } 202 | 203 | func (base *SecHub) Overlay(overlay *SecHub) { 204 | base.overlay(overlay) 205 | 206 | for _, r := range base.Regions { 207 | if or := overlay.Regions.findByRegionName(r.region); or != nil { 208 | r.overlay(or) 209 | } 210 | } 211 | for _, or := range overlay.Regions { 212 | if r := base.Regions.findByRegionName(or.region); r == nil { 213 | base.Regions = append(base.Regions, or) 214 | } 215 | } 216 | } 217 | 218 | func (base *SecHub) overlay(overlay *SecHub) { 219 | // AutoEnable 220 | if overlay.AutoEnable != nil { 221 | base.AutoEnable = overlay.AutoEnable 222 | } 223 | 224 | // Standards 225 | for _, std := range base.Standards { 226 | as := overlay.Standards.findByKey(std.Key) 227 | if as == nil { 228 | continue 229 | } 230 | // Standards.Enable 231 | if as.Enable != nil { 232 | std.Enable = as.Enable 233 | } 234 | // Standards.Controls 235 | if as.Controls != nil { 236 | if len(as.Controls.Enable) > 0 { 237 | std.Controls.Enable = unique(append(std.Controls.Enable, as.Controls.Enable...)) 238 | } 239 | if len(as.Controls.Disable) > 0 { 240 | // If 'Enable' and 'Disable' contain the same key, 'Enable' has priority. 241 | disable := yaml.MapSlice{} 242 | for _, d := range append(std.Controls.Disable, as.Controls.Disable...) { 243 | if !contains(std.Controls.Enable, d.Key.(string)) { 244 | disable = append(disable, d) 245 | } 246 | } 247 | std.Controls.Disable = uniqueMapSlice(disable) 248 | } 249 | } 250 | // Standards.Findings 251 | std.Findings = overlayFindingGroups(std.Findings, as.Findings) 252 | } 253 | for _, k := range diff(base.Standards.keys(), overlay.Standards.keys()) { 254 | as := overlay.Standards.findByKey(k) 255 | if as == nil { 256 | continue 257 | } 258 | base.Standards = append(overlay.Standards, as) 259 | } 260 | 261 | // Notifications 262 | ns := overlay.Notifications 263 | for _, b := range base.Notifications { 264 | exist := false 265 | for _, o := range overlay.Notifications { 266 | if b.If == o.If && b.WebhookURL == o.WebhookURL { 267 | exist = true 268 | } 269 | } 270 | if !exist { 271 | ns = append(ns, b) 272 | } 273 | } 274 | base.Notifications = ns 275 | } 276 | 277 | func (sh *SecHub) Validate() error { 278 | if err := sh.validate(); err != nil { 279 | return err 280 | } 281 | for _, r := range sh.Regions { 282 | if err := r.validate(); err != nil { 283 | return err 284 | } 285 | } 286 | return nil 287 | } 288 | 289 | func (sh *SecHub) validate() error { 290 | for _, std := range sh.Standards { 291 | disableKeys := []string{} 292 | m := map[string]struct{}{} 293 | if std.Controls != nil && len(std.Controls.Disable) > 0 { 294 | for _, d := range std.Controls.Disable { 295 | key := d.Key.(string) 296 | if _, ok := m[key]; ok { 297 | return fmt.Errorf("duplicate key: disable control %s", key) 298 | } 299 | disableKeys = append(disableKeys, key) 300 | m[key] = struct{}{} 301 | } 302 | } 303 | dup := intersect(std.Controls.Enable, disableKeys) 304 | if len(dup) > 0 { 305 | return fmt.Errorf("it exists for both enable contorol and disable contorol: %s", dup) 306 | } 307 | } 308 | return nil 309 | } 310 | 311 | func (rs Regions) findByRegionName(name string) *SecHub { 312 | for _, r := range rs { 313 | if r.region == name { 314 | return r 315 | } 316 | } 317 | return nil 318 | } 319 | 320 | func (stds Standards) findByKey(key string) *Standard { 321 | for _, std := range stds { 322 | if std.Key == key { 323 | return std 324 | } 325 | } 326 | return nil 327 | } 328 | 329 | func (stds Standards) keys() []string { 330 | keys := []string{} 331 | for _, std := range stds { 332 | keys = append(keys, std.Key) 333 | } 334 | return keys 335 | } 336 | 337 | func standards(ctx context.Context, c *securityhub.Client) (Standards, error) { 338 | stds := Standards{} 339 | r, err := c.DescribeStandards(ctx, &securityhub.DescribeStandardsInput{}) 340 | if err != nil { 341 | return nil, err 342 | } 343 | for _, s := range r.Standards { 344 | key := key(*s.StandardsArn) 345 | stds = append(stds, &Standard{ 346 | Key: key, 347 | Enable: aws.Bool(false), 348 | arn: s.StandardsArn, 349 | enabledByDefault: s.EnabledByDefault, 350 | }) 351 | } 352 | enabled, err := c.GetEnabledStandards(ctx, &securityhub.GetEnabledStandardsInput{}) 353 | if err != nil { 354 | return nil, err 355 | } 356 | for _, s := range enabled.StandardsSubscriptions { 357 | std := stds.findByKey(key(*s.StandardsArn)) 358 | std.Enable = aws.Bool(true) 359 | std.subscriptionArn = s.StandardsSubscriptionArn 360 | } 361 | 362 | return stds, nil 363 | } 364 | 365 | func ctrls(ctx context.Context, c *securityhub.Client, subscriptionArn *string) (*Controls, error) { 366 | cs := &Controls{ 367 | arns: map[string]*string{}, 368 | } 369 | var nt *string 370 | for { 371 | ctrls, err := c.DescribeStandardsControls(ctx, &securityhub.DescribeStandardsControlsInput{ 372 | StandardsSubscriptionArn: subscriptionArn, 373 | NextToken: nt, 374 | }) 375 | if err != nil { 376 | return nil, err 377 | } 378 | for _, ctrl := range ctrls.Controls { 379 | cs.arns[*ctrl.ControlId] = ctrl.StandardsControlArn 380 | switch ctrl.ControlStatus { 381 | case types.ControlStatusEnabled: 382 | cs.Enable = append(cs.Enable, *ctrl.ControlId) 383 | case types.ControlStatusDisabled: 384 | reason := "No reason" 385 | if ctrl.DisabledReason != nil { 386 | reason = *ctrl.DisabledReason 387 | } 388 | cs.Disable = append(cs.Disable, yaml.MapItem{Key: *ctrl.ControlId, Value: reason}) 389 | } 390 | } 391 | nt = ctrls.NextToken 392 | if ctrls.NextToken == nil { 393 | break 394 | } 395 | } 396 | return cs, nil 397 | } 398 | 399 | func intersect(a, b []string) []string { 400 | i := []string{} 401 | for _, e := range a { 402 | if contains(b, e) { 403 | i = append(i, e) 404 | } 405 | } 406 | return i 407 | } 408 | 409 | func intersectMapSlice(a, b yaml.MapSlice) yaml.MapSlice { 410 | i := yaml.MapSlice{} 411 | for _, e := range a { 412 | if containsMapSlice(b, e.Key.(string), e.Value.(string)) { 413 | i = append(i, e) 414 | } 415 | } 416 | return i 417 | } 418 | 419 | func diff(base, a []string) []string { 420 | i := []string{} 421 | for _, e := range a { 422 | if !contains(base, e) { 423 | i = append(i, e) 424 | } 425 | } 426 | return i 427 | } 428 | 429 | func diffMapSlice(base, a yaml.MapSlice) yaml.MapSlice { 430 | i := yaml.MapSlice{} 431 | for _, e := range a { 432 | if !containsMapSlice(base, e.Key.(string), e.Value.(string)) { 433 | i = append(i, e) 434 | } 435 | } 436 | return i 437 | } 438 | 439 | func contains(s []string, e string) bool { 440 | for _, v := range s { 441 | if e == v { 442 | return true 443 | } 444 | } 445 | return false 446 | } 447 | 448 | func containsMapSlice(s yaml.MapSlice, k, v string) bool { 449 | for _, ss := range s { 450 | if k == ss.Key.(string) && v == ss.Value.(string) { 451 | return true 452 | } 453 | } 454 | return false 455 | } 456 | 457 | func unique(in []string) []string { 458 | u := []string{} 459 | m := map[string]struct{}{} 460 | for _, s := range in { 461 | if _, ok := m[s]; ok { 462 | continue 463 | } 464 | u = append(u, s) 465 | m[s] = struct{}{} 466 | } 467 | return u 468 | } 469 | 470 | func uniqueMapSlice(in yaml.MapSlice) yaml.MapSlice { 471 | keys := []string{} 472 | m := map[string]yaml.MapItem{} 473 | for _, s := range in { 474 | if _, ok := m[s.Key.(string)]; !ok { 475 | keys = append(keys, s.Key.(string)) 476 | } 477 | m[s.Key.(string)] = s 478 | } 479 | u := yaml.MapSlice{} 480 | for _, k := range keys { 481 | u = append(u, m[k]) 482 | } 483 | return u 484 | } 485 | 486 | func contextcopy(in *SecHub) (*SecHub, error) { 487 | b, err := yaml.Marshal(in) 488 | if err != nil { 489 | return nil, err 490 | } 491 | out := &SecHub{} 492 | if err := yaml.UnmarshalWithOptions(b, out, yaml.DisallowDuplicateKey()); err != nil { 493 | return nil, err 494 | } 495 | out.Regions = nil 496 | return out, nil 497 | } 498 | 499 | func key(arn string) string { 500 | splitted := strings.SplitN(arn, "/", 2) 501 | return splitted[1] 502 | } 503 | -------------------------------------------------------------------------------- /sechub/sechub_test.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/goccy/go-yaml" 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/google/go-cmp/cmp/cmpopts" 11 | ) 12 | 13 | func TestIntersect(t *testing.T) { 14 | tests := []struct { 15 | a *SecHub 16 | b *SecHub 17 | want *SecHub 18 | }{ 19 | { 20 | &SecHub{}, 21 | &SecHub{}, 22 | &SecHub{ 23 | AutoEnable: aws.Bool(true), 24 | Standards: Standards{}, 25 | }, 26 | }, 27 | { 28 | &SecHub{AutoEnable: aws.Bool(false)}, 29 | &SecHub{AutoEnable: aws.Bool(true)}, 30 | &SecHub{ 31 | AutoEnable: aws.Bool(true), 32 | Standards: Standards{}, 33 | }, 34 | }, 35 | { 36 | &SecHub{AutoEnable: aws.Bool(false)}, 37 | &SecHub{AutoEnable: aws.Bool(false)}, 38 | &SecHub{ 39 | AutoEnable: aws.Bool(false), 40 | Standards: Standards{}, 41 | }, 42 | }, 43 | { 44 | &SecHub{Standards: Standards{ 45 | &Standard{ 46 | Key: "aws-foundational-security-best-practices/v/1.0.0", 47 | Enable: aws.Bool(true), 48 | enabledByDefault: true, 49 | }, 50 | }}, 51 | &SecHub{Standards: Standards{ 52 | &Standard{ 53 | Key: "aws-foundational-security-best-practices/v/1.0.0", 54 | Enable: aws.Bool(false), 55 | enabledByDefault: true, 56 | }, 57 | }}, 58 | &SecHub{ 59 | AutoEnable: aws.Bool(true), 60 | Standards: Standards{ 61 | &Standard{ 62 | Key: "aws-foundational-security-best-practices/v/1.0.0", 63 | Enable: aws.Bool(true), 64 | enabledByDefault: true, 65 | Findings: FindingGroups{}, 66 | }, 67 | }, 68 | }, 69 | }, 70 | { 71 | &SecHub{Standards: Standards{ 72 | &Standard{ 73 | Key: "aws-foundational-security-best-practices/v/1.0.0", 74 | enabledByDefault: true, 75 | Controls: &Controls{ 76 | Enable: []string{"IAM.1", "IAM.2"}, 77 | }, 78 | }, 79 | }}, 80 | &SecHub{Standards: Standards{ 81 | &Standard{ 82 | Key: "aws-foundational-security-best-practices/v/1.0.0", 83 | enabledByDefault: true, 84 | Controls: &Controls{ 85 | Enable: []string{"IAM.1", "IAM.3"}, 86 | }, 87 | }, 88 | }}, 89 | &SecHub{ 90 | AutoEnable: aws.Bool(true), 91 | Standards: Standards{ 92 | &Standard{ 93 | Key: "aws-foundational-security-best-practices/v/1.0.0", 94 | Enable: aws.Bool(true), 95 | enabledByDefault: true, 96 | Controls: &Controls{ 97 | Enable: []string{"IAM.1"}, 98 | Disable: yaml.MapSlice{}, 99 | }, 100 | Findings: FindingGroups{}, 101 | }, 102 | }, 103 | }, 104 | }, 105 | } 106 | for _, tt := range tests { 107 | got := Intersect(tt.a, tt.b) 108 | opt := cmpopts.IgnoreUnexported(SecHub{}, Standard{}, Controls{}) 109 | if diff := cmp.Diff(got, tt.want, opt); diff != "" { 110 | t.Errorf("%s", diff) 111 | } 112 | } 113 | } 114 | 115 | func TestOverlay(t *testing.T) { 116 | tests := []struct { 117 | base *SecHub 118 | overlay *SecHub 119 | want *SecHub 120 | }{ 121 | { 122 | &SecHub{}, 123 | &SecHub{}, 124 | &SecHub{}, 125 | }, 126 | { 127 | &SecHub{AutoEnable: aws.Bool(false)}, 128 | &SecHub{AutoEnable: aws.Bool(true)}, 129 | &SecHub{ 130 | AutoEnable: aws.Bool(true), 131 | }, 132 | }, 133 | { 134 | &SecHub{Standards: Standards{ 135 | &Standard{ 136 | Key: "aws-foundational-security-best-practices/v/1.0.0", 137 | Enable: aws.Bool(true), 138 | Controls: &Controls{ 139 | Enable: []string{"IAM.1", "IAM.2"}, 140 | }, 141 | }, 142 | }}, 143 | &SecHub{Standards: Standards{ 144 | &Standard{ 145 | Key: "aws-foundational-security-best-practices/v/1.0.0", 146 | Enable: aws.Bool(false), 147 | }, 148 | }}, 149 | &SecHub{Standards: Standards{ 150 | &Standard{ 151 | Key: "aws-foundational-security-best-practices/v/1.0.0", 152 | Enable: aws.Bool(false), 153 | Controls: &Controls{ 154 | Enable: []string{"IAM.1", "IAM.2"}, 155 | }, 156 | }, 157 | }}, 158 | }, 159 | { 160 | &SecHub{Standards: Standards{ 161 | &Standard{ 162 | Key: "aws-foundational-security-best-practices/v/1.0.0", 163 | Controls: &Controls{ 164 | Enable: []string{"IAM.1", "IAM.2"}, 165 | Disable: yaml.MapSlice{ 166 | yaml.MapItem{Key: "Redshift.4", Value: "Redshit is not running."}, 167 | yaml.MapItem{Key: "Redshift.6", Value: "Redshit is not running."}, 168 | }, 169 | }, 170 | }, 171 | }}, 172 | &SecHub{Standards: Standards{ 173 | &Standard{ 174 | Key: "aws-foundational-security-best-practices/v/1.0.0", 175 | Controls: &Controls{ 176 | Enable: []string{"IAM.1", "IAM.3", "Redshift.6"}, 177 | Disable: yaml.MapSlice{ 178 | yaml.MapItem{Key: "Redshift.7", Value: "Redshit is not running."}, 179 | }, 180 | }, 181 | }, 182 | }}, 183 | &SecHub{Standards: Standards{ 184 | &Standard{ 185 | Key: "aws-foundational-security-best-practices/v/1.0.0", 186 | Controls: &Controls{ 187 | Enable: []string{"IAM.1", "IAM.2", "IAM.3", "Redshift.6"}, 188 | Disable: yaml.MapSlice{ 189 | yaml.MapItem{Key: "Redshift.4", Value: "Redshit is not running."}, 190 | yaml.MapItem{Key: "Redshift.7", Value: "Redshit is not running."}, 191 | }, 192 | }, 193 | }, 194 | }}, 195 | }, 196 | } 197 | 198 | for _, tt := range tests { 199 | tt.base.Overlay(tt.overlay) 200 | opt := cmpopts.IgnoreUnexported(SecHub{}, Standard{}, Controls{}) 201 | if diff := cmp.Diff(tt.base, tt.want, opt); diff != "" { 202 | t.Errorf("%s", diff) 203 | } 204 | } 205 | } 206 | 207 | func TestDiff(t *testing.T) { 208 | tests := []struct { 209 | base *SecHub 210 | a *SecHub 211 | want *SecHub 212 | }{ 213 | { 214 | &SecHub{Standards: Standards{ 215 | &Standard{ 216 | Key: "aws-foundational-security-best-practices/v/1.0.0", 217 | Enable: aws.Bool(true), 218 | Controls: &Controls{ 219 | Enable: []string{"IAM.1", "IAM.2"}, 220 | }, 221 | }, 222 | }}, 223 | &SecHub{Standards: Standards{ 224 | &Standard{ 225 | Key: "aws-foundational-security-best-practices/v/1.0.0", 226 | Enable: aws.Bool(true), 227 | Controls: &Controls{ 228 | Enable: []string{"IAM.1", "IAM.2", "IAM.3"}, 229 | }, 230 | }, 231 | }}, 232 | &SecHub{Standards: Standards{ 233 | &Standard{ 234 | Key: "aws-foundational-security-best-practices/v/1.0.0", 235 | Enable: nil, 236 | Controls: &Controls{ 237 | Enable: []string{"IAM.3"}, 238 | }, 239 | Findings: FindingGroups{}, 240 | }, 241 | }}, 242 | }, 243 | { 244 | &SecHub{Standards: Standards{ 245 | &Standard{ 246 | Key: "aws-foundational-security-best-practices/v/1.0.0", 247 | Enable: aws.Bool(false), 248 | Controls: &Controls{}, 249 | }, 250 | &Standard{ 251 | Key: "pci-dss/v/3.2.1", 252 | Enable: aws.Bool(false), 253 | Controls: &Controls{}, 254 | }, 255 | }}, 256 | &SecHub{Standards: Standards{ 257 | &Standard{ 258 | Key: "aws-foundational-security-best-practices/v/1.0.0", 259 | Enable: aws.Bool(false), 260 | Controls: &Controls{}, 261 | }, 262 | &Standard{ 263 | Key: "pci-dss/v/3.2.1", 264 | Enable: aws.Bool(true), 265 | Controls: &Controls{}, 266 | }, 267 | }}, 268 | &SecHub{Standards: Standards{ 269 | &Standard{ 270 | Key: "pci-dss/v/3.2.1", 271 | Enable: aws.Bool(true), 272 | Controls: &Controls{}, 273 | Findings: FindingGroups{}, 274 | }, 275 | }}, 276 | }, 277 | { 278 | &SecHub{Standards: Standards{ 279 | &Standard{ 280 | Key: "aws-foundational-security-best-practices/v/1.0.0", 281 | Enable: aws.Bool(false), 282 | Controls: &Controls{}, 283 | }, 284 | &Standard{ 285 | Key: "pci-dss/v/3.2.1", 286 | Enable: aws.Bool(false), 287 | Controls: &Controls{}, 288 | }, 289 | }}, 290 | &SecHub{Standards: Standards{ 291 | &Standard{ 292 | Key: "aws-foundational-security-best-practices/v/1.0.0", 293 | Enable: aws.Bool(false), 294 | Controls: &Controls{}, 295 | }, 296 | &Standard{ 297 | Key: "pci-dss/v/3.2.1", 298 | Enable: aws.Bool(false), 299 | Controls: &Controls{}, 300 | }, 301 | }}, 302 | nil, 303 | }, 304 | { 305 | &SecHub{Standards: Standards{ 306 | &Standard{ 307 | Key: "aws-foundational-security-best-practices/v/1.0.0", 308 | Enable: aws.Bool(true), 309 | Controls: &Controls{ 310 | Enable: []string{"IAM.1", "IAM.2"}, 311 | Disable: yaml.MapSlice{ 312 | yaml.MapItem{Key: "IAM.3", Value: "some reason"}, 313 | }, 314 | }, 315 | }, 316 | &Standard{ 317 | Key: "cis-aws-foundations-benchmark/v/1.2.0", 318 | Enable: aws.Bool(true), 319 | Controls: &Controls{}, 320 | }, 321 | }}, 322 | &SecHub{Standards: Standards{ 323 | &Standard{ 324 | Key: "aws-foundational-security-best-practices/v/1.0.0", 325 | Enable: aws.Bool(true), 326 | Controls: &Controls{ 327 | Enable: []string{"IAM.1", "IAM.2"}, 328 | Disable: yaml.MapSlice{ 329 | yaml.MapItem{Key: "IAM.3", Value: "some reason"}, 330 | }, 331 | }, 332 | }, 333 | &Standard{ 334 | Key: "cis-aws-foundations-benchmark/v/1.2.0", 335 | Enable: aws.Bool(true), 336 | Controls: &Controls{ 337 | Disable: yaml.MapSlice{ 338 | yaml.MapItem{Key: "CIS.1.1", Value: "some reason"}, 339 | }, 340 | }, 341 | }, 342 | }}, 343 | &SecHub{Standards: Standards{ 344 | &Standard{ 345 | Key: "cis-aws-foundations-benchmark/v/1.2.0", 346 | Enable: nil, 347 | Controls: &Controls{ 348 | Disable: yaml.MapSlice{ 349 | yaml.MapItem{Key: "CIS.1.1", Value: "some reason"}, 350 | }, 351 | }, 352 | Findings: FindingGroups{}, 353 | }, 354 | }}, 355 | }, 356 | } 357 | 358 | for i, tt := range tests { 359 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 360 | got, err := Diff(tt.base, tt.a) 361 | if err != nil { 362 | t.Error(err) 363 | } 364 | opt := cmpopts.IgnoreUnexported(SecHub{}, Standard{}, Controls{}) 365 | if diff := cmp.Diff(got, tt.want, opt); diff != "" { 366 | t.Errorf("%s", diff) 367 | } 368 | }) 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /sechub/testdata/change_header.golden: -------------------------------------------------------------------------------- 1 | {"blocks":[{"text":{"text":"Notification!!","type":"mrkdwn"},"type":"section"},{"text":{"text":"Notified because condition *'true'* is met.","type":"mrkdwn"},"type":"section"},{"fields":[{"text":"*CRITICAL:*\n1","type":"mrkdwn"},{"text":"*HIGH:*\n0","type":"mrkdwn"}],"type":"section"},{"fields":[{"text":"*MEDIUM:*\n0","type":"mrkdwn"},{"text":"*LOW:*\n0","type":"mrkdwn"}],"type":"section"},{"text":{"text":"","type":"mrkdwn"},"type":"section"}]} 2 | -------------------------------------------------------------------------------- /sechub/testdata/change_message.golden: -------------------------------------------------------------------------------- 1 | {"blocks":[{"text":{"text":"*AWS Security Hub Notification*","type":"mrkdwn"},"type":"section"},{"text":{"text":"Notice!!","type":"mrkdwn"},"type":"section"},{"fields":[{"text":"*CRITICAL:*\n1","type":"mrkdwn"},{"text":"*HIGH:*\n0","type":"mrkdwn"}],"type":"section"},{"fields":[{"text":"*MEDIUM:*\n0","type":"mrkdwn"},{"text":"*LOW:*\n0","type":"mrkdwn"}],"type":"section"},{"text":{"text":"","type":"mrkdwn"},"type":"section"}]} 2 | -------------------------------------------------------------------------------- /sechub/testdata/notify_critical.golden: -------------------------------------------------------------------------------- 1 | {"blocks":[{"text":{"text":"*AWS Security Hub Notification*","type":"mrkdwn"},"type":"section"},{"text":{"text":"Notified because condition *'critical > 0'* is met.","type":"mrkdwn"},"type":"section"},{"fields":[{"text":"*CRITICAL:*\n1","type":"mrkdwn"},{"text":"*HIGH:*\n0","type":"mrkdwn"}],"type":"section"},{"fields":[{"text":"*MEDIUM:*\n0","type":"mrkdwn"},{"text":"*LOW:*\n0","type":"mrkdwn"}],"type":"section"},{"text":{"text":"","type":"mrkdwn"},"type":"section"}]} 2 | -------------------------------------------------------------------------------- /sechub/testdata/use_custom_template.golden: -------------------------------------------------------------------------------- 1 | {"critical":"CRITICAL: 1"} 2 | -------------------------------------------------------------------------------- /sechub/testdata/use_default_template.golden: -------------------------------------------------------------------------------- 1 | {"blocks":[{"text":{"text":"*AWS Security Hub Notification*","type":"mrkdwn"},"type":"section"},{"text":{"text":"Notified because condition *'true'* is met.","type":"mrkdwn"},"type":"section"},{"fields":[{"text":"*CRITICAL:*\n1","type":"mrkdwn"},{"text":"*HIGH:*\n0","type":"mrkdwn"}],"type":"section"},{"fields":[{"text":"*MEDIUM:*\n0","type":"mrkdwn"},{"text":"*LOW:*\n0","type":"mrkdwn"}],"type":"section"},{"text":{"text":"","type":"mrkdwn"},"type":"section"}]} 2 | -------------------------------------------------------------------------------- /sechub/yaml.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/goccy/go-yaml" 7 | ) 8 | 9 | func (s *SecHub) MarshalYAML() ([]byte, error) { 10 | stds := yaml.MapSlice{} 11 | for _, std := range s.Standards { 12 | k := std.Key 13 | fgs := yaml.MapSlice{} 14 | for _, fg := range std.Findings { 15 | rs := yaml.MapSlice{} 16 | for _, r := range fg.Resources { 17 | rs = append(rs, yaml.MapItem{ 18 | Key: r.Arn, 19 | Value: yaml.MapSlice{ 20 | yaml.MapItem{Key: "status", Value: r.Status}, 21 | yaml.MapItem{Key: "note", Value: r.Note}, 22 | }, 23 | }) 24 | } 25 | fgs = append(fgs, yaml.MapItem{ 26 | Key: fg.ControlID, 27 | Value: rs, 28 | }) 29 | } 30 | v := &StandardForYAML{ 31 | Enable: std.Enable, 32 | Controls: std.Controls, 33 | Findings: fgs, 34 | } 35 | 36 | stds = append(stds, yaml.MapItem{ 37 | Key: k, 38 | Value: v, 39 | }) 40 | } 41 | 42 | // regions := yaml.MapSlice{} 43 | // for _, hub := range s.Regions { 44 | // k := hub.region 45 | // v := hub 46 | // regions = append(regions, yaml.MapItem{ 47 | // Key: k, 48 | // Value: v, 49 | // }) 50 | // } 51 | 52 | regions := map[string]*SecHub{} 53 | for _, hub := range s.Regions { 54 | k := hub.region 55 | regions[k] = hub 56 | } 57 | 58 | s2 := struct { 59 | AutoEnable *bool `yaml:"autoEnable,omitempty"` 60 | Standards yaml.MapSlice `yaml:"standards,omitempty"` 61 | Regions map[string]*SecHub `yaml:"regions,omitempty"` 62 | }{ 63 | AutoEnable: s.AutoEnable, 64 | Standards: stds, 65 | Regions: regions, 66 | } 67 | return yaml.Marshal(s2) 68 | } 69 | 70 | type SecHubForUnmarshal struct { 71 | AutoEnable *bool `yaml:"autoEnable,omitempty"` 72 | Standards map[string]*Standard `yaml:"standards,omitempty"` 73 | Regions map[string]*SecHubForUnmarshal `yaml:"regions,omitempty"` 74 | Notifications Notifications `yaml:"notifications,omitempty"` 75 | } 76 | 77 | func (s *SecHub) UnmarshalYAML(b []byte) error { 78 | tmp := &SecHubForUnmarshal{} 79 | if err := yaml.Unmarshal(b, tmp); err != nil { 80 | return err 81 | } 82 | s.AutoEnable = tmp.AutoEnable 83 | for k, std := range tmp.Standards { 84 | if std.Controls == nil { 85 | std.Controls = &Controls{} 86 | } 87 | std.Key = k 88 | s.Standards = append(s.Standards, std) 89 | } 90 | for r, tmphub := range tmp.Regions { 91 | hub := New(r) 92 | hub.AutoEnable = tmphub.AutoEnable 93 | for k, std := range tmphub.Standards { 94 | if std.Controls == nil { 95 | std.Controls = &Controls{} 96 | } 97 | std.Key = k 98 | hub.Standards = append(hub.Standards, std) 99 | } 100 | s.Regions = append(s.Regions, hub) 101 | } 102 | s.Notifications = tmp.Notifications 103 | return nil 104 | } 105 | 106 | type StandardForYAML struct { 107 | Enable *bool `yaml:"enable,omitempty"` 108 | Controls *Controls `yaml:"controls,omitempty"` 109 | Findings yaml.MapSlice `yaml:"findings,omitempty"` 110 | } 111 | 112 | type StandardForUnmarshal struct { 113 | Key string `yaml:"key,omitempty"` 114 | Enable *bool `yaml:"enable,omitempty"` 115 | Controls *Controls `yaml:"controls,omitempty"` 116 | Findings yaml.MapSlice `yaml:"findings,omitempty"` 117 | } 118 | 119 | func (s *Standard) UnmarshalYAML(b []byte) error { 120 | tmp := &StandardForUnmarshal{} 121 | if err := yaml.UnmarshalWithOptions(b, tmp, yaml.UseOrderedMap()); err != nil { 122 | return err 123 | } 124 | s.Key = tmp.Key 125 | s.Enable = tmp.Enable 126 | s.Controls = tmp.Controls 127 | for _, f := range tmp.Findings { 128 | fg := &FindingGroup{ 129 | ControlID: f.Key.(string), 130 | } 131 | r, ok := f.Value.(yaml.MapSlice) 132 | if !ok { 133 | return fmt.Errorf("invalid format: %v", string(b)) 134 | } 135 | for _, kv := range r { 136 | fr := &FindingResource{ 137 | Arn: kv.Key.(string), 138 | } 139 | rr, ok := kv.Value.(yaml.MapSlice) 140 | if !ok { 141 | return fmt.Errorf("invalid format: %v", string(b)) 142 | } 143 | for _, kkv := range rr { 144 | switch kkv.Key.(string) { 145 | case "status": 146 | fr.Status = kkv.Value.(string) 147 | case "note": 148 | fr.Note = kkv.Value.(string) 149 | } 150 | } 151 | fg.Resources = append(fg.Resources, fr) 152 | } 153 | s.Findings = append(s.Findings, fg) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func (c *Controls) UnmarshalYAML(b []byte) error { 160 | s := struct { 161 | Enable []string `yaml:"enable,flow,omitempty"` 162 | Disable yaml.MapSlice `yaml:"disable,omitempty"` 163 | }{} 164 | if err := yaml.Unmarshal(b, &s); err == nil { 165 | c.Enable = s.Enable 166 | c.Disable = s.Disable 167 | return nil 168 | } 169 | 170 | // fallback as slice 171 | s2 := struct { 172 | Enable []string `yaml:"enable,flow,omitempty"` 173 | Disable []string `yaml:"disable,flow,omitempty"` 174 | }{} 175 | if err := yaml.Unmarshal(b, &s2); err != nil { 176 | return err 177 | } 178 | 179 | c.Enable = s2.Enable 180 | for _, d := range s2.Disable { 181 | c.Disable = append(c.Disable, yaml.MapItem{ 182 | Key: d, 183 | Value: "", 184 | }) 185 | } 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /sechub/yaml_test.go: -------------------------------------------------------------------------------- 1 | package sechub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/goccy/go-yaml" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | ) 11 | 12 | func TestYAML(t *testing.T) { 13 | tests := []struct { 14 | in *SecHub 15 | want string 16 | }{ 17 | { 18 | &SecHub{ 19 | AutoEnable: aws.Bool(true), 20 | Standards: []*Standard{ 21 | &Standard{ 22 | Key: "cis-aws-foundations-benchmark/v/1.2.0", 23 | Enable: aws.Bool(true), 24 | Controls: &Controls{ 25 | Enable: []string{"CIS.1.1", "CIS.1.2"}, 26 | }, 27 | }, 28 | }, 29 | }, 30 | `autoEnable: true 31 | standards: 32 | cis-aws-foundations-benchmark/v/1.2.0: 33 | enable: true 34 | controls: 35 | enable: [CIS.1.1, CIS.1.2] 36 | `}, 37 | } 38 | for _, tt := range tests { 39 | b, err := yaml.Marshal(tt.in) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | got := string(b) 44 | if got != tt.want { 45 | t.Errorf("got %v\nwant %v", got, tt.want) 46 | } 47 | 48 | hub := &SecHub{} 49 | if err := yaml.Unmarshal(b, hub); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | opt := cmpopts.IgnoreUnexported(SecHub{}, Standard{}, Controls{}) 54 | if diff := cmp.Diff(hub, tt.in, opt); diff != "" { 55 | t.Errorf("%s", diff) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Name for this 4 | const Name string = "control-controls" 5 | 6 | // Version for this 7 | var Version = "0.8.4" 8 | --------------------------------------------------------------------------------