diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md old mode 100755 new mode 100644 index c250e688..27805bcb --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -22,16 +22,16 @@ assignees: "" - [ ] Version: [e.g. v1.0.0] -To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0158) - The AWS CloudFormation template for deployment of the Amazon CloudWatch Monitoring Framework. Version **v1.0.0**_". You can also find the version from [releases](https://github.com/awslabs/amazon-cloudwatch-monitoring-framework/releases) +To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, *"(SO0111) AWS Security Hub Automated Response & Remediation Administrator Stack, v1.4.0"*. You can also find the version from [releases](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/releases) - [ ] Region: [e.g. us-east-1] - [ ] Was the solution modified from the version published on this repository? - [ ] If the answer to the previous question was yes, are the changes available on GitHub? - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? -- [ ] Were there any errors in the CloudWatch Logs? [How to enable debug mode?](https://docs.aws.amazon.com/solutions/latest/amazon-cloudwatch-monitoring-framework/troubleshooting.html) +- [ ] Were there any errors in the CloudWatch Logs? [Troubleshooting](https://docs.aws.amazon.com/solutions/latest/aws-security-hub-automated-response-and-remediation/troubleshooting.html) **Screenshots** If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). **Additional context** -Add any other context about the problem here. +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation-improvements.md b/.github/ISSUE_TEMPLATE/documentation-improvements.md index 0921f632..207b5c38 100644 --- a/.github/ISSUE_TEMPLATE/documentation-improvements.md +++ b/.github/ISSUE_TEMPLATE/documentation-improvements.md @@ -14,4 +14,4 @@ assignees: "" **Describe how we could make it clearer** -**If you have a proposed update, please share it here** +**If you have a proposed update, please share it here** \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md old mode 100755 new mode 100644 index 05364d35..abab44df --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -16,4 +16,4 @@ assignees: "" **Additional context** - + \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6bdaa999..da057da3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,4 +3,4 @@ *Description of changes:* -By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. +By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index de7c22b2..8c885fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2021-12-13 + +### Changed +- Bug fixes for AFSBP EC2.1, CIS 3.x +- Separated Member roles from the remediations so that roles can be deployed once per account +- Roles are now global +- Cross-region remediation is now supported +- Deployment using stacksets is documented in the IG and supported by the templates +- Member account roles for remediation runbooks are now retained when the stack is deleted so that remediations that use these roles continue to function if the solution is removed + +### Added +- Added a get_approval_requirement lambda that customers can use to implement custom business logic +- Added the ability for customers to route findings to an alterate runbook when the finding meets criteria. For example, potentially destructive remediations can be sent to a runbook that sends the finding data to Incident Manager. +- New remediation for AFSBP & PCI S3.5 + ## [1.3.2] - 2021-11-09 - Corrected CIS 3.1 filter pattern - Corrected SNS Access Policy for SO0111-SHARR-LocalAlarmNotification diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14a8a8ec..93686ef7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ information to effectively respond to your bug report or contribution. We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check [existing open](https://github.com/awslabs/%%SOLUTION_NAME%%/issues), or [recently closed](https://github.com/awslabs/%%SOLUTION_NAME%%/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/issues), or [recently closed](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps @@ -41,7 +41,7 @@ GitHub provides additional document on [forking a repository](https://help.githu ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/%%SOLUTION_NAME%%/labels/help%20wanted) issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/labels/help%20wanted) issues is a great place to start. ## Code of Conduct @@ -56,6 +56,6 @@ If you discover a potential security issue in this project we ask that you notif ## Licensing -See the [LICENSE](https://github.com/awslabs/aws-security-hub-automated-response-and-remediation/blob/main/LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/blob/main/LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/README.md b/README.md index f45dadab..daa62d06 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [🚀 Solution Landing Page](https://aws.amazon.com/solutions/implementations/aws-security-hub-automated-response-and-remediation/) \| [🚧 Feature -request](https://github.com/awslabs/aws-security-hub-automated-response-and-remediation/issues/new?assignees=&labels=feature-request%2C+enhancement&template=feature_request.md&title=) +request](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/issues/new?assignees=&labels=feature-request%2C+enhancement&template=feature_request.md&title=) \| [🐛 Bug -Report](https://github.com/awslabs/aws-security-hub-automated-response-and-remediation%3E/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=) +Report](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation%3E/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=) Note: If you want to use the solution without building from source, navigate to Solution Landing Page @@ -89,13 +89,13 @@ Clone or download the repository to a local directory on your linux client. Note **Git Clone example:** ```bash -git clone https://github.com/awslabs/aws-security-hub-automated-response-and-remediation.git +git clone https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation.git ``` **Download Zip example:** ```bash -wget https://github.com/awslabs/aws-security-hub-automated-response-and-remediation/archive/main.zip +wget https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/archive/main.zip ``` ### Custom Playbooks @@ -230,4 +230,4 @@ this capability, please see the # License See license -[here](https://github.com/awslabs/aws-security-hub-automated-response-and-remediation/blob/main/LICENSE.txt) +[here](https://github.com/aws-solutions/aws-security-hub-automated-response-and-remediation/blob/main/LICENSE.txt) diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 6522f646..f2ee27a8 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -22,6 +22,13 @@ # choose the latest AWS Solutions Constructs version. required_cdk_version=1.132.0 +# Get reference for all important folders +template_dir="$PWD" +template_dist_dir="$template_dir/global-s3-assets" +build_dist_dir="$template_dir/regional-s3-assets" +source_dir="../source" +temp_work_dir="${template_dir}/temp" + # Functions to reduce repetitive code # do_cmd will exit if the command has a non-zero return code. do_cmd () { @@ -40,6 +47,20 @@ do_replace() { do_cmd sed -i '' -e $replace $file } +clean() { + echo "------------------------------------------------------------------------------" + echo "[Init] Clean old dist, node_modules and bower_components folders" + echo "------------------------------------------------------------------------------" + do_cmd rm -rf $template_dist_dir + do_cmd rm -rf $build_dist_dir + do_cmd rm -rf $temp_work_dir + do_cmd rm -rf ${template_dir}/${source_dir}/node_modules + cd $source_dir + # remove node_modules + find . -name node_modules | while read file;do rm -rf $file; done + cd $template_dir +} + #------------------------------------------------------------------------------ # Validate command line parameters #------------------------------------------------------------------------------ @@ -47,18 +68,22 @@ do_replace() { # Command line from the buildspec is, by convention: # chmod +x ./build-s3-dist.sh && ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $SOLUTION_NAME $VERSION $DEVBUILD -while getopts ":b:v:t:h" opt; +while getopts ":b:v:tch" opt; do case "${opt}" in b ) bucket=${OPTARG};; v ) version=${OPTARG};; - t ) devtest=${OPTARG};; - h ) - echo "Usage: $0 -b [-v ] [-t DEVTEST]" + t ) devtest=1;; + c) + clean + exit 0 + ;; + *) + echo "Usage: $0 -b [-v ] [-t]" echo "Version must be provided via a parameter or ../version.txt. Others are optional." - echo "-t DEVTEST indicates this is a pre-prod build and instructs the build to use a non-prod Solution ID, DEV-SOxxxx" + echo "-t indicates this is a pre-prod build and instructs the build to use a non-prod Solution ID, DEV-SOxxxx" echo "Production example: ./build-s3-dist.sh -b solutions -v v1.0.0" - echo "Dev example: ./build-s3-dist.sh -b solutions -v v1.0.0 -t DEVTEST" + echo "Dev example: ./build-s3-dist.sh -b solutions -v v1.0.0 -t" exit 1 ;; esac @@ -77,6 +102,8 @@ echo "export DIST_OUTPUT_BUCKET=$bucket" > ./setenv.sh # Version from the command line is definitive. Otherwise, use version.txt if [[ ! -z "$version" ]]; then echo Version is $version from the command line +elif ( command -v jq ) && [[ -f ${template_dir}/${source_dir}/package.json ]]; then + version=`cat ${template_dir}/${source_dir}/package.json | jq -r .version` elif [[ -e ../source/version.txt ]]; then version=`cat ../source/version.txt` echo Version is $version from ../source/version.txt @@ -146,51 +173,23 @@ echo "========================================================================== echo "Building $SOLUTION_NAME ($SOLUTION_ID) version $version for bucket $bucket" echo "==========================================================================" -# Get reference for all important folders -template_dir="$PWD" -template_dist_dir="$template_dir/global-s3-assets" -build_dist_dir="$template_dir/regional-s3-assets" -source_dir="$template_dir/../source" -temp_work_dir="${template_dir}/temp" +clean echo "------------------------------------------------------------------------------" -echo "[Init] Clean old dist, node_modules and bower_components folders" +echo "[Init] Create folders" echo "------------------------------------------------------------------------------" -do_cmd rm -rf $template_dist_dir do_cmd mkdir -p $template_dist_dir -do_cmd rm -rf $build_dist_dir do_cmd mkdir -p $build_dist_dir -do_cmd rm -rf $temp_work_dir do_cmd mkdir -p $temp_work_dir - -echo "------------------------------------------------------------------------------" -echo "[Init] Create folders" -echo "------------------------------------------------------------------------------" -mkdir ${build_dist_dir}/lambda -mkdir -p ${template_dist_dir}/playbooks - -echo "------------------------------------------------------------------------------" -echo "[Copy] Copy source to temp, remove unwanted files" -echo "------------------------------------------------------------------------------" -do_cmd cp -r $source_dir $temp_work_dir # make a copy to work from -cd $temp_work_dir -# remove node_modules -find . -name node_modules | while read file;do rm -rf $file; done -# remove package-lock.json -find . -name package-lock.json | while read file;do rm $file; done - -# Propagate the $required_cdk_version to all of the package.json files. -# This makes it very simple to update the version by changing the value above. -cd $temp_work_dir/source -find . -name package.json | while read package; do - do_replace $package "###CDK###" $required_cdk_version -done +do_cmd mkdir ${build_dist_dir}/lambda +do_cmd mkdir -p ${template_dist_dir}/playbooks echo "------------------------------------------------------------------------------" echo "[Install] CDK" echo "------------------------------------------------------------------------------" -cd $temp_work_dir/source +# cd $temp_work_dir/source +cd $source_dir do_cmd npm install # local install per package.json do_cmd npm install aws-cdk@$required_cdk_version export PATH=$(npm bin):$PATH @@ -207,9 +206,16 @@ echo "-------------------------------------------------------------------------- echo "[Pack] Lambda Layer (used by playbooks)" echo "------------------------------------------------------------------------------" cd $template_dir +do_cmd cp -r $source_dir $temp_work_dir # make a copy to work from +cd $temp_work_dir +# remove node_modules +find . -name node_modules | while read file;do rm -rf $file; done +# remove package-lock.json +find . -name package-lock.json | while read file;do rm $file; done + mkdir -p $temp_work_dir/source/solution_deploy/lambdalayer/python -cp $source_dir/LambdaLayers/*.py $temp_work_dir/source/solution_deploy/lambdalayer/python -pip install -r ./requirements.txt -t $temp_work_dir/source/solution_deploy/lambdalayer/python +cp ${template_dir}/${source_dir}/LambdaLayers/*.py $temp_work_dir/source/solution_deploy/lambdalayer/python +pip install -r $template_dir/requirements.txt -t $temp_work_dir/source/solution_deploy/lambdalayer/python cd $temp_work_dir/source/solution_deploy/lambdalayer zip --recurse-paths ${build_dist_dir}/lambda/layer.zip python @@ -226,7 +232,7 @@ do_cmd cp ../../LambdaLayers/*.py . echo "------------------------------------------------------------------------------" echo "[Pack] Orchestrator Lambdas" echo "------------------------------------------------------------------------------" -cd $template_dir +# cd $template_dir cd $temp_work_dir/source/Orchestrator ls | while read file; do if [ ! -d $file ]; then @@ -240,12 +246,12 @@ do_cmd cp ../LambdaLayers/*.py . echo "------------------------------------------------------------------------------" echo "[Create] Playbooks" echo "------------------------------------------------------------------------------" -for playbook in `ls ${temp_work_dir}/source/playbooks`; do - if [ $playbook == 'NEWPLAYBOOK' ]; then +for playbook in `ls ${template_dir}/${source_dir}/playbooks`; do + if [ $playbook == 'NEWPLAYBOOK' ] || [ $playbook == '.coverage' ]; then continue fi echo Create $playbook playbook - do_cmd cd $temp_work_dir/source/playbooks/${playbook} + do_cmd cd ${template_dir}/${source_dir}/playbooks/${playbook} for template in `cdk list`; do echo Create $playbook template $template # do_cmd npm run build @@ -257,7 +263,7 @@ echo "-------------------------------------------------------------------------- echo "[Create] Deployment Templates" echo "------------------------------------------------------------------------------" # Don't build the deployment template until AFTER the playbooks -cd $temp_work_dir/source/solution_deploy +cd ${template_dir}/${source_dir}/solution_deploy # Output YAML - this is currently the only way to do this for multiple templates for template in `cdk ls`; do @@ -273,5 +279,6 @@ mv ${template_dist_dir}/SolutionDeployStack.template ${template_dist_dir}/aws-sh mv ${template_dist_dir}/MemberStack.template ${template_dist_dir}/aws-sharr-member.template mv ${template_dist_dir}/RunbookStack.template ${template_dist_dir}/aws-sharr-remediations.template mv ${template_dist_dir}/OrchestratorLogStack.template ${template_dist_dir}/aws-sharr-orchestrator-log.template +mv ${template_dist_dir}/MemberRoleStack.template ${template_dist_dir}/aws-sharr-member-roles.template echo Build Complete diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index 6abdb8aa..b1c65667 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -15,14 +15,36 @@ export overrideWarningsEnabled=false echo "UPDATE MODE: CDK Snapshots will be updated. CDK UNIT TESTS WILL BE SKIPPED" } || update="false" -#!/bin/bash echo 'Installing required Python testing modules' pip install -r ./testing_requirements.txt # Get reference for all important folders template_dir="$PWD" -source_dir="$template_dir/../source" +cd ../source +source_dir="$PWD" +cd ${template_dir} temp_source_dir="$template_dir/temp/source" +coverage_report_path="${template_dir}/test/coverage-reports" +mkdir -p ${coverage_report_path} + +run_pytest() { + cd ${1} + report_file="${coverage_report_path}/${2}.coverage.xml" + echo "coverage report path set to ${report_file}" + + # Use -vv for debugging + python3 -m pytest --cov --cov-report=term-missing --cov-report "xml:$report_file" + rc=$? + + if [ "$rc" -ne "0" ]; then + echo "** UNIT TESTS FAILED **" + else + echo "Unit Tests Successful" + fi + if [ "$rc" -gt "$maxrc" ]; then + maxrc=$rc + fi +} if [[ -e './solution_env.sh' ]]; then chmod +x ./solution_env.sh @@ -56,14 +78,9 @@ fi echo "------------------------------------------------------------------------------" echo "[Test] CDK Unit Tests" echo "------------------------------------------------------------------------------" -cd $temp_source_dir +cd $source_dir [[ $update == "true" ]] && { npm run test -- -u - cp -f test/__snapshots__/* $source_dir/test/__snapshots__/ - cp -f playbooks/CIS120/test/__snapshots__/* $source_dir/playbooks/CIS120/test/__snapshots__/ - cp -f playbooks/AFSBP/test/__snapshots__/* $source_dir/playbooks/AFSBP/test/__snapshots__/ - cp -f playbooks/PCI321/test/__snapshots__/* $source_dir/playbooks/PCI321/test/__snapshots__/ - cp -f remediation_runbooks/test/__snapshots__/* $source_dir/remediation_runbooks/test/__snapshots__/ } || { npm run test rc=$? @@ -80,122 +97,41 @@ cd $temp_source_dir echo "------------------------------------------------------------------------------" echo "[Test] Python Unit Tests - Orchestrator Lambdas" echo "------------------------------------------------------------------------------" -cd ${temp_source_dir}/Orchestrator - -# setup coverage report path -mkdir -p ${temp_source_dir}/test/coverage-reports -coverage_report_path=${template_dir}/test/coverage-reports/OrchestratorLambda.coverage.xml -echo "coverage report path set to $coverage_report_path" - -# Use -vv for debugging -python3 -m pytest --cov --cov-report=term-missing --cov-report "xml:$coverage_report_path" -rc=$? +run_pytest "${temp_source_dir}/Orchestrator" "Orchestrator" -if [ "$rc" -ne "0" ]; then - echo "** UNIT TESTS FAILED **" -else - echo "Unit Tests Successful" -fi -if [ "$rc" -gt "$maxrc" ]; then - maxrc=$rc -fi - -# The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list -# with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different -# absolute paths for source directories, this substitution is used to convert each absolute source directory -# path to the corresponding project relative path. The $source_dir holds the absolute path for source directory. -sed -i -e "s,${temp_source_dir}/Orchestrator,deployment/temp/source/Orchestrator,g" $coverage_report_path +echo "------------------------------------------------------------------------------" +echo "[Test] Python Unit Tests - SolutionDeploy" +echo "------------------------------------------------------------------------------" +run_pytest "${temp_source_dir}/solution_deploy/source" "SolutionDeploy" echo "------------------------------------------------------------------------------" echo "[Test] Python Unit Tests - LambdaLayers" echo "------------------------------------------------------------------------------" -cd ${temp_source_dir}/LambdaLayers - -# setup coverage report path -mkdir -p ${template_dir}/test/coverage-reports -coverage_report_path=${template_dir}/test/coverage-reports/LambdaLayers.coverage.xml -echo "coverage report path set to $coverage_report_path" - -# Use -vv for debugging -python3 -m pytest --cov=${temp_source_dir}/LambdaLayers --cov-report=term-missing --cov-report "xml:$coverage_report_path" -rc=$? - -if [ "$rc" -ne "0" ]; then - echo "** UNIT TESTS FAILED **" -else - echo "Unit Tests Successful" -fi -if [ "$rc" -gt "$maxrc" ]; then - maxrc=$rc -fi - -# The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list -# with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different -# absolute paths for source directories, this substitution is used to convert each absolute source directory -# path to the corresponding project relative path. The $source_dir holds the absolute path for source directory. -sed -i -e "s,${temp_source_dir}/LambdaLayers,deployment/temp/source/LambdaLayers,g" $coverage_report_path +run_pytest "${source_dir}/LambdaLayers" "LambdaLayers" echo "------------------------------------------------------------------------------" echo "[Test] Python Scripts for Remediation Runbooks" echo "------------------------------------------------------------------------------" -cd ${temp_source_dir}/remediation_runbooks/scripts - -# setup coverage report path -mkdir -p ${temp_source_dir}/test/coverage-reports -coverage_report_path=${template_dir}/test/coverage-reports/RemediationRunbooks.coverage.xml -echo "coverage report path set to $coverage_report_path" - -# Use -vv for debugging -python3 -m pytest --cov --cov-report=term-missing --cov-report "xml:$coverage_report_path" -rc=$? - -if [ "$rc" -ne "0" ]; then - echo "** UNIT TESTS FAILED **" -else - echo "Unit Tests Successful" -fi -if [ "$rc" -gt "$maxrc" ]; then - maxrc=$rc -fi - -# The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list -# with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different -# absolute paths for source directories, this substitution is used to convert each absolute source directory -# path to the corresponding project relative path. The $source_dir holds the absolute path for source directory. -sed -i -e "s,${temp_source_dir}/remediation_runbooks,deployment/temp/source/remediation_runbooks,g" $coverage_report_path +run_pytest "${source_dir}/remediation_runbooks/scripts" "RemediationRunbooks" echo "------------------------------------------------------------------------------" echo "[Test] Python Scripts for Playbook Scripts" echo "------------------------------------------------------------------------------" -for playbook in `ls ${temp_source_dir}/playbooks`; do - if [ $playbook == 'NEWPLAYBOOK' ]; then - continue - fi - cd ${temp_source_dir}/playbooks/$playbook/ssmdocs/scripts - - # setup coverage report path - mkdir -p ${temp_source_dir}/test/coverage-reports - coverage_report_path=${template_dir}/test/coverage-reports/Playbook${playbook}.coverage.xml - echo "coverage report path set to $coverage_report_path" +for playbook in `ls ${source_dir}/playbooks`; do + # if [ $playbook == 'NEWPLAYBOOK' ]; then + # continue + # fi + run_pytest "${source_dir}/playbooks/${playbook}/ssmdocs/scripts" "Playbook${playbook}" +done - # Use -vv for debugging - python3 -m pytest --cov --cov-report=term-missing --cov-report "xml:$coverage_report_path" - rc=$? - if [ "$rc" -ne "0" ]; then - echo "** UNIT TESTS FAILED **" - else - echo "Unit Tests Successful" - fi - if [ "$rc" -gt "$maxrc" ]; then - maxrc=$rc - fi - # The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list - # with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different - # absolute paths for source directories, this substitution is used to convert each absolute source directory - # path to the corresponding project relative path. The $source_dir holds the absolute path for source directory. - sed -i -e "s,${temp_source_dir}/playbooks/${playbook},deployment/temp/source/playbooks/${playbook},g" $coverage_report_path +# The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list +# with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different +# absolute paths for source directories, this substitution is used to convert each absolute source directory +# path to the corresponding project relative path. -done +coverage_report_path=${template_dir}/test/coverage-reports/*.xml +sed -i -e "s|.*${source_dir}|source|g" $coverage_report_path +sed -i -e "s|.*${temp_source_dir}|source|g" $coverage_report_path echo "=========================================================================" if [ "$maxrc" -ne "0" ]; then diff --git a/source/LambdaLayers/applogger.py b/source/LambdaLayers/applogger.py index 1f2e108b..fe35c3c3 100644 --- a/source/LambdaLayers/applogger.py +++ b/source/LambdaLayers/applogger.py @@ -138,6 +138,7 @@ def flush(self): self.stream_name, self._stream_token))) except: self._stream_token = None + raise else: print(("Error logstream {}, {}".format(self.stream_name, str(ex)))) break diff --git a/source/LambdaLayers/metrics.py b/source/LambdaLayers/metrics.py index cc0e4100..23bb48fd 100644 --- a/source/LambdaLayers/metrics.py +++ b/source/LambdaLayers/metrics.py @@ -160,4 +160,4 @@ def post_metrics_to_api(self, request_data): url = 'https://metrics.awssolutionsbuilder.com/generic' req = Request(url, method='POST', data=bytes(json.dumps( request_data), encoding='utf8'), headers={'Content-Type': 'application/json'}) - urlopen(req) + urlopen(req) # nosec diff --git a/source/LambdaLayers/sechub_findings.py b/source/LambdaLayers/sechub_findings.py index e8218b4f..40a0180d 100644 --- a/source/LambdaLayers/sechub_findings.py +++ b/source/LambdaLayers/sechub_findings.py @@ -47,6 +47,7 @@ class Finding(object): details = {} # Assuming ONE finding per event. We'll take the first. generator_id = 'error' account_id = 'error' + resource_region = 'error' standard_name = '' standard_shortname = 'error' standard_version = 'error' @@ -56,14 +57,20 @@ class Finding(object): title = '' description = '' region = None + arn = '' + uuid = '' def __init__(self, finding_rec): self.region = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') self.aws_api_client = AWSCachedClient(self.region) self.details = finding_rec + self.arn = self.details.get('Id', 'error') + self.uuid = self.arn.split ('/finding/')[1] self.generator_id = self.details.get('GeneratorId', 'error') self.account_id = self.details.get('AwsAccountId', 'error') + resource = self.details.get('Resources',[])[0] + self.resource_region = resource.get('Region','error') if not self.is_valid_finding_json(): raise InvalidFindingJson @@ -225,7 +232,8 @@ class SHARRNotification(object): message = '' logdata = [] send_to_sns = False - + finding_info = {} + def __init__(self, security_standard, region, controlid=None): """ Initialize the class @@ -259,10 +267,23 @@ def notify(self): """ Send notifications to the application CW Logs stream and sns """ + sns_notify_json = { + 'severity': self.severity, + 'message': self.message, + 'finding': self.finding_info + } if self.send_to_sns: - publish_to_sns('SO0111-SHARR_Topic', self.severity + ':' + self.message, self.__region) - + sent_id = publish_to_sns( + 'SO0111-SHARR_Topic', + json.dumps( + sns_notify_json, + indent=2, + default=str + ), + self.__region + ) + print(f'Notification message ID {sent_id} sent.') self.applogger.add_message( self.severity + ': ' + self.message ) diff --git a/source/LambdaLayers/test/test_applogger.py b/source/LambdaLayers/test/test_applogger.py index e7bd6ed9..2519ce34 100644 --- a/source/LambdaLayers/test/test_applogger.py +++ b/source/LambdaLayers/test/test_applogger.py @@ -20,15 +20,16 @@ """ import os from datetime import date +import boto3 from botocore.stub import Stubber, ANY import pytest from pytest_mock import mocker from applogger import LogHandler -from awsapi_cached_client import AWSCachedClient -AWS = AWSCachedClient('us-east-1') +my_session = boto3.session.Session() +my_region = my_session.region_name -logsclient = AWS.get_connection('logs') +logsclient = boto3.client('logs') #------------------------------------------------------------------------------ # diff --git a/source/LambdaLayers/test/test_logger.py b/source/LambdaLayers/test/test_logger.py new file mode 100644 index 00000000..a5567243 --- /dev/null +++ b/source/LambdaLayers/test/test_logger.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import pytest +from pytest_mock import mocker +from logger import Logger + +def test_logger_init_debug(): + logger_test = Logger(loglevel='debug') + assert logger_test.log.getEffectiveLevel() == 10 + +def test_logger_init_info(): + logger_test = Logger(loglevel='info') + assert logger_test.log.getEffectiveLevel() == 20 + +def test_logger_init_warning(): + logger_test = Logger(loglevel='warning') + assert logger_test.log.getEffectiveLevel() == 30 + +# TODO +# 1. Add a test for DateTimeEncoder +# 2. Add a test for _format \ No newline at end of file diff --git a/source/LambdaLayers/test/test_metrics.py b/source/LambdaLayers/test/test_metrics.py index 99b824dd..2daa8cfe 100644 --- a/source/LambdaLayers/test/test_metrics.py +++ b/source/LambdaLayers/test/test_metrics.py @@ -34,10 +34,10 @@ "Parameter": { "Name": "/Solutions/SO0111/anonymous_metrics_uuid", "Type": "String", - "Value": "12345678-1234-1234-1234-123412341234", + "Value": "11111111-1111-1111-1111-111111111111", "Version": 1, "LastModifiedDate": "2021-02-25T12:58:50.591000-05:00", - "ARN": f'arn:aws:ssm:{my_region}:1111111111111111:parameter/Solutions/SO0111/anonymous_metrics_uuid', + "ARN": f'arn:aws:ssm:{my_region}:111111111111:parameter/Solutions/SO0111/anonymous_metrics_uuid', "DataType": "text" } } @@ -48,7 +48,7 @@ "Value": "v1.2.0TEST", "Version": 1, "LastModifiedDate": "2021-02-25T12:58:50.591000-05:00", - "ARN": f'arn:aws:ssm:{my_region}1:1111111111111111:parameter/Solutions/SO0111/solution_version', + "ARN": f'arn:aws:ssm:{my_region}1:111111111111:parameter/Solutions/SO0111/solution_version', "DataType": "text" } } @@ -60,7 +60,7 @@ "Value": "Yes", "Version": 1, "LastModifiedDate": "2021-02-25T12:58:50.591000-05:00", - "ARN": f'arn:aws:ssm:{my_region}:1111111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', + "ARN": f'arn:aws:ssm:{my_region}:111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', "DataType": "text" } } @@ -72,7 +72,7 @@ "Value": "No", "Version": 1, "LastModifiedDate": "2021-02-25T12:58:50.591000-05:00", - "ARN": f'arn:aws:ssm:{my_region}:1111111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', + "ARN": f'arn:aws:ssm:{my_region}:111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', "DataType": "text" } } @@ -84,7 +84,7 @@ "Value": "slartibartfast", "Version": 1, "LastModifiedDate": "2021-02-25T12:58:50.591000-05:00", - "ARN": f'arn:aws:ssm:{my_region}:1111111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', + "ARN": f'arn:aws:ssm:{my_region}:111111111111:parameter/Solutions/SO0111/sendAnonymousMetrics', "DataType": "text" } } @@ -114,7 +114,7 @@ def test_metrics_construction(mocker): metrics = Metrics({"detail-type": "unit-test"}) - assert metrics.solution_uuid == "12345678-1234-1234-1234-123412341234" + assert metrics.solution_uuid == "11111111-1111-1111-1111-111111111111" assert metrics.solution_version == "v1.2.0TEST" #------------------------------------------------------------------------------ @@ -188,7 +188,7 @@ def test_send_metrics(mocker): expected_response = { 'Solution': 'SO0111', - 'UUID': '12345678-1234-1234-1234-123412341234', + 'UUID': '11111111-1111-1111-1111-111111111111', 'TimeStamp': mocker.ANY, 'Data': { 'generator_id': 'arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3', @@ -249,7 +249,7 @@ def test_do_not_send_metrics(mocker): expected_response = { 'Solution': 'SO0111', - 'UUID': '12345678-1234-1234-1234-123412341234', + 'UUID': '11111111-1111-1111-1111-111111111111', 'TimeStamp': mocker.ANY, 'Data': { 'generator_id': 'arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3', diff --git a/source/LambdaLayers/test/test_sechub_findings.py b/source/LambdaLayers/test/test_sechub_findings.py index 859db631..a585bbb6 100644 --- a/source/LambdaLayers/test/test_sechub_findings.py +++ b/source/LambdaLayers/test/test_sechub_findings.py @@ -27,7 +27,6 @@ from logger import Logger from applogger import LogHandler import utils -from awsapi_cached_client import AWSCachedClient log_level = 'info' logger = Logger(loglevel=log_level) @@ -37,9 +36,7 @@ my_session = boto3.session.Session() my_region = my_session.region_name -AWS = AWSCachedClient(my_region) - -ssmclient = AWS.get_connection('ssm') +ssmclient = boto3.client('ssm') stubbed_ssm_client = Stubber(ssmclient) #------------------------------------------------------------------------------ @@ -115,7 +112,6 @@ def test_parse_unsupported_version(mocker): event = json.loads(test_data_in.read()) test_data_in.close() - # ssmclient = AWS.get_connection('ssm') stubbed_ssm_client = Stubber(ssmclient) stubbed_ssm_client.add_response( @@ -161,7 +157,6 @@ def test_parse_afsbp_v100(mocker): event = json.loads(test_data_in.read()) test_data_in.close() - # ssmclient = AWS.get_connection('ssm') stubbed_ssm_client = Stubber(ssmclient) stubbed_ssm_client.add_response( @@ -220,7 +215,6 @@ def test_undefined_security_standard(mocker): event['detail']['findings'][0]['ProductFields']['StandardsControlArn'] = \ "arn:aws:securityhub:::standards/aws-invalid-security-standard/v/1.2.3/ABC.1" - # ssmclient = AWS.get_connection('ssm') stubbed_ssm_client = Stubber(ssmclient) stubbed_ssm_client.add_client_error( @@ -250,3 +244,27 @@ def test_undefined_security_standard(mocker): assert finding.standard_version_supported == 'False' stubbed_ssm_client.deactivate() + +# def test_simple_notification(mocker): +# mocker.patch('utils.publish_to_sns', return_value='11111111') +# notification = findings.SHARRNotification( +# 'AFSBP', +# my_region, +# 's3.5' +# ) +# notification.severity = 'INFO' +# notification.send_to_sns = True +# notification.finding_info = { +# 'finding_id': 'aaaaaaaa-bbbb-cccc-dddd-123456789012', +# 'finding_description': 'finding description', +# 'standard_name': 'standard long name', +# 'standard_version': 'v1.0.0', +# 'standard_control': 's3.5', +# 'title': 'A door should not be ajar', +# 'region': 'us-west-2', +# 'account': '111122223333', +# 'finding_arn': "arn:aws:securityhub:us-east-1:111122223333:subscription/pci-dss/v/3.2.1/PCI.S3.1/finding/3f74c9bf-bbb3-40d8-8781-796096b35571", +# } + +# assert notification.notify() == '11111111' +# utils.publish_to_sns.assert_called_once_with('foo') diff --git a/source/LambdaLayers/test/test_utils.py b/source/LambdaLayers/test/test_utils.py index ce5bd1a9..3bb1d10a 100644 --- a/source/LambdaLayers/test/test_utils.py +++ b/source/LambdaLayers/test/test_utils.py @@ -20,11 +20,23 @@ # import pytest -from botocore.stub import Stubber, ANY from utils import resource_from_arn, partition_from_region, publish_to_sns -from awsapi_cached_client import AWSCachedClient -AWS = AWSCachedClient('us-east-1') +def test_notification(): + return { + 'INFO': 'A door is ajar', + 'finding': { + 'finding_id': '8f400859-26b0-4cd4-a935-1f96c82343e9', + 'finding_description': 'Description of the finding', + 'standard_name': 'Long Standard Name', + 'standard_version': 'v1.0.0', + 'standard_control': 'Control.Id', + 'title': 'CloudTrail.1 CloudTrail should be enabled and configured with at least one multi-region trail', + 'region': 'ap-northwest-1', + 'account': '111122223333', + 'finding_arn': "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/CloudTrail.1/finding/8f400859-26b0-4cd4-a935-1f96c82343e9" + } + } def test_resource_from_arn(): @@ -42,55 +54,3 @@ def test_partition_from_region(): # Note: does not validate region name. default expected assert partition_from_region('foo') == 'aws' assert partition_from_region('eu-west-1') == 'aws' - -#------------------------------------------------------------------------------ -# -#------------------------------------------------------------------------------ -def test_publish_to_sns_local(): - - stsclient = AWS.get_connection('sts') # in us-east-1 - stubber1 = Stubber(stsclient) - stubber1.add_response( - 'get_caller_identity', - {} - ) - stubber1.activate() - snsclient = AWS.get_connection('sns') # in us-east-1 - stubber2 = Stubber(snsclient) - stubber2.add_response( - 'publish', - {}, - { - 'TopicArn': ANY, - 'Message': ANY, - 'MessageStructure': 'json' - } - ) - stubber2.activate() - publish_to_sns('test-topic', 'Test SNS message') - -#------------------------------------------------------------------------------ -# -#------------------------------------------------------------------------------ -def test_publish_to_sns_remote(): - - stsclient = AWS.get_connection('sts') # in us-east-1 - snsclient = AWS.get_connection('sns','eu-west-1') # in eu-west-1 - stubber1 = Stubber(stsclient) - stubber1.add_response( - 'get_caller_identity', - {} - ) - stubber1.activate() - stubber2 = Stubber(snsclient) - stubber2.add_response( - 'publish', - {}, - { - 'TopicArn': ANY, - 'Message': ANY, - 'MessageStructure': 'json' - } - ) - stubber2.activate() - publish_to_sns('test-topic', 'Test SNS message', region='eu-west-1') diff --git a/source/LambdaLayers/utils.py b/source/LambdaLayers/utils.py index 817e9e03..1038f26e 100644 --- a/source/LambdaLayers/utils.py +++ b/source/LambdaLayers/utils.py @@ -16,8 +16,12 @@ import json import re +import os +import boto3 from awsapi_cached_client import AWSCachedClient +AWS_REGION = os.getenv('AWS_REGION', 'us-east-1') + class StepFunctionLambdaAnswer: """ Maintains a hash of AWS API Client connections by region and service @@ -35,7 +39,12 @@ class StepFunctionLambdaAnswer: accountid = '' automationdocid = '' remediationrole = '' + workflowdoc = '' + workflowaccount = '' eventtype = '' + resourceregion = '' + workflow_data = {} # Hash for workflow data so that it can be modified in + # in the future without changing the source code def __init__(self): """Set message and status - minimum required fields""" @@ -106,6 +115,34 @@ def update_eventtype(self, value): """Set eventtype (string)""" self.eventtype = value + def update_workflow_data(self, value): + """Set eventtype (string)""" + self.workflow_data = value + + def update_workflowdoc(self, value): + """Set eventtype (string)""" + self.workflowdoc = value + + def update_workflowaccount(self, value): + """Set eventtype (string)""" + self.workflowaccount = value + + def update_workflowrole(self, value): + """Set eventtype (string)""" + self.workflowrole = value + + def update_resourceregion(self, value): + """Set eventtype (string)""" + self.resourceregion = value + + def update_executionregion(self, value): + """Set eventtype (string)""" + self.executionregion = value + + def update_executionaccount(self, value): + """Set eventtype (string)""" + self.executionaccount = value + def update(self, answer_data): if "status" in answer_data: self.update_status(answer_data['status']) @@ -135,6 +172,20 @@ def update(self, answer_data): self.update_remediationrole(answer_data['remediationrole']) if "eventtype" in answer_data: self.update_eventtype(answer_data['eventtype']) + if "workflow_data" in answer_data: + self.update_workflow_data(answer_data['workflow_data']) + if "workflowdoc" in answer_data: + self.update_workflowdoc(answer_data['workflowdoc']) + if "workflowaccount" in answer_data: + self.update_workflowaccount(answer_data['workflowaccount']) + if "workflowrole" in answer_data: + self.update_workflowrole(answer_data['workflowrole']) + if "resourceregion" in answer_data: + self.update_resourceregion(answer_data['resourceregion']) + if "executionregion" in answer_data: + self.update_executionregion(answer_data['executionregion']) + if "executionaccount" in answer_data: + self.update_executionaccount(answer_data['executionaccount']) def resource_from_arn(arn): """ @@ -166,29 +217,23 @@ def partition_from_region(region_name): else: return 'aws' except: - return + raise -def publish_to_sns(topic_name, message, region=None): +def publish_to_sns(topic_name, message, region=''): """ Post a message to an SNS topic """ + if not region: + region = AWS_REGION + partition = partition_from_region(region) AWS = AWSCachedClient(region) # cached client object + account = boto3.client('sts').get_caller_identity()['Account'] - partition = None - - if region: - partition = partition_from_region(region) - else: - partition = 'aws' - region = 'us-east-1' - - topic_arn = 'arn:' + partition + ':sns:' + region + ':' + AWS.account + ':' + topic_name + topic_arn = f'arn:{partition}:sns:{region}:{account}:{topic_name}' - json_message = json.dumps({"default":json.dumps(message)}) message_id = AWS.get_connection('sns', region).publish( TopicArn=topic_arn, - Message=json_message, - MessageStructure='json' + Message=message ).get('MessageId', 'error') return message_id diff --git a/source/Orchestrator/check_ssm_doc_state.py b/source/Orchestrator/check_ssm_doc_state.py index 7ba5fd19..197ca800 100644 --- a/source/Orchestrator/check_ssm_doc_state.py +++ b/source/Orchestrator/check_ssm_doc_state.py @@ -24,72 +24,37 @@ from sechub_findings import Finding import utils -# Get AWS region from Lambda environment. If not present then we're not -# running under lambda, so defaulting to us-east-1 -AWS_REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') # MUST BE SET in global variables -AWS_PARTITION = os.getenv('AWS_PARTITION', 'aws') # MUST BE SET in global variables -ORCH_ROLE_BASE_NAME = 'SO0111-SHARR-Orchestrator-Member' # role to use for cross-account +ORCH_ROLE_NAME = 'SO0111-SHARR-Orchestrator-Member' # role to use for cross-account # initialise loggers LOG_LEVEL = os.getenv('log_level', 'info') LOGGER = Logger(loglevel=LOG_LEVEL) -def _get_ssm_client(account, role, region): +def _get_ssm_client(account, role, region=''): """ Create a client for ssm """ - sess = BotoSession( - account, - f'{role}_{region}' - ) - return sess.client('ssm') - -def lambda_handler(event, context): - - answer = utils.StepFunctionLambdaAnswer() - LOGGER.info(event) - if "Finding" not in event or \ - "EventType" not in event: - answer.update({ - 'status':'ERROR', - 'message':'Missing required data in request' - }) - LOGGER.error(answer.message) - return answer.json() + kwargs = {} - finding = Finding(event['Finding']) + if region: + kwargs['region_name'] = region - answer.update({ - 'securitystandard': finding.standard_shortname, - 'securitystandardversion': finding.standard_version, - 'controlid': finding.standard_control, - 'standardsupported': finding.standard_version_supported, # string True/False - 'accountid': finding.account_id - }) - - if finding.standard_version_supported != 'True': - answer.update({ - 'status':'NOTENABLED', - 'message':f'Security Standard is not enabled": "{finding.standard_name} version {finding.standard_version}"' - }) - return answer.json() + return BotoSession( + account, + f'{role}' + ).client('ssm', **kwargs) +def _add_doc_state_to_answer(doc, account, region, answer): # Connect to APIs - - ssm = _get_ssm_client(finding.account_id, ORCH_ROLE_BASE_NAME, AWS_REGION) - - automation_docid = f'SHARR-{finding.standard_shortname}_{finding.standard_version}_{finding.remediation_control}' - remediation_role = f'SO0111-Remediate-{finding.standard_shortname}-{finding.standard_version}-{finding.remediation_control}' - - answer.update({ - 'automationdocid': automation_docid, - 'remediationrole': remediation_role - }) - + ssm = _get_ssm_client( + account, + ORCH_ROLE_NAME, + region + ) # Validate input try: docinfo = ssm.describe_document( - Name=automation_docid + Name=doc )['Document'] doctype = docinfo.get('DocumentType', 'unknown') @@ -118,7 +83,7 @@ def lambda_handler(event, context): if exception_type in "InvalidDocument": answer.update({ 'status':'NOTFOUND', - 'message': f'Document {automation_docid} does not exist.' + 'message': f'Document {doc} does not exist.' }) LOGGER.error(answer.message) else: @@ -134,5 +99,61 @@ def lambda_handler(event, context): 'message':'An unhandled error occurred: ' + str(e) }) LOGGER.error(answer.message) + +def lambda_handler(event, context): + + answer = utils.StepFunctionLambdaAnswer() # holds the response to the step function + LOGGER.info(event) + if "Finding" not in event or \ + "EventType" not in event: + answer.update({ + 'status':'ERROR', + 'message':'Missing required data in request' + }) + LOGGER.error(answer.message) + return answer.json() + + finding = Finding(event['Finding']) + + answer.update({ + 'securitystandard': finding.standard_shortname, + 'securitystandardversion': finding.standard_version, + 'controlid': finding.standard_control, + 'standardsupported': finding.standard_version_supported, + 'accountid': finding.account_id, + 'resourceregion': finding.resource_region + }) + + if finding.standard_version_supported != 'True': + answer.update({ + 'status':'NOTENABLED', + 'message':f'Security Standard is not enabled": "{finding.standard_name} version {finding.standard_version}"' + }) + return answer.json() + + # Is there alt workflow configuration? + alt_workflow_doc = event.get('Workflow',{}).get('WorkflowDocument', None) + automation_docid = f'SHARR-{finding.standard_shortname}_{finding.standard_version}_{finding.remediation_control}' + remediation_role = f'SO0111-Remediate-{finding.standard_shortname}-{finding.standard_version}-{finding.remediation_control}' + + answer.update({ + 'automationdocid': automation_docid, + 'remediationrole': remediation_role + }) + + # If alt workflow is configured we don't need to check doc state, as we checked + # it in get_approval_requirement + if alt_workflow_doc: + answer.update({ + 'status': 'ACTIVE' + }) + else: + _add_doc_state_to_answer( + automation_docid, + finding.account_id, + finding.resource_region, + answer + ) + return answer.json() diff --git a/source/Orchestrator/check_ssm_execution.py b/source/Orchestrator/check_ssm_execution.py index c8da87db..2cfd9655 100644 --- a/source/Orchestrator/check_ssm_execution.py +++ b/source/Orchestrator/check_ssm_execution.py @@ -22,28 +22,29 @@ from botocore.config import Config from logger import Logger from awsapi_cached_client import BotoSession +from sechub_findings import Finding import utils from metrics import Metrics -# Get AWS region from Lambda environment. If not present then we're not -# running under lambda, so defaulting to us-east-1 -AWS_REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') # MUST BE SET in global variables -AWS_PARTITION = os.getenv('AWS_PARTITION', 'aws') # MUST BE SET in global variables -ORCH_ROLE_BASE_NAME = 'SO0111-SHARR-Orchestrator-Member' # role to use for cross-account +ORCH_ROLE_NAME = 'SO0111-SHARR-Orchestrator-Member' # role to use for cross-account # initialise loggers LOG_LEVEL = os.getenv('log_level', 'info') LOGGER = Logger(loglevel=LOG_LEVEL) -def _get_ssm_client(account, role, region): +def _get_ssm_client(account, role, region=''): """ Create a client for ssm """ - sess = BotoSession( + kwargs = {} + + if region: + kwargs['region_name'] = region + + return BotoSession( account, - f'{role}_{region}' - ) - return sess.client('ssm') + f'{role}' + ).client('ssm', **kwargs) class ParameterError(Exception): error = 'Invalid parameter input' @@ -62,7 +63,7 @@ class AutomationExecution(object): exec_id = None account = None role_base_name = None - region = None + region = None # Region where the ssm doc is running _ssm_client = None def __init__(self, exec_id, account, role_base_name, region): @@ -77,7 +78,7 @@ def __init__(self, exec_id, account, role_base_name, region): self.region = region if not re.match('^[a-zA-Z0-9_+=,.@-]{1,64}$', role_base_name): raise ParameterError(f'Invalid Value for Role_Base_Name: {role_base_name}') - self.region = region + self._ssm_client = _get_ssm_client(self.account, role_base_name, self.region) self.get_execution_state() @@ -107,11 +108,11 @@ def get_execution_state(self): {} ) - if 'Remediation.Output' in self.outputs: - if isinstance(self.outputs['Remediation.Output'], list): - if len(self.outputs['Remediation.Output']) == 1: - if self.outputs['Remediation.Output'][0] == "No output available yet because the step is not successfully executed": - self.outputs['Remediation.Output'][0] = "See Automation Execution output for details" + if 'Remediation.Output' in self.outputs and \ + isinstance(self.outputs['Remediation.Output'], list) and \ + len(self.outputs['Remediation.Output']) == 1 and \ + self.outputs['Remediation.Output'][0] == "No output available yet because the step is not successfully executed": + self.outputs['Remediation.Output'][0] = "See Automation Execution output for details" self.failure_message = automation_exec_info.get( "AutomationExecutionMetadataList" @@ -120,9 +121,6 @@ def get_execution_state(self): "" ) -def get_lambda_role(role_base_name, security_standard, aws_region): - return role_base_name + '-' + security_standard + '_' + aws_region - def valid_automation_doc(automation_doc): return "SecurityStandard" in automation_doc and \ "ControlId" in automation_doc and \ @@ -141,6 +139,13 @@ def lambda_handler(event, context): return answer.json() SSM_EXEC_ID = event['SSMExecution']['ExecId'] + SSM_ACCOUNT = event['SSMExecution'].get('Account') + SSM_REGION = event['SSMExecution'].get('Region') + + if not SSM_ACCOUNT or not SSM_REGION: + exit('ERROR: missing remediation account information. SSMExecution missing region or account.') + + finding = Finding(event['Finding']) metrics_obj = Metrics( event['EventType'] @@ -148,7 +153,7 @@ def lambda_handler(event, context): metrics_data = metrics_obj.get_metrics_from_finding(event['Finding']) try: - automation_exec_info = AutomationExecution(SSM_EXEC_ID, automation_doc['AccountId'], ORCH_ROLE_BASE_NAME, AWS_REGION) + automation_exec_info = AutomationExecution(SSM_EXEC_ID, SSM_ACCOUNT, ORCH_ROLE_NAME, SSM_REGION) except Exception as e: LOGGER.error(f'Unable to retrieve AutomationExecution data: {str(e)}') raise e diff --git a/source/Orchestrator/exec_ssm_doc.py b/source/Orchestrator/exec_ssm_doc.py index 7cc8f5c0..ed8bc692 100644 --- a/source/Orchestrator/exec_ssm_doc.py +++ b/source/Orchestrator/exec_ssm_doc.py @@ -22,29 +22,31 @@ from logger import Logger from awsapi_cached_client import BotoSession from applogger import LogHandler +from sechub_findings import Finding, SHARRNotification import utils -# Get AWS region from Lambda environment. If not present then we're not -# running under lambda, so defaulting to us-east-1 -AWS_REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') # MUST BE SET in global variables -AWS_PARTITION = os.getenv('AWS_PARTITION', 'aws') # MUST BE SET in global variables +AWS_PARTITION = os.getenv('AWS_PARTITION', 'aws') +AWS_REGION = os.getenv('AWS_REGION', 'aws') SOLUTION_ID = os.getenv('SOLUTION_ID', 'SO0111') SOLUTION_ID = re.sub(r'^DEV-', '', SOLUTION_ID) -SOLUTION_VERSION = os.getenv('SOLUTION_VERSION', 'undefined') # initialise loggers LOG_LEVEL = os.getenv('log_level', 'info') LOGGER = Logger(loglevel=LOG_LEVEL) -def _get_ssm_client(accountid, role): +def _get_ssm_client(account, role, region=''): """ Create a client for ssm """ - return BotoSession( - accountid, - role - ).client('ssm') + kwargs = {} + if region: + kwargs['region_name'] = region + + return BotoSession( + account, + f'{role}' + ).client('ssm', **kwargs) def _get_iam_client(accountid, role): """ @@ -55,9 +57,13 @@ def _get_iam_client(accountid, role): role ).client('iam') -def lambda_role_exists(client, rolename): +def lambda_role_exists(account, rolename): + iam = _get_iam_client( + account, + SOLUTION_ID + '-SHARR-Orchestrator-Member' + ) try: - client.get_role( + iam.get_role( RoleName=rolename ) return True @@ -78,7 +84,8 @@ def lambda_handler(event, context): # ControlId: string # }, # RemediationRole: string, - # AutomationDocId: string + # AutomationDocId: string. + # SSMExecution: json data # } # Returns: # { @@ -86,10 +93,29 @@ def lambda_handler(event, context): # message: { '' | string }, # executionid: { '' | string } # } - answer = utils.StepFunctionLambdaAnswer() + LOGGER.info(event) + if "Finding" not in event or \ + "EventType" not in event: + answer.update({ + 'status':'ERROR', + 'message':'Missing required data in request' + }) + LOGGER.error(answer.message) + return answer.json() + + finding = Finding(event['Finding']) automation_doc = event['AutomationDocument'] + alt_workflow_doc = event.get('Workflow',{}).get('WorkflowDocument', None) + alt_workflow_account = event.get('Workflow',{}).get('WorkflowAccount', None) + alt_workflow_role = event.get('Workflow',{}).get('WorkflowRole', None) + alt_workflow_config = event.get('Workflow',{}).get('WorkflowConfig', None) + + remote_workflow_doc = alt_workflow_doc if alt_workflow_doc else event['AutomationDocument']['AutomationDocId'] + + execution_account = alt_workflow_account if alt_workflow_account else automation_doc['AccountId'] + execution_region = AWS_REGION if alt_workflow_account else automation_doc.get('ResourceRegion', '') if "SecurityStandard" not in automation_doc or \ "ControlId" not in automation_doc or \ @@ -101,44 +127,52 @@ def lambda_handler(event, context): LOGGER.error(answer.message) return answer.json() - orchestrator_member_role = SOLUTION_ID + '-SHARR-Orchestrator-Member_' + AWS_REGION - standard_role = automation_doc['RemediationRole'] + '_' + AWS_REGION - - # If the standard/version/control has a specific role defined then use it - # Otherwise, use the Orchestrator Member role - remediation_role = orchestrator_member_role - iam = _get_iam_client(automation_doc['AccountId'], remediation_role) - - if lambda_role_exists(iam, standard_role): - remediation_role = standard_role - - print(f'Using role {remediation_role} for remediation in {automation_doc["AccountId"]} document {automation_doc["AutomationDocId"]}') - remediation_role_arn = 'arn:' + AWS_PARTITION + ':iam::' + automation_doc['AccountId'] + \ - ':role/' + remediation_role + # Execution role will be, in order of precedence + # 1) remote_workflow_role + # 2) Derived from standard and control if it exists + # 3) Orchestrator Member role + # + # In most cases the Orchestrator Member role is used, and it passes + # the value in RemediationRole as the AutomationExectutionRole + remediation_role = SOLUTION_ID + '-SHARR-Orchestrator-Member' # default + if alt_workflow_doc and alt_workflow_role: + remediation_role = alt_workflow_role + elif lambda_role_exists(execution_account, automation_doc['RemediationRole']): + remediation_role = automation_doc['RemediationRole'] + + print(f'Using role {remediation_role} to execute {remote_workflow_doc} in {execution_account} {execution_region}') + + remediation_role_arn = f'arn:{AWS_PARTITION}:iam::{execution_account}:role/{remediation_role}' print(f'ARN: {remediation_role_arn}') - ssm = _get_ssm_client(automation_doc['AccountId'], remediation_role) + ssm = _get_ssm_client(execution_account, remediation_role, execution_region) + + ssm_parameters = { + "Finding": [ + json.dumps(event['Finding']) + ], + "AutomationAssumeRole": [ + remediation_role_arn + ] + } + if remote_workflow_doc != automation_doc['AutomationDocId']: + ssm_parameters["RemediationDoc"] = [automation_doc['AutomationDocId']] + ssm_parameters["Workflow"] = [json.dumps(event.get('Workflow', {}))] exec_id = ssm.start_automation_execution( # Launch SSM Doc via Automation - DocumentName=automation_doc['AutomationDocId'], - Parameters={ - "Finding": [ - json.dumps(event['Finding']) - ], - "AutomationAssumeRole": [ - remediation_role_arn - ] - } + DocumentName=remote_workflow_doc, + Parameters=ssm_parameters )['AutomationExecutionId'] answer.update({ - 'status':'SUCCESS', - 'message': automation_doc['ControlId'] + - ' remediation was successfully invoked via AWS Systems Manager in account ' + - automation_doc['AccountId'] + ': ' + exec_id, - 'executionid': exec_id + 'status':'QUEUED', + 'message': f'{exec_id}: {automation_doc["ControlId"]} remediation was successfully invoked via AWS Systems Manager in account {automation_doc["AccountId"]} {execution_region}', + 'executionid': exec_id, + 'executionregion': execution_region, + 'executionaccount': execution_account }) + LOGGER.info(answer.message) return answer.json() diff --git a/source/Orchestrator/get_approval_requirement.py b/source/Orchestrator/get_approval_requirement.py new file mode 100644 index 00000000..4de848f3 --- /dev/null +++ b/source/Orchestrator/get_approval_requirement.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +""" +Check the value of the Lambda Environmental variable RUN_WORKFLOW. If set, +send the remediation input to the member account runbook named in the +RUN_WORKFLOW variable. + +This Lambda can be further modified by the customer to gather additional +information to determine when to inject RUN_WORKFLOW. Methods are defined +and stubbed out to support this: _is_remediation_destructive(), etc. +""" +import json +import boto3 +import os +import re +from botocore.config import Config +from botocore.exceptions import ClientError +from logger import Logger +from awsapi_cached_client import BotoSession +from sechub_findings import Finding +import utils + +# initialise loggers +LOG_LEVEL = os.getenv('log_level', 'info') +LOGGER = Logger(loglevel=LOG_LEVEL) +# If env WORKFLOW_RUNBOOK is set and not blank then all remediations will be +# executed through this runbook, if it is present and enabled in the member +# account. +SOLUTION_ID = os.getenv('SOLUTION_ID', 'SO0111') +SOLUTION_ID = re.sub(r'^DEV-', '', SOLUTION_ID) + +def _get_ssm_client(account, role, region=''): + """ + Create a client for ssm + """ + sess = BotoSession( + account, + f'{role}' + ) + kwargs = {} + if region: + kwargs['region_name'] = region + + return sess.client('ssm', **kwargs) + +def _is_remediation_destructive(standard, version, control): + return False + +def _is_account_sensitive(accountid): + return False + +def _is_automatic_trigger(event_type): + if event_type == 'Security Hub Findings - Imported': + return False + else: + return True + +def _is_custom_action_trigger(event_type): + if event_type == 'Security Hub Findings - Imported': + return True + else: + return False + +def _get_alternate_workflow(accountid): + """ + Get the alt workflow based on environmental variables for this lambda + and whether the alt runbook is active. Note that alt workflow must run + in the same region as the Step Function. + """ + running_account = boto3.client('sts').get_caller_identity()['Account'] + + # Is an alternate workflow defined? + WORKFLOW_RUNBOOK = os.getenv('WORKFLOW_RUNBOOK', '') + WORKFLOW_RUNBOOK_ACCOUNT = os.getenv('WORKFLOW_RUNBOOK_ACCOUNT', 'member') + WORKFLOW_RUNBOOK_ROLE = os.getenv('WORKFLOW_RUNBOOK_ROLE', '') + + # Disabled by removing the Lambda environmental var or setting to '' + if not WORKFLOW_RUNBOOK: + return (None, None, None) + + if WORKFLOW_RUNBOOK_ACCOUNT.lower() == 'member': + WORKFLOW_RUNBOOK_ACCOUNT = accountid + elif WORKFLOW_RUNBOOK_ACCOUNT.lower() == 'admin': + WORKFLOW_RUNBOOK_ACCOUNT = running_account + else: + # log an error - bad config + LOGGER.error(f'WORKFLOW_RUNBOOK_ACCOUNT config error: "{WORKFLOW_RUNBOOK_ACCOUNT}" is not valid. Must be "member" or "admin"') + return (None, None, None) + + # Make sure it exists and is active + if _doc_is_active(WORKFLOW_RUNBOOK, WORKFLOW_RUNBOOK_ACCOUNT): + return(WORKFLOW_RUNBOOK, WORKFLOW_RUNBOOK_ACCOUNT, WORKFLOW_RUNBOOK_ROLE) + else: + return(None, None, None) + +def _doc_is_active(doc, account): + try: + ssm = _get_ssm_client(account, SOLUTION_ID + '-SHARR-Orchestrator-Member') + docinfo = ssm.describe_document( + Name=doc + )['Document'] + + doctype = docinfo.get('DocumentType', 'unknown') + docstate = docinfo.get('Status', 'unknown') + + if doctype == "Automation" and \ + docstate == "Active": + return True + else: + return False + + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + if exception_type in "InvalidDocument": + return False + else: + LOGGER.error('An unhandled client error occurred: ' + exception_type) + return False + + except Exception as e: + LOGGER.error('An unhandled error occurred: ' + str(e)) + return False + +def lambda_handler(event, context): + answer = utils.StepFunctionLambdaAnswer() + answer.update({ + 'workflowdoc': '', + 'workflowaccount': '', + 'workflowrole': '', + 'workflow_data': { + 'impact': 'nondestructive', + 'approvalrequired': 'false' + } + }) + LOGGER.info(event) + if "Finding" not in event or \ + "EventType" not in event: + answer.update({ + 'status':'ERROR', + 'message':'Missing required data in request' + }) + LOGGER.error(answer.message) + return answer.json() + + finding = Finding(event['Finding']) + + auto_trigger = _is_automatic_trigger(event['EventType']) + is_destructive = _is_remediation_destructive(finding.standard_shortname, finding.standard_version, finding.standard_control) + is_sensitive = _is_account_sensitive(finding.account_id) + + approval_required = 'false' + remediation_impact = 'nondestructive' + use_alt_workflow = 'false' + + # + # PUT ADDITIONAL CRITERIA HERE. When done, remediation_impact and approval_required + # must be set per your needs + #---------------------------------------------------------------------------------- + if auto_trigger and is_destructive and is_sensitive: + remediation_impact = 'destructive' + approval_required = 'true' + use_alt_workflow = 'true' + + #---------------------------------------------------------------------------------- + + # Is there an alternative workflow configured? + (alt_workflow, alt_account, alt_role) = _get_alternate_workflow(finding.account_id) + + # If so, update workflow_data + # --------------------------- + # workflow_data can be modified to suit your needs. This data is passed to the + # alt_workflow. Using the alt_workflow redirects the remediation to your workflow + # only! The normal SHARR workflow will not be executed. + #---------------------------------------------------------------------------------- + if alt_workflow and use_alt_workflow: + answer.update({ + 'workflowdoc': alt_workflow, + 'workflowaccount': alt_account, + 'workflowrole': alt_role, + 'workflow_data': { + 'impact': remediation_impact, + 'approvalrequired': approval_required + } + }) + + return answer.json() diff --git a/source/Orchestrator/lib/common-orchestrator-construct.ts b/source/Orchestrator/lib/common-orchestrator-construct.ts index 512cd8f7..d5bb8426 100644 --- a/source/Orchestrator/lib/common-orchestrator-construct.ts +++ b/source/Orchestrator/lib/common-orchestrator-construct.ts @@ -34,6 +34,7 @@ export interface ConstructProps { ssmExecDocLambda: string; ssmExecMonitorLambda: string; notifyLambda: string; + getApprovalRequirementLambda: string; solutionId: string; solutionName: string; solutionVersion: string; @@ -82,13 +83,19 @@ export class OrchestratorConstruct extends cdk.Construct { let execRemediationFunc: lambda.IFunction = lambda.Function.fromFunctionAttributes(this, 'execRemediationFunc',{ functionArn: props.ssmExecDocLambda }) + let execMonFunc: lambda.IFunction = lambda.Function.fromFunctionAttributes(this, 'getExecStatusFunc',{ functionArn: props.ssmExecMonitorLambda }) + let notifyFunc: lambda.IFunction = lambda.Function.fromFunctionAttributes(this, 'notifyFunc',{ functionArn: props.notifyLambda }) + let getApprovalRequirementFunc: lambda.IFunction = lambda.Function.fromFunctionAttributes(this, 'getRequirementFunc',{ + functionArn: props.getApprovalRequirementLambda + }) + const orchestratorFailed = new sfn.Pass(this, 'Orchestrator Failed', { parameters: { "Notification": { @@ -113,12 +120,27 @@ export class OrchestratorConstruct extends cdk.Construct { "ControlId.$": "$.Payload.controlid", "AccountId.$": "$.Payload.accountid", "RemediationRole.$": "$.Payload.remediationrole", - "AutomationDocId.$": "$.Payload.automationdocid" + "AutomationDocId.$": "$.Payload.automationdocid", + "ResourceRegion.$": "$.Payload.resourceregion" }, resultPath: "$.AutomationDocument" }) getDocState.addCatch(orchestratorFailed) + const getApprovalRequirement = new LambdaInvoke(this, 'Get Remediation Approval Requirement', { + comment: "Determine whether the selected remediation requires manual approval", + lambdaFunction: getApprovalRequirementFunc, + timeout: cdk.Duration.minutes(5), + resultSelector: { + "WorkflowDocument.$": "$.Payload.workflowdoc", + "WorkflowAccount.$": "$.Payload.workflowaccount", + "WorkflowRole.$": "$.Payload.workflowrole", + "WorkflowConfig.$": "$.Payload.workflow_data" + }, + resultPath: "$.Workflow" + }) + getApprovalRequirement.addCatch(orchestratorFailed) + const remediateFinding = new LambdaInvoke(this, 'Execute Remediation', { comment: "Execute the SSM Automation Document in the target account", lambdaFunction: execRemediationFunc, @@ -127,7 +149,9 @@ export class OrchestratorConstruct extends cdk.Construct { resultSelector: { "ExecState.$": "$.Payload.status", "Message.$": "$.Payload.message", - "ExecId.$": "$.Payload.executionid" + "ExecId.$": "$.Payload.executionid", + "Account.$": "$.Payload.executionaccount", + "Region.$": "$.Payload.executionregion" }, resultPath: "$.SSMExecution" }) @@ -157,6 +181,14 @@ export class OrchestratorConstruct extends cdk.Construct { timeout: cdk.Duration.minutes(5) }) + const notifyQueued = new LambdaInvoke(this, 'Queued Notification', { + comment: "Send notification that a remediation has queued", + lambdaFunction: notifyFunc, + heartbeat: cdk.Duration.seconds(60), + timeout: cdk.Duration.minutes(5), + resultPath: "$.notificationResult" + }) + new sfn.Fail(this, 'Job Failed', { cause: 'AWS Batch Job Failed', error: 'DescribeJob returned FAILED', @@ -267,12 +299,8 @@ export class OrchestratorConstruct extends cdk.Construct { parameters: { "EventType.$": "$.EventType", "Finding.$": "$.Finding", - "AccountId.$": "$.AutomationDocument.AccountId", - "AutomationDocId.$": "$.AutomationDocument.AutomationDocId", - "RemediationRole.$": "$.AutomationDocument.RemediationRole", - "ControlId.$": "$.AutomationDocument.ControlId", - "SecurityStandard.$": "$.AutomationDocument.SecurityStandard", - "SecurityStandardVersion.$": "$.AutomationDocument.SecurityStandardVersion", + "SSMExecution.$": "$.SSMExecution", + "AutomationDocument.$": "$.AutomationDocument", "Notification": { "Message.$": "States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)", "State.$": "$.Remediation.ExecState", @@ -304,6 +332,21 @@ export class OrchestratorConstruct extends cdk.Construct { } }) + const remediationQueued = new sfn.Pass(this, 'Remediation Queued', { + comment: 'Set parameters for notification', + parameters: { + "EventType.$": "$.EventType", + "Finding.$": "$.Finding", + "AutomationDocument.$": "$.AutomationDocument", + "SSMExecution.$": "$.SSMExecution", + "Notification": { + "Message.$": "States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)", + "State.$": "States.Format('QUEUED')", + "ExecId.$": "$.SSMExecution.ExecId" + } + } + }) + //----------------------------------------------------------------- // State Machine // @@ -326,7 +369,7 @@ export class OrchestratorConstruct extends cdk.Construct { ), ) ), - getDocState + getApprovalRequirement ) checkWorkflowNew.otherwise(docNotNew) @@ -335,6 +378,8 @@ export class OrchestratorConstruct extends cdk.Construct { // Call Lambda to get status of the automation document in the target account getDocState.next(checkDocState) + getApprovalRequirement.next(getDocState) + checkDocState.when( sfn.Condition.stringEquals( '$.AutomationDocument.DocState', @@ -370,7 +415,14 @@ export class OrchestratorConstruct extends cdk.Construct { docStateError.next(notify) // Execute the remediation - remediateFinding.next(execMonitor) + // remediateFinding.next(execMonitor) + + // Send a notification + remediateFinding.next(remediationQueued) + + remediationQueued.next(notifyQueued) + + notifyQueued.next(execMonitor) execMonitor.next(isdone) @@ -448,7 +500,8 @@ export class OrchestratorConstruct extends cdk.Construct { `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${getDocStateFunc.functionName}`, `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${execRemediationFunc.functionName}`, `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${execMonFunc.functionName}`, - `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${notifyFunc.functionName}` + `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${notifyFunc.functionName}`, + `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${getApprovalRequirementFunc.functionName}` ] }) ) @@ -473,6 +526,7 @@ export class OrchestratorConstruct extends cdk.Construct { 'BasePolicy': orchestratorPolicy } }); + orchestratorRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) { let childToMod = orchestratorRole.node.defaultChild as CfnRole diff --git a/source/Orchestrator/send_notifications.py b/source/Orchestrator/send_notifications.py index 13fd6c87..7612176d 100644 --- a/source/Orchestrator/send_notifications.py +++ b/source/Orchestrator/send_notifications.py @@ -73,12 +73,28 @@ def lambda_handler(event, context): # Get finding status finding_status = 'FAILED' # default state - if event['Notification']['State'] == 'SUCCESS': + if event['Notification']['State'].upper == 'SUCCESS': finding_status = 'RESOLVED' + elif event['Notification']['State'].upper == 'QUEUED': + finding_status = 'PENDING' + # elif event['Notification']['State'].upper == 'FAILED': + # finding_status = 'FAILED' finding = None + finding_info = '' if 'Finding' in event: finding = sechub_findings.Finding(event['Finding']) + finding_info = { + 'finding_id': finding.uuid, + 'finding_description': finding.description, + 'standard_name': finding.standard_name, + 'standard_version': finding.standard_version, + 'standard_control': finding.standard_control, + 'title': finding.title, + 'region': finding.region, + 'account': finding.account_id, + 'finding_arn': finding.arn + } # Send anonymous metrics if 'EventType' in event and 'Finding' in event: @@ -87,7 +103,7 @@ def lambda_handler(event, context): metrics_data['status'] = finding_status metrics.send_metrics(metrics_data) - if event['Notification']['State'].upper() == 'SUCCESS': + if event['Notification']['State'].upper() in ('SUCCESS', 'QUEUED'): notification = sechub_findings.SHARRNotification( event.get('SecurityStandard', 'SHARR'), AWS_REGION, @@ -96,6 +112,15 @@ def lambda_handler(event, context): notification.severity = 'INFO' notification.send_to_sns = True + elif event['Notification']['State'].upper() == 'FAILED': + notification = sechub_findings.SHARRNotification( + event.get('SecurityStandard', 'SHARR'), + AWS_REGION, + event.get('ControlId', None) + ) + notification.severity = 'ERROR' + notification.send_to_sns = True + elif event['Notification']['State'].upper() == 'WRONGSTANDARD': notification = sechub_findings.SHARRNotification('SHARR',AWS_REGION, None) notification.severity = 'ERROR' @@ -117,4 +142,6 @@ def lambda_handler(event, context): notification.message = message_prefix + event['Notification']['Message'] + message_suffix if 'Details' in event['Notification'] and event['Notification']['Details'] != 'MISSING': notification.logdata = format_details_for_output(event['Notification']['Details']) + + notification.finding_info = finding_info notification.notify() diff --git a/source/Orchestrator/test/test_check_ssm_doc_state.py b/source/Orchestrator/test/test_check_ssm_doc_state.py index f225ee69..8b95d8b2 100644 --- a/source/Orchestrator/test/test_check_ssm_doc_state.py +++ b/source/Orchestrator/test/test_check_ssm_doc_state.py @@ -21,12 +21,70 @@ import os import pytest import boto3 +import botocore.session +from botocore.config import Config from botocore.stub import Stubber, ANY from pytest_mock import mocker from check_ssm_doc_state import lambda_handler from awsapi_cached_client import AWSCachedClient +import sechub_findings -REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region +) + +def workflow_doc(): + return { + "Document": { + "Status": "Active", + "Hash": "15b9f136e2cb0b47490dc5b38b439905e3f36fe1a8a411c1d278f2f2eb6fe633", + "Name": "test-workflow", + "Parameters": [ + { + "Type": "String", + "Name": "AutomationAssumeRole", + "Description": "The ARN of the role that allows Automation to perform the actions on your behalf." + }, + { + "Type": "StringMap", + "Name": "Finding", + "Description": "The Finding data from the Orchestrator Step Function" + }, + { + "Type": "StringMap", + "Name": "SSMExec", + "Description": "Data for decision support in this runbook" + }, + { + "Type": "String", + "Name": "RemediationDoc", + "Description": "the SHARR Remediation (ingestion) runbook to execute" + } + ], + "Tags": [], + "DocumentType": "Automation", + "PlatformTypes": [ + "Windows", + "Linux", + "MacOS" + ], + "DocumentVersion": "1", + "HashType": "Sha256", + "CreatedDate": 1633985125.065, + "Owner": "111111111111", + "SchemaVersion": "0.3", + "DefaultVersion": "1", + "DocumentFormat": "YAML", + "LatestVersion": "1", + "Description": "### Document Name - SHARR-Run_Remediation\n\n## What does this document do?\nThis document is executed by the AWS Security Hub Automated Response and Remediation Orchestrator Step Function. It implements controls such as manual approvals based on criteria passed by the Orchestrator.\n\n## Input Parameters\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n* Finding: (Required) json-formatted finding data\n* RemedationDoc: (Required) remediation runbook to execute after approval\n* SSMExec: (Required) json-formatted data for decision support in determining approval requirement\n" + } + } def test_sunny_day(mocker): test_input = { @@ -66,13 +124,14 @@ def test_sunny_day(mocker): 'message': '', 'remediation_status': '', 'remediationrole': 'SO0111-Remediate-AFSBP-1.0.0-AutoScaling.1', + 'resourceregion': 'us-east-1', 'securitystandard': 'AFSBP', 'securitystandardversion': '1.0.0', 'standardsupported': 'True', 'status': 'ACTIVE' } # use AWSCachedClient as it will us the same stub for any calls - AWS = AWSCachedClient(REGION) + AWS = AWSCachedClient(my_region) ssm_c = AWS.get_connection('ssm') testing_account = boto3.client('sts').get_caller_identity()['Account'] @@ -223,6 +282,7 @@ def test_doc_not_active(mocker): 'logdata': [], 'message': 'Document SHARR-AFSBP_1.0.0_AutoScaling.17 does not exist.', 'remediation_status': '', + 'resourceregion': 'us-east-1', 'remediationrole': 'SO0111-Remediate-AFSBP-1.0.0-AutoScaling.17', 'securitystandard': 'AFSBP', 'securitystandardversion': '1.0.0', @@ -230,7 +290,7 @@ def test_doc_not_active(mocker): 'status': 'NOTFOUND' } # use AWSCachedClient as it will us the same stub for any calls - AWS = AWSCachedClient(REGION) + AWS = AWSCachedClient(my_region) ssm_c = AWS.get_connection('ssm') testing_account = boto3.client('sts').get_caller_identity()['Account'] @@ -318,13 +378,14 @@ def test_client_error(mocker): 'message': 'An unhandled client error occurred: ADoorIsAjar', 'remediation_status': '', 'remediationrole': 'SO0111-Remediate-AFSBP-1.0.0-AutoScaling.1', + 'resourceregion': 'us-east-1', 'securitystandard': 'AFSBP', 'securitystandardversion': '1.0.0', 'standardsupported': 'True', 'status': 'CLIENTERROR' } # use AWSCachedClient as it will us the same stub for any calls - AWS = AWSCachedClient(REGION) + AWS = AWSCachedClient(my_region) ssm_c = AWS.get_connection('ssm') testing_account = boto3.client('sts').get_caller_identity()['Account'] @@ -410,13 +471,14 @@ def test_control_remap(mocker): 'logdata': [], 'message': '', 'remediation_status': '', + 'resourceregion': 'us-east-1', 'remediationrole': 'SO0111-Remediate-CIS-1.2.0-1.5', 'securitystandard': 'CIS', 'securitystandardversion': '1.2.0', 'standardsupported': 'True', 'status': 'ACTIVE' } - AWS = AWSCachedClient(REGION) + AWS = AWSCachedClient(my_region) ssm_c = AWS.get_connection('ssm') testing_account = boto3.client('sts').get_caller_identity()['Account'] @@ -533,3 +595,102 @@ def test_control_remap(mocker): assert lambda_handler(test_input, {}) == expected_good_response ssmc_stub.deactivate() + +#=============================================================================== +def test_alt_workflow_with_role(mocker): + test_input = { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "GeneratorId": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.6", + "RecordState": "ACTIVE", + "Workflow": { + "Status": "NEW" + }, + "WorkflowState": "NEW", + "ProductFields": { + "RuleId": "1.6", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/cis-aws-foundations-benchmark/v/1.2.0/1.6", + }, + "AwsAccountId": "111111111111", + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/1.6/finding/3fe13eb6-b093-48b2-ba3b-b975347c3183", + "Resources": [ + { + "Partition": "aws", + "Type": "AwsAccount", + "Region": "us-east-1", + "Id": "AWS::::Account:111111111111" + } + ] + }, + "Workflow": { + "WorkflowDocument": "AlternateDoc" + } + } + + expected_good_response = { + 'accountid': '111111111111', + 'automationdocid': 'SHARR-CIS_1.2.0_1.6', + 'controlid': '1.6', + 'logdata': [], + 'message': '', + 'remediation_status': '', + 'resourceregion': 'us-east-1', + 'remediationrole': 'SO0111-Remediate-CIS-1.2.0-1.6', + 'securitystandard': 'CIS', + 'securitystandardversion': '1.2.0', + 'standardsupported': 'True', + 'status': 'ACTIVE' + } + + ssm = botocore.session.get_session().create_client('ssm', config=BOTO_CONFIG) + ssm_stubber = Stubber(ssm) + ssm_stubber.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/shortname", + "Type": "String", + "Value": "CIS", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "DataType": "text" + } + },{ + "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/shortname" + } + ) + + ssm_stubber.add_client_error( + 'get_parameter', + 'ParameterNotFound' + ) + + ssm_stubber.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/1.2.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text" + } + } + ) + + ssm_stubber.add_response( + 'describe_document', + workflow_doc() + ) + + ssm_stubber.activate() + mocker.patch('check_ssm_doc_state._get_ssm_client', return_value=ssm) + mocker.patch('sechub_findings.get_ssm_connection', return_value=ssm) + + result = lambda_handler(test_input, {}) + + assert result == expected_good_response diff --git a/source/Orchestrator/test/test_check_ssm_execution.py b/source/Orchestrator/test/test_check_ssm_execution.py index 709bc226..fe886d7a 100644 --- a/source/Orchestrator/test/test_check_ssm_execution.py +++ b/source/Orchestrator/test/test_check_ssm_execution.py @@ -108,7 +108,9 @@ "SSMExecution": { "Message": "AutoScaling.1remediation was successfully invoked via AWS Systems Manager in account 111111111111: 43374019-a309-4627-b8a2-c641e0140262", "ExecId": "43374019-a309-4627-b8a2-c641e0140262", - "ExecState": "SUCCESS" + "ExecState": "SUCCESS", + "Account": "111111111111", + "Region": "us-east-1" }, "Remediation": { "LogData": [], diff --git a/source/Orchestrator/test/test_exec_ssm_doc.py b/source/Orchestrator/test/test_exec_ssm_doc.py index 1da49d57..a9a9090e 100644 --- a/source/Orchestrator/test/test_exec_ssm_doc.py +++ b/source/Orchestrator/test/test_exec_ssm_doc.py @@ -24,10 +24,11 @@ import boto3 from botocore.stub import Stubber, ANY from exec_ssm_doc import lambda_handler -from awsapi_cached_client import AWSCachedClient from pytest_mock import mocker -REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') +my_session = boto3.session.Session() +my_region = my_session.region_name +os.environ['AWS_REGION'] = my_region def test_exec_runbook(mocker): """ @@ -108,6 +109,12 @@ def test_exec_runbook(mocker): "ControlId": "AutoScaling.1", "SecurityStandard": "AFSBP", "SecurityStandardSupported": "True" + }, + "SSMExecution": { + "workflow_data": { + "impact": "nondestructive", + "approvalrequired": "false" + } } } @@ -116,13 +123,12 @@ def test_exec_runbook(mocker): 'logdata': [], 'message': 'AutoScaling.1 remediation was successfully invoked via AWS Systems Manager in account 111111111111: 43374019-a309-4627-b8a2-c641e0140262', 'remediation_status': '', - 'status': 'SUCCESS' + 'status': 'QUEUED' } - AWS = AWSCachedClient(REGION) - account = AWS.get_connection('sts').get_caller_identity()['Account'] + account = boto3.client('sts').get_caller_identity()['Account'] step_input['AutomationDocument']['AccountId'] = account - iam_c = AWS.get_connection('iam') + iam_c = boto3.client('iam') iamc_stub = Stubber(iam_c) iamc_stub.add_client_error( 'get_role', @@ -130,7 +136,7 @@ def test_exec_runbook(mocker): ) iamc_stub.activate() - ssm_c = AWS.get_connection('ssm') + ssm_c = boto3.client('ssm') ssmc_stub = Stubber(ssm_c) ssmc_stub.add_response( 'start_automation_execution', @@ -147,12 +153,12 @@ def test_exec_runbook(mocker): ANY ] } - } - ) + }) ssmc_stub.activate() mocker.patch('exec_ssm_doc._get_ssm_client', return_value=ssm_c) mocker.patch('exec_ssm_doc._get_iam_client', return_value=iam_c) + mocker.patch('sechub_findings.SHARRNotification.notify') response = lambda_handler(step_input, {}) assert response['executionid'] == expected_result['executionid'] diff --git a/source/Orchestrator/test/test_get_approval_requirement.py b/source/Orchestrator/test/test_get_approval_requirement.py new file mode 100644 index 00000000..1d738ac7 --- /dev/null +++ b/source/Orchestrator/test/test_get_approval_requirement.py @@ -0,0 +1,442 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +""" +Unit Test: exec_ssm_doc.py +Run from /deployment/temp/source/Orchestrator after running build-s3-dist.sh +""" + +import os +import pytest +import boto3 +from botocore.stub import Stubber, ANY +from get_approval_requirement import lambda_handler +from awsapi_cached_client import AWSCachedClient +from pytest_mock import mocker + +LOCAL_ACCOUNT = boto3.client('sts').get_caller_identity()['Account'] +REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') + +def step_input(): + return { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "SchemaVersion": "2018-10-08", + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1/finding/635ceb5d-3dfd-4458-804e-48a42cd723e4", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "GeneratorId": "aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "AwsAccountId": "111111111111", + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/AWS-Foundational-Security-Best-Practices" + ], + "FirstObservedAt": "2020-07-24T01:34:19.369Z", + "LastObservedAt": "2021-02-18T13:45:30.638Z", + "CreatedAt": "2020-07-24T01:34:19.369Z", + "UpdatedAt": "2021-02-18T13:45:28.802Z", + "Severity": { + "Product": 0, + "Label": "INFORMATIONAL", + "Normalized": 0, + "Original": "INFORMATIONAL" + }, + "Title": "AutoScaling.1 Auto scaling groups associated with a load balancer should use load balancer health checks", + "Description": "This control checks whether your Auto Scaling groups that are associated with a load balancer are using Elastic Load Balancing health checks.", + "Remediation": { + "Recommendation": { + "Text": "For directions on how to fix this issue, please consult the AWS Security Hub Foundational Security Best Practices documentation.", + "Url": "https://docs.aws.amazon.com/console/securityhub/AutoScaling.1/remediation" + } + }, + "ProductFields": { + "StandardsArn": "arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0", + "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0", + "ControlId": "AutoScaling.1", + "RecommendationUrl": "https://docs.aws.amazon.com/console/securityhub/AutoScaling.1/remediation", + "RelatedAWSResources:0/name": "securityhub-autoscaling-group-elb-healthcheck-required-f986ecc9", + "RelatedAWSResources:0/type": "AWS::Config::ConfigRule", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "aws/securityhub/ProductName": "Security Hub", + "aws/securityhub/CompanyName": "AWS", + "aws/securityhub/annotation": "AWS Config evaluated your resources against the rule. The rule did not apply to the AWS resources in its scope, the specified resources were deleted, or the evaluation results were deleted.", + "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-1::product/aws/securityhub/arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1/finding/635ceb5d-3dfd-4458-804e-48a42cd723e4" + }, + "Resources": [ + { + "Type": "AwsAccount", + "Id": "arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1", + "Partition": "aws", + "Region": "us-east-1" + } + ], + "Compliance": { + "Status": "FAILED", + "StatusReasons": [ + { + "ReasonCode": "CONFIG_EVALUATIONS_EMPTY", + "Description": "AWS Config evaluated your resources against the rule. The rule did not apply to the AWS resources in its scope, the specified resources were deleted, or the evaluation results were deleted." + } + ] + }, + "WorkflowState": "NEW", + "Workflow": { + "Status": "NEW" + }, + "RecordState": "ACTIVE" + }, + "AutomationDocument": { + "DocState": "ACTIVE", + "SecurityStandardVersion": "1.0.0", + "AccountId": "111111111111", + "Message": "Document Status is not \"Active\": unknown", + "AutomationDocId": "SHARR-AFSBP_1.0.0_AutoScaling.1", + "RemediationRole": "SO0111-Remediate-AFSBP-1.0.0-AutoScaling.1", + "ControlId": "AutoScaling.1", + "SecurityStandard": "AFSBP", + "SecurityStandardSupported": "True" + }, + } + +def test_get_approval_req(mocker): + """ + Verifies that it returns the fanout runbook name + """ + os.environ['WORKFLOW_RUNBOOK'] = 'SHARR-RunWorkflow' + os.environ['WORKFLOW_RUNBOOK_ACCOUNT'] = 'member' + expected_result = { + 'workflowdoc': "SHARR-RunWorkflow", + 'workflowaccount': '111111111111', + 'workflowrole': '', + 'workflow_data': { + 'impact': 'nondestructive', + 'approvalrequired': 'false' + } + } + + AWS = AWSCachedClient(REGION) + account = AWS.get_connection('sts').get_caller_identity()['Account'] + step_input()['AutomationDocument']['AccountId'] = account + + ssm_c = AWS.get_connection('ssm') + ssmc_stub = Stubber(ssm_c) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "DataType": "text" + } + },{ + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname" + } + ) + ssmc_stub.add_client_error( + 'get_parameter', + 'ParameterNotFound' + ) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text" + } + } + ) + ssmc_stub.add_response( + 'describe_document', + { + "Document": { + "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", + "HashType": "Sha256", + "Name": "SHARR-RunWorkflow", + "Owner": "111111111111", + "CreatedDate": "2021-05-13T09:01:20.399000-04:00", + "Status": "Active", + "DocumentVersion": "1", + "Description": "### Document Name - SHARR-RunWorkflow", + "Parameters": [ + { + "Name": "AutomationAssumeRole", + "Type": "String", + "Description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "DefaultValue": "" + }, + { + "Name": "Finding", + "Type": "StringMap", + "Description": "The input from Step function for ASG1 finding" + } + ], + "PlatformTypes": [ + "Windows", + "Linux", + "MacOS" + ], + "DocumentType": "Automation", + "SchemaVersion": "0.3", + "LatestVersion": "1", + "DefaultVersion": "1", + "DocumentFormat": "JSON", + "Tags": [] + } + },{ + "Name": "SHARR-RunWorkflow" + } + ) + + ssmc_stub.activate() + mocker.patch('get_approval_requirement._get_ssm_client', return_value=ssm_c) + + response = lambda_handler(step_input(), {}) + + assert response['workflow_data'] == expected_result['workflow_data'] + assert response['workflowdoc'] == expected_result['workflowdoc'] + assert response['workflowaccount'] == expected_result['workflowaccount'] + assert response['workflowrole'] == expected_result['workflowrole'] + + ssmc_stub.deactivate() + +def test_get_approval_req_no_fanout(mocker): + """ + Verifies that it does not return workflow_status at all + """ + os.environ['WORKFLOW_RUNBOOK'] = '' + expected_result = { + 'workflowdoc': "", + 'workflowaccount': '', + 'workflowrole': '', + 'workflow_data': { + 'impact': 'nondestructive', + 'approvalrequired': 'false' + } + } + + AWS = AWSCachedClient(REGION) + account = AWS.get_connection('sts').get_caller_identity()['Account'] + step_input()['AutomationDocument']['AccountId'] = account + + ssm_c = AWS.get_connection('ssm') + ssmc_stub = Stubber(ssm_c) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "DataType": "text" + } + },{ + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname" + } + ) + ssmc_stub.add_client_error( + 'get_parameter', + 'ParameterNotFound' + ) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text" + } + } + ) + ssmc_stub.add_response( + 'describe_document', + { + "Document": { + "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", + "HashType": "Sha256", + "Name": "SHARR-RunWorkflow", + "Owner": "111111111111", + "CreatedDate": "2021-05-13T09:01:20.399000-04:00", + "Status": "Active", + "DocumentVersion": "1", + "Description": "### Document Name - SHARR-RunWorkflow", + "Parameters": [ + { + "Name": "AutomationAssumeRole", + "Type": "String", + "Description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "DefaultValue": "" + }, + { + "Name": "Finding", + "Type": "StringMap", + "Description": "The input from Step function for ASG1 finding" + } + ], + "PlatformTypes": [ + "Windows", + "Linux", + "MacOS" + ], + "DocumentType": "Automation", + "SchemaVersion": "0.3", + "LatestVersion": "1", + "DefaultVersion": "1", + "DocumentFormat": "JSON", + "Tags": [] + } + },{ + "Name": "SHARR-RunWorkflow" + } + ) + + ssmc_stub.activate() + mocker.patch('get_approval_requirement._get_ssm_client', return_value=ssm_c) + + response = lambda_handler(step_input(), {}) + print(response) + + assert response['workflow_data'] == expected_result['workflow_data'] + assert response['workflowdoc'] == expected_result['workflowdoc'] + assert response['workflowaccount'] == expected_result['workflowaccount'] + assert response['workflowrole'] == expected_result['workflowrole'] + + ssmc_stub.deactivate() + +#================================================================================== +def test_workflow_in_admin(mocker): + """ + Verifies that it returns the fanout runbook name + """ + os.environ['WORKFLOW_RUNBOOK'] = 'SHARR-RunWorkflow' + os.environ['WORKFLOW_RUNBOOK_ACCOUNT'] = 'admin' + os.environ['WORKFLOW_RUNBOOK_ROLE'] = 'someotheriamrole' + expected_result = { + 'workflowdoc': "SHARR-RunWorkflow", + 'workflowaccount': LOCAL_ACCOUNT, + 'workflowrole': 'someotheriamrole', + 'workflow_data': { + 'impact': 'nondestructive', + 'approvalrequired': 'false' + } + } + + AWS = AWSCachedClient(REGION) + account = AWS.get_connection('sts').get_caller_identity()['Account'] + step_input()['AutomationDocument']['AccountId'] = account + + ssm_c = AWS.get_connection('ssm') + ssmc_stub = Stubber(ssm_c) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/shortname", + "DataType": "text" + } + },{ + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname" + } + ) + ssmc_stub.add_client_error( + 'get_parameter', + 'ParameterNotFound' + ) + ssmc_stub.add_response( + 'get_parameter', + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text" + } + } + ) + ssmc_stub.add_response( + 'describe_document', + { + "Document": { + "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", + "HashType": "Sha256", + "Name": "SHARR-RunWorkflow", + "Owner": "111111111111", + "CreatedDate": "2021-05-13T09:01:20.399000-04:00", + "Status": "Active", + "DocumentVersion": "1", + "Description": "### Document Name - SHARR-RunWorkflow", + "Parameters": [ + { + "Name": "AutomationAssumeRole", + "Type": "String", + "Description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "DefaultValue": "" + }, + { + "Name": "Finding", + "Type": "StringMap", + "Description": "The input from Step function for ASG1 finding" + } + ], + "PlatformTypes": [ + "Windows", + "Linux", + "MacOS" + ], + "DocumentType": "Automation", + "SchemaVersion": "0.3", + "LatestVersion": "1", + "DefaultVersion": "1", + "DocumentFormat": "JSON", + "Tags": [] + } + },{ + "Name": "SHARR-RunWorkflow" + } + ) + + ssmc_stub.activate() + mocker.patch('get_approval_requirement._get_ssm_client', return_value=ssm_c) + + response = lambda_handler(step_input(), {}) + print(response) + assert response['workflow_data'] == expected_result['workflow_data'] + assert response['workflowdoc'] == expected_result['workflowdoc'] + assert response['workflowaccount'] == expected_result['workflowaccount'] + assert response['workflowrole'] == expected_result['workflowrole'] + + ssmc_stub.deactivate() diff --git a/source/lib/orchestrator_roles-construct.ts b/source/lib/orchestrator_roles-construct.ts new file mode 100644 index 00000000..b1810c0e --- /dev/null +++ b/source/lib/orchestrator_roles-construct.ts @@ -0,0 +1,154 @@ +#!/usr/bin/env node +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + +import * as cdk from '@aws-cdk/core'; +import { + PolicyStatement, + Effect, + Role, + PolicyDocument, + ArnPrincipal, + ServicePrincipal, + CompositePrincipal, + CfnRole +} from '@aws-cdk/aws-iam'; + +export interface OrchRoleProps { + solutionId: string; + adminAccountId: string; + adminRoleName: string; +} + +export class OrchestratorMemberRole extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: OrchRoleProps) { + super(scope, id); + const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name + const stack = cdk.Stack.of(this); + const memberPolicy = new PolicyDocument(); + + /** + * @description Cross-account permissions for Orchestration role + * @type {PolicyStatement} + */ + const iamPerms = new PolicyStatement(); + iamPerms.addActions( + "iam:PassRole", + "iam:GetRole" + ) + iamPerms.effect = Effect.ALLOW + iamPerms.addResources( + `arn:${stack.partition}:iam::${stack.account}:role/${RESOURCE_PREFIX}-*` + ); + memberPolicy.addStatements(iamPerms) + const ssmROPerms = new PolicyStatement() + ssmROPerms.addActions( + "ssm:DescribeAutomationExecutions", + "ssm:DescribeDocument", + "ssm:GetParameters" + ) + ssmROPerms.effect = Effect.ALLOW; + ssmROPerms.addResources( + "arn:" + stack.partition + ":ssm:*:*:*" + ) + memberPolicy.addStatements(ssmROPerms) + + const ssmRWPerms = new PolicyStatement() + ssmRWPerms.addActions( + "ssm:StartAutomationExecution", + "ssm:GetAutomationExecution" + ) + ssmRWPerms.addResources( + // `arn:${stack.partition}:ssm:*:${stack.account}:document/SHARR-*`, + // `arn:${stack.partition}:ssm:*:${stack.account}:automation-definition/*`, + // `arn:${stack.partition}:ssm:*:${stack.account}:document/SHARR-*`, + stack.formatArn({ + service: 'ssm', + region: '*', + resource: 'document', + resourceName: 'SHARR-*', + sep: '/' + }), + stack.formatArn({ + service: 'ssm', + region: '*', + resource: 'automation-definition', + resourceName: '*', + sep: '/' + }), + stack.formatArn({ + service: 'ssm', + region: '*', + resource: 'automation-definition', + account:'', + resourceName: '*', + sep: '/' + }), + stack.formatArn({ + service: 'ssm', + region: '*', + resource: 'automation-execution', + resourceName: '*', + sep: '/' + }) + ); + memberPolicy.addStatements(ssmRWPerms) + + const sechubPerms = new PolicyStatement(); + sechubPerms.addActions("cloudwatch:PutMetricData") + sechubPerms.addActions("securityhub:BatchUpdateFindings") + sechubPerms.effect = Effect.ALLOW + sechubPerms.addResources("*") + + memberPolicy.addStatements(sechubPerms) + + let principalPolicyStatement = new PolicyStatement(); + + principalPolicyStatement.addActions("sts:AssumeRole"); + principalPolicyStatement.effect = Effect.ALLOW; + + let roleprincipal = new ArnPrincipal( + `arn:${stack.partition}:iam::${props.adminAccountId}:role/${props.adminRoleName}` + ); + + let principals = new CompositePrincipal(roleprincipal); + principals.addToPolicy(principalPolicyStatement); + + let serviceprincipal = new ServicePrincipal('ssm.amazonaws.com') + principals.addPrincipals(serviceprincipal); + + let memberRole = new Role(this, 'MemberAccountRole', { + assumedBy: principals, + inlinePolicies: { + 'member_orchestrator': memberPolicy + }, + roleName: `${RESOURCE_PREFIX}-SHARR-Orchestrator-Member` + }); + + const memberRoleResource = memberRole.node.findChild('Resource') as CfnRole; + + memberRoleResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W11', + reason: 'Resource * is required due to the administrative nature of the solution.' + },{ + id: 'W28', + reason: 'Static names chosen intentionally to provide integration in cross-account permissions' + }] + } + } + } +} \ No newline at end of file diff --git a/source/lib/sharrplaybook-construct.ts b/source/lib/sharrplaybook-construct.ts index 9fe42ec5..bc357515 100644 --- a/source/lib/sharrplaybook-construct.ts +++ b/source/lib/sharrplaybook-construct.ts @@ -21,8 +21,7 @@ // import * as cdk from '@aws-cdk/core'; import { StringParameter } from '@aws-cdk/aws-ssm'; -import { Trigger } from './ssmplaybook'; -import { SsmPlaybook } from './ssmplaybook'; +import { Trigger, SsmPlaybook } from './ssmplaybook'; import { AdminAccountParm } from './admin_account_parm-construct'; export interface IControl { @@ -79,10 +78,17 @@ export class PlaybookPrimaryStack extends cdk.Stack { stringValue: `${controlSpec.executes}` }); } + let generatorId = '' + if (props.securityStandard === 'CIS' && props.securityStandardVersion === '1.2.0') { + // CIS 1.2.0 uses an arn-like format: arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3 + generatorId = `arn:${stack.partition}:securityhub:::ruleset/${props.securityStandardLongName}/v/${props.securityStandardVersion}/${controlSpec.control}` + } else { + generatorId = `${props.securityStandardLongName}/v/${props.securityStandardVersion}/${controlSpec.control}` + } new Trigger(stack, `${props.securityStandard} ${controlSpec.control}`, { securityStandard: props.securityStandard, controlId: controlSpec.control, - generatorId: `arn:${stack.partition}:securityhub:::ruleset/${props.securityStandardLongName}/v/${props.securityStandardVersion}/rule/${controlSpec.control}`, + generatorId: generatorId, targetArn: orchestratorArn }) } diff --git a/source/lib/ssmplaybook.ts b/source/lib/ssmplaybook.ts index e9fdd281..0622dffe 100644 --- a/source/lib/ssmplaybook.ts +++ b/source/lib/ssmplaybook.ts @@ -32,6 +32,7 @@ import { } from '@aws-cdk/aws-iam'; import { StateMachine } from '@aws-cdk/aws-stepfunctions'; import { IRuleTarget, EventPattern, Rule } from '@aws-cdk/aws-events'; +import { MemberRoleStack } from '../solution_deploy/lib/remediation_runbook-stack'; /* * @author AWS Solutions Development @@ -125,7 +126,7 @@ export class SsmPlaybook extends cdk.Construct { export interface ITriggerProps { description?: string, securityStandard: string; // ex. AFSBP - generatorId: string; // ex. arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3 + generatorId: string; // ex. "arn:aws-cn:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0" controlId: string; targetArn: string; } @@ -180,18 +181,29 @@ export class Trigger extends cdk.Construct { }); enable_auto_remediation_param.overrideLogicalId(`${props.securityStandard}${props.controlId.replace(illegalChars, '')}AutoTrigger`) - - let triggerPattern: events.EventPattern = { + + interface IPattern { + source: any, + detailType: any + detail: any + } + let eventPattern: IPattern = { source: ["aws.securityhub"], detailType: ["Security Hub Findings - Imported"], detail: { findings: { - GeneratorId: [ props.generatorId ], + GeneratorId: [props.generatorId], + ProductFields: { + ControlId: [props.controlId] + }, Workflow: workflowStatusFilter, Compliance: complianceStatusFilter } } } + + let triggerPattern: events.EventPattern = eventPattern + // Adding an automated even rule for the playbook const eventRule_auto = new events.Rule(this, 'AutoEventRule', { description: description + ' automatic remediation trigger event rule.', @@ -208,7 +220,8 @@ export class Trigger extends cdk.Construct { export interface IOneTriggerProps { description?: string, targetArn: string; - prereq: cdk.CfnResource; + serviceToken: string; + prereq: cdk.CfnResource[]; } export class OneTrigger extends cdk.Construct { // used in place of Trigger. Sends all finding events for which the @@ -236,7 +249,7 @@ export class OneTrigger extends cdk.Construct { // Note: Id is max 20 characters const customAction = new cdk.CustomResource(this, 'Custom Action', { - serviceToken: `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:SO0111-SHARR-CustomAction`, + serviceToken: props.serviceToken, resourceType: 'Custom::ActionTarget', properties: { Name: 'Remediate with SHARR', @@ -246,7 +259,9 @@ export class OneTrigger extends cdk.Construct { }); { let child = customAction.node.defaultChild as cdk.CfnCustomResource - child.addDependsOn(props.prereq) + for (var prereq of props.prereq) { + child.addDependsOn(prereq) + } } // Create an IAM role for Events to start the State Machine @@ -288,11 +303,10 @@ export class OneTrigger extends cdk.Construct { } export interface RoleProps { - solutionId: string; - ssmDocName: string; - adminAccountNumber: string; - remediationPolicy: Policy; - remediationRoleName: string; + readonly solutionId: string; + readonly ssmDocName: string; + readonly remediationPolicy: Policy; + readonly remediationRoleName: string; } export class SsmRole extends cdk.Construct { @@ -300,9 +314,11 @@ export class SsmRole extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: RoleProps) { super(scope, id); const stack = cdk.Stack.of(this) + const roleStack = MemberRoleStack.of(this) const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name - const adminRoleName = `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin_${stack.region}` + const adminRoleName = `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin` const basePolicy = new Policy(this, 'SHARR-Member-Base-Policy') + const adminAccount = roleStack.node.findChild('AdminAccountParameter').node.findChild('Admin Account Number') as cdk.CfnParameter; const ssmParmPerms = new PolicyStatement(); ssmParmPerms.addActions( @@ -312,7 +328,7 @@ export class SsmRole extends cdk.Construct { ) ssmParmPerms.effect = Effect.ALLOW ssmParmPerms.addResources( - `arn:${stack.partition}:ssm:${stack.region}:${stack.account}:parameter/Solutions/SO0111/*` + `arn:${stack.partition}:ssm:*:${stack.account}:parameter/Solutions/SO0111/*` ); basePolicy.addStatements(ssmParmPerms) @@ -322,7 +338,7 @@ export class SsmRole extends cdk.Construct { principalPolicyStatement.effect = Effect.ALLOW; let roleprincipal = new ArnPrincipal( - 'arn:' + stack.partition + ':iam::' + props.adminAccountNumber + + 'arn:' + stack.partition + ':iam::' + adminAccount.valueAsString + ':role/' + adminRoleName ); @@ -339,6 +355,7 @@ export class SsmRole extends cdk.Construct { memberRole.attachInlinePolicy(basePolicy) memberRole.attachInlinePolicy(props.remediationPolicy) + memberRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) const memberRoleResource = memberRole.node.findChild('Resource') as CfnRole; @@ -362,9 +379,7 @@ export interface RemediationRunbookProps { ssmDocFileName: string; solutionVersion: string; solutionDistBucket: string; - adminRoleName?: string; remediationPolicy?: Policy; - adminAccountNumber?: string; solutionId?: string; scriptPath?: string; } @@ -373,7 +388,6 @@ export class SsmRemediationRunbook extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: RemediationRunbookProps) { super(scope, id); - const stack = cdk.Stack.of(this) // Add prefix to ssmDocName let ssmDocName = `SHARR-${props.ssmDocName}` @@ -417,17 +431,5 @@ export class SsmRemediationRunbook extends cdk.Construct { documentType: 'Automation', name: ssmDocName }) - - if (props.adminRoleName && props.solutionId && props.adminAccountNumber && props.remediationPolicy) { - const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name - const remediationRoleNameBase = `${RESOURCE_PREFIX}-Remediate-${props.ssmDocName}-` - new SsmRole(this, 'RemediationRole ' + props.ssmDocName, { - solutionId: props.solutionId, - ssmDocName: ssmDocName, - adminAccountNumber: props.adminAccountNumber, - remediationPolicy: props.remediationPolicy, - remediationRoleName: `${remediationRoleNameBase}_${stack.region}` - }) - } } } diff --git a/source/package.json b/source/package.json index e249bbc2..5a9b3a7b 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "1.3.1", + "version": "1.4.0", "description": "Automated remediation for AWS Security Hub (SO0111)", "bin": { "solution_deploy": "bin/solution_deploy.js" @@ -18,29 +18,29 @@ "cdk": "cdk" }, "devDependencies": { - "@aws-cdk/assert": "~###CDK###", + "@aws-cdk/assert": "~1.132.0", + "@aws-cdk/aws-events": "~1.132.0", + "@aws-cdk/aws-iam": "~1.132.0", + "@aws-cdk/aws-kms": "~1.132.0", + "@aws-cdk/aws-lambda": "~1.132.0", + "@aws-cdk/aws-logs": "~1.132.0", + "@aws-cdk/aws-s3": "~1.132.0", + "@aws-cdk/aws-sns": "~1.132.0", + "@aws-cdk/aws-ssm": "~1.132.0", + "@aws-cdk/aws-stepfunctions": "~1.132.0", + "@aws-cdk/aws-stepfunctions-tasks": "~1.132.0", + "@aws-cdk/core": "~1.132.0", + "@types/jest": "^27.0.2", + "@types/js-yaml": "^4.0.4", "@types/node": "16.11.7", - "aws-cdk": "~###CDK###", - "ts-node": "^10.2.1", - "typescript": "^4.3.5", - "@aws-cdk/aws-events": "~###CDK###", - "@aws-cdk/aws-iam": "~###CDK###", - "@aws-cdk/aws-kms": "~###CDK###", - "@aws-cdk/aws-lambda": "~###CDK###", - "@aws-cdk/aws-logs": "~###CDK###", - "@aws-cdk/aws-s3": "~###CDK###", - "@aws-cdk/aws-sns": "~###CDK###", - "@aws-cdk/aws-ssm": "~###CDK###", - "@aws-cdk/aws-stepfunctions": "~###CDK###", - "@aws-cdk/aws-stepfunctions-tasks": "~###CDK###", - "@aws-cdk/core": "~###CDK###", - "@types/jest": "^27.0.1", - "@types/js-yaml": "^4.0.3", - "cdk": "~###CDK###", - "fs": "^0.0.2", - "jest": "^27.0.6", + "aws-cdk": "^1.132.0", + "cdk": "~1.132.0", + "fs": "^0.0.1-security", + "jest": "^27.3.1", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", - "ts-jest": "^27.0.5" + "ts-jest": "^27.0.7", + "ts-node": "^10.4.0", + "typescript": "^4.5.2" } } diff --git a/source/playbooks/AFSBP/bin/afsbp.ts b/source/playbooks/AFSBP/bin/afsbp.ts index 5b6a97d9..cd84fcb4 100644 --- a/source/playbooks/AFSBP/bin/afsbp.ts +++ b/source/playbooks/AFSBP/bin/afsbp.ts @@ -54,7 +54,8 @@ const remediations: IControl[] = [ { "control": 'S3.3', "executes": 'S3.2' - } + }, + { "control": 'S3.5' } ] const adminStack = new PlaybookPrimaryStack(app, 'AFSBPStack', { diff --git a/source/playbooks/AFSBP/cdk.out/AFSBPMemberStack.template.json b/source/playbooks/AFSBP/cdk.out/AFSBPMemberStack.template.json new file mode 100644 index 00000000..7ff286c7 --- /dev/null +++ b/source/playbooks/AFSBP/cdk.out/AFSBPMemberStack.template.json @@ -0,0 +1,2177 @@ +{ + "Description": "(SO0111C) AWS Security Hub Automated Response & Remediation AFSBP 1.0.0 Compliance Pack - Member Account, v1.4.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "SecHubAdminAccount": { + "Type": "String", + "AllowedPattern": "\\d{12}", + "Description": "Admin account number" + }, + "AFSBPAutoScaling1Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control AutoScaling.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPCloudTrail1Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control CloudTrail.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPCloudTrail2Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control CloudTrail.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPConfig1Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control Config.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPEC21Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control EC2.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPEC22Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control EC2.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPEC26Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control EC2.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPEC27Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control EC2.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPIAM7Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control IAM.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPIAM8Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control IAM.8 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPLambda1Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control Lambda.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPRDS1Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control RDS.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPRDS6Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control RDS.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPRDS7Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control RDS.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPS31Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control S3.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPS32Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control S3.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + }, + "AFSBPS35Active": { + "Type": "String", + "Default": "Available", + "AllowedValues": [ + "Available", + "NOT Available" + ], + "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control S3.5 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account." + } + }, + "Conditions": { + "AFSBPAutoScaling1EnableAutoScaling1ConditionCE5EE793": { + "Fn::Equals": [ + { + "Ref": "AFSBPAutoScaling1Active" + }, + "Available" + ] + }, + "AFSBPCloudTrail1EnableCloudTrail1ConditionC860CCC8": { + "Fn::Equals": [ + { + "Ref": "AFSBPCloudTrail1Active" + }, + "Available" + ] + }, + "AFSBPCloudTrail2EnableCloudTrail2Condition0DA5CF58": { + "Fn::Equals": [ + { + "Ref": "AFSBPCloudTrail2Active" + }, + "Available" + ] + }, + "AFSBPConfig1EnableConfig1Condition7EADFF11": { + "Fn::Equals": [ + { + "Ref": "AFSBPConfig1Active" + }, + "Available" + ] + }, + "AFSBPEC21EnableEC21ConditionA4D0F59B": { + "Fn::Equals": [ + { + "Ref": "AFSBPEC21Active" + }, + "Available" + ] + }, + "AFSBPEC22EnableEC22Condition2349855D": { + "Fn::Equals": [ + { + "Ref": "AFSBPEC22Active" + }, + "Available" + ] + }, + "AFSBPEC26EnableEC26Condition4F53E96A": { + "Fn::Equals": [ + { + "Ref": "AFSBPEC26Active" + }, + "Available" + ] + }, + "AFSBPEC27EnableEC27ConditionFC2F394C": { + "Fn::Equals": [ + { + "Ref": "AFSBPEC27Active" + }, + "Available" + ] + }, + "AFSBPIAM7EnableIAM7Condition9B59FBA4": { + "Fn::Equals": [ + { + "Ref": "AFSBPIAM7Active" + }, + "Available" + ] + }, + "AFSBPIAM8EnableIAM8Condition41C0E0BC": { + "Fn::Equals": [ + { + "Ref": "AFSBPIAM8Active" + }, + "Available" + ] + }, + "AFSBPLambda1EnableLambda1Condition4E1A1855": { + "Fn::Equals": [ + { + "Ref": "AFSBPLambda1Active" + }, + "Available" + ] + }, + "AFSBPRDS1EnableRDS1ConditionB553606B": { + "Fn::Equals": [ + { + "Ref": "AFSBPRDS1Active" + }, + "Available" + ] + }, + "AFSBPRDS6EnableRDS6ConditionB5AA0302": { + "Fn::Equals": [ + { + "Ref": "AFSBPRDS6Active" + }, + "Available" + ] + }, + "AFSBPRDS7EnableRDS7ConditionC6475B87": { + "Fn::Equals": [ + { + "Ref": "AFSBPRDS7Active" + }, + "Available" + ] + }, + "AFSBPS31EnableS31Condition57BE9782": { + "Fn::Equals": [ + { + "Ref": "AFSBPS31Active" + }, + "Available" + ] + }, + "AFSBPS32EnableS32ConditionE7296642": { + "Fn::Equals": [ + { + "Ref": "AFSBPS32Active" + }, + "Available" + ] + }, + "AFSBPS35EnableS35Condition73E5EDAD": { + "Fn::Equals": [ + { + "Ref": "AFSBPS35Active" + }, + "Available" + ] + } + }, + "Resources": { + "AFSBPAutoScaling1AutomationDocumentA3B91B9D": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_AutoScaling.1\n\n## What does this document do?\nThis document enables ELB healthcheck on a given AutoScaling Group using the [UpdateAutoScalingGroup] API.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* HealthCheckGracePeriod: (Optional) Health check grace period when ELB health check is Enabled\nDefault: 30 seconds\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-autoscaling-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for ASG1 finding" + }, + "HealthCheckGracePeriod": { + "type": "Integer", + "default": 30, + "description": "ELB Health Check Grace Period" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "AutoScalingGroupName", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:autoScalingGroup:(?i:[0-9a-f]{11}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}):autoScalingGroupName/(.*)$", + "expected_control_id": "AutoScaling.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableAutoScalingGroupELBHealthCheck", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck", + "AutoScalingGroupName": "{{ParseInput.AutoScalingGroupName}}" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "ASG health check type updated to ELB", + "UpdatedBy": "SHARR-AFSBP_1.0.0_AutoScaling.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_AutoScaling.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP AutoScaling.1/Automation Document" + }, + "Condition": "AFSBPAutoScaling1EnableAutoScaling1ConditionCE5EE793" + }, + "AFSBPCloudTrail1AutomationDocumentB4BC3C6B": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.1\n## What does this document do?\nCreates a multi-region trail with KMS encryption and enables CloudTrail\nNote: this remediation will create a NEW trail.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Documentation Links\n* [AFSBP CloudTrail.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for the finding" + }, + "KMSKeyArn": { + "type": "String", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", + "description": "The ARN of the KMS key created by SHARR for this remediation", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$" + } + }, + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "region": "{{global:REGION}}", + "parse_id_pattern": "", + "expected_control_id": "CloudTrail.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-CreateCloudTrailMultiRegionTrail", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail", + "AWSPartition": "{{global:AWS_PARTITION}}" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Multi-region, encrypted AWS CloudTrail successfully created", + "UpdatedBy": "SHARR-AFSBP_1.0.0_CloudTrail.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_CloudTrail.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP CloudTrail.1/Automation Document" + }, + "Condition": "AFSBPCloudTrail1EnableCloudTrail1ConditionC860CCC8" + }, + "AFSBPCloudTrail2AutomationDocument2FEFCDA3": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.2\n## What does this document do?\nThis document enables SSE KMS encryption for log files using the SHARR remediation KMS CMK\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n## Output Parameters\n* Remediation.Output - Output from the remediation\n\n## Documentation Links\n* [AFSBP CloudTrail.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-2)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for the finding" + }, + "KMSKeyArn": { + "type": "String", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + }, + { + "Name": "TrailArn", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "TrailRegion", + "Selector": "$.Payload.resource.Region", + "Type": "String" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "CloudTrail.2" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + } + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "SHARR-EnableCloudTrailEncryption", + "RuntimeParameters": { + "TrailRegion": "{{ParseInput.TrailRegion}}", + "TrailArn": "{{ParseInput.TrailArn}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption", + "KMSKeyArn": "{{KMSKeyArn}}" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Encryption enabled on CloudTrail", + "UpdatedBy": "SHARR-AFSBP_1.0.0_CloudTrail.2" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_CloudTrail.2" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP CloudTrail.2/Automation Document" + }, + "Condition": "AFSBPCloudTrail2EnableCloudTrail2Condition0DA5CF58" + }, + "AFSBPConfig1AutomationDocument77EA43B9": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_Config.1\n## What does this document do?\nEnables AWS Config:\n* Turns on recording for all resources.\n* Creates an encrypted bucket for Config logging.\n* Creates a logging bucket for access logs for the config bucket\n* Creates an SNS topic for Config notifications\n* Creates a service-linked role\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Documentation Links\n* [AFSBP Config.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-config-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from the Orchestrator Step function for finding" + }, + "KMSKeyArn": { + "type": "String", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", + "description": "The ARN of the KMS key created by SHARR for remediations", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$" + } + }, + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "Config.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableAWSConfig", + "RuntimeParameters": { + "SNSTopicName": "SO0111-SHARR-AWSConfigNotification", + "KMSKeyArn": "{{KMSKeyArn}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "AWS Config enabled", + "UpdatedBy": "SHARR-AFSBP_1.0.0_Config.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_Config.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP Config.1/Automation Document" + }, + "Condition": "AFSBPConfig1EnableConfig1Condition7EADFF11" + }, + "AFSBPEC21AutomationDocument39E9DD5A": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_EC2.1\n## What does this document do?\nThis document changes all public EC2 snapshots to private\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Documentation Links\n* [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for EC2.1 finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + }, + { + "Name": "AccountId", + "Selector": "$.Payload.account_id", + "Type": "String" + }, + { + "Name": "TestMode", + "Selector": "$.Payload.testmode", + "Type": "Boolean" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "resource_index": 2, + "expected_control_id": "EC2.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "SHARR-MakeEBSSnapshotsPrivate", + "RuntimeParameters": { + "AccountId": "{{ParseInput.AccountId}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate", + "TestMode": "{{ParseInput.TestMode}}" + } + }, + "isEnd": false + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "EBS Snapshot modified to private", + "UpdatedBy": "SHARR-AFSBP_1.0.0_EC2.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_EC2.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP EC2.1/Automation Document" + }, + "Condition": "AFSBPEC21EnableEC21ConditionA4D0F59B" + }, + "AFSBPEC22AutomationDocument3AD0236F": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_EC2.2\n\n## What does this document do?\nThis document deletes ingress and egress rules from default security \ngroup using the AWS SSM Runbook AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output - Output from AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules SSM doc\n\n## Documentation Links\n* [AFSBP EC2.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-2)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for EC2.2 finding" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "GroupId", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:security-group/(sg-[0-9a-f]*)$", + "expected_control_id": "EC2.2" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-RemoveVPCDefaultSecurityGroupRules", + "RuntimeParameters": { + "GroupId": "{{ParseInput.GroupId}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Removed rules on default security group", + "UpdatedBy": "SHARR-AFSBP_1.0.0_EC2.2" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_EC2.2" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP EC2.2/Automation Document" + }, + "Condition": "AFSBPEC22EnableEC22Condition2349855D" + }, + "AFSBPEC26AutomationDocumentA0710E01": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_EC2.6\n\n## What does this document do?\nEnables VPC Flow Logs for a VPC\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output - Remediation results\n\n## Documentation Links\n* [AFSBP EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-6)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for ASG1 finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "VPC", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:.*:\\d{12}:vpc/(vpc-[0-9a-f]{8,17})$", + "expected_control_id": "EC2.6" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableVPCFlowLogs", + "RuntimeParameters": { + "VPC": "{{ParseInput.VPC}}", + "RemediationRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Enabled VPC Flow Logs for {{ParseInput.VPC}}", + "UpdatedBy": "SHARR-AFSBP_1.0.0_EC2.6" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_EC2.6" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP EC2.6/Automation Document" + }, + "Condition": "AFSBPEC26EnableEC26Condition4F53E96A" + }, + "AFSBPEC27AutomationDocument2B9BDB9C": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_EC2.7\n## What does this document do?\nThis document enables `EBS Encryption by default` for an AWS account in the current region by calling another SSM document\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP EC2.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-7)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for RDS7 finding" + } + }, + "outputs": [ + "ExecRemediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "EC2.7" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "ExecRemediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableEbsEncryptionByDefault", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEbsEncryptionByDefault" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Enabled EBS encryption by default", + "UpdatedBy": "SHARR-AFSBP_1.0.0_EC2.7" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_EC2.7" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP EC2.7/Automation Document" + }, + "Condition": "AFSBPEC27EnableEC27ConditionFC2F394C" + }, + "AFSBPIAM7AutomationDocument30C0FB77": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_IAM.7\n\n## What does this document do?\nThis document establishes a default password policy.\n\n## Security Standards and Controls\n* AFSBP IAM.7\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP IAM.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-7)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": [ + "IAM.7" + ] + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-SetIAMPasswordPolicy", + "RuntimeParameters": { + "AllowUsersToChangePassword": true, + "HardExpiry": true, + "MaxPasswordAge": 90, + "MinimumPasswordLength": 14, + "RequireSymbols": true, + "RequireNumbers": true, + "RequireUppercaseCharacters": true, + "RequireLowercaseCharacters": true, + "PasswordReusePrevention": 24, + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", + "UpdatedBy": "SHARR-AFSBP_1.0.0_IAM.7" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_IAM.7" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP IAM.7/Automation Document" + }, + "Condition": "AFSBPIAM7EnableIAM7Condition9B59FBA4" + }, + "AFSBPIAM8AutomationDocumentE62407D1": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_IAM.8\n\n## What does this document do?\nThis document ensures that credentials unused for 90 days or greater are disabled.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output - Output of remediation runbook\n\nSEE AWSConfigRemediation-RevokeUnusedIAMUserCredentials\n\n## Documentation Links\n* [AFSBP IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-8)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for ASG1 finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "IAMResourceId", + "Selector": "$.Payload.details.AwsIamUser.UserId", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "IAM.8" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-RevokeUnusedIAMUserCredentials", + "RuntimeParameters": { + "IAMResourceId": "{{ ParseInput.IAMResourceId }}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Deactivated unused keys and expired logins using the AWSConfigRemediation-RevokeUnusedIAMUserCredentials runbook.", + "UpdatedBy": "SHARR-AFSBP_1.0.0_IAM.8" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_IAM.8" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP IAM.8/Automation Document" + }, + "Condition": "AFSBPIAM8EnableIAM8Condition41C0E0BC" + }, + "AFSBPLambda1AutomationDocumentB7954EC2": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_Lambda.1 \n\n## What does this document do?\nThis document removes the public resource policy. A public resource policy\ncontains a principal \"*\" or AWS: \"*\", which allows public access to the \nfunction. The remediation is to remove the SID of the public policy.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Documentation Links\n* [AFSBP Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for the finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + }, + { + "Name": "FunctionName", + "Selector": "$.Payload.resource_id", + "Type": "String" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-us-gov|aws-cn):lambda:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:function:([a-zA-Z0-9\\-_]{1,64})$", + "expected_control_id": "Lambda.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + } + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-RemoveLambdaPublicAccess", + "RuntimeParameters": { + "FunctionName": "{{ ParseInput.FunctionName }}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Lamdba {{ParseInput.FunctionName}} policy updated to remove public access", + "UpdatedBy": "SHARR-AFSBP_1.0.0_Lambda.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_Lambda.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP Lambda.1/Automation Document" + }, + "Condition": "AFSBPLambda1EnableLambda1Condition4E1A1855" + }, + "AFSBPRDS1AutomationDocumentF363990A": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_RDS.1\n## What does this document do?\nThis document changes public RDS snapshot to private\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Documentation Links\n* [AFSBP RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for RDS.1 finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "DBSnapshotId", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "DBSnapshotType", + "Selector": "$.Payload.matches[0]", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + }, + { + "Name": "Type", + "Selector": "$.Payload.type", + "Type": "String" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(cluster-snapshot|snapshot):([a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,})$", + "resource_index": 2, + "expected_control_id": "RDS.1" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "nextStep": "Remediation" + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-MakeRDSSnapshotPrivate", + "RuntimeParameters": { + "DBSnapshotId": "{{ParseInput.DBSnapshotId}}", + "DBSnapshotType": "{{ParseInput.DBSnapshotType}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate" + } + }, + "nextStep": "UpdateFinding" + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "RDS DB Snapshot modified to private", + "UpdatedBy": "SHARR-AFSBP_1.0.0_RDS.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_RDS.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP RDS.1/Automation Document" + }, + "Condition": "AFSBPRDS1EnableRDS1ConditionB553606B" + }, + "AFSBPRDS6AutomationDocument3E6395D2": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_RDS.6\n\n## What does this document do?\nThis document enables `Enhanced Monitoring` on a given Amazon RDS instance by calling another SSM document.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* VerifyRemediation.Output - The standard HTTP response from the ModifyDBInstance API.\n## Documentation Links\n\n* [AFSBP RDS.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-6)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for RDS7 finding" + } + }, + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "ResourceId", + "Selector": "$.Payload.details.AwsRdsDbInstance.DbiResourceId", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "RDS.6" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "GetMonitoringRoleArn", + "action": "aws:executeAwsApi", + "description": "## GetRole API to get EnhancedMonitoring IAM role ARN\n", + "timeoutSeconds": 600, + "isEnd": false, + "inputs": { + "Service": "iam", + "Api": "GetRole", + "RoleName": "SO0111-RDSMonitoring-remediationRole" + }, + "outputs": [ + { + "Name": "Arn", + "Selector": "$.Role.Arn", + "Type": "String" + } + ] + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableEnhancedMonitoringOnRDSInstance", + "RuntimeParameters": { + "ResourceId": "{{ ParseInput.ResourceId }}", + "MonitoringRoleArn": "{{GetMonitoringRoleArn.Arn}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEnhancedMonitoringOnRDSInstance" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Enhanced Monitoring enabled on RDS DB cluster", + "UpdatedBy": "SHARR-AFSBP_1.0.0_RDS.6" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_RDS.6" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP RDS.6/Automation Document" + }, + "Condition": "AFSBPRDS6EnableRDS6ConditionB5AA0302" + }, + "AFSBPRDS7AutomationDocumentD26D79A2": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_RDS.7\n\n## What does this document do?\nThis document enables `Deletion Protection` on a given Amazon RDS cluster by calling another SSM document.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output - The standard HTTP response from the ModifyDBInstance API.\n\n## Documentation Links\n* [AFSBP RDS.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-7)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "parameters": { + "AutomationAssumeRole": { + "type": "String", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + }, + "Finding": { + "type": "StringMap", + "description": "The input from Step function for RDS7 finding" + } + }, + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject" + ], + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "ResourceId", + "Selector": "$.Payload.details.AwsRdsDbCluster.DbClusterResourceId", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": "RDS.7" + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-EnableRDSClusterDeletionProtection", + "RuntimeParameters": { + "ClusterId": "{{ ParseInput.ResourceId }}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableRDSClusterDeletionProtection" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Deletion protection enabled on RDS DB cluster", + "UpdatedBy": "SHARR-AFSBP_1.0.0_RDS.7" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_RDS.7" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP RDS.7/Automation Document" + }, + "Condition": "AFSBPRDS7EnableRDS7ConditionC6475B87" + }, + "AFSBPS31AutomationDocument59937F91": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_S3.1\n\n## What does this document do?\nThis document blocks public access to all buckets by default at the account level.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP v1.0.0 S3.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-1)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "AccountId", + "Selector": "$.Payload.account_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "", + "expected_control_id": [ + "S3.1" + ] + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-ConfigureS3PublicAccessBlock", + "RuntimeParameters": { + "AccountId": "{{ParseInput.AccountId}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock", + "RestrictPublicBuckets": true, + "BlockPublicAcls": true, + "IgnorePublicAcls": true, + "BlockPublicPolicy": true + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Configured the account to block public S3 access.", + "UpdatedBy": "SHARR-AFSBP_1.0.0_S3.1" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_S3.1" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP S3.1/Automation Document" + }, + "Condition": "AFSBPS31EnableS31Condition57BE9782" + }, + "AFSBPS32AutomationDocument6F58830D": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_S3.2\n\n## What does this document do?\nThis document blocks public access to an S3 bucket.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP v1.0.0 S3.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-2)\n* [AFSBP v1.0.0 S3.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-3)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "BucketName", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$", + "expected_control_id": [ + "S3.2", + "S3.3" + ] + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-ConfigureS3BucketPublicAccessBlock", + "RuntimeParameters": { + "BucketName": "{{ParseInput.BucketName}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock", + "RestrictPublicBuckets": true, + "BlockPublicAcls": true, + "IgnorePublicAcls": true, + "BlockPublicPolicy": true + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Disabled public access to S3 bucket.", + "UpdatedBy": "SHARR-AFSBP_1.0.0_S3.2" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_S3.2" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP S3.2/Automation Document" + }, + "Condition": "AFSBPS32EnableS32ConditionE7296642" + }, + "AFSBPS35AutomationDocument19342239": { + "Type": "AWS::SSM::Document", + "Properties": { + "Content": { + "description": "### Document Name - SHARR-AFSBP_1.0.0_S3.5\n\n## What does this document do?\nThis document adds a bucket policy to restrict internet access to https only.\n\n## Input Parameters\n* Finding: (Required) Security Hub finding details JSON\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n\n## Output Parameters\n* Remediation.Output\n\n## Documentation Links\n* [AFSBP v1.0.0 S3.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-5)\n", + "schemaVersion": "0.3", + "assumeRole": "{{ AutomationAssumeRole }}", + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output" + ], + "parameters": { + "Finding": { + "type": "StringMap", + "description": "The input from Step function for finding" + }, + "AutomationAssumeRole": { + "type": "String", + "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", + "default": "", + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+" + } + }, + "mainSteps": [ + { + "name": "ParseInput", + "action": "aws:executeScript", + "outputs": [ + { + "Name": "BucketName", + "Selector": "$.Payload.resource_id", + "Type": "String" + }, + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String" + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String" + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap" + }, + { + "Name": "AccountId", + "Selector": "$.Payload.account_id", + "Type": "String" + } + ], + "inputs": { + "InputPayload": { + "Finding": "{{Finding}}", + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$", + "expected_control_id": [ + "S3.5" + ] + }, + "Runtime": "python3.7", + "Handler": "parse_event", + "Script": "#!/usr/bin/python\n###############################################################################\n# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #\n# #\n# Licensed under the Apache License Version 2.0 (the \"License\"). You may not #\n# use this file except in compliance with the License. A copy of the License #\n# is located at #\n# #\n# http://www.apache.org/licenses/LICENSE-2.0/ #\n# #\n# or in the \"license\" file accompanying this file. This file is distributed #\n# on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express #\n# or implied. See the License for the specific language governing permis- #\n# sions and limitations under the License. #\n###############################################################################\nimport re\n\ndef get_control_id_from_arn(finding_id_arn):\n check_finding_id = re.match(\n '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\.0\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',\n finding_id_arn\n )\n if check_finding_id:\n control_id = check_finding_id.group(1)\n return control_id\n else:\n exit(f'ERROR: Finding Id is invalid: {finding_id_arn}')\n\ndef parse_event(event, context):\n expected_control_id = event['expected_control_id']\n parse_id_pattern = event['parse_id_pattern']\n resource_id_matches = []\n finding = event['Finding']\n testmode = bool('testmode' in finding)\n\n finding_id = finding['Id']\n \n account_id = finding.get('AwsAccountId', '')\n if not re.match('^\\\\d{12}$', account_id):\n exit(f'ERROR: AwsAccountId is invalid: {account_id}')\n\n control_id = get_control_id_from_arn(finding['Id'])\n\n # ControlId present and valid\n if not control_id:\n exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id')\n\n # ControlId is the expected value\n if control_id not in expected_control_id:\n exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}')\n\n # ProductArn present and valid\n product_arn = finding['ProductArn']\n if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', product_arn):\n exit(f'ERROR: ProductArn is invalid: {product_arn}')\n\n resource = finding['Resources'][0]\n\n # Details\n details = finding['Resources'][0].get('Details', {})\n\n # Regex match Id to get remediation-specific identifier\n identifier_raw = finding['Resources'][0]['Id']\n resource_id = identifier_raw\n\n if parse_id_pattern:\n identifier_match = re.match(\n parse_id_pattern,\n identifier_raw\n )\n\n if identifier_match:\n for group in range(1, len(identifier_match.groups())+1):\n resource_id_matches.append(identifier_match.group(group))\n resource_id = identifier_match.group(event.get('resource_index', 1))\n else:\n exit(f'ERROR: Invalid resource Id {identifier_raw}') \n\n if not resource_id:\n exit('ERROR: Resource Id is missing from the finding json Resources (Id)')\n\n affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'}\n return {\n \"account_id\": account_id,\n \"resource_id\": resource_id, \n \"finding_id\": finding_id, \n \"control_id\": control_id,\n \"product_arn\": product_arn, \n \"object\": affected_object,\n \"matches\": resource_id_matches,\n \"details\": details,\n \"testmode\": testmode,\n \"resource\": resource\n }" + }, + "isEnd": false + }, + { + "name": "Remediation", + "action": "aws:executeAutomation", + "isEnd": false, + "inputs": { + "DocumentName": "SHARR-SetSSLBucketPolicy", + "RuntimeParameters": { + "BucketName": "{{ParseInput.BucketName}}", + "AccountId": "{{ParseInput.AccountId}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetSSLBucketPolicy" + } + } + }, + { + "name": "UpdateFinding", + "action": "aws:executeAwsApi", + "inputs": { + "Service": "securityhub", + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}" + } + ], + "Note": { + "Text": "Added SSL-only access policy to S3 bucket.", + "UpdatedBy": "SHARR-AFSBP_1.0.0_S3.5" + }, + "Workflow": { + "Status": "RESOLVED" + } + }, + "description": "Update finding", + "isEnd": true + } + ] + }, + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_S3.5" + }, + "Metadata": { + "aws:cdk:path": "AFSBPMemberStack/AFSBP S3.5/Automation Document" + }, + "Condition": "AFSBPS35EnableS35Condition73E5EDAD" + } + } +} \ No newline at end of file diff --git a/source/playbooks/AFSBP/cdk.out/AFSBPStack.template.json b/source/playbooks/AFSBP/cdk.out/AFSBPStack.template.json new file mode 100644 index 00000000..68667672 --- /dev/null +++ b/source/playbooks/AFSBP/cdk.out/AFSBPStack.template.json @@ -0,0 +1,2090 @@ +{ + "Description": "(SO0111P) AWS Security Hub Automated Response & Remediation AFSBP 1.0.0 Compliance Pack - Admin Account, v1.4.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/Solutions/SO0111/OrchestratorArn" + }, + "AFSBPAutoScaling1AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP AutoScaling.1" + }, + "AFSBPCloudTrail1AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP CloudTrail.1" + }, + "AFSBPCloudTrail2AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP CloudTrail.2" + }, + "AFSBPConfig1AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP Config.1" + }, + "AFSBPEC21AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP EC2.1" + }, + "AFSBPEC22AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP EC2.2" + }, + "AFSBPEC26AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP EC2.6" + }, + "AFSBPEC27AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP EC2.7" + }, + "AFSBPIAM7AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP IAM.7" + }, + "AFSBPIAM8AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP IAM.8" + }, + "AFSBPLambda1AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP Lambda.1" + }, + "AFSBPRDS1AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP RDS.1" + }, + "AFSBPRDS6AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP RDS.6" + }, + "AFSBPRDS7AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP RDS.7" + }, + "AFSBPS31AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP S3.1" + }, + "AFSBPS32AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP S3.2" + }, + "AFSBPS33AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP S3.3" + }, + "AFSBPS35AutoTrigger": { + "Type": "String", + "Default": "DISABLED", + "AllowedValues": [ + "ENABLED", + "DISABLED" + ], + "Description": "This will fully enable automated remediation for AFSBP S3.5" + } + }, + "Resources": { + "StandardShortName7DDF6BE6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "AFSBP", + "Description": "Provides a short (1-12) character abbreviation for the standard.", + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname" + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/StandardShortName/Resource" + } + }, + "StandardVersionCB2C6951": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "enabled", + "Description": "This parameter controls whether the SHARR step function will process findings for this version of the standard.", + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/status" + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/StandardVersion/Resource" + } + }, + "AFSBPAutoScaling1EventsRuleRole40C42B87": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP AutoScaling.1/EventsRuleRole/Resource" + } + }, + "AFSBPAutoScaling1EventsRuleRoleDefaultPolicy8681AE72": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPAutoScaling1EventsRuleRoleDefaultPolicy8681AE72", + "Roles": [ + { + "Ref": "AFSBPAutoScaling1EventsRuleRole40C42B87" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP AutoScaling.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPAutoScaling1AutoEventRule0BE9087E": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP AutoScaling.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1" + ], + "ProductFields": { + "ControlId": [ + "AutoScaling.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_AutoScaling.1_AutoTrigger", + "State": { + "Ref": "AFSBPAutoScaling1AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPAutoScaling1EventsRuleRole40C42B87", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP AutoScaling.1/AutoEventRule/Resource" + } + }, + "AFSBPCloudTrail1EventsRuleRole71D06D7A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.1/EventsRuleRole/Resource" + } + }, + "AFSBPCloudTrail1EventsRuleRoleDefaultPolicy685CCD45": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPCloudTrail1EventsRuleRoleDefaultPolicy685CCD45", + "Roles": [ + { + "Ref": "AFSBPCloudTrail1EventsRuleRole71D06D7A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPCloudTrail1AutoEventRule468DC6B9": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP CloudTrail.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/CloudTrail.1" + ], + "ProductFields": { + "ControlId": [ + "CloudTrail.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_CloudTrail.1_AutoTrigger", + "State": { + "Ref": "AFSBPCloudTrail1AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPCloudTrail1EventsRuleRole71D06D7A", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.1/AutoEventRule/Resource" + } + }, + "AFSBPCloudTrail2EventsRuleRole94823032": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.2/EventsRuleRole/Resource" + } + }, + "AFSBPCloudTrail2EventsRuleRoleDefaultPolicyF9708F0D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPCloudTrail2EventsRuleRoleDefaultPolicyF9708F0D", + "Roles": [ + { + "Ref": "AFSBPCloudTrail2EventsRuleRole94823032" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.2/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPCloudTrail2AutoEventRule1FF1B100": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP CloudTrail.2 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/CloudTrail.2" + ], + "ProductFields": { + "ControlId": [ + "CloudTrail.2" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_CloudTrail.2_AutoTrigger", + "State": { + "Ref": "AFSBPCloudTrail2AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPCloudTrail2EventsRuleRole94823032", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP CloudTrail.2/AutoEventRule/Resource" + } + }, + "AFSBPConfig1EventsRuleRole837C8327": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Config.1/EventsRuleRole/Resource" + } + }, + "AFSBPConfig1EventsRuleRoleDefaultPolicy7196564A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPConfig1EventsRuleRoleDefaultPolicy7196564A", + "Roles": [ + { + "Ref": "AFSBPConfig1EventsRuleRole837C8327" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Config.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPConfig1AutoEventRule0D737DBE": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP Config.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/Config.1" + ], + "ProductFields": { + "ControlId": [ + "Config.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_Config.1_AutoTrigger", + "State": { + "Ref": "AFSBPConfig1AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPConfig1EventsRuleRole837C8327", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Config.1/AutoEventRule/Resource" + } + }, + "AFSBPEC21EventsRuleRole43C39877": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.1/EventsRuleRole/Resource" + } + }, + "AFSBPEC21EventsRuleRoleDefaultPolicy9E00CFC6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPEC21EventsRuleRoleDefaultPolicy9E00CFC6", + "Roles": [ + { + "Ref": "AFSBPEC21EventsRuleRole43C39877" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPEC21AutoEventRule25775F17": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP EC2.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/EC2.1" + ], + "ProductFields": { + "ControlId": [ + "EC2.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_EC2.1_AutoTrigger", + "State": { + "Ref": "AFSBPEC21AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPEC21EventsRuleRole43C39877", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.1/AutoEventRule/Resource" + } + }, + "AFSBPEC22EventsRuleRoleDE163F68": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.2/EventsRuleRole/Resource" + } + }, + "AFSBPEC22EventsRuleRoleDefaultPolicy17C27BDE": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPEC22EventsRuleRoleDefaultPolicy17C27BDE", + "Roles": [ + { + "Ref": "AFSBPEC22EventsRuleRoleDE163F68" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.2/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPEC22AutoEventRule58F6227A": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP EC2.2 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/EC2.2" + ], + "ProductFields": { + "ControlId": [ + "EC2.2" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_EC2.2_AutoTrigger", + "State": { + "Ref": "AFSBPEC22AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPEC22EventsRuleRoleDE163F68", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.2/AutoEventRule/Resource" + } + }, + "AFSBPEC26EventsRuleRole1B433881": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.6/EventsRuleRole/Resource" + } + }, + "AFSBPEC26EventsRuleRoleDefaultPolicy1F92B9E6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPEC26EventsRuleRoleDefaultPolicy1F92B9E6", + "Roles": [ + { + "Ref": "AFSBPEC26EventsRuleRole1B433881" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.6/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPEC26AutoEventRule8A6BD817": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP EC2.6 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/EC2.6" + ], + "ProductFields": { + "ControlId": [ + "EC2.6" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_EC2.6_AutoTrigger", + "State": { + "Ref": "AFSBPEC26AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPEC26EventsRuleRole1B433881", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.6/AutoEventRule/Resource" + } + }, + "AFSBPEC27EventsRuleRole60F20186": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.7/EventsRuleRole/Resource" + } + }, + "AFSBPEC27EventsRuleRoleDefaultPolicy93044CCF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPEC27EventsRuleRoleDefaultPolicy93044CCF", + "Roles": [ + { + "Ref": "AFSBPEC27EventsRuleRole60F20186" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.7/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPEC27AutoEventRule0E7F144F": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP EC2.7 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/EC2.7" + ], + "ProductFields": { + "ControlId": [ + "EC2.7" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_EC2.7_AutoTrigger", + "State": { + "Ref": "AFSBPEC27AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPEC27EventsRuleRole60F20186", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP EC2.7/AutoEventRule/Resource" + } + }, + "AFSBPIAM7EventsRuleRoleB27A2436": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.7/EventsRuleRole/Resource" + } + }, + "AFSBPIAM7EventsRuleRoleDefaultPolicyBA6C7A51": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPIAM7EventsRuleRoleDefaultPolicyBA6C7A51", + "Roles": [ + { + "Ref": "AFSBPIAM7EventsRuleRoleB27A2436" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.7/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPIAM7AutoEventRule461297C4": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP IAM.7 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/IAM.7" + ], + "ProductFields": { + "ControlId": [ + "IAM.7" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_IAM.7_AutoTrigger", + "State": { + "Ref": "AFSBPIAM7AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPIAM7EventsRuleRoleB27A2436", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.7/AutoEventRule/Resource" + } + }, + "AFSBPIAM8EventsRuleRoleA1AE4233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.8/EventsRuleRole/Resource" + } + }, + "AFSBPIAM8EventsRuleRoleDefaultPolicy52D789CD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPIAM8EventsRuleRoleDefaultPolicy52D789CD", + "Roles": [ + { + "Ref": "AFSBPIAM8EventsRuleRoleA1AE4233" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.8/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPIAM8AutoEventRule9290B681": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP IAM.8 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/IAM.8" + ], + "ProductFields": { + "ControlId": [ + "IAM.8" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_IAM.8_AutoTrigger", + "State": { + "Ref": "AFSBPIAM8AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPIAM8EventsRuleRoleA1AE4233", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP IAM.8/AutoEventRule/Resource" + } + }, + "AFSBPLambda1EventsRuleRoleF9D0E68B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Lambda.1/EventsRuleRole/Resource" + } + }, + "AFSBPLambda1EventsRuleRoleDefaultPolicy65DBADE9": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPLambda1EventsRuleRoleDefaultPolicy65DBADE9", + "Roles": [ + { + "Ref": "AFSBPLambda1EventsRuleRoleF9D0E68B" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Lambda.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPLambda1AutoEventRuleB663D23F": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP Lambda.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/Lambda.1" + ], + "ProductFields": { + "ControlId": [ + "Lambda.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_Lambda.1_AutoTrigger", + "State": { + "Ref": "AFSBPLambda1AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPLambda1EventsRuleRoleF9D0E68B", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP Lambda.1/AutoEventRule/Resource" + } + }, + "AFSBPRDS1EventsRuleRoleA28379F4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.1/EventsRuleRole/Resource" + } + }, + "AFSBPRDS1EventsRuleRoleDefaultPolicy3F204712": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPRDS1EventsRuleRoleDefaultPolicy3F204712", + "Roles": [ + { + "Ref": "AFSBPRDS1EventsRuleRoleA28379F4" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPRDS1AutoEventRuleF3474A4C": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP RDS.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/RDS.1" + ], + "ProductFields": { + "ControlId": [ + "RDS.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_RDS.1_AutoTrigger", + "State": { + "Ref": "AFSBPRDS1AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPRDS1EventsRuleRoleA28379F4", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.1/AutoEventRule/Resource" + } + }, + "AFSBPRDS6EventsRuleRoleC40F92FF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.6/EventsRuleRole/Resource" + } + }, + "AFSBPRDS6EventsRuleRoleDefaultPolicyC9A77BA4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPRDS6EventsRuleRoleDefaultPolicyC9A77BA4", + "Roles": [ + { + "Ref": "AFSBPRDS6EventsRuleRoleC40F92FF" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.6/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPRDS6AutoEventRule68B8F587": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP RDS.6 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/RDS.6" + ], + "ProductFields": { + "ControlId": [ + "RDS.6" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_RDS.6_AutoTrigger", + "State": { + "Ref": "AFSBPRDS6AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPRDS6EventsRuleRoleC40F92FF", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.6/AutoEventRule/Resource" + } + }, + "AFSBPRDS7EventsRuleRole4E57829C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.7/EventsRuleRole/Resource" + } + }, + "AFSBPRDS7EventsRuleRoleDefaultPolicyEA7C7B62": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPRDS7EventsRuleRoleDefaultPolicyEA7C7B62", + "Roles": [ + { + "Ref": "AFSBPRDS7EventsRuleRole4E57829C" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.7/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPRDS7AutoEventRule848E787E": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP RDS.7 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/RDS.7" + ], + "ProductFields": { + "ControlId": [ + "RDS.7" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_RDS.7_AutoTrigger", + "State": { + "Ref": "AFSBPRDS7AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPRDS7EventsRuleRole4E57829C", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP RDS.7/AutoEventRule/Resource" + } + }, + "AFSBPS31EventsRuleRole7E5BE635": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.1/EventsRuleRole/Resource" + } + }, + "AFSBPS31EventsRuleRoleDefaultPolicyF4925CF9": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPS31EventsRuleRoleDefaultPolicyF4925CF9", + "Roles": [ + { + "Ref": "AFSBPS31EventsRuleRole7E5BE635" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.1/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPS31AutoEventRule98E1B83D": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP S3.1 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/S3.1" + ], + "ProductFields": { + "ControlId": [ + "S3.1" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_S3.1_AutoTrigger", + "State": { + "Ref": "AFSBPS31AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPS31EventsRuleRole7E5BE635", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.1/AutoEventRule/Resource" + } + }, + "AFSBPS32EventsRuleRole087A54B3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.2/EventsRuleRole/Resource" + } + }, + "AFSBPS32EventsRuleRoleDefaultPolicy35402CF3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPS32EventsRuleRoleDefaultPolicy35402CF3", + "Roles": [ + { + "Ref": "AFSBPS32EventsRuleRole087A54B3" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.2/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPS32AutoEventRule1F14B41A": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP S3.2 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/S3.2" + ], + "ProductFields": { + "ControlId": [ + "S3.2" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_S3.2_AutoTrigger", + "State": { + "Ref": "AFSBPS32AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPS32EventsRuleRole087A54B3", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.2/AutoEventRule/Resource" + } + }, + "RemapAFSBPS33665DB5F0": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "S3.2", + "Description": "Remap the AFSBP S3.3 finding to AFSBP S3.2 remediation", + "Name": "/Solutions/SO0111/AFSBP/1.0.0/S3.3/remap" + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/Remap AFSBP S3.3/Resource" + } + }, + "AFSBPS33EventsRuleRoleA177EAAA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.3/EventsRuleRole/Resource" + } + }, + "AFSBPS33EventsRuleRoleDefaultPolicy97673B3D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPS33EventsRuleRoleDefaultPolicy97673B3D", + "Roles": [ + { + "Ref": "AFSBPS33EventsRuleRoleA177EAAA" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.3/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPS33AutoEventRule056D09B9": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP S3.3 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/S3.3" + ], + "ProductFields": { + "ControlId": [ + "S3.3" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_S3.3_AutoTrigger", + "State": { + "Ref": "AFSBPS33AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPS33EventsRuleRoleA177EAAA", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.3/AutoEventRule/Resource" + } + }, + "AFSBPS35EventsRuleRole1330F6DC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.5/EventsRuleRole/Resource" + } + }, + "AFSBPS35EventsRuleRoleDefaultPolicy83E3156B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AFSBPS35EventsRuleRoleDefaultPolicy83E3156B", + "Roles": [ + { + "Ref": "AFSBPS35EventsRuleRole1330F6DC" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.5/EventsRuleRole/DefaultPolicy/Resource" + } + }, + "AFSBPS35AutoEventRuleA902DF8E": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Remediate AFSBP S3.5 automatic remediation trigger event rule.", + "EventPattern": { + "source": [ + "aws.securityhub" + ], + "detail-type": [ + "Security Hub Findings - Imported" + ], + "detail": { + "findings": { + "GeneratorId": [ + "aws-foundational-security-best-practices/v/1.0.0/S3.5" + ], + "ProductFields": { + "ControlId": [ + "S3.5" + ] + }, + "Workflow": { + "Status": [ + "NEW" + ] + }, + "Compliance": { + "Status": [ + "FAILED", + "WARNING" + ] + } + } + } + }, + "Name": "AFSBP_S3.5_AutoTrigger", + "State": { + "Ref": "AFSBPS35AutoTrigger" + }, + "Targets": [ + { + "Arn": { + "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "AFSBPS35EventsRuleRole1330F6DC", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "AFSBPStack/AFSBP S3.5/AutoEventRule/Resource" + } + } + }, + "Mappings": { + "SourceCode": { + "General": { + "S3Bucket": "solutions", + "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.4.0" + } + } + } +} \ No newline at end of file diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml index 5b62cb73..331a70d0 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml @@ -68,7 +68,7 @@ mainSteps: inputs: DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck' AutoScalingGroupName: '{{ParseInput.AutoScalingGroupName}}' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml index 466da4d5..b780a757 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml @@ -62,7 +62,7 @@ mainSteps: inputs: DocumentName: SHARR-CreateCloudTrailMultiRegionTrail RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml index 6f43af55..40857d84 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml @@ -47,7 +47,7 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.details.AwsCloudTrailTrail.HomeRegion + Selector: $.Payload.resource.Region Type: String inputs: InputPayload: @@ -59,8 +59,6 @@ mainSteps: Script: |- %%SCRIPT=afsbp_parse_input.py%% - isEnd: false - - name: Remediation action: 'aws:executeAutomation' @@ -69,9 +67,8 @@ mainSteps: RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption' KMSKeyArn: '{{KMSKeyArn}}' - isEnd: false - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml index 8a027c10..73ecb6cd 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml @@ -69,7 +69,7 @@ mainSteps: RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml index f04afda5..f653022b 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml @@ -66,7 +66,7 @@ mainSteps: DocumentName: SHARR-MakeEBSSnapshotsPrivate RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate' TestMode: '{{ParseInput.TestMode}}' isEnd: false diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml index 09c3a247..d2785056 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules RuntimeParameters: GroupId: '{{ParseInput.GroupId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules' - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml index 3c30d548..81945997 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml @@ -63,8 +63,8 @@ mainSteps: DocumentName: SHARR-EnableVPCFlowLogs RuntimeParameters: VPC: '{{ParseInput.VPC}}' - RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole_{{global:REGION}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs_{{global:REGION}}' + RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml index 7332f21d..0fe5341b 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml @@ -56,7 +56,7 @@ mainSteps: inputs: DocumentName: SHARR-EnableEbsEncryptionByDefault RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEbsEncryptionByDefault_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEbsEncryptionByDefault' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml index c685f0d1..6dba0bd7 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml @@ -69,7 +69,7 @@ mainSteps: RequireUppercaseCharacters: True RequireLowercaseCharacters: True PasswordReusePrevention: 24 - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml index cb174843..8003db72 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml @@ -26,10 +26,6 @@ parameters: Finding: type: StringMap description: The input from Step function for ASG1 finding - HealthCheckGracePeriod: - type: Integer - default: 30 - description: ELB Health Check Grace Period AutomationAssumeRole: type: String description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. @@ -40,8 +36,8 @@ mainSteps: - name: ParseInput action: 'aws:executeScript' outputs: - - Name: IAMUser - Selector: $.Payload.resource_id + - Name: IAMResourceId + Selector: $.Payload.details.AwsIamUser.UserId Type: String - Name: FindingId Selector: $.Payload.finding_id @@ -52,13 +48,10 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap - - Name: IAMResourceId - Selector: $.Payload.details.AwsIamUser.UserId - Type: String inputs: InputPayload: Finding: '{{Finding}}' - parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):iam::\d{12}:user/([A-Za-z0-9=,.@\_\-+]{1,64})$' + parse_id_pattern: '' expected_control_id: 'IAM.8' Runtime: python3.7 Handler: parse_event @@ -72,7 +65,7 @@ mainSteps: DocumentName: SHARR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml index 9779dc9d..49b40562 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml @@ -42,18 +42,17 @@ mainSteps: Selector: $.Payload.object Type: StringMap - Name: FunctionName - Selector: $.Payload.details.AwsLambdaFunction.FunctionName + Selector: $.Payload.resource_id Type: String inputs: InputPayload: Finding: '{{Finding}}' - parse_id_pattern: '' + parse_id_pattern: '^arn:(?:aws|aws-us-gov|aws-cn):lambda:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:function:([a-zA-Z0-9\-_]{1,64})$' expected_control_id: 'Lambda.1' Runtime: python3.7 Handler: parse_event Script: |- %%SCRIPT=afsbp_parse_input.py%% - isEnd: false - name: Remediation @@ -63,7 +62,7 @@ mainSteps: DocumentName: SHARR-RemoveLambdaPublicAccess RuntimeParameters: FunctionName: '{{ ParseInput.FunctionName }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml index dbd64322..56fc9810 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml @@ -66,7 +66,7 @@ mainSteps: RuntimeParameters: DBSnapshotId: '{{ParseInput.DBSnapshotId}}' DBSnapshotType: '{{ParseInput.DBSnapshotType}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate' nextStep: UpdateFinding - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml index c4f44ab7..6e35ee15 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml @@ -65,7 +65,7 @@ mainSteps: inputs: Service: iam Api: GetRole - RoleName: 'SO0111-RDSMonitoring-remediationRole_{{global:REGION}}' + RoleName: 'SO0111-RDSMonitoring-remediationRole' outputs: - Name: Arn Selector: $.Role.Arn @@ -80,7 +80,7 @@ mainSteps: RuntimeParameters: ResourceId: '{{ ParseInput.ResourceId }}' MonitoringRoleArn: '{{GetMonitoringRoleArn.Arn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEnhancedMonitoringOnRDSInstance_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEnhancedMonitoringOnRDSInstance' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml index 76bfb162..4211fe52 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-EnableRDSClusterDeletionProtection RuntimeParameters: ClusterId: '{{ ParseInput.ResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableRDSClusterDeletionProtection_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableRDSClusterDeletionProtection' - name: UpdateFinding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml index 31ca9dcc..e9cd4777 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-ConfigureS3PublicAccessBlock RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock' RestrictPublicBuckets: true BlockPublicAcls: true IgnorePublicAcls: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml index 92816462..b731268b 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml @@ -65,7 +65,7 @@ mainSteps: DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' RestrictPublicBuckets: true BlockPublicAcls: true IgnorePublicAcls: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml new file mode 100644 index 00000000..786b5130 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml @@ -0,0 +1,87 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_S3.5 + + ## What does this document do? + This document adds a bucket policy to restrict internet access to https only. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * Remediation.Output + + ## Documentation Links + * [AFSBP v1.0.0 S3.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-5) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from Step function for finding + AutomationAssumeRole: + type: String + description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + default: '' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+' + +mainSteps: + - + name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: BucketName + Selector: $.Payload.resource_id + Type: String + - Name: FindingId + Selector: $.Payload.finding_id + Type: String + - Name: ProductArn + Selector: $.Payload.product_arn + Type: String + - Name: AffectedObject + Selector: $.Payload.object + Type: StringMap + - Name: AccountId + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$' + expected_control_id: [ 'S3.5' ] + Runtime: python3.7 + Handler: parse_event + Script: |- + %%SCRIPT=afsbp_parse_input.py%% + isEnd: false + - + name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-SetSSLBucketPolicy + RuntimeParameters: + BucketName: '{{ParseInput.BucketName}}' + AccountId: '{{ParseInput.AccountId}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetSSLBucketPolicy' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Added SSL-only access policy to S3 bucket.' + UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.5' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py b/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py index 3f4e8b51..bdd8202c 100644 --- a/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py +++ b/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py @@ -1,41 +1,46 @@ #!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\.0\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\.0\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id - ) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -49,16 +54,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -66,22 +69,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { "account_id": account_id, "resource_id": resource_id, @@ -91,5 +89,6 @@ def parse_event(event, context): "object": affected_object, "matches": resource_id_matches, "details": details, - "testmode": testmode + "testmode": testmode, + "resource": resource } \ No newline at end of file diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py b/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py index 951cf14a..999484f6 100644 --- a/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py +++ b/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py @@ -100,7 +100,8 @@ def expected(): "OutputKey": 'Remediation.Output' }, "matches": [ "sharr-test-autoscaling-1" ], - 'details': {} + 'details': {}, + 'resource': event().get('Finding').get('Resources')[0] } def test_parse_event(): diff --git a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap index cb3dc302..36aab28b 100644 --- a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap +++ b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap @@ -93,43 +93,48 @@ This document changes all public EC2 snapshots to private }, "Runtime": "python3.7", "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id - ) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -143,16 +148,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -160,22 +163,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -185,7 +183,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -224,7 +223,7 @@ def parse_event(event, context): "DocumentName": "SHARR-MakeEBSSnapshotsPrivate", "RuntimeParameters": Object { "AccountId": "{{ParseInput.AccountId}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate", "TestMode": "{{ParseInput.TestMode}}", }, }, @@ -305,47 +304,52 @@ function. The remediation is to remove the SID of the public policy. "InputPayload": Object { "Finding": "{{Finding}}", "expected_control_id": "Lambda.1", - "parse_id_pattern": "", + "parse_id_pattern": "^arn:(?:aws|aws-us-gov|aws-cn):lambda:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:function:([a-zA-Z0-9\\\\-_]{1,64})$", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id - ) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -359,16 +363,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -376,22 +378,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -401,10 +398,10 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, - "isEnd": false, "name": "ParseInput", "outputs": Array [ Object { @@ -424,7 +421,7 @@ def parse_event(event, context): }, Object { "Name": "FunctionName", - "Selector": "$.Payload.details.AwsLambdaFunction.FunctionName", + "Selector": "$.Payload.resource_id", "Type": "String", }, ], @@ -434,7 +431,7 @@ def parse_event(event, context): "inputs": Object { "DocumentName": "SHARR-RemoveLambdaPublicAccess", "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess", "FunctionName": "{{ ParseInput.FunctionName }}", }, }, @@ -517,43 +514,48 @@ This document changes public RDS snapshot to private }, "Runtime": "python3.7", "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/aws-foundational-security-best-practices/v/1\\\\\\\\.0\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id - ) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -567,16 +569,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -584,22 +584,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -609,7 +604,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "name": "ParseInput", @@ -652,7 +648,7 @@ def parse_event(event, context): "inputs": Object { "DocumentName": "SHARR-MakeRDSSnapshotPrivate", "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate", "DBSnapshotId": "{{ParseInput.DBSnapshotId}}", "DBSnapshotType": "{{ParseInput.DBSnapshotType}}", }, @@ -770,19 +766,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/aws-foundational-security-best-practices/v/1.0.0/rule/Example.1", - ], - ], - }, + "aws-foundational-security-best-practices/v/1.0.0/Example.1", ], + "ProductFields": Object { + "ControlId": Array [ + "Example.1", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -871,19 +861,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/aws-foundational-security-best-practices/v/1.0.0/rule/Example.3", - ], - ], - }, + "aws-foundational-security-best-practices/v/1.0.0/Example.3", ], + "ProductFields": Object { + "ControlId": Array [ + "Example.3", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -972,19 +956,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/aws-foundational-security-best-practices/v/1.0.0/rule/Example.5", - ], - ], - }, + "aws-foundational-security-best-practices/v/1.0.0/Example.5", ], + "ProductFields": Object { + "ControlId": Array [ + "Example.5", + ], + }, "Workflow": Object { "Status": Array [ "NEW", diff --git a/source/playbooks/CIS120/bin/cis120.ts b/source/playbooks/CIS120/bin/cis120.ts index b793006e..4da720ed 100644 --- a/source/playbooks/CIS120/bin/cis120.ts +++ b/source/playbooks/CIS120/bin/cis120.ts @@ -35,6 +35,7 @@ const app = new cdk.App(); // Security Standard and Control Id. See cis-member-stack let remediations: IControl[] = [ { "control": "1.3" }, + { "control": "1.4" }, { "control": "1.5" }, { "control": "1.6", diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml index 4c3336a9..e5d41920 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml @@ -36,6 +36,9 @@ mainSteps: - Name: IAMUser Selector: $.Payload.resource_id Type: String + - Name: IAMResourceId + Selector: $.Payload.details.AwsIamUser.UserId + Type: String - Name: FindingId Selector: $.Payload.finding_id Type: String @@ -45,9 +48,6 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap - - Name: IAMResourceId - Selector: $.Payload.details.AwsIamUser.UserId - Type: String inputs: InputPayload: Finding: '{{Finding}}' @@ -65,7 +65,7 @@ mainSteps: DocumentName: SHARR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml new file mode 100644 index 00000000..6821110d --- /dev/null +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml @@ -0,0 +1,86 @@ +description: | + ### Document Name - SHARR-CIS_1.2.0_1.4 + + ## What does this document do? + This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * Remediation.Output + + ## Documentation Links + * [CIS v1.2.0 1.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.4) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from Step function for finding + AutomationAssumeRole: + type: String + description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + default: '' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+' + MaxCredentialUsageAge: + type: String + description: (Required) Maximum number of days a key can be unrotated. The default value is 90 days. + allowedPattern: ^(\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\b)$ + default: "90" +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: IAMResourceId + Selector: $.Payload.resource_id + Type: String + - Name: FindingId + Selector: $.Payload.finding_id + Type: String + - Name: ProductArn + Selector: $.Payload.product_arn + Type: String + - Name: AffectedObject + Selector: $.Payload.object + Type: StringMap + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):iam::\d{12}:user/([A-Za-z0-9=,.@\_\-+]{1,64})$' + expected_control_id: '1.4' + Runtime: python3.7 + Handler: parse_event + Script: |- + %%SCRIPT=cis_parse_input.py%% + isEnd: false + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-RevokeUnrotatedKeys + RuntimeParameters: + IAMResourceId: '{{ ParseInput.IAMResourceId }}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnrotatedKeys' + MaxCredentialUsageAge: '{{MaxCredentialUsageAge}}' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Deactivated unrotated keys for {{ ParseInput.IAMUser }}.' + UpdatedBy: 'SHARR-CIS_1.2.0_1.4' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml index 55d711d3..e31258c0 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml @@ -76,7 +76,7 @@ mainSteps: RequireUppercaseCharacters: True RequireLowercaseCharacters: True PasswordReusePrevention: 24 - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml index 34b8309a..cb143f75 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml @@ -69,7 +69,7 @@ mainSteps: inputs: DocumentName: SHARR-CreateCloudTrailMultiRegionTrail RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' - name: UpdateFinding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml index 39438435..c9883d17 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-EnableCloudTrailLogFileValidation RuntimeParameters: TrailName: '{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml index aae53181..9b4ba970 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' RestrictPublicBuckets: true BlockPublicAcls: true IgnorePublicAcls: true diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml index d51a9a29..63c95448 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml @@ -63,9 +63,9 @@ mainSteps: DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging RuntimeParameters: TrailName: '{{ ParseInput.TrailName }}' - CloudWatchLogsRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CloudTrailToCloudWatchLogs_{{global:REGION}}' + CloudWatchLogsRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CloudTrailToCloudWatchLogs' LogGroupName: 'CloudTrail/{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailToCloudWatchLogging_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailToCloudWatchLogging' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml index 20e9cb5d..b62a0025 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml @@ -69,7 +69,7 @@ mainSteps: RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - name: UpdateFinding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml index 3e8e1716..bb57c5f7 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-CreateAccessLoggingBucket RuntimeParameters: BucketName: 'so0111-cloudtrailaccesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket' - name: Remediation action: 'aws:executeAutomation' @@ -77,7 +77,7 @@ mainSteps: GranteeUri: ['http://acs.amazonaws.com/groups/s3/LogDelivery'] TargetPrefix: ['{{ParseInput.CloudTrailBucket}}/'] TargetBucket: ['so0111-cloudtrailaccesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}'] - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketLogging_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketLogging' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml index ebb48888..cd7500f0 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml @@ -45,7 +45,7 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.details.AwsCloudTrailTrail.HomeRegion + Selector: $.Payload.resource.Region Type: String inputs: InputPayload: @@ -56,7 +56,6 @@ mainSteps: Handler: parse_event Script: |- %%SCRIPT=cis_parse_input.py%% - isEnd: false - name: Remediation @@ -66,9 +65,8 @@ mainSteps: RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption' KMSKeyArn: '{{KMSKeyArn}}' - isEnd: false - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml index 65c80ef7..580cf679 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml @@ -33,7 +33,7 @@ mainSteps: action: 'aws:executeScript' outputs: - Name: KMSKeyId - Selector: $.Payload.details.AwsKmsKey.KeyId + Selector: $.Payload.resource_id Type: String - Name: FindingId Selector: $.Payload.finding_id @@ -47,7 +47,7 @@ mainSteps: inputs: InputPayload: Finding: '{{Finding}}' - parse_id_pattern: '' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:key/([A-Za-z0-9-]{36})$' expected_control_id: '2.8' Runtime: python3.7 Handler: parse_event @@ -62,7 +62,7 @@ mainSteps: DocumentName: SHARR-EnableKeyRotation RuntimeParameters: KeyId: '{{ParseInput.KMSKeyId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableKeyRotation_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableKeyRotation' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml index 5b6cbe7e..6844aa3e 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml @@ -62,8 +62,8 @@ mainSteps: DocumentName: SHARR-EnableVPCFlowLogs RuntimeParameters: VPC: '{{ParseInput.VPC}}' - RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole_{{global:REGION}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs_{{global:REGION}}' + RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml index 0ec95155..5f115d57 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml @@ -141,7 +141,7 @@ mainSteps: inputs: DocumentName: SHARR-CreateLogMetricFilterAndAlarm RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm' FilterName: '{{ GetMetricFilterAndAlarmInputValue.FilterName }}' FilterPattern: '{{ GetMetricFilterAndAlarmInputValue.FilterPattern }}' MetricName: '{{ GetMetricFilterAndAlarmInputValue.MetricName }}' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml index 2417a548..adbcc43e 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: AWS-DisablePublicAccessForSecurityGroup RuntimeParameters: GroupId: '{{ ParseInput.GroupId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml index 6e3bf9ed..84969417 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules RuntimeParameters: GroupId: '{{ ParseInput.GroupId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py b/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py index c4b51a47..8d9de456 100644 --- a/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py +++ b/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py @@ -1,38 +1,46 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\.2\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\.2\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -46,16 +54,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -63,22 +69,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { "account_id": account_id, "resource_id": resource_id, @@ -88,5 +89,6 @@ def parse_event(event, context): "object": affected_object, "matches": resource_id_matches, "details": details, - "testmode": testmode + "testmode": testmode, + "resource": resource } \ No newline at end of file diff --git a/source/playbooks/CIS120/ssmdocs/scripts/test/test_cis_get_input_values.sh b/source/playbooks/CIS120/ssmdocs/scripts/test/test_cis_get_input_values.sh new file mode 100644 index 00000000..3283b6b7 --- /dev/null +++ b/source/playbooks/CIS120/ssmdocs/scripts/test/test_cis_get_input_values.sh @@ -0,0 +1,32 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import pytest + +from cis_get_input_values import verify + +def expected(): + return { + "filter_name": "SHARR_Filter_CIS_1_2_Finding_3_13_RouteTableChanges", + "filter_pattern": '{($.eventName=CreateRoute) || ($.eventName=CreateRouteTable) || ($.eventName=ReplaceRoute) || ($.eventName=ReplaceRouteTableAssociation) || ($.eventName=DeleteRouteTable) || ($.eventName=DeleteRoute) || ($.eventName=DisassociateRouteTable)}', + "metric_name": "SHARR_CIS_1_2_Finding_3_13_RouteTableChanges", + "metric_value": 1, + "alarm_name": "SHARR_Alarm_CIS_1_2_Finding_3_13_RouteTableChanges", + "alarm_desc": "Alarm for CIS finding 3.13 RouteTableChanges", + "alarm_threshold": 1 + } + +def test_verify(): + assert verify({'ControlId': '3.13'}, {}) == expected() diff --git a/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py b/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py index 81d09996..7f1271e3 100644 --- a/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py +++ b/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py @@ -102,7 +102,8 @@ def expected(): }, "matches": [ "cloudtrail-awslogs-111111111111-kjfskljdfl" ], 'details': {}, - 'testmode': False + 'testmode': False, + 'resource': event().get('Finding').get('Resources')[0] } def cis41_event(): @@ -229,16 +230,8 @@ def cis41_expected(): "OutputKey": 'Remediation.Output' }, "matches": [ "sg-087af114e4ae4c6ea" ], - 'details': {'AwsEc2SecurityGroup': {'GroupId': 'sg-087af114e4ae4c6ea', - 'GroupName': 'launch-wizard-17', - 'IpPermissions': [{'FromPort': 22, - 'IpProtocol': 'tcp', - 'IpRanges': [{'CidrIp': '0.0.0.0/0'}], - 'ToPort': 22}], - 'IpPermissionsEgress': [{'IpProtocol': '-1', - 'IpRanges': [{'CidrIp': '0.0.0.0/0'}]}], - 'OwnerId': '111111111111', - 'VpcId': 'vpc-e5b8f483'}}, + 'details': cis41_event().get('Finding').get('Resources')[0].get('Details'), + 'resource': cis41_event().get('Finding').get('Resources')[0] } def test_parse_event(): parsed_event = parse_event(event(), {}) diff --git a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap index f848d6c3..f1e25986 100644 --- a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap +++ b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap @@ -66,11 +66,16 @@ Object { Object { "Ref": "AWS::Partition", }, - ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.1", + ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/1.1", ], ], }, ], + "ProductFields": Object { + "ControlId": Array [ + "1.1", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -167,11 +172,16 @@ Object { Object { "Ref": "AWS::Partition", }, - ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.2", + ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/1.2", ], ], }, ], + "ProductFields": Object { + "ControlId": Array [ + "1.2", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -268,11 +278,16 @@ Object { Object { "Ref": "AWS::Partition", }, - ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3", + ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/1.3", ], ], }, ], + "ProductFields": Object { + "ControlId": Array [ + "1.3", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -465,41 +480,49 @@ This document ensures that credentials unused for 90 days or greater are disable "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):iam::\\\\d{12}:user/([A-Za-z0-9=,.@\\\\_\\\\-+]{1,64})$", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -513,16 +536,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -530,22 +551,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -555,7 +571,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -566,6 +583,11 @@ def parse_event(event, context): "Selector": "$.Payload.resource_id", "Type": "String", }, + Object { + "Name": "IAMResourceId", + "Selector": "$.Payload.details.AwsIamUser.UserId", + "Type": "String", + }, Object { "Name": "FindingId", "Selector": "$.Payload.finding_id", @@ -581,11 +603,6 @@ def parse_event(event, context): "Selector": "$.Payload.object", "Type": "StringMap", }, - Object { - "Name": "IAMResourceId", - "Selector": "$.Payload.details.AwsIamUser.UserId", - "Type": "String", - }, ], }, Object { @@ -593,7 +610,7 @@ def parse_event(event, context): "inputs": Object { "DocumentName": "SHARR-RevokeUnusedIAMUserCredentials", "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials", "IAMResourceId": "{{ ParseInput.IAMResourceId }}", }, }, @@ -694,41 +711,49 @@ This document establishes a default password policy. "parse_id_pattern": "", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -742,16 +767,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -759,22 +782,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -784,7 +802,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -813,7 +832,7 @@ def parse_event(event, context): "DocumentName": "SHARR-SetIAMPasswordPolicy", "RuntimeParameters": Object { "AllowUsersToChangePassword": true, - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy", "HardExpiry": true, "MaxPasswordAge": 90, "MinimumPasswordLength": 14, @@ -903,41 +922,49 @@ Note: this remediation will create a NEW trail. "parse_id_pattern": "", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\\\\\\\.2\\\\\\\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') @@ -951,16 +978,14 @@ def parse_event(event, context): if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -968,22 +993,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -993,7 +1013,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -1032,7 +1053,7 @@ def parse_event(event, context): "DocumentName": "SHARR-CreateCloudTrailMultiRegionTrail", "RuntimeParameters": Object { "AWSPartition": "{{global:AWS_PARTITION}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail", }, }, "isEnd": false, diff --git a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml index 74e48997..a9815984 100644 --- a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml @@ -101,7 +101,7 @@ mainSteps: inputs: Service: iam Api: GetRole - RoleName: 'SO0111-SHARR-RDSEnhancedMonitoring_{{global:REGION}}' + RoleName: 'SO0111-SHARR-RDSEnhancedMonitoring' outputs: - Name: Arn Selector: $.Role.Arn diff --git a/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/test/test_parse_event.py b/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/test/test_parse_event.py index 58692a1c..90e15011 100644 --- a/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/test/test_parse_event.py +++ b/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/test/test_parse_event.py @@ -15,7 +15,7 @@ ############################################################################### import pytest -from cis_parse_input import parse_event +from newplaybook_parse_input import parse_event def event(): return { 'expected_control_id': '2.3', diff --git a/source/playbooks/PCI321/bin/pci321.ts b/source/playbooks/PCI321/bin/pci321.ts index be62c25d..6c4e6e55 100644 --- a/source/playbooks/PCI321/bin/pci321.ts +++ b/source/playbooks/PCI321/bin/pci321.ts @@ -40,10 +40,10 @@ const remediations: IControl[] = [ { "control": "PCI.IAM.7" }, { "control": "PCI.CloudTrail.2" }, { "control": "PCI.CW.1" }, - // { "control": "PCI.S3.6" }, // == AFSBP S3.1 { "control": "PCI.EC2.1" }, { "control": "PCI.EC2.2" }, { "control": "PCI.IAM.8" }, + { "control": "PCI.KMS.1" }, { "control": "PCI.Lambda.1" }, { "control": "PCI.RDS.1" }, { "control": "PCI.CloudTrail.1" }, @@ -56,6 +56,7 @@ const remediations: IControl[] = [ "control": "PCI.S3.2", "executes": "PCI.S3.1" }, + { "control": "PCI.S3.5" }, { "control": "PCI.S3.6" } ] diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml index e359784d..7c657015 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml @@ -69,7 +69,7 @@ mainSteps: inputs: DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck' AutoScalingGroupName: '{{ParseInput.AutoScalingGroupName}}' - name: UpdateFinding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml index 476a675d..075d8a02 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml @@ -36,6 +36,13 @@ parameters: type: String default: 'LogMetrics' description: The name of the metric namespace where the metrics will be logged + KMSKeyArn: + type: String + default: >- + {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} + description: The ARN of the KMS key created by SHARR for remediations + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' + mainSteps: - name: ParseInput action: 'aws:executeScript' @@ -104,7 +111,7 @@ mainSteps: inputs: DocumentName: SHARR-CreateLogMetricFilterAndAlarm RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm' FilterName: '{{ GetMetricFilterAndAlarmInputValue.FilterName }}' FilterPattern: '{{ GetMetricFilterAndAlarmInputValue.FilterPattern }}' MetricName: '{{ GetMetricFilterAndAlarmInputValue.MetricName }}' @@ -114,6 +121,8 @@ mainSteps: AlarmDesc: '{{ GetMetricFilterAndAlarmInputValue.AlarmDesc }}' AlarmThreshold: '{{ GetMetricFilterAndAlarmInputValue.AlarmThreshold }}' LogGroupName: '{{ LogGroupName }}' + SNSTopicName: 'SO0111-SHARR-LocalAlarmNotification' + KMSKeyArn: '{{KMSKeyArn}}' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml index da40fa1e..ba759ca8 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml @@ -47,7 +47,7 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.details.AwsCloudTrailTrail.HomeRegion + Selector: $.Payload.resource.Region Type: String inputs: InputPayload: @@ -68,7 +68,7 @@ mainSteps: RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailEncryption' KMSKeyArn: '{{KMSKeyArn}}' isEnd: false diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml index e41ea804..fa57435b 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml @@ -67,7 +67,7 @@ mainSteps: inputs: DocumentName: SHARR-CreateCloudTrailMultiRegionTrail RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' - diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml index 491a2d3d..c0627f66 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-EnableCloudTrailLogFileValidation RuntimeParameters: TrailName: '{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml index 5c47393d..f0f91366 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml @@ -63,9 +63,9 @@ mainSteps: DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging RuntimeParameters: TrailName: '{{ ParseInput.TrailName }}' - CloudWatchLogsRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CloudTrailToCloudWatchLogs_{{global:REGION}}' + CloudWatchLogsRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CloudTrailToCloudWatchLogs' LogGroupName: 'CloudTrail/{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailToCloudWatchLogging_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailToCloudWatchLogging' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml index b277c358..5fec0a1c 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml @@ -72,7 +72,7 @@ mainSteps: RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - name: UpdateFinding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml index 8856ef98..a2c4e9a4 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml @@ -66,7 +66,7 @@ mainSteps: DocumentName: SHARR-MakeEBSSnapshotsPrivate RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate' TestMode: '{{ParseInput.TestMode}}' isEnd: false diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml index 9e902966..466fb102 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules RuntimeParameters: GroupId: '{{ParseInput.GroupId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules' - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml index c7633c1d..a347751d 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml @@ -63,8 +63,8 @@ mainSteps: DocumentName: SHARR-EnableVPCFlowLogs RuntimeParameters: VPC: '{{ParseInput.VPC}}' - RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole_{{global:REGION}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs_{{global:REGION}}' + RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml index 85bf073c..6c5bdaa6 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml @@ -39,8 +39,8 @@ mainSteps: - name: ParseInput action: 'aws:executeScript' outputs: - - Name: IAMUser - Selector: $.Payload.resource_id + - Name: IAMResourceId + Selector: $.Payload.details.AwsIamUser.UserId Type: String - Name: FindingId Selector: $.Payload.finding_id @@ -51,13 +51,10 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap - - Name: IAMResourceId - Selector: $.Payload.details.AwsIamUser.UserId - Type: String inputs: InputPayload: Finding: '{{Finding}}' - parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):iam::\d{12}:user/([A-Za-z0-9=,.@\_\-+]{1,64})$' + parse_id_pattern: '' expected_control_id: 'PCI.IAM.7' Runtime: python3.7 Handler: parse_event @@ -71,7 +68,7 @@ mainSteps: DocumentName: SHARR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml index cdbe455f..05bc8b5c 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml @@ -70,7 +70,7 @@ mainSteps: RequireUppercaseCharacters: True RequireLowercaseCharacters: True PasswordReusePrevention: 24 - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml new file mode 100644 index 00000000..b67e2f01 --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml @@ -0,0 +1,85 @@ +description: | + ### Document Name - SHARR-PCI_3.2.1_PCI.KMS.1 + + ## What does this document do? + Enables rotation for customer-managed KMS keys. + + ## Security Standards and Controls + * CIS 2.8 + * PCI KMS.1 + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * Remediation.Output - Remediation results + + ## Documentation Links + * [PCI v3.2.1 PCI.KMS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-kms-1) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from Step function for finding + AutomationAssumeRole: + type: String + description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + default: '' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+' + +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: KMSKeyId + Selector: $.Payload.resource_id + Type: String + - Name: FindingId + Selector: $.Payload.finding_id + Type: String + - Name: ProductArn + Selector: $.Payload.product_arn + Type: String + - Name: AffectedObject + Selector: $.Payload.object + Type: StringMap + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:key/([A-Za-z0-9-]{36})$' + expected_control_id: 'PCI.KMS.1' + Runtime: python3.7 + Handler: parse_event + Script: |- + %%SCRIPT=pci_parse_input.py%% + isEnd: false + + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableKeyRotation + RuntimeParameters: + KeyId: '{{ParseInput.KMSKeyId}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableKeyRotation' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Enabled KMS Customer Managed Key rotation for {{ParseInput.KMSKeyId}}' + UpdatedBy: 'SHARR-PCI_3.2.1_PCI.KMS.1' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml index 34cea2d0..b5f44db1 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml @@ -42,12 +42,12 @@ mainSteps: Selector: $.Payload.object Type: StringMap - Name: FunctionName - Selector: $.Payload.details.AwsLambdaFunction.FunctionName + Selector: $.Payload.resource_id Type: String inputs: InputPayload: Finding: '{{Finding}}' - parse_id_pattern: '' + parse_id_pattern: '^arn:(?:aws|aws-us-gov|aws-cn):lambda:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:function:([a-zA-Z0-9\-_]{1,64})$' expected_control_id: 'PCI.Lambda.1' Runtime: python3.7 Handler: parse_event @@ -63,7 +63,7 @@ mainSteps: DocumentName: SHARR-RemoveLambdaPublicAccess RuntimeParameters: FunctionName: '{{ ParseInput.FunctionName }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess' - name: UpdateFinding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml index 898a3eb1..db8e3a8f 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml @@ -66,7 +66,7 @@ mainSteps: RuntimeParameters: DBSnapshotId: '{{ParseInput.DBSnapshotId}}' DBSnapshotType: '{{ParseInput.DBSnapshotType}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate' nextStep: UpdateFinding - name: UpdateFinding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml index 7dab4ad6..824bb700 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml @@ -65,7 +65,7 @@ mainSteps: DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' RestrictPublicBuckets: true BlockPublicAcls: true IgnorePublicAcls: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml new file mode 100644 index 00000000..ee8e5558 --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml @@ -0,0 +1,87 @@ +description: | + ### Document Name - SHARR-PCI_3.2.1_PCI.S3.5 + + ## What does this document do? + This document adds a bucket policy to restrict internet access to https only. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * Remediation.Output + + ## Documentation Links + * [PCI v3.2.1 S3.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-s3-5) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from Step function for finding + AutomationAssumeRole: + type: String + description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + default: '' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+' + +mainSteps: + - + name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: BucketName + Selector: $.Payload.resource_id + Type: String + - Name: FindingId + Selector: $.Payload.finding_id + Type: String + - Name: ProductArn + Selector: $.Payload.product_arn + Type: String + - Name: AffectedObject + Selector: $.Payload.object + Type: StringMap + - Name: AccountId + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$' + expected_control_id: [ 'PCI.S3.5' ] + Runtime: python3.7 + Handler: parse_event + Script: |- + %%SCRIPT=pci_parse_input.py%% + isEnd: false + - + name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-SetSSLBucketPolicy + RuntimeParameters: + BucketName: '{{ParseInput.BucketName}}' + AccountId: '{{ParseInput.AccountId}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetSSLBucketPolicy' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Added SSL-only access policy to S3 bucket.' + UpdatedBy: 'SHARR-PCI_3.2.1_PCI.S3.5' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml index d38fe5d9..6ee186f0 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-ConfigureS3PublicAccessBlock RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock' RestrictPublicBuckets: true BlockPublicAcls: true IgnorePublicAcls: true diff --git a/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py b/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py index 89c94717..f9b4e6ca 100644 --- a/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py +++ b/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py @@ -1,61 +1,67 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### import re -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/pci-dss/v/3\\.2\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): - my_control_id = event['expected_control_id'] + expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/pci-dss/v/3\\.2\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: - exit(f'ERROR: Finding is missing Control Id: {finding_id}') + exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') # ControlId is the expected value - if control_id not in my_control_id: - exit(f'ERROR: Control Id from input ({control_id}) does not match {str(my_control_id)}') + if control_id not in expected_control_id: + exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}') # ProductArn present and valid product_arn = finding['ProductArn'] if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -63,22 +69,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { "account_id": account_id, "resource_id": resource_id, @@ -88,5 +89,6 @@ def parse_event(event, context): "object": affected_object, "matches": resource_id_matches, "details": details, - "testmode": testmode - } \ No newline at end of file + "testmode": testmode, + "resource": resource + } diff --git a/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_get_input_values.py b/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_get_input_values.py new file mode 100644 index 00000000..f73e671e --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_get_input_values.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import pytest + +from pci_get_input_values import verify + +def expected(): + return { + "filter_name": "SHARR_Filter_PCI_321_Finding_CW1_RootAccountUsage", + "filter_pattern": '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}', + "metric_name": "SHARR_PCI_321_Finding_CW1_RootAccountUsage", + "metric_value": 1, + "alarm_name": "SHARR_Alarm_PCI_321_Finding_CW1_RootAccountUsage", + "alarm_desc": "Alarm for PCI finding CW.1 RootAccountUsage", + "alarm_threshold": 1 + } + +def test_verify(): + assert verify({'ControlId': 'PCI.CW.1'}, {}) == expected() diff --git a/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py b/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py index dec5c9bf..06495904 100644 --- a/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py +++ b/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py @@ -1,12 +1,12 @@ #!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the "License"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0/ # # # # or in the "license" file accompanying this file. This file is distributed # # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # @@ -113,8 +113,22 @@ def expected(): "OutputKey": 'Remediation.Output' }, "matches": [ "foo-bar@baz" ], - 'details': {'AwsIamUser': {'CreateDate': '2016-09-23T12:42:13.000Z', 'Path': '/', 'UserId': 'AIDAIMALBCBBI4ZZHJVTO', 'UserName': 'foo-bar@baz'}} - } + 'details': {'AwsIamUser': {'CreateDate': '2016-09-23T12:42:13.000Z', 'Path': '/', 'UserId': 'AIDAIMALBCBBI4ZZHJVTO', 'UserName': 'foo-bar@baz'}}, + 'resource': { + "Type": "AwsIamUser", + "Id": "arn:aws:iam::111111111111:user/foo-bar@baz", + "Partition": "aws", + "Region": "us-east-1", + "Details": { + "AwsIamUser": { + "CreateDate": "2016-09-23T12:42:13.000Z", + "Path": "/", + "UserId": "AIDAIMALBCBBI4ZZHJVTO", + "UserName": "foo-bar@baz" + } + } + } + } def test_parse_event(): parsed_event = parse_event(event(), {}) @@ -146,7 +160,7 @@ def test_bad_control_id(): with pytest.raises(SystemExit) as pytest_wrapped_e: parsed_event = parse_event(test_event, {}) assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 'ERROR: Finding is missing Control Id: arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1//finding/fec91aaf-5016-4c40-9d24-9966e4be80c4' + assert pytest_wrapped_e.value.code == 'ERROR: Finding Id is invalid: arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1//finding/fec91aaf-5016-4c40-9d24-9966e4be80c4 - missing Control Id' def test_control_id_nomatch(): test_event = event() diff --git a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap index 8c1f8e4f..f5f9d549 100644 --- a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap +++ b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap @@ -58,19 +58,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/pci-dss/v/3.2.1/rule/PCI.AutoScaling.1", - ], - ], - }, + "pci-dss/v/3.2.1/PCI.AutoScaling.1", ], + "ProductFields": Object { + "ControlId": Array [ + "PCI.AutoScaling.1", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -159,19 +153,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/pci-dss/v/3.2.1/rule/PCI.EC2.6", - ], - ], - }, + "pci-dss/v/3.2.1/PCI.EC2.6", ], + "ProductFields": Object { + "ControlId": Array [ + "PCI.EC2.6", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -260,19 +248,13 @@ Object { ], }, "GeneratorId": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":securityhub:::ruleset/pci-dss/v/3.2.1/rule/PCI.IAM.8", - ], - ], - }, + "pci-dss/v/3.2.1/PCI.IAM.8", ], + "ProductFields": Object { + "ControlId": Array [ + "PCI.IAM.8", + ], + }, "Workflow": Object { "Status": Array [ "NEW", @@ -467,64 +449,70 @@ Default: 30 seconds "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:autoScalingGroup:(?i:[0-9a-f]{11}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}):autoScalingGroupName/(.*)$", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): - my_control_id = event['expected_control_id'] + expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: - exit(f'ERROR: Finding is missing Control Id: {finding_id}') + exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') # ControlId is the expected value - if control_id not in my_control_id: - exit(f'ERROR: Control Id from input ({control_id}) does not match {str(my_control_id)}') + if control_id not in expected_control_id: + exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}') # ProductArn present and valid product_arn = finding['ProductArn'] if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -532,22 +520,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -557,7 +540,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -591,7 +575,7 @@ def parse_event(event, context): "DocumentName": "SHARR-EnableAutoScalingGroupELBHealthCheck", "RuntimeParameters": Object { "AutoScalingGroupName": "{{ParseInput.AutoScalingGroupName}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck", }, }, "isEnd": false, @@ -680,64 +664,70 @@ Enables VPC Flow Logs for a VPC "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:.*:\\\\d{12}:vpc/(vpc-[0-9a-f]{8,17}$)", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): - my_control_id = event['expected_control_id'] + expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: - exit(f'ERROR: Finding is missing Control Id: {finding_id}') + exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') # ControlId is the expected value - if control_id not in my_control_id: - exit(f'ERROR: Control Id from input ({control_id}) does not match {str(my_control_id)}') + if control_id not in expected_control_id: + exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}') # ProductArn present and valid product_arn = finding['ProductArn'] if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -745,22 +735,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -770,7 +755,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -803,8 +789,8 @@ def parse_event(event, context): "inputs": Object { "DocumentName": "SHARR-EnableVPCFlowLogs", "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs_{{global:REGION}}", - "RemediationRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs", + "RemediationRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole", "VPC": "{{ParseInput.VPC}}", }, }, @@ -895,64 +881,70 @@ This document establishes a default password policy. "parse_id_pattern": "", }, "Runtime": "python3.7", - "Script": "import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import re + +def get_control_id_from_arn(finding_id_arn): + check_finding_id = re.match( + '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + finding_id_arn + ) + if check_finding_id: + control_id = check_finding_id.group(1) + return control_id + else: + exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') def parse_event(event, context): - my_control_id = event['expected_control_id'] + expected_control_id = event['expected_control_id'] parse_id_pattern = event['parse_id_pattern'] resource_id_matches = [] - finding = event['Finding'] - - testmode = False - if 'testmode' in finding: - testmode = True + testmode = bool('testmode' in finding) finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d):\\\\\\\\d{12}:subscription/pci-dss/v/3\\\\\\\\.2\\\\\\\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] + + account_id = finding.get('AwsAccountId', '') if not re.match('^\\\\\\\\d{12}$', account_id): exit(f'ERROR: AwsAccountId is invalid: {account_id}') + control_id = get_control_id_from_arn(finding['Id']) + # ControlId present and valid if not control_id: - exit(f'ERROR: Finding is missing Control Id: {finding_id}') + exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') # ControlId is the expected value - if control_id not in my_control_id: - exit(f'ERROR: Control Id from input ({control_id}) does not match {str(my_control_id)}') + if control_id not in expected_control_id: + exit(f'ERROR: Control Id from input ({control_id}) does not match {str(expected_control_id)}') # ProductArn present and valid product_arn = finding['ProductArn'] if not re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\\\\\d)::product/aws/securityhub$', product_arn): exit(f'ERROR: ProductArn is invalid: {product_arn}') - # ResourceType - resource_type = finding['Resources'][0]['Type'] + resource = finding['Resources'][0] - details = {} # Details - if 'Details' in finding['Resources'][0]: - details = finding['Resources'][0]['Details'] + details = finding['Resources'][0].get('Details', {}) # Regex match Id to get remediation-specific identifier identifier_raw = finding['Resources'][0]['Id'] + resource_id = identifier_raw if parse_id_pattern: identifier_match = re.match( @@ -960,22 +952,17 @@ def parse_event(event, context): identifier_raw ) - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: + if identifier_match: for group in range(1, len(identifier_match.groups())+1): resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw + resource_id = identifier_match.group(event.get('resource_index', 1)) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') if not resource_id: exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} + affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} return { \\"account_id\\": account_id, \\"resource_id\\": resource_id, @@ -985,7 +972,8 @@ def parse_event(event, context): \\"object\\": affected_object, \\"matches\\": resource_id_matches, \\"details\\": details, - \\"testmode\\": testmode + \\"testmode\\": testmode, + \\"resource\\": resource }", }, "isEnd": false, @@ -1014,7 +1002,7 @@ def parse_event(event, context): "DocumentName": "SHARR-SetIAMPasswordPolicy", "RuntimeParameters": Object { "AllowUsersToChangePassword": true, - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy_{{global:REGION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy", "HardExpiry": true, "MaxPasswordAge": 90, "MinimumPasswordLength": 14, diff --git a/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml b/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml index 5eaa9b6c..98d160d2 100644 --- a/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml +++ b/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml @@ -91,7 +91,7 @@ mainSteps: isCritical: true isEnd: true inputs: - Runtime: python3.6 + Runtime: python3.7 Handler: validate_s3_bucket_publicaccessblock InputPayload: Bucket: "{{BucketName}}" diff --git a/source/remediation_runbooks/EnableAWSConfig.yaml b/source/remediation_runbooks/EnableAWSConfig.yaml index ab671e1c..5f40c51d 100644 --- a/source/remediation_runbooks/EnableAWSConfig.yaml +++ b/source/remediation_runbooks/EnableAWSConfig.yaml @@ -64,7 +64,7 @@ mainSteps: DocumentName: SHARR-CreateAccessLoggingBucket RuntimeParameters: BucketName: 'so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket_{{global:REGION}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket' - name: CreateConfigBucket action: 'aws:executeScript' diff --git a/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml b/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml index 7b7274a7..ac32ff96 100644 --- a/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml +++ b/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml @@ -115,7 +115,7 @@ mainSteps: isEnd: true timeoutSeconds: 600 inputs: - Runtime: python3.6 + Runtime: python3.7 Handler: handler InputPayload: MonitoringInterval: "{{ MonitoringInterval }}" diff --git a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml index f54e87a4..be00f7ea 100644 --- a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml +++ b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml @@ -40,7 +40,7 @@ mainSteps: outputs: - Name: Snapshots Selector: $.Payload - Type: MapList + Type: StringList inputs: InputPayload: region: '{{global:REGION}}' diff --git a/source/remediation_runbooks/RevokeUnrotatedKeys.yaml b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml new file mode 100644 index 00000000..74fb66cb --- /dev/null +++ b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml @@ -0,0 +1,56 @@ +schemaVersion: "0.3" +description: | + ### Document Name - SHARR-RevokeUnrotatedKeys + + ## What does this document do? + This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. It will disabled keys that have been used within the previous 90 days by have not been rotated by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html). Please note, this automation document requires AWS Config to be enabled. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * MaxCredentialUsageAge: (Optional) Maximum number of days a key is allowed to be unrotated before revoking it. DEFAULT: 90 + + ## Output Parameters + * RevokeUnrotatedKeys.Output + +assumeRole: "{{ AutomationAssumeRole }}" +parameters: + AutomationAssumeRole: + type: String + description: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + allowedPattern: ^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/[\w+=,.@-]+$ + IAMResourceId: + type: String + description: (Required) IAM resource unique identifier. + allowedPattern: ^[\w+=,.@_-]{1,128}$ + MaxCredentialUsageAge: + type: String + description: (Required) Maximum number of days within which a credential must be used. The default value is 90 days. + allowedPattern: ^(\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\b)$ + default: "90" +outputs: + - RevokeUnrotatedKeys.Output +mainSteps: + - name: RevokeUnrotatedKeys + action: aws:executeScript + timeoutSeconds: 600 + isEnd: true + description: | + ## RevokeUnrotatedKeys + + This step deactivates IAM user access keys that have not been rotated in more than MaxCredentialUsageAge days + ## Outputs + * Output: Success message or failure Exception. + inputs: + Runtime: python3.7 + Handler: unrotated_key_handler + InputPayload: + IAMResourceId: "{{ IAMResourceId }}" + MaxCredentialUsageAge: "{{ MaxCredentialUsageAge }}" + Script: |- + %%SCRIPT=EnableCloudTrailEncryption.py%% + + outputs: + - Name: Output + Selector: $.Payload + Type: StringMap diff --git a/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml b/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml index 6ae89ac3..36b00961 100644 --- a/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml +++ b/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml @@ -43,7 +43,7 @@ mainSteps: ## Outputs * Output: Success message or failure Exception. inputs: - Runtime: python3.6 + Runtime: python3.7 Handler: unused_iam_credentials_handler InputPayload: IAMResourceId: "{{ IAMResourceId }}" diff --git a/source/remediation_runbooks/SetSSLBucketPolicy.yaml b/source/remediation_runbooks/SetSSLBucketPolicy.yaml new file mode 100644 index 00000000..517074ed --- /dev/null +++ b/source/remediation_runbooks/SetSSLBucketPolicy.yaml @@ -0,0 +1,53 @@ +schemaVersion: "0.3" +description: | + ### Document name - SHARR-SetSSLBucketPolicy + + ## What does this document do? + This document adds a bucket policy to require transmission over HTTPS for the given S3 bucket by adding a policy statement to the bucket policy. + + ## Input Parameters + * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. + * BucketName: (Required) Name of the bucket to modify. + * AccountId: (Required) Account to which the bucket belongs + + ## Output Parameters + + * Remediation.Output - stdout messages from the remediation + + ## Security Standards / Controls + * AFSBP v1.0.0: S3.5 + * CIS v1.2.0: n/a + * PCI: S3.5 + +assumeRole: "{{ AutomationAssumeRole }}" +parameters: + AccountId: + type: String + description: Account ID of the account for the finding + allowedPattern: ^[0-9]{12}$ + AutomationAssumeRole: + type: String + description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. + allowedPattern: ^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/[\w+=,.@/-]+$ + BucketName: + type: String + description: Name of the bucket to have a policy added + allowedPattern: (?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$) + +outputs: + - Remediation.Output +mainSteps: + - name: Remediation + action: 'aws:executeScript' + outputs: + - Name: Output + Selector: $.Payload.response + Type: StringMap + inputs: + InputPayload: + accountid: '{{AccountId}}' + bucket: '{{BucketName}}' + Runtime: python3.7 + Handler: add_ssl_bucket_policy + Script: |- + %%SCRIPT=SetSSLBucketPolicy.py%% diff --git a/source/remediation_runbooks/rds6-remediation-resources.ts b/source/remediation_runbooks/rds6-remediation-resources.ts index aeadbdff..88d93c5c 100644 --- a/source/remediation_runbooks/rds6-remediation-resources.ts +++ b/source/remediation_runbooks/rds6-remediation-resources.ts @@ -74,6 +74,7 @@ export class Rds6EnhancedMonitoringRole extends cdk.Construct { }); rds6Role.attachInlinePolicy(rds6Policy) + rds6Role.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) let roleResource = rds6Role.node.findChild('Resource') as CfnRole; roleResource.cfnOptions.metadata = { diff --git a/source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_waitforloggroup.py b/source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_waitforloggroup.py index 42b31438..d52e264d 100644 --- a/source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_waitforloggroup.py +++ b/source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_waitforloggroup.py @@ -35,13 +35,12 @@ def wait_for_loggroup(event, context): try: describe_group = cwl_client.describe_log_groups(logGroupNamePrefix=event['LogGroup']) print(len(describe_group['logGroups'])) - if len(describe_group['logGroups']) == 1: - return str(describe_group['logGroups'][0]['arn']) - elif len(describe_group['logGroups']) > 1: - exit(f'More than one Log Group matches {event["LogGroup"]}') - else: - time.sleep(2) - attempts += 1 + for group in describe_group['logGroups']: + if group['logGroupName'] == event['LogGroup']: + return str(group['arn']) + # no match - wait and retry + time.sleep(2) + attempts += 1 except Exception as e: exit(f'Failed to create Log Group {event["LogGroup"]}: {str(e)}') diff --git a/source/remediation_runbooks/scripts/GetPublicEBSSnapshots.py b/source/remediation_runbooks/scripts/GetPublicEBSSnapshots.py index 25976475..79fba703 100644 --- a/source/remediation_runbooks/scripts/GetPublicEBSSnapshots.py +++ b/source/remediation_runbooks/scripts/GetPublicEBSSnapshots.py @@ -34,131 +34,11 @@ def get_public_snapshots(event, context): if 'testmode' in event and event['testmode']: return [ - { - "Description": "Snapshot of idle volume before deletion", - "Encrypted": False, - "OwnerId": "111111111111", - "Progress": "100%", - "SnapshotId": "snap-12341234123412345", - "StartTime": "2021-03-11T08:23:02.785Z", - "State": "completed", - "VolumeId": "vol-12341234123412345", - "VolumeSize": 4, - "Tags": [ - { - "Key": "SnapshotDate", - "Value": "2021-03-11 08:23:02.376859" - }, - { - "Key": "DeleteEBSVolOnCompletion", - "Value": "False" - }, - { - "Key": "SnapshotReason", - "Value": "Idle Volume" - } - ] - }, - { - "Description": "Snapshot of idle volume before deletion", - "Encrypted": False, - "OwnerId": "111111111111", - "Progress": "100%", - "SnapshotId": "snap-12341234123412345", - "StartTime": "2021-03-11T08:20:37.399Z", - "State": "completed", - "VolumeId": "vol-12341234123412345", - "VolumeSize": 4, - "Tags": [ - { - "Key": "DeleteEBSVolOnCompletion", - "Value": "False" - }, - { - "Key": "SnapshotDate", - "Value": "2021-03-11 08:20:37.224101" - }, - { - "Key": "SnapshotReason", - "Value": "Idle Volume" - } - ] - }, - { - "Description": "Snapshot of idle volume before deletion", - "Encrypted": False, - "OwnerId": "111111111111", - "Progress": "100%", - "SnapshotId": "snap-12341234123412345", - "StartTime": "2021-03-11T08:22:48.936Z", - "State": "completed", - "VolumeId": "vol-12341234123412345", - "VolumeSize": 4, - "Tags": [ - { - "Key": "SnapshotReason", - "Value": "Idle Volume" - }, - { - "Key": "SnapshotDate", - "Value": "2021-03-11 08:22:48.714893" - }, - { - "Key": "DeleteEBSVolOnCompletion", - "Value": "False" - } - ] - }, - { - "Description": "Snapshot of idle volume before deletion", - "Encrypted": False, - "OwnerId": "111111111111", - "Progress": "100%", - "SnapshotId": "snap-12341234123412345", - "StartTime": "2021-03-11T08:23:05.156Z", - "State": "completed", - "VolumeId": "vol-12341234123412345", - "VolumeSize": 4, - "Tags": [ - { - "Key": "DeleteEBSVolOnCompletion", - "Value": "False" - }, - { - "Key": "SnapshotReason", - "Value": "Idle Volume" - }, - { - "Key": "SnapshotDate", - "Value": "2021-03-11 08:23:04.876640" - } - ] - }, - { - "Description": "Snapshot of idle volume before deletion", - "Encrypted": False, - "OwnerId": "111111111111", - "Progress": "100%", - "SnapshotId": "snap-12341234123412345", - "StartTime": "2021-03-11T08:22:34.850Z", - "State": "completed", - "VolumeId": "vol-12341234123412345", - "VolumeSize": 4, - "Tags": [ - { - "Key": "DeleteEBSVolOnCompletion", - "Value": "False" - }, - { - "Key": "SnapshotReason", - "Value": "Idle Volume" - }, - { - "Key": "SnapshotDate", - "Value": "2021-03-11 08:22:34.671355" - } - ] - } + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345" ] return list_public_snapshots(account_id) @@ -168,7 +48,7 @@ def list_public_snapshots(account_id): control_token = 'start' try: - buffer = [] + public_snapshot_ids = [] while control_token: @@ -182,19 +62,21 @@ def list_public_snapshots(account_id): } if control_token: kwargs['NextToken'] = control_token - + response = ec2.describe_snapshots( - **kwargs - ) + **kwargs + ) + + for snapshot in response['Snapshots']: + public_snapshot_ids.append(snapshot['SnapshotId']) if 'NextToken' in response: control_token = response['NextToken'] else: control_token = '' - buffer += response['Snapshots'] - - return buffer + return public_snapshot_ids + except Exception as e: print(e) exit('Failed to describe_snapshots') diff --git a/source/remediation_runbooks/scripts/MakeEBSSnapshotsPrivate.py b/source/remediation_runbooks/scripts/MakeEBSSnapshotsPrivate.py index 5d4a35f6..3671f152 100644 --- a/source/remediation_runbooks/scripts/MakeEBSSnapshotsPrivate.py +++ b/source/remediation_runbooks/scripts/MakeEBSSnapshotsPrivate.py @@ -36,22 +36,22 @@ def make_snapshots_private(event, context): success_count = 0 - for snapshot in snapshots: + for snapshot_id in snapshots: try: ec2.modify_snapshot_attribute( Attribute='CreateVolumePermission', CreateVolumePermission={ 'Remove': [{'Group': 'all'}] }, - SnapshotId=snapshot['SnapshotId'] + SnapshotId=snapshot_id ) - print('Snapshot ' + snapshot['SnapshotId'] + ' permissions set to private') + print(f'Snapshot {snapshot_id} permissions set to private') - remediated.append(snapshot['SnapshotId']) + remediated.append(snapshot_id) success_count += 1 except Exception as e: print(e) - print('FAILED to remediate Snapshot ' + snapshot['SnapshotId']) + print(f'FAILED to remediate Snapshot {snapshot_id}') result=json.dumps(ec2.describe_snapshots( SnapshotIds=remediated diff --git a/source/remediation_runbooks/scripts/MakeRDSSnapshotPrivate.py b/source/remediation_runbooks/scripts/MakeRDSSnapshotPrivate.py index 51875d55..e5a28a95 100644 --- a/source/remediation_runbooks/scripts/MakeRDSSnapshotPrivate.py +++ b/source/remediation_runbooks/scripts/MakeRDSSnapshotPrivate.py @@ -19,16 +19,17 @@ from botocore.config import Config from botocore.exceptions import ClientError -def connect_to_rds(boto_config): - return boto3.client('rds', config=boto_config) - -def make_snapshot_private(event, context): +def connect_to_rds(): boto_config = Config( retries ={ 'mode': 'standard' } ) - rds_client = connect_to_rds(boto_config) + return boto3.client('rds', config=boto_config) + +def make_snapshot_private(event, context): + + rds_client = connect_to_rds() snapshot_id = event['DBSnapshotId'] snapshot_type = event['DBSnapshotType'] try: @@ -44,6 +45,9 @@ def make_snapshot_private(event, context): AttributeName='restore', ValuesToRemove=['all'] ) + else: + exit(f'Unrecognized snapshot_type {snapshot_type}') + print(f'Remediation completed: {snapshot_id} public access removed.') return { "response": { diff --git a/source/remediation_runbooks/scripts/RemoveLambdaPublicAccess.py b/source/remediation_runbooks/scripts/RemoveLambdaPublicAccess.py index 245e4c6f..a47944a3 100644 --- a/source/remediation_runbooks/scripts/RemoveLambdaPublicAccess.py +++ b/source/remediation_runbooks/scripts/RemoveLambdaPublicAccess.py @@ -43,6 +43,13 @@ def remove_resource_policy(functionname, sid, client): except Exception as e: exit(f'FAILED: SID {sid} was NOT removed from Lambda function {functionname} - {str(e)}') +def remove_public_statement(client, functionname, statement, principal_source): + for principal in list(principal_source): + if principal == "*" or (isinstance(principal, dict) and principal.get("AWS","") == "*"): + print_policy_before(statement) + remove_resource_policy(functionname, statement['Sid'], client) + break # there will only be one that matches + def remove_lambda_public_access(event, context): client = connect_to_lambda(boto_config) @@ -57,20 +64,9 @@ def remove_lambda_public_access(event, context): print('Scanning for public resource policies in ' + functionname) for statement in statements: - principal_statements = [] - - if isinstance(statement['Principal'], list): - principal_statements = statement['Principal'] - else: - principal_statements = [statement['Principal']] - - for principal in principal_statements: - if principal == "*" or (isinstance(principal, dict) and principal.get("AWS","") == "*"): - print_policy_before(statement) - remove_resource_policy(functionname, statement['Sid'], client) - break + remove_public_statement(client, functionname, statement, list(statement['Principal'])) - result = client.get_policy(FunctionName=functionname) + client.get_policy(FunctionName=functionname) verify(functionname) except ClientError as ex: diff --git a/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py b/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py new file mode 100644 index 00000000..31bf0437 --- /dev/null +++ b/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +from datetime import datetime +from datetime import timedelta +import boto3 +from botocore.config import Config + +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) + +responses = {} +responses["DeactivateUnusedKeysResponse"] = [] + +def str_time_to_datetime(dt_str): + dt_obj = datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=None) + return dt_obj + +def connect_to_iam(boto_config): + return boto3.client('iam', config=boto_config) + +def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + +def get_user_name(resource_id): + config_client = connect_to_config(boto_config) + list_discovered_resources_response = config_client.list_discovered_resources( + resourceType='AWS::IAM::User', + resourceIds=[resource_id] + ) + resource_name = list_discovered_resources_response.get("resourceIdentifiers")[0].get("resourceName") + return resource_name + +def list_access_keys(user_name, include_inactive=False): + iam_client = connect_to_iam(boto_config) + active_keys = [] + keys = iam_client.list_access_keys(UserName=user_name).get("AccessKeyMetadata", []) + for key in keys: + if include_inactive or key.get('Status') == 'Active': + active_keys.append(key) + return active_keys + +def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): + iam_client = connect_to_iam(boto_config) + for key in access_keys: + print(key) + last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get("AccessKeyId")).get("AccessKeyLastUsed") + deactivate = False + + days_since_creation = (datetime.now() - str_time_to_datetime(key.get("CreateDate"))).days + last_used_days = (datetime.now() - str_time_to_datetime(last_used.get("LastUsedDate"))).days + + print(f'Key {key.get("AccessKeyId")} is {days_since_creation} days old and last used {last_used_days} days ago') + + if days_since_creation > max_credential_usage_age: + deactivate = True + + if last_used_days > max_credential_usage_age: + deactivate = True + + if deactivate: + deactivate_key(user_name, key.get("AccessKeyId")) + +def deactivate_key(user_name, access_key): + iam_client = connect_to_iam(boto_config) + responses["DeactivateUnusedKeysResponse"].append({"AccessKeyId": access_key, "Response": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status="Inactive")}) + +def verify_expired_credentials_revoked(responses, user_name): + if responses.get("DeactivateUnusedKeysResponse"): + for key in responses.get("DeactivateUnusedKeysResponse"): + key_data = next(filter(lambda x: x.get("AccessKeyId") == key.get("AccessKeyId"), list_access_keys(user_name, True))) + if key_data.get("Status") != "Inactive": + error_message = "VERIFICATION FAILED. ACCESS KEY {} NOT DEACTIVATED".format(key_data.get("AccessKeyId")) + raise Exception(error_message) + + return { + "output": "Verification of unrotated access keys is successful.", + "http_responses": responses + } + +def unrotated_key_handler(event, context): + user_name = get_user_name(event.get("IAMResourceId")) + max_credential_usage_age = int(event.get("MaxCredentialUsageAge")) + access_keys = list_access_keys(user_name) + deactivate_unused_keys(access_keys, max_credential_usage_age, user_name) + return verify_expired_credentials_revoked(responses, user_name) diff --git a/source/remediation_runbooks/scripts/SetSSLBucketPolicy.py b/source/remediation_runbooks/scripts/SetSSLBucketPolicy.py new file mode 100644 index 00000000..7d201780 --- /dev/null +++ b/source/remediation_runbooks/scripts/SetSSLBucketPolicy.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + +def connect_to_s3(): + return boto3.client('s3', config=boto_config) + +def policy_to_add(bucket): + return { + "Sid": "AllowSSLRequestsOnly", + "Action": "s3:*", + "Effect": "Deny", + "Resource": [ + f'arn:aws:s3:::{bucket}', + f'arn:aws:s3:::{bucket}/*' + ], + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Principal": "*" + } +def new_policy(): + return { + "Id": "BucketPolicy", + "Version": "2012-10-17", + "Statement": [] + } + +def add_ssl_bucket_policy(event, context): + bucket_name = event['bucket'] + account_id = event['accountid'] + s3 = connect_to_s3() + bucket_policy = {} + try: + existing_policy = s3.get_bucket_policy( + Bucket=bucket_name, + ExpectedBucketOwner=account_id + ) + bucket_policy = json.loads(existing_policy['Policy']) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # delivery channel already exists - return + if exception_type not in ["NoSuchBucketPolicy"]: + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR getting bucket policy for {bucket_name}: {str(e)}') + + if not bucket_policy: + bucket_policy = new_policy() + + print(f'Existing policy: {bucket_policy}') + bucket_policy['Statement'].append(policy_to_add(bucket_name)) + + try: + result = s3.put_bucket_policy( + Bucket=bucket_name, + Policy=json.dumps(bucket_policy, indent=4, default=str), + ExpectedBucketOwner=account_id + ) + print(result) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR putting bucket policy for {bucket_name}: {str(e)}') + + print(f'New policy: {bucket_policy}') diff --git a/source/remediation_runbooks/scripts/test/test_createlogmetricfilterandalarm.py b/source/remediation_runbooks/scripts/test/test_createlogmetricfilterandalarm.py index 46ae964e..77a79b5d 100644 --- a/source/remediation_runbooks/scripts/test/test_createlogmetricfilterandalarm.py +++ b/source/remediation_runbooks/scripts/test/test_createlogmetricfilterandalarm.py @@ -21,12 +21,12 @@ import pytest import CreateLogMetricFilterAndAlarm as logMetricAlarm +import CreateLogMetricFilterAndAlarm_createtopic as topicutil import unittest my_session = boto3.session.Session() my_region = my_session.region_name - def test_verify(mocker): event = { @@ -235,3 +235,52 @@ def test_put_metric_alarm_error(mocker): ) assert pytest_wrapped_exception.type == SystemExit cloudwatch_stubber.deactivate() + +def topic_event(): + return { + 'topic_name': 'sharr-test-topic', + 'kms_key_arn': 'arn:aws:kms:ap-northeast-1:111122223333:key/foobarbaz' + } + +def test_create_new_topic(mocker): + BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region + ) + ssm_client = botocore.session.get_session().create_client('ssm', config=BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + ssm_stubber.add_response( + 'put_parameter', + {}, + { + 'Name': '/Solutions/SO0111/SNS_Topic_CIS3.x', + 'Description': 'SNS Topic for AWS Config updates', + 'Type': 'String', + 'Overwrite': True, + 'Value': 'arn:aws:sns:us-east-1:111111111111:sharr-test-topic' + } + ) + ssm_stubber.activate() + + sns_client = botocore.session.get_session().create_client('sns', config=BOTO_CONFIG) + sns_stubber = Stubber(sns_client) + sns_stubber.add_response( + 'create_topic', + { + 'TopicArn': 'arn:aws:sns:us-east-1:111111111111:sharr-test-topic' + } + ) + sns_stubber.add_response( + 'set_topic_attributes', + {} + ) + sns_stubber.activate() + mocker.patch('CreateLogMetricFilterAndAlarm_createtopic.connect_to_ssm', return_value=ssm_client) + mocker.patch('CreateLogMetricFilterAndAlarm_createtopic.connect_to_sns', return_value=sns_client) + + assert topicutil.create_encrypted_topic(topic_event(), {}) == { + 'topic_arn': 'arn:aws:sns:us-east-1:111111111111:sharr-test-topic' + } + \ No newline at end of file diff --git a/source/remediation_runbooks/scripts/test/test_enablevpcflowlogs.py b/source/remediation_runbooks/scripts/test/test_enablevpcflowlogs.py index 6798b710..dee35e6b 100644 --- a/source/remediation_runbooks/scripts/test/test_enablevpcflowlogs.py +++ b/source/remediation_runbooks/scripts/test/test_enablevpcflowlogs.py @@ -54,7 +54,7 @@ def test_EnableVPCFlowLogs_success(mocker): 'FlowLogs': [ { "CreationTime": "2020-10-27T19:37:52.871000+00:00", - "DeliverLogsPermissionArn": f'arn:aws:iam::111122223333:role/{event["remediation_role"]}_{my_region}', + "DeliverLogsPermissionArn": f'arn:aws:iam::111111111111:role/{event["remediation_role"]}_{my_region}', "DeliverLogsStatus": "SUCCESS", "FlowLogId": "fl-0a3f6513bef12ff9a", "FlowLogStatus": "ACTIVE", @@ -174,7 +174,7 @@ def test_EnableVPCFlowLogs_loggroup_exists(mocker): 'FlowLogs': [ { "CreationTime": "2020-10-27T19:37:52.871000+00:00", - "DeliverLogsPermissionArn": f'arn:aws:iam::111122223333:role/{event["remediation_role"]}_{my_region}', + "DeliverLogsPermissionArn": f'arn:aws:iam::111111111111:role/{event["remediation_role"]}_{my_region}', "DeliverLogsStatus": "SUCCESS", "FlowLogId": "fl-0a3f6513bef12ff9a", "FlowLogStatus": "ACTIVE", diff --git a/source/remediation_runbooks/scripts/test/test_makeebssnapshotsprivate.py b/source/remediation_runbooks/scripts/test/test_makeebssnapshotsprivate.py index 47cd13ba..a89ac4e3 100644 --- a/source/remediation_runbooks/scripts/test/test_makeebssnapshotsprivate.py +++ b/source/remediation_runbooks/scripts/test/test_makeebssnapshotsprivate.py @@ -165,6 +165,14 @@ def snaplist(): ] } +snapids = [ + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345" +] + def test_get_snaps(mocker): event = { 'account_id': '111111111111', @@ -198,7 +206,7 @@ def test_get_snaps(mocker): ec2_stubber.activate() mocker.patch('GetPublicEBSSnapshots.connect_to_ec2', return_value=ec2) - assert getsnaps.get_public_snapshots(event, {}) == snaps['Snapshots'] + snaplist()['Snapshots'] + assert getsnaps.get_public_snapshots(event, {}) == snapids + snapids ec2_stubber.assert_no_pending_responses() ec2_stubber.deactivate() @@ -208,7 +216,7 @@ def test_get_snaps_testmode(mocker): 'testmode': True } - assert getsnaps.get_public_snapshots(event, {}) == snaplist()['Snapshots'] + assert getsnaps.get_public_snapshots(event, {}) == snapids def test_make_snaps_private(mocker): event = { @@ -219,8 +227,8 @@ def test_make_snaps_private(mocker): ec2 = botocore.session.get_session().create_client('ec2', config=BOTO_CONFIG) ec2_stubber = Stubber(ec2) - event['snapshots'] = snaplist()['Snapshots'] - for snaps in range(0, len(event['snapshots'])): + event['snapshots'] = snapids + for snaps in range(0, len(snapids)): ec2_stubber.add_response( 'modify_snapshot_attribute', {}, @@ -229,7 +237,7 @@ def test_make_snaps_private(mocker): 'CreateVolumePermission': { 'Remove': [{'Group': 'all'}] }, - 'SnapshotId': event['snapshots'][snaps]['SnapshotId'] + 'SnapshotId': snapids[snaps] } ) diff --git a/source/remediation_runbooks/scripts/test/test_makerdssnapshotprivate.py b/source/remediation_runbooks/scripts/test/test_makerdssnapshotprivate.py new file mode 100644 index 00000000..425adf6a --- /dev/null +++ b/source/remediation_runbooks/scripts/test/test_makerdssnapshotprivate.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import boto3 +import json +import botocore.session +from botocore.stub import Stubber, ANY +from botocore.config import Config +import pytest +from pytest_mock import mocker + +import MakeRDSSnapshotPrivate as remediate + +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region +) + +db_snap_event = { + 'DBSnapshotId': 'snap-111111111111', + 'DBSnapshotType': 'snapshot' +} + +cluster_snap_event = { + 'DBSnapshotId': 'snap-111111111111', + 'DBSnapshotType': 'cluster-snapshot' +} + +def test_make_clustersnap_private(mocker): + event = cluster_snap_event + rds = botocore.session.get_session().create_client('rds', config=BOTO_CONFIG) + rds_stubber = Stubber(rds) + rds_stubber.add_response( + 'modify_db_cluster_snapshot_attribute', + {} + ) + rds_stubber.activate() + mocker.patch('MakeRDSSnapshotPrivate.connect_to_rds', return_value=rds) + assert remediate.make_snapshot_private(event, {}) == { + "response": { + "message": "Snapshot snap-111111111111 permissions set to private", + "status": "Success" + } + } + rds_stubber.assert_no_pending_responses() + rds_stubber.deactivate() + +def test_make_db_private(mocker): + event = db_snap_event + rds = botocore.session.get_session().create_client('rds', config=BOTO_CONFIG) + rds_stubber = Stubber(rds) + rds_stubber.add_response( + 'modify_db_snapshot_attribute', + {} + ) + rds_stubber.activate() + mocker.patch('MakeRDSSnapshotPrivate.connect_to_rds', return_value=rds) + assert remediate.make_snapshot_private(event, {}) == { + "response": { + "message": "Snapshot snap-111111111111 permissions set to private", + "status": "Success" + } + } + rds_stubber.assert_no_pending_responses() + rds_stubber.deactivate() diff --git a/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py b/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py new file mode 100644 index 00000000..86b3ddcf --- /dev/null +++ b/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import boto3 +import botocore.session +from botocore.stub import Stubber +from botocore.config import Config +import pytest +from pytest_mock import mocker + +import RevokeUnrotatedKeys as remediation + +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region +) + +def iam_resource(): + return { + "resourceIdentifiers": [ + { + "resourceType": "AWS::IAM::User", + "resourceId": "AIDACKCEVSQ6C2EXAMPLE", + "resourceName": "someuser" + } + ] + } + +def event(): + return { + "IAMResourceId": "AIDACKCEVSQ6C2EXAMPLE", + "MaxCredentialUsageAge": "90" + } + +def access_keys(): + return { + "AccessKeyMetadata": [ + { + "UserName": "someuser", + "Status": "Active", + "CreateDate": "2015-05-22T14:43:16Z", + "AccessKeyId": "AKIAIOSFODNN7EXAMPLE" + }, + { + "UserName": "someuser", + "Status": "Active", + "CreateDate": "2032-09-15T15:20:04Z", + "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" + }, + { + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": "2017-10-15T15:20:04Z", + "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" + } + ] + } + +def updated_keys(): + return { + "AccessKeyMetadata": [ + { + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": "2015-05-22T14:43:16Z", + "AccessKeyId": "AKIAIOSFODNN7EXAMPLE" + }, + { + "UserName": "someuser", + "Status": "Active", + "CreateDate": "2032-09-15T15:20:04Z", + "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" + }, + { + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": "2017-10-15T15:20:04Z", + "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" + } + ] + } + +def last_accessed_key(id): + return { + "AKIAIOSFODNN7EXAMPLE": { + "UserName": "someuser", + "AccessKeyLastUsed": { + "Region": "N/A", + "ServiceName": "s3", + "LastUsedDate": "2016-03-23T19:55:00Z" + } + }, + "AKIAI44QH8DHBEXAMPLE": { + "UserName": "someuser", + "AccessKeyLastUsed": { + "Region": "N/A", + "ServiceName": "s3", + "LastUsedDate": "2032-10-01T19:55:00Z" + } + } + }[id] + +def successful(): + return { + 'http_responses': { + 'DeactivateUnusedKeysResponse': [ + { + 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE', + 'Response': { + 'ResponseMetadata': { + 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE' + } + } + } + ] + }, + 'output': 'Verification of unrotated access keys is successful.' + } + +#===================================================================================== +# SUCCESS +#===================================================================================== +def test_success(mocker): + + ### Clients + cfg_client = botocore.session.get_session().create_client('config', config=BOTO_CONFIG) + cfg_stubber = Stubber(cfg_client) + + cfg_stubber.add_response( + 'list_discovered_resources', + iam_resource(), + { + 'resourceType': 'AWS::IAM::User', + 'resourceIds': ['AIDACKCEVSQ6C2EXAMPLE'] + } + ) + + cfg_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'list_access_keys', + access_keys(), + { + 'UserName': 'someuser' + } + ) + + iam_stubber.add_response( + 'get_access_key_last_used', + last_accessed_key("AKIAIOSFODNN7EXAMPLE"), + { + 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE' + } + ) + + iam_stubber.add_response( + 'update_access_key', + { + "ResponseMetadata": { + "AccessKeyId": "AKIAIOSFODNN7EXAMPLE" + } + }, + { + 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE', + 'UserName': 'someuser', + 'Status': 'Inactive' + } + ) + + iam_stubber.add_response( + 'get_access_key_last_used', + last_accessed_key("AKIAI44QH8DHBEXAMPLE"), + { + 'AccessKeyId': 'AKIAI44QH8DHBEXAMPLE' + } + ) + + iam_stubber.add_response( + 'list_access_keys', + updated_keys(), + { + 'UserName': 'someuser' + } + ) + + iam_stubber.activate() + + mocker.patch('RevokeUnrotatedKeys.connect_to_config', return_value=cfg_client) + mocker.patch('RevokeUnrotatedKeys.connect_to_iam', return_value=iam_client) + + assert remediation.unrotated_key_handler(event(), {}) == successful() + + cfg_stubber.deactivate() + iam_stubber.deactivate() diff --git a/source/remediation_runbooks/scripts/test/test_s3SslOnlyBucketPolicy.py b/source/remediation_runbooks/scripts/test/test_s3SslOnlyBucketPolicy.py new file mode 100644 index 00000000..ac600c6a --- /dev/null +++ b/source/remediation_runbooks/scripts/test/test_s3SslOnlyBucketPolicy.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import json +import boto3 +import botocore.session +from botocore.stub import Stubber +from botocore.config import Config +import pytest +from pytest_mock import mocker + +import SetSSLBucketPolicy as remediation + +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region +) + +def existing_policy(): + return { + "Version": "2008-10-17", + "Id": "ExistingBucketPolicy", + "Statement": [ + { + "Sid": "S3ReplicationPolicyStmt1", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root" + }, + "Action": [ + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:ReplicateObject", + "s3:ReplicateDelete" + ], + "Resource": [ + "arn:aws:s3:::abucket", + "arn:aws:s3:::abucket/*" + ] + } + ] + } + +def policy_to_add(): + return { + "Sid": "AllowSSLRequestsOnly", + "Action": "s3:*", + "Effect": "Deny", + "Resource": [ + "arn:aws:s3:::abucket", + "arn:aws:s3:::abucket/*" + ], + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Principal": "*" + } + +def new_policy_json(): + return { + "Id": "BucketPolicy", + "Version": "2012-10-17", + "Statement": [ + policy_to_add() + ] + } + +def response_metadata(): + return { + 'ResponseMetadata': { + 'RequestId': 'A6NCY16443JH271V', + 'HostId': 'vmM0qqMatvgqF2uRvfI79NWUbKaEZHHk49er2WIptAvH420Euq3Ac+cg+CXUEl9kFe3x49Cl/+I=', + 'HTTPStatusCode': 204, + 'HTTPHeaders': { + 'x-amz-id-2': 'vmM0qqMatvgqF2uRvfI79NWUbKaEZHHk49er2WIptAvH420Euq3Ac+cg+CXUEl9kFe3x49Cl/+I=', + 'x-amz-request-id': 'A6NCY16443JH271V', + 'date': 'Wed, 20 Oct 2021 17:40:32 GMT', + 'server': 'AmazonS3' + }, + 'RetryAttempts': + 0 + } + } + +def event(): + return { + 'bucket': 'abucket', + 'accountid': '111111111111' + } + +def test_new_policy(mocker): + s3_client = botocore.session.get_session().create_client('s3', config=BOTO_CONFIG) + s3_stubber = Stubber(s3_client) + s3_stubber.add_client_error( + 'get_bucket_policy', + service_error_code='NoSuchBucketPolicy', + expected_params={ + 'Bucket': 'abucket', + 'ExpectedBucketOwner': '111111111111' + } + ) + s3_stubber.add_response( + 'put_bucket_policy', + response_metadata(), + expected_params={ + 'Bucket': 'abucket', + 'Policy': json.dumps(new_policy_json(), indent=4), + 'ExpectedBucketOwner': '111111111111' + } + ) + s3_stubber.activate() + mocker.patch('SetSSLBucketPolicy.connect_to_s3', return_value=s3_client) + assert remediation.add_ssl_bucket_policy(event(), {}) == None + s3_stubber.deactivate() + +def test_add_to_policy(mocker): + s3_client = botocore.session.get_session().create_client('s3', config=BOTO_CONFIG) + s3_stubber = Stubber(s3_client) + s3_stubber.add_response( + 'get_bucket_policy', + { 'Policy': json.dumps(existing_policy()) }, + expected_params={ + 'Bucket': 'abucket', + 'ExpectedBucketOwner': '111111111111' + } + ) + new_policy = existing_policy() + new_policy['Statement'].append(policy_to_add()) + print(new_policy) + s3_stubber.add_response( + 'put_bucket_policy', + {}, + expected_params={ + 'Bucket': 'abucket', + 'Policy': json.dumps(new_policy, indent=4), + 'ExpectedBucketOwner': '111111111111' + } + ) + s3_stubber.activate() + mocker.patch('SetSSLBucketPolicy.connect_to_s3', return_value=s3_client) + assert remediation.add_ssl_bucket_policy(event(), {}) == None + s3_stubber.deactivate() diff --git a/source/solution_deploy/bin/solution_deploy.ts b/source/solution_deploy/bin/solution_deploy.ts index 937f4452..a076ef5e 100644 --- a/source/solution_deploy/bin/solution_deploy.ts +++ b/source/solution_deploy/bin/solution_deploy.ts @@ -18,7 +18,7 @@ import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import { SolutionDeployStack } from '../lib/solution_deploy-stack'; import { MemberStack } from '../lib/sharr_member-stack'; -import { RemediationRunbookStack } from '../lib/remediation_runbook-stack'; +import { RemediationRunbookStack, MemberRoleStack } from '../lib/remediation_runbook-stack'; import { OrchLogStack } from '../lib/orchestrator-log-stack'; const SOLUTION_ID = process.env['SOLUTION_ID'] || 'unknown'; @@ -54,12 +54,22 @@ const memberStack = new MemberStack(app, 'MemberStack', { }); memberStack.templateOptions.templateFormatVersion = "2010-09-09" +const roleStack = new MemberRoleStack(app, 'MemberRoleStack', { + description: '(' + SOLUTION_ID + 'R) ' + SOLUTION_NAME + + ' Remediation Roles, ' + SOLUTION_VERSION, + solutionId: SOLUTION_ID, + solutionVersion: SOLUTION_VERSION, + solutionDistBucket: SOLUTION_BUCKET, +}); +roleStack.templateOptions.templateFormatVersion = "2010-09-09" + const runbookStack = new RemediationRunbookStack(app, 'RunbookStack', { description: '(' + SOLUTION_ID + 'R) ' + SOLUTION_NAME + ' Remediation Runbooks, ' + SOLUTION_VERSION, solutionId: SOLUTION_ID, solutionVersion: SOLUTION_VERSION, solutionDistBucket: SOLUTION_BUCKET, + roleStack: roleStack }); runbookStack.templateOptions.templateFormatVersion = "2010-09-09" diff --git a/source/solution_deploy/lib/orchestrator-log-stack.ts b/source/solution_deploy/lib/orchestrator-log-stack.ts index b73e04e9..9e0971cb 100644 --- a/source/solution_deploy/lib/orchestrator-log-stack.ts +++ b/source/solution_deploy/lib/orchestrator-log-stack.ts @@ -16,8 +16,7 @@ import * as cdk from '@aws-cdk/core'; import { LogGroup, CfnLogGroup, RetentionDays } from '@aws-cdk/aws-logs'; -import { Key, IKey } from '@aws-cdk/aws-kms'; -import { StringParameter } from '@aws-cdk/aws-ssm'; +import { Key } from '@aws-cdk/aws-kms'; export interface OrchLogStackProps extends cdk.StackProps { description: string; diff --git a/source/solution_deploy/lib/remediation_runbook-stack.ts b/source/solution_deploy/lib/remediation_runbook-stack.ts index 4300464a..78642da7 100644 --- a/source/solution_deploy/lib/remediation_runbook-stack.ts +++ b/source/solution_deploy/lib/remediation_runbook-stack.ts @@ -29,24 +29,52 @@ import { CfnPolicy, CfnRole } from '@aws-cdk/aws-iam'; -// import { Key } from '@aws-cdk/aws-kms'; -// import { StringParameter } from '@aws-cdk/aws-ssm'; +import { OrchestratorMemberRole } from '../../lib/orchestrator_roles-construct' import { SsmRemediationRunbook, SsmRole } from '../../lib/ssmplaybook'; import { AdminAccountParm } from '../../lib/admin_account_parm-construct'; import { Rds6EnhancedMonitoringRole } from '../../remediation_runbooks/rds6-remediation-resources'; +export interface MemberRoleStackProps { + readonly description: string; + readonly solutionId: string; + readonly solutionVersion: string; + readonly solutionDistBucket: string; +} + +export class MemberRoleStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: MemberRoleStackProps) { + super(scope, id, props); + /******************** + ** Parameters + ********************/ + const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name + const adminRoleName = `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin` + + const adminAccount = new AdminAccountParm(this, 'AdminAccountParameter', { + solutionId: props.solutionId + }) + new OrchestratorMemberRole(this, 'OrchestratorMemberRole', { + solutionId: props.solutionId, + adminAccountId: adminAccount.adminAccountNumber.valueAsString, + adminRoleName: adminRoleName + }) + } +} + export interface StackProps { - description: string; - solutionId: string; - solutionVersion: string; - solutionDistBucket: string; + readonly description: string; + readonly solutionId: string; + readonly solutionVersion: string; + readonly solutionDistBucket: string; ssmdocs?: string; + roleStack: MemberRoleStack; } export class RemediationRunbookStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: StackProps) { super(scope, id, props); + let ssmdocs = '' if (props.ssmdocs == undefined) { ssmdocs = '../remediation_runbooks' @@ -55,19 +83,14 @@ export class RemediationRunbookStack extends cdk.Stack { } const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name - - const adminAccount = new AdminAccountParm(this, 'AdminAccountParameter', { - solutionId: props.solutionId - }) - // const adminRoleName = `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin_${this.region}` const remediationRoleNameBase = `${RESOURCE_PREFIX}-` - + //----------------------- // CreateCloudTrailMultiRegionTrail // { const remediationName = 'CreateCloudTrailMultiRegionTrail' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const cloudtrailPerms = new PolicyStatement(); cloudtrailPerms.addActions( "cloudtrail:CreateTrail", @@ -93,13 +116,11 @@ export class RemediationRunbookStack extends cdk.Stack { ); inlinePolicy.addStatements(s3Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - // adminRoleName: adminRoleName, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -131,7 +152,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'CreateLogMetricFilterAndAlarm' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( @@ -152,17 +173,16 @@ export class RemediationRunbookStack extends cdk.Stack { ) snsPerms.effect = Effect.ALLOW snsPerms.addResources( - `arn:${this.partition}:sns:${this.region}:${this.account}:SO0111-SHARR-LocalAlarmNotification` + `arn:${this.partition}:sns:*:${this.account}:SO0111-SHARR-LocalAlarmNotification` ); inlinePolicy.addStatements(snsPerms) } - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -179,7 +199,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableAutoScalingGroupELBHealthCheck' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const asPerms = new PolicyStatement(); asPerms.addActions( "autoscaling:UpdateAutoScalingGroup", @@ -189,13 +209,11 @@ export class RemediationRunbookStack extends cdk.Stack { asPerms.addResources("*"); inlinePolicy.addStatements(asPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - // adminRoleName: adminRoleName, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -223,10 +241,10 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableAWSConfig' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); { - var iamPerms = new PolicyStatement(); + let iamPerms = new PolicyStatement(); iamPerms.addActions( "iam:GetRole", "iam:PassRole" @@ -234,19 +252,19 @@ export class RemediationRunbookStack extends cdk.Stack { iamPerms.effect = Effect.ALLOW iamPerms.addResources( `arn:${this.partition}:iam::${this.account}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig`, - `arn:${this.partition}:iam::${this.account}:role/SO0111-CreateAccessLoggingBucket_*` + `arn:${this.partition}:iam::${this.account}:role/SO0111-CreateAccessLoggingBucket` ); inlinePolicy.addStatements(iamPerms) } { - var snsPerms = new PolicyStatement(); + let snsPerms = new PolicyStatement(); snsPerms.addActions( "sns:CreateTopic", "sns:SetTopicAttributes" ) snsPerms.effect = Effect.ALLOW snsPerms.addResources( - `arn:${this.partition}:sns:${this.region}:${this.account}:SO0111-SHARR-AWSConfigNotification` + `arn:${this.partition}:sns:*:${this.account}:SO0111-SHARR-AWSConfigNotification` ); inlinePolicy.addStatements(snsPerms) } @@ -257,7 +275,7 @@ export class RemediationRunbookStack extends cdk.Stack { ) ssmPerms.effect = Effect.ALLOW ssmPerms.addResources( - `arn:${this.partition}:ssm:${this.region}:${this.account}:automation-definition/SHARR-CreateAccessLoggingBucket:*` + `arn:${this.partition}:ssm:*:${this.account}:automation-definition/SHARR-CreateAccessLoggingBucket:*` ); inlinePolicy.addStatements(ssmPerms) } @@ -292,13 +310,11 @@ export class RemediationRunbookStack extends cdk.Stack { ); inlinePolicy.addStatements(s3Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - // adminRoleName: adminRoleName, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -326,7 +342,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableCloudTrailToCloudWatchLogging' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); // Role for CT->CW logging const ctcw_remediation_policy_statement_1 = new PolicyStatement() @@ -347,13 +363,14 @@ export class RemediationRunbookStack extends cdk.Stack { ctcw_remediation_policy_doc.addStatements(ctcw_remediation_policy_statement_1) ctcw_remediation_policy_doc.addStatements(ctcw_remediation_policy_statement_2) - const ctcw_remediation_role = new Role(this, 'ctcwremediationrole', { + const ctcw_remediation_role = new Role(props.roleStack, 'ctcwremediationrole', { assumedBy: new ServicePrincipal(`cloudtrail.${this.urlSuffix}`), inlinePolicies: { 'default_lambdaPolicy': ctcw_remediation_policy_doc }, - roleName: RESOURCE_PREFIX + '-CloudTrailToCloudWatchLogs_' + this.region + roleName: `${RESOURCE_PREFIX}-CloudTrailToCloudWatchLogs` }); + ctcw_remediation_role.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) { let childToMod = ctcw_remediation_role.node.findChild('Resource') as CfnRole; childToMod.cfnOptions.metadata = { @@ -396,12 +413,11 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(ctcwlogs) } - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR ' + remediationName, { @@ -432,7 +448,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableCloudTrailEncryption' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const cloudtrailPerms = new PolicyStatement(); cloudtrailPerms.addActions("cloudtrail:UpdateTrail") @@ -440,12 +456,11 @@ export class RemediationRunbookStack extends cdk.Stack { cloudtrailPerms.addResources("*"); inlinePolicy.addStatements(cloudtrailPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -478,7 +493,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableVPCFlowLogs' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); { let remediationPerms = new PolicyStatement(); @@ -492,7 +507,6 @@ export class RemediationRunbookStack extends cdk.Stack { ); inlinePolicy.addStatements(remediationPerms) } - { let iamPerms = new PolicyStatement() iamPerms.addActions( @@ -500,11 +514,21 @@ export class RemediationRunbookStack extends cdk.Stack { ) iamPerms.effect = Effect.ALLOW iamPerms.addResources( - `arn:${this.partition}:iam::${this.account}:role/${RESOURCE_PREFIX}-${remediationName}-remediationRole_${this.region}` + `arn:${this.partition}:iam::${this.account}:role/${RESOURCE_PREFIX}-${remediationName}-remediationRole` ); inlinePolicy.addStatements(iamPerms) } - + { + let ssmPerms = new PolicyStatement() + ssmPerms.addActions( + "ssm:GetParameter" + ) + ssmPerms.effect = Effect.ALLOW + ssmPerms.addResources( + `arn:${this.partition}:ssm:*:${this.account}:parameter/${RESOURCE_PREFIX}/CMK_REMEDIATION_ARN` + ); + inlinePolicy.addStatements(ssmPerms) + } { let validationPerms = new PolicyStatement() validationPerms.addActions( @@ -517,7 +541,7 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(validationPerms) } - // Remediation Role + // Remediation Role - used in the remediation const remediation_policy = new PolicyStatement() remediation_policy.effect = Effect.ALLOW remediation_policy.addActions( @@ -532,13 +556,14 @@ export class RemediationRunbookStack extends cdk.Stack { const remediation_doc = new PolicyDocument() remediation_doc.addStatements(remediation_policy) - const remediation_role = new Role(this, 'EnableVPCFlowLogs-remediationrole', { + const remediation_role = new Role(props.roleStack, 'EnableVPCFlowLogs-remediationrole', { assumedBy: new ServicePrincipal('vpc-flow-logs.amazonaws.com'), inlinePolicies: { 'default_lambdaPolicy': remediation_doc }, - roleName: `${RESOURCE_PREFIX}-EnableVPCFlowLogs-remediationRole_${this.region}` + roleName: `${RESOURCE_PREFIX}-EnableVPCFlowLogs-remediationRole` }); + remediation_role.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) const roleResource = remediation_role.node.findChild('Resource') as CfnRole; @@ -554,12 +579,11 @@ export class RemediationRunbookStack extends cdk.Stack { } }; - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -587,7 +611,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'CreateAccessLoggingBucket' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const s3Perms = new PolicyStatement(); s3Perms.addActions( "s3:CreateBucket", @@ -599,12 +623,11 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(s3Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -632,7 +655,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'MakeEBSSnapshotsPrivate' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const ec2Perms = new PolicyStatement(); ec2Perms.addActions( "ec2:ModifySnapshotAttribute", @@ -642,12 +665,11 @@ export class RemediationRunbookStack extends cdk.Stack { ec2Perms.addResources("*"); inlinePolicy.addStatements(ec2Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -678,7 +700,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'MakeRDSSnapshotPrivate' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPerms = new PolicyStatement(); remediationPerms.addActions( "rds:ModifyDBSnapshotAttribute", @@ -688,12 +710,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPerms.addResources("*"); inlinePolicy.addStatements(remediationPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -724,7 +745,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'RemoveLambdaPublicAccess' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const lambdaPerms = new PolicyStatement(); lambdaPerms.addActions( @@ -735,12 +756,11 @@ export class RemediationRunbookStack extends cdk.Stack { lambdaPerms.addResources('*') inlinePolicy.addStatements(lambdaPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -765,7 +785,106 @@ export class RemediationRunbookStack extends cdk.Stack { } } } + + //----------------------- + // RevokeUnrotatedKeys + // + { + const remediationName = 'RevokeUnrotatedKeys' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + "iam:UpdateAccessKey", + "iam:ListAccessKeys", + "iam:GetAccessKeyLastUsed", + "iam:GetUser" + ); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources( + "arn:" + this.partition + ":iam::" + this.account + ":user/*" + ); + inlinePolicy.addStatements(remediationPolicy) + + const cfgPerms = new PolicyStatement(); + cfgPerms.addActions( + "config:ListDiscoveredResources" + ) + cfgPerms.effect = Effect.ALLOW + cfgPerms.addResources("*") + inlinePolicy.addStatements(cfgPerms) + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket + }) + + let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; + childToMod.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W12', + reason: 'Resource * is required for to allow remediation for any resource.' + }] + } + } + } + //----------------------- + // SetSSLBucketPolicy + // + { + const remediationName = 'SetSSLBucketPolicy' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + { + let remediationPerms = new PolicyStatement(); + remediationPerms.addActions( + "s3:GetBucketPolicy", + "s3:PutBucketPolicy" + ) + remediationPerms.effect = Effect.ALLOW + remediationPerms.addResources("*"); + inlinePolicy.addStatements(remediationPerms) + } + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; + childToMod.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W12', + reason: 'Resource * is required for to allow remediation for *any* resource.' + }] + } + } + + new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket + }) + } + //========================================================================= // The following are permissions only for use with AWS-owned documents that // are available to GovCloud and China partition customers. @@ -775,7 +894,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'ConfigureS3BucketLogging' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const s3Perms = new PolicyStatement(); s3Perms.addActions( @@ -789,13 +908,11 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(s3Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - // adminRoleName: adminRoleName, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -813,7 +930,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'DisablePublicAccessForSecurityGroup' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPermsEc2 = new PolicyStatement(); remediationPermsEc2.addActions( @@ -828,12 +945,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPermsEc2.addResources("*"); inlinePolicy.addStatements(remediationPermsEc2) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -857,7 +973,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'ConfigureS3BucketPublicAccessBlock' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( @@ -868,12 +984,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPolicy.addResources("*") inlinePolicy.addStatements(remediationPolicy) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -900,7 +1015,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'ConfigureS3PublicAccessBlock' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( @@ -911,12 +1026,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPolicy.addResources("*") inlinePolicy.addStatements(remediationPolicy) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -943,7 +1057,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableCloudTrailLogFileValidation' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( "cloudtrail:UpdateTrail", @@ -955,12 +1069,11 @@ export class RemediationRunbookStack extends cdk.Stack { ); inlinePolicy.addStatements(remediationPolicy) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -978,7 +1091,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableEbsEncryptionByDefault' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const ec2Perms = new PolicyStatement(); ec2Perms.addActions( "ec2:EnableEBSEncryptionByDefault", @@ -988,13 +1101,11 @@ export class RemediationRunbookStack extends cdk.Stack { ec2Perms.addResources("*"); inlinePolicy.addStatements(ec2Perms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - // adminRoleName: adminRoleName, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1024,7 +1135,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableEnhancedMonitoringOnRDSInstance' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); { let iamPerms = new PolicyStatement() iamPerms.addActions( @@ -1033,7 +1144,7 @@ export class RemediationRunbookStack extends cdk.Stack { ) iamPerms.effect = Effect.ALLOW iamPerms.addResources( - `arn:${this.partition}:iam::${this.account}:role/${RESOURCE_PREFIX}-RDSMonitoring-remediationRole_${this.region}` + `arn:${this.partition}:iam::${this.account}:role/${RESOURCE_PREFIX}-RDSMonitoring-remediationRole` ); inlinePolicy.addStatements(iamPerms) } @@ -1048,12 +1159,11 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(rdsPerms) } - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1078,8 +1188,8 @@ export class RemediationRunbookStack extends cdk.Stack { } } - new Rds6EnhancedMonitoringRole(this, 'Rds6EnhancedMonitoringRole', { - roleName: `${RESOURCE_PREFIX}-RDSMonitoring-remediationRole_${this.region}` + new Rds6EnhancedMonitoringRole(props.roleStack, 'Rds6EnhancedMonitoringRole', { + roleName: `${RESOURCE_PREFIX}-RDSMonitoring-remediationRole` }) } //----------------------- @@ -1087,7 +1197,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableKeyRotation' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPerms = new PolicyStatement(); remediationPerms.addActions( "kms:EnableKeyRotation", @@ -1097,12 +1207,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPerms.addResources("*"); inlinePolicy.addStatements(remediationPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1132,7 +1241,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'EnableRDSClusterDeletionProtection' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const iamPerms = new PolicyStatement(); iamPerms.addActions("iam:GetRole") @@ -1157,12 +1266,11 @@ export class RemediationRunbookStack extends cdk.Stack { rdsPerms.addResources("*"); inlinePolicy.addStatements(rdsPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1192,7 +1300,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'RemoveVPCDefaultSecurityGroupRules' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy1 = new PolicyStatement(); remediationPolicy1.addActions( @@ -1214,12 +1322,11 @@ export class RemediationRunbookStack extends cdk.Stack { inlinePolicy.addStatements(remediationPolicy1, remediationPolicy2) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1249,7 +1356,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'RevokeUnusedIAMUserCredentials' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( "iam:UpdateAccessKey", @@ -1273,12 +1380,11 @@ export class RemediationRunbookStack extends cdk.Stack { cfgPerms.addResources("*") inlinePolicy.addStatements(cfgPerms) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { @@ -1305,7 +1411,7 @@ export class RemediationRunbookStack extends cdk.Stack { // { const remediationName = 'SetIAMPasswordPolicy' - const inlinePolicy = new Policy(this, `SHARR-Remediation-Policy-${remediationName}`); + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( @@ -1319,12 +1425,11 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPolicy.addResources("*") inlinePolicy.addStatements(remediationPolicy) - new SsmRole(this, 'RemediationRole ' + remediationName, { + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, - adminAccountNumber: adminAccount.adminAccountNumber.valueAsString, remediationPolicy: inlinePolicy, - remediationRoleName: `${remediationRoleNameBase}${remediationName}_${this.region}` + remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { diff --git a/source/solution_deploy/lib/sharr_member-stack.ts b/source/solution_deploy/lib/sharr_member-stack.ts index 5c35b2f6..3b05849e 100644 --- a/source/solution_deploy/lib/sharr_member-stack.ts +++ b/source/solution_deploy/lib/sharr_member-stack.ts @@ -17,31 +17,33 @@ import * as cdk from '@aws-cdk/core'; import * as fs from 'fs'; import { AdminAccountParm } from '../../lib/admin_account_parm-construct'; +import { StringParameter } from '@aws-cdk/aws-ssm'; +import { Key } from '@aws-cdk/aws-kms'; import { PolicyStatement, Effect, - Role, PolicyDocument, - ArnPrincipal, - ServicePrincipal, - CompositePrincipal, - CfnRole + ServicePrincipal } from '@aws-cdk/aws-iam'; -import { Key } from '@aws-cdk/aws-kms'; -import { StringParameter } from '@aws-cdk/aws-ssm'; -export interface OrchRoleProps { +export interface SolutionProps { + description: string; solutionId: string; - adminAccountId: string; - adminRoleName: string; + solutionDistBucket: string; + solutionTMN: string; + solutionVersion: string; } -export class OrchestratorMemberRole extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: OrchRoleProps) { - super(scope, id); - const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name +export class MemberStack extends cdk.Stack { + + constructor(scope: cdk.App, id: string, props: SolutionProps) { + super(scope, id, props); const stack = cdk.Stack.of(this); - const memberPolicy = new PolicyDocument(); + const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name + + const adminAccount = new AdminAccountParm(this, 'AdminAccountParameter', { + solutionId: props.solutionId + }) //-------------------------- // KMS Customer Managed Key @@ -65,9 +67,10 @@ export class OrchestratorMemberRole extends cdk.Construct { kmsPerms.addResources("*") // Only the key the policydocument is attached to kmsPerms.addPrincipals(new ServicePrincipal('sns.amazonaws.com')) kmsPerms.addPrincipals(new ServicePrincipal('s3.amazonaws.com')) + kmsPerms.addPrincipals(new ServicePrincipal(`logs.${stack.urlSuffix}`)) kmsPerms.addPrincipals(new ServicePrincipal(`logs.${stack.region}.${stack.urlSuffix}`)) kmsPerms.addPrincipals(new ServicePrincipal(`cloudtrail.${stack.urlSuffix}`)) - kmsPerms.addPrincipals(new ServicePrincipal(`cloudwatch.${stack.urlSuffix}`)) + kmsPerms.addPrincipals(new ServicePrincipal('cloudwatch.amazonaws.com')) kmsKeyPolicy.addStatements(kmsPerms) const kmsKey:Key = new Key(this, 'SHARR Remediation Key', { @@ -83,135 +86,6 @@ export class OrchestratorMemberRole extends cdk.Construct { stringValue: kmsKey.keyArn }); - /** - * @description Cross-account permissions for Orchestration role - * @type {PolicyStatement} - */ - const iamPerms = new PolicyStatement(); - iamPerms.addActions( - "iam:PassRole", - "iam:GetRole" - ) - iamPerms.effect = Effect.ALLOW - iamPerms.addResources( - `arn:${stack.partition}:iam::${stack.account}:role/${RESOURCE_PREFIX}-*` - ); - memberPolicy.addStatements(iamPerms) - const ssmROPerms = new PolicyStatement() - ssmROPerms.addActions( - "ssm:DescribeAutomationExecutions", - "ssm:DescribeDocument", - "ssm:GetParameters" - ) - ssmROPerms.effect = Effect.ALLOW; - ssmROPerms.addResources( - "arn:" + stack.partition + ":ssm:" + stack.region + ":*:*" - ) - memberPolicy.addStatements(ssmROPerms) - - const ssmRWPerms = new PolicyStatement() - ssmRWPerms.addActions( - "ssm:StartAutomationExecution", - "ssm:GetAutomationExecution" - ) - ssmRWPerms.addResources( - stack.formatArn({ - service: 'ssm', - resource: 'document', - resourceName: 'SHARR-*', - sep: '/' - }), - stack.formatArn({ - service: 'ssm', - resource: 'automation-definition', - resourceName: '*', - sep: '/' - }), - stack.formatArn({ - service: 'ssm', - resource: 'automation-definition', - account:'', - resourceName: '*', - sep: '/' - }), - stack.formatArn({ - service: 'ssm', - resource: 'automation-execution', - resourceName: '*', - sep: '/' - }) - ); - memberPolicy.addStatements(ssmRWPerms) - - const sechubPerms = new PolicyStatement(); - sechubPerms.addActions("cloudwatch:PutMetricData") - sechubPerms.addActions("securityhub:BatchUpdateFindings") - sechubPerms.effect = Effect.ALLOW - sechubPerms.addResources("*") - - memberPolicy.addStatements(sechubPerms) - - let principalPolicyStatement = new PolicyStatement(); - - principalPolicyStatement.addActions("sts:AssumeRole"); - principalPolicyStatement.effect = Effect.ALLOW; - - let roleprincipal = new ArnPrincipal( - 'arn:' + stack.partition + ':iam::' + props.adminAccountId + ':role/' + - props.adminRoleName - ); - - let principals = new CompositePrincipal(roleprincipal); - principals.addToPolicy(principalPolicyStatement); - - let serviceprincipal = new ServicePrincipal('ssm.amazonaws.com') - principals.addPrincipals(serviceprincipal); - - let memberRole = new Role(this, 'MemberAccountRole', { - assumedBy: principals, - inlinePolicies: { - 'member_orchestrator': memberPolicy - }, - roleName: RESOURCE_PREFIX + '-SHARR-Orchestrator-Member_' + stack.region - }); - - const memberRoleResource = memberRole.node.findChild('Resource') as CfnRole; - - memberRoleResource.cfnOptions.metadata = { - cfn_nag: { - rules_to_suppress: [{ - id: 'W11', - reason: 'Resource * is required due to the administrative nature of the solution.' - },{ - id: 'W28', - reason: 'Static names chosen intentionally to provide integration in cross-account permissions' - }] - } - } - } -} - -export interface SolutionProps { - description: string; - solutionId: string; - solutionDistBucket: string; - solutionTMN: string; - solutionVersion: string; -} - -export class MemberStack extends cdk.Stack { - - constructor(scope: cdk.App, id: string, props: SolutionProps) { - super(scope, id, props); - - const stack = cdk.Stack.of(this); - const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name - const adminRoleName = RESOURCE_PREFIX + '-SHARR-Orchestrator-Admin_' + stack.region - - const adminAccount = new AdminAccountParm(this, 'AdminAccountParameter', { - solutionId: props.solutionId - }) - /******************** ** Parameters ********************/ @@ -222,26 +96,6 @@ export class MemberStack extends cdk.Stack { description: "Name of the log group to be used to create metric filters and cloudwatch alarms. You must use a Log Group that is the the logging destination of a multi-region CloudTrail" }); - /******************** - ** Metadata - ********************/ - - stack.templateOptions.metadata = { - "AWS::CloudFormation::Interface": { - ParameterGroups: [ - { - Label: {default: "LogGroup Configuration"}, - Parameters: [logGroupName.logicalId] - } - ], - ParameterLabels: { - [logGroupName.logicalId]: { - default: "Provide the name of the LogGroup to be used to create Metric Filters and Alarms", - } - }, - }, - }; - /********************************************* ** Create SSM Parameter to store log group name *********************************************/ @@ -251,12 +105,6 @@ export class MemberStack extends cdk.Stack { stringValue: logGroupName.valueAsString }) - new OrchestratorMemberRole(this, 'OrchestratorMemberRole', { - solutionId: props.solutionId, - adminAccountId: adminAccount.adminAccountNumber.valueAsString, - adminRoleName: adminRoleName - }) - new cdk.CfnMapping(this, 'SourceCode', { mapping: { "General": { "S3Bucket": props.solutionDistBucket, @@ -267,26 +115,23 @@ export class MemberStack extends cdk.Stack { //------------------------------------------------------------------------- // Runbooks - shared automations // - new cdk.CfnStack(this, `RunbookStack`, { - parameters: { - 'SecHubAdminAccount': adminAccount.adminAccountNumber.valueAsString - }, + new cdk.CfnStack(this, `RunbookStackNoRoles`, { templateUrl: "https://" + cdk.Fn.findInMap("SourceCode", "General", "S3Bucket") + "-reference.s3.amazonaws.com/" + cdk.Fn.findInMap("SourceCode", "General", "KeyPrefix") + "/aws-sharr-remediations.template" }) + //------------------------------------------------------------------------- // Loop through all of the Playbooks and create a Product for each SET of playbooks // const PB_DIR = `${__dirname}/../../playbooks` - var ignore = ['.DS_Store', 'core', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK']; + var ignore = ['.DS_Store', 'core', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK', '.coverage']; let illegalChars = /[\._]/g; - + var list_of_playbooks: string[] = [] fs.readdir(PB_DIR, (err, items) => { items.forEach(file => { if (!ignore.includes(file)) { var template_file = `${file}MemberStack.template` - //--------------------------------------------------------------------- // Playbook Member Template Nested Stack // @@ -294,10 +139,11 @@ export class MemberStack extends cdk.Stack { let memberStackOption = new cdk.CfnParameter(this, `LoadMemberStack${parmname}`, { type: "String", description: `Load Playbook member stack for ${file}?`, - default: "no", + default: "yes", allowedValues: ["yes", "no"], }) memberStackOption.overrideLogicalId(`Load${parmname}MemberStack`) + list_of_playbooks.push(memberStackOption.logicalId) let memberStack = new cdk.CfnStack(this, `PlaybookMemberStack${file}`, { parameters: { @@ -315,5 +161,27 @@ export class MemberStack extends cdk.Stack { } }); }) + /******************** + ** Metadata + ********************/ + stack.templateOptions.metadata = { + "AWS::CloudFormation::Interface": { + ParameterGroups: [ + { + Label: {default: "LogGroup Configuration"}, + Parameters: [logGroupName.logicalId] + }, + { + Label: {default: "Playbooks"}, + Parameters: list_of_playbooks + } + ], + ParameterLabels: { + [logGroupName.logicalId]: { + default: "Provide the name of the LogGroup to be used to create Metric Filters and Alarms", + } + } + } + }; } } diff --git a/source/solution_deploy/lib/solution_deploy-stack.ts b/source/solution_deploy/lib/solution_deploy-stack.ts index 162b999c..6ad0b5f7 100644 --- a/source/solution_deploy/lib/solution_deploy-stack.ts +++ b/source/solution_deploy/lib/solution_deploy-stack.ts @@ -14,7 +14,6 @@ *****************************************************************************/ import * as cdk from '@aws-cdk/core'; -import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -22,6 +21,10 @@ import { StringParameter, CfnParameter } from '@aws-cdk/aws-ssm'; import * as kms from '@aws-cdk/aws-kms'; import * as fs from 'fs'; import { + Role, + CfnRole, + Policy, + CfnPolicy, PolicyStatement, PolicyDocument, ServicePrincipal, @@ -177,12 +180,12 @@ export class SolutionDeployStack extends cdk.Stack { /** * @description Policy for role used by common Orchestrator Lambdas - * @type {iam.Policy} + * @type {Policy} */ - const orchestratorPolicy = new iam.Policy(this, 'orchestratorPolicy', { + const orchestratorPolicy = new Policy(this, 'orchestratorPolicy', { policyName: RESOURCE_PREFIX + '-SHARR_Orchestrator', statements: [ - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'logs:CreateLogGroup', 'logs:CreateLogStream', @@ -190,7 +193,7 @@ export class SolutionDeployStack extends cdk.Stack { ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'ssm:GetParameter', 'ssm:GetParameters', @@ -198,13 +201,12 @@ export class SolutionDeployStack extends cdk.Stack { ], resources: [`arn:${this.partition}:ssm:*:${this.account}:parameter/Solutions/SO0111/*`] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'sts:AssumeRole' ], resources: [ - 'arn:' + this.partition + ':iam::*:role/' + RESOURCE_PREFIX + - '-SHARR-Orchestrator-Member_' + this.region, + `arn:${this.partition}:iam::*:role/${RESOURCE_PREFIX}-SHARR-Orchestrator-Member`, 'arn:' + this.partition + ':iam::*:role/' + RESOURCE_PREFIX + '-Remediate-*', ] @@ -213,7 +215,7 @@ export class SolutionDeployStack extends cdk.Stack { }) { - let childToMod = orchestratorPolicy.node.findChild('Resource') as iam.CfnPolicy; + let childToMod = orchestratorPolicy.node.findChild('Resource') as CfnPolicy; childToMod.cfnOptions.metadata = { cfn_nag: { rules_to_suppress: [{ @@ -226,19 +228,19 @@ export class SolutionDeployStack extends cdk.Stack { /** * @description Role used by common Orchestrator Lambdas - * @type {iam.Role} + * @type {Role} */ - const orchestratorRole = new iam.Role(this, 'orchestratorRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + const orchestratorRole = new Role(this, 'orchestratorRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda role to allow cross account read-only SHARR orchestrator functions', - roleName: RESOURCE_PREFIX + '-SHARR-Orchestrator-Admin_' + this.region + roleName: `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin` }); orchestratorRole.attachInlinePolicy(orchestratorPolicy); { - let childToMod = orchestratorRole.node.findChild('Resource') as iam.CfnRole; + let childToMod = orchestratorRole.node.findChild('Resource') as CfnRole; childToMod.cfnOptions.metadata = { cfn_nag: { rules_to_suppress: [{ @@ -297,6 +299,53 @@ export class SolutionDeployStack extends cdk.Stack { }; } + /** + * @description getApprovalRequirement - determine whether manual approval is required + * @type {lambda.Function} + */ + const getApprovalRequirement = new lambda.Function(this, 'getApprovalRequirement', { + functionName: RESOURCE_PREFIX + '-SHARR-getApprovalRequirement', + handler: 'get_approval_requirement.lambda_handler', + runtime: props.runtimePython, + description: 'Determines if a manual approval is required for remediation', + code: lambda.Code.fromBucket( + SolutionsBucket, + props.solutionTMN + '/' + props.solutionVersion + '/lambda/get_approval_requirement.py.zip' + ), + environment: { + log_level: 'info', + AWS_PARTITION: this.partition, + SOLUTION_ID: props.solutionId, + SOLUTION_VERSION: props.solutionVersion, + WORKFLOW_RUNBOOK: '' + }, + memorySize: 256, + timeout: cdk.Duration.seconds(600), + role: orchestratorRole, + layers: [sharrLambdaLayer] + }); + + { + const childToMod = getApprovalRequirement.node.findChild('Resource') as lambda.CfnFunction; + + childToMod.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W58', + reason: 'False positive. Access is provided via a policy' + },{ + id: 'W89', + reason: 'There is no need to run this lambda in a VPC' + }, + { + id: 'W92', + reason: 'There is no need for Reserved Concurrency' + }] + } + }; + } + + /** * @description execAutomation - initiate an SSM automation document in a target account * @type {lambda.Function} @@ -389,12 +438,12 @@ export class SolutionDeployStack extends cdk.Stack { /** * @description Policy for role used by common Orchestrator notification lambda - * @type {iam.Policy} + * @type {Policy} */ - const notifyPolicy = new iam.Policy(this, 'notifyPolicy', { + const notifyPolicy = new Policy(this, 'notifyPolicy', { policyName: RESOURCE_PREFIX + '-SHARR_Orchestrator_Notifier', statements: [ - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'logs:CreateLogGroup', 'logs:CreateLogStream', @@ -402,19 +451,19 @@ export class SolutionDeployStack extends cdk.Stack { ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'securityhub:BatchUpdateFindings' ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'ssm:GetParameter' ], - resources: [`arn:${this.partition}:ssm:*:${this.account}:parameter/Solutions/SO0111/*`] + resources: [`arn:${this.partition}:ssm:${this.region}:${this.account}:parameter/Solutions/SO0111/*`] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'kms:Encrypt', 'kms:Decrypt', @@ -422,18 +471,19 @@ export class SolutionDeployStack extends cdk.Stack { ], resources: [kmsKey.keyArn] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'sns:Publish' ], - resources: ['arn:' + this.partition + ':sns:' + this.region + ':' + - this.account + ':' + RESOURCE_PREFIX + '-SHARR_Topic'] + resources: [ + `arn:${this.partition}:sns:${this.region}:${this.account}:${RESOURCE_PREFIX}-SHARR_Topic` + ] }) ] }) { - let childToMod = notifyPolicy.node.findChild('Resource') as iam.CfnPolicy; + let childToMod = notifyPolicy.node.findChild('Resource') as CfnPolicy; childToMod.cfnOptions.metadata = { cfn_nag: { rules_to_suppress: [{ @@ -447,20 +497,22 @@ export class SolutionDeployStack extends cdk.Stack { } } + notifyPolicy.attachToRole(orchestratorRole) // Any Orchestrator Lambda can send to sns + /** * @description Role used by common Orchestrator Lambdas - * @type {iam.Role} + * @type {Role} */ - const notifyRole = new iam.Role(this, 'notifyRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + const notifyRole = new Role(this, 'notifyRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda role to perform notification and logging from orchestrator step function' }); notifyRole.attachInlinePolicy(notifyPolicy); { - let childToMod = notifyRole.node.findChild('Resource') as iam.CfnRole; + let childToMod = notifyRole.node.findChild('Resource') as CfnRole; childToMod.cfnOptions.metadata = { cfn_nag: { rules_to_suppress: [{ @@ -519,16 +571,16 @@ export class SolutionDeployStack extends cdk.Stack { //------------------------------------------------------------------------- // Custom Lambda Policy // - const createCustomActionPolicy = new iam.Policy(this, 'createCustomActionPolicy', { + const createCustomActionPolicy = new Policy(this, 'createCustomActionPolicy', { policyName: RESOURCE_PREFIX + '-SHARR_Custom_Action', statements: [ - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'cloudwatch:PutMetricData' ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'logs:CreateLogGroup', 'logs:CreateLogStream', @@ -536,14 +588,14 @@ export class SolutionDeployStack extends cdk.Stack { ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'securityhub:CreateActionTarget', 'securityhub:DeleteActionTarget' ], resources: ['*'] }), - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ 'ssm:GetParameter', 'ssm:GetParameters', @@ -554,7 +606,7 @@ export class SolutionDeployStack extends cdk.Stack { ] }) - const createCAPolicyResource = createCustomActionPolicy.node.findChild('Resource') as iam.CfnPolicy; + const createCAPolicyResource = createCustomActionPolicy.node.findChild('Resource') as CfnPolicy; createCAPolicyResource.cfnOptions.metadata = { cfn_nag: { @@ -568,14 +620,14 @@ export class SolutionDeployStack extends cdk.Stack { //------------------------------------------------------------------------- // Custom Lambda Role // - const createCustomActionRole = new iam.Role(this, 'createCustomActionRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + const createCustomActionRole = new Role(this, 'createCustomActionRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda role to allow creation of Security Hub Custom Actions' }); createCustomActionRole.attachInlinePolicy(createCustomActionPolicy); - const createCARoleResource = createCustomActionRole.node.findChild('Resource') as iam.CfnRole; + const createCARoleResource = createCustomActionRole.node.findChild('Resource') as CfnRole; createCARoleResource.cfnOptions.metadata = { cfn_nag: { @@ -637,6 +689,7 @@ export class SolutionDeployStack extends cdk.Stack { ssmExecDocLambda: execAutomation.functionArn, ssmExecMonitorLambda: monitorSSMExecState.functionArn, notifyLambda: sendNotifications.functionArn, + getApprovalRequirementLambda: getApprovalRequirement.functionArn, solutionId: RESOURCE_PREFIX, solutionName: props.solutionName, solutionVersion: props.solutionVersion, @@ -654,14 +707,18 @@ export class SolutionDeployStack extends cdk.Stack { // new OneTrigger(this, 'RemediateWithSharr', { targetArn: orchStateMachine.stateMachineArn, - prereq: createCAFuncResource + serviceToken: createCustomAction.functionArn, + prereq: [ + createCAFuncResource, + createCAPolicyResource + ] }) //------------------------------------------------------------------------- // Loop through all of the Playbooks and create an option to load each // const PB_DIR = `${__dirname}/../../playbooks` - var ignore = ['.DS_Store', 'core', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK']; + var ignore = ['.DS_Store', 'core', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK', '.coverage']; let illegalChars = /[\._]/g; var standardLogicalNames: string[] = [] @@ -677,8 +734,8 @@ export class SolutionDeployStack extends cdk.Stack { let parmname = file.replace(illegalChars, '') let adminStackOption = new cdk.CfnParameter(this, `LoadAdminStack${parmname}`, { type: "String", - description: `Load Playbook Admin stack for ${file}?`, - default: "no", + description: `Load CloudWatch Event Rules for ${file}?`, + default: "yes", allowedValues: ["yes", "no"], }) adminStackOption.overrideLogicalId(`Load${parmname}AdminStack`) @@ -702,20 +759,11 @@ export class SolutionDeployStack extends cdk.Stack { stack.templateOptions.metadata = { "AWS::CloudFormation::Interface": { ParameterGroups: [ - // { - // Label: {default: "Service Catalog Configuration"}, - // Parameters: [useServiceCatalog.logicalId] - // }, { Label: {default: "Security Standard Playbooks"}, Parameters: standardLogicalNames } - ], - // ParameterLabels: { - // [useServiceCatalog.logicalId]: { - // default: "Choose whether to use Service Catalog for Security Standard templates in the Admin account.", - // } - // }, + ] }, }; } diff --git a/source/solution_deploy/source/createCustomAction.py b/source/solution_deploy/source/createCustomAction.py index 1057f0d6..076c662b 100644 --- a/source/solution_deploy/source/createCustomAction.py +++ b/source/solution_deploy/source/createCustomAction.py @@ -1,202 +1,201 @@ #!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the "License"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0/ # # # # or in the "license" file accompanying this file. This file is distributed # # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # # or implied. See the License for the specific language governing permis- # # sions and limitations under the License. # ############################################################################### - +# Test Event +# { +# "ResourceProperties": { +# "Name": "Remediate with SHARR", +# "Description": "Submit the finding to AWS Security Hub Automated Response and Remediation", +# "Id": "SHARRRemediation" +# }, +# "RequestType": "create", +# "ResponseURL": "https://bogus" +# } import os import json import boto3 from botocore.config import Config from botocore.exceptions import ClientError -import hashlib from logger import Logger import requests -from urllib.request import Request, urlopen -from datetime import datetime -from metrics import Metrics -from awsapi_cached_client import AWSCachedClient +from urllib.request import Request -# initialise logger +# initialize logger LOG_LEVEL = os.getenv('log_level', 'info') logger_obj = Logger(loglevel=LOG_LEVEL) -SEND_METRICS = os.environ.get('sendAnonymousMetrics', 'No') +REGION = os.getenv('AWS_REGION', 'us-east-1') +PARTITION = os.getenv('AWS_PARTITION', default='aws') # Set by deployment template -def send_status_to_cfn(event, context, response_status, response_data, physical_resource_id, logger_obj, reason=None): +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + } +) +CLIENTS = {} +def get_securityhub_client(): + if 'securityhub' not in CLIENTS: + CLIENTS['securityhub'] = boto3.client('securityhub', config=BOTO_CONFIG) + return CLIENTS['securityhub'] - response_url = event['ResponseURL'] - logger_obj.debug("CFN response URL: " + response_url) +class InvalidCustomAction(Exception): + pass - response_body = {} - response_body['Status'] = response_status - response_body['PhysicalResourceId'] = physical_resource_id or context.log_stream_name - - msg = 'See details in CloudWatch Log Stream: ' + context.log_stream_name - - logger_obj.debug('PhysicalResourceId: ' + physical_resource_id) - if not reason: - response_body['Reason'] = msg - else: - response_body['Reason'] = str(reason)[0:255] + '... ' + msg - - response_body['StackId'] = event['StackId'] - response_body['RequestId'] = event['RequestId'] - response_body['LogicalResourceId'] = event['LogicalResourceId'] +class CustomAction(object): + """ + Security Hub CustomAction class + """ + name = '' + description = '' + id = '' + account = '' + + def __init__(self, account, properties): + self.name = properties.get('Name', '') + self.description = properties.get('Description', '') + self.id = properties.get('Id', '') + self.account = account + if not self.name or not self.description or not self.id: + raise InvalidCustomAction - if response_data and response_data != {} and response_data != [] and isinstance(response_data, dict): - response_body['Data'] = response_data + def create(self): + client = get_securityhub_client() + try: + return client.create_action_target( + Name=self.name, + Description=self.description, + Id=self.id + )['ActionTargetArn'] + except ClientError as error: + if error.response['Error']['Code'] == 'ResourceConflictException': + logger_obj.info('ResourceConflictException: already exists. Continuing') + return + elif error.response['Error']['Code'] == 'InvalidAccessException': + logger_obj.info('InvalidAccessException - Account is not subscribed to AWS Security Hub.') + return 'FAILED' + else: + logger_obj.error(error) + return 'FAILED' + except Exception as e: + return 'FAILED' - logger_obj.debug("<<<<<<< Response body >>>>>>>>>>") - logger_obj.debug(response_body) - json_response_body = json.dumps(response_body) + def delete(self): + client = get_securityhub_client() + try: + target_arn = f'arn:{PARTITION}:securityhub:{REGION}:{self.account}:action/custom/{self.id}' + logger_obj.info(target_arn) + client.delete_action_target(ActionTargetArn=target_arn) + return 'SUCCESS' + except ClientError as error: + if error.response['Error']['Code'] == 'ResourceNotFoundException': + logger_obj.info('ResourceNotFoundException - nothing to delete.') + return 'SUCCESS' + elif error.response['Error']['Code'] == 'InvalidAccessException': + logger_obj.info('InvalidAccessException - not subscribed to Security Hub (nothing to delete).') + return 'SUCCESS' + else: + logger_obj.error(error) + return 'FAILED' + except Exception as e: + logger_obj.error(e) + return 'FAILED' - headers = { - 'content-type': '', - 'content-length': str(len(json_response_body)) - } +class CfnResponse(object): + response_body = {} + response_url = '' + response_headers = {} - try: - if response_url == 'http://pre-signed-S3-url-for-response': - logger_obj.info("CloudFormation returned status code: THIS IS A TEST OUTSIDE OF CLOUDFORMATION") - else: - response = requests.put(response_url, - data=json_response_body, - headers=headers) - logger_obj.info("CloudFormation returned status code: " + response.reason) - except Exception as e: - logger_obj.error("send(..) failed executing requests.put(..): " + str(e)) - raise + def __init__(self, event, context, response_status, response_data, physical_resource_id, reason=None): + self.response_url = event['ResponseURL'] + + message = 'See details in CloudWatch Log Stream: ' + context.log_stream_name + if reason: + message = str(reason)[0:255] + '... ' + message -def lambda_handler(event, context): + raw_response_body = { + 'Status': response_status, + 'PhysicalResourceId': physical_resource_id or context.log_stream_name, + 'Reason': message, + 'StackId': event['StackId'], + 'RequestId': event['RequestId'], + 'LogicalResourceId': event['LogicalResourceId'] + } + if response_data and isinstance(response_data, dict): + raw_response_body['Data'] = response_data + + self.response_body = json.dumps(raw_response_body) - boto3_session = boto3.session.Session() - region = boto3_session.region_name + self.response_headers = { + 'content-type': '', + 'content-length': str(len(self.response_body)) + } + + def send(self): + try: + if self.response_url == 'http://pre-signed-S3-url-for-response': + logger_obj.info("CloudFormation returned status code: THIS IS A TEST OUTSIDE OF CLOUDFORMATION") + else: + response = requests.put(self.response_url, + data=self.response_body, + headers=self.response_headers) + logger_obj.info("CloudFormation returned status code: " + response.reason) + except Exception as e: + logger_obj.error("send(..) failed executing requests.put(..): " + str(e)) + raise +def lambda_handler(event, context): response_data = {} physical_resource_id = '' - metrics = Metrics({ - 'detail-type': 'installation' - }) - metrics.get_metrics_from_finding({ - 'GeneratorId': 'createCustomAction lambda', - 'Title': 'SHARR Installation - Create Custom Action', - 'ProductArn': 'N/A' - }) + err_msg = '' + + properties = event.get('ResourceProperties', {}) + logger_obj.info(json.dumps(properties)) + account_id = boto3.client('sts').get_caller_identity()['Account'] + customAction = CustomAction(account_id, properties) + physical_resource_id = 'CustomAction' + properties.get('Id', 'ERROR') try: - properties = event['ResourceProperties'] - logger_obj.debug(json.dumps(properties)) - region = os.environ['AWS_REGION'] - partition = os.getenv('AWS_PARTITION', default='aws') # Set by deployment template - client = AWSCachedClient(region).get_connection('securityhub') + status = 'ERROR' + if event['RequestType'].upper() == 'CREATE' or event['RequestType'].upper() == 'UPDATE': + logger_obj.info(event['RequestType'].upper() + ': ' + physical_resource_id) + custom_action_result = customAction.create() + if custom_action_result == 'FAILED': + status = 'FAILED' + else: + response_data['Arn'] = custom_action_result + status = 'SUCCESS' + + elif event['RequestType'].upper() == 'DELETE': + logger_obj.info('DELETE: ' + physical_resource_id) + status = customAction.delete() - physical_resource_id = 'CustomAction' + properties.get('Id', 'ERROR') - - if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': - try: - logger_obj.info(event['RequestType'].upper() + ": " + physical_resource_id) - response = client.create_action_target( - Name=properties['Name'], - Description=properties['Description'], - Id=properties['Id'] - ) - response_data['Arn'] = response['ActionTargetArn'] - except ClientError as error: - if error.response['Error']['Code'] == 'ResourceConflictException': - logger_obj.info('ResourceConflictException: already exists. Continuing') - elif error.response['Error']['Code'] == 'InvalidAccessException': - logger_obj.info('InvalidAccessException - Account is not subscribed to AWS Security Hub.') - raise - else: - logger_obj.error(error) - raise - except Exception as e: - metrics_data = { - 'status': 'Failed', - 'Id': event['StackId'], - 'err_msg': event['RequestType'] - } - metrics.send_metrics(metrics_data) - logger_obj.error(e) - raise - elif event['RequestType'] == 'Delete': - try: - logger_obj.info('DELETE: ' + physical_resource_id) - account_id = context.invoked_function_arn.split(":")[4] - client.delete_action_target( - ActionTargetArn=f"arn:{partition}:securityhub:{region}:{account_id}:action/custom/{properties['Id']}" - ) - except ClientError as error: - if error.response['Error']['Code'] == 'ResourceNotFoundException': - logger_obj.info('ResourceNotFoundException - nothing to delete.') - elif error.response['Error']['Code'] == 'InvalidAccessException': - logger_obj.info('InvalidAccessException - not subscribed to Security Hub (nothing to delete).') - else: - logger_obj.error(error) - raise - except Exception as e: - metrics_data = { - 'status': 'Failed', - 'Id': event['StackId'], - 'err_msg': event['RequestType'] - } - metrics.send_metrics(metrics_data) - logger_obj.error(e) - raise else: err_msg = 'Invalid RequestType: ' + event['RequestType'] logger_obj.error(err_msg) - send_status_to_cfn( - event, context, - "FAILED", - response_data, - physical_resource_id, - logger_obj, - reason=err_msg, - ) - - send_status_to_cfn( + + cloudformation = CfnResponse( event, context, - "SUCCESS", + status, response_data, physical_resource_id, - logger_obj + err_msg ) - metrics_data = { - 'status': 'Success', - 'message': f'Created custom action {properties["Name"]}', - 'Id': event['StackId'] - } - metrics.send_metrics(metrics_data) + cloudformation.send() return except Exception as err: logger_obj.error('An exception occurred: ') err_msg = err.__class__.__name__ + ': ' + str(err) logger_obj.error(err_msg) - metrics_data = { - 'status': 'Failed', - 'Id': event['StackId'], - 'err_msg': 'stack installation failed.' - } - metrics.send_metrics(metrics_data) - send_status_to_cfn( - event, - context, - "FAILED", - response_data, - event.get('physical_resource_id', 'ERROR'), - logger_obj=logger_obj, - reason=err_msg - ) diff --git a/source/solution_deploy/source/test/test_createCustomAction.py b/source/solution_deploy/source/test/test_createCustomAction.py new file mode 100644 index 00000000..0c9fafc1 --- /dev/null +++ b/source/solution_deploy/source/test/test_createCustomAction.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### +import os +import pytest +from pytest_mock import mocker +import boto3 +import botocore.session +from botocore.stub import Stubber, ANY +from botocore.exceptions import ClientError +import random +from createCustomAction import lambda_handler, CustomAction, CfnResponse, get_securityhub_client +from botocore.config import Config + +os.environ['AWS_REGION'] = 'us-east-1' +os.environ['AWS_PARTITION'] = 'aws' +sechub = boto3.client('securityhub') + +class MockContext(object): + def __init__(self, name, version): + self.function_name = name + self.function_version = version + self.invoked_function_arn = ( + "arn:aws:lambda:us-east-1:123456789012:function:{name}:{version}".format(name=name, version=version)) + self.memory_limit_in_mb = float('inf') + self.log_group_name = 'test-group' + self.log_stream_name = 'test-stream' + self.client_context = None + + self.aws_request_id = '-'.join([''.join([random.choice('0123456789abcdef') for _ in range(0, n)]) for n in [8, 4, 4, 4, 12]]) + +context = MockContext('SO0111-SHARR-Custom-Action-Lambda', 'v1.0.0') + +def event(type): + return { + "ResourceProperties": { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + }, + "RequestType": type, + "ResponseURL": "https://bogus" + } + +def test_get_client(mocker): + client1 = get_securityhub_client() + assert client1 + client2 = get_securityhub_client() + assert client2 == client1 + +def test_lambda_handler(mocker): + """ + Basic check for errors + """ + mocker.patch('createCustomAction.CustomAction.create', return_value='12341234') + lambda_handler(event('create'),{}) + +def test_create(mocker): + """ + Test that the correct API call is executed + """ + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_response( + 'create_action_target', + { + 'ActionTargetArn': 'foobarbaz' + }, + { + "Name": "Remediate with SHARR Test", + "Description": " Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + lambda_handler(event('create'), {}) + sechub_stub.deactivate() + +def test_create_already_exists(mocker): + """ + Test that there is no error when it already exists + """ + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'create_action_target', + 'ResourceConflictException' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.create() == None + sechub_stub.assert_no_pending_responses() + sechub_stub.deactivate() + +def test_create_no_sechub(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'create_action_target', + 'InvalidAccessException' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.create() == 'FAILED' + sechub_stub.assert_no_pending_responses() + sechub_stub.deactivate() + +def test_create_other_client_error(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'create_action_target', + 'ADoorIsAjar' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.create() == 'FAILED' + sechub_stub.assert_no_pending_responses() + sechub_stub.deactivate() + +def test_delete(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_response( + 'delete_action_target', + { + 'ActionTargetArn': 'foobarbaz' + }, + { + 'ActionTargetArn': ANY + } + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.delete() == 'SUCCESS' + sechub_stub.assert_no_pending_responses() + sechub_stub.deactivate() + + +def test_delete_already_exists(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'delete_action_target', + 'ResourceNotFoundException' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.delete() == 'SUCCESS' + sechub_stub.deactivate() + +def test_delete_no_sechub(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'delete_action_target', + 'InvalidAccessException' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.delete() == 'SUCCESS' + sechub_stub.deactivate() + +def test_delete_other_client_error(mocker): + sechub_stub = Stubber(sechub) + # Note: boto mock appears to be broken for the Sec Hub API + # It only works if the response containts "ActionTargetArn" + sechub_stub.add_client_error( + 'delete_action_target', + 'ADoorIsAjar' + ) + sechub_stub.activate() + mocker.patch('createCustomAction.get_securityhub_client', return_value=sechub) + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + customAction = CustomAction( + '111122223333', + { + "Name": "Remediate with SHARR Test", + "Description": "Test Submit the finding to AWS Security Hub Automated Response and Remediation", + "Id": "SHARRRemediationTest" + } + ) + assert customAction.delete() == 'FAILED' + sechub_stub.deactivate() + +def test_customaction(): + test_object = CustomAction( + '111122223333', + { + 'Name': 'foo', + 'Description': 'bar', + 'Id': 'baz' + } + ) + assert test_object.name == 'foo' + assert test_object.description == 'bar' + assert test_object.id == 'baz' + assert test_object.account == '111122223333' + +def test_cfn_response_success(mocker): + mocker.patch('createCustomAction.CfnResponse.send', return_value=None) + response_obj = CfnResponse( + { + 'StackId': 'foobarbaz', + 'RequestId': 'thisisarequestid', + 'LogicalResourceId': 'SHARR-thingy', + 'ResponseURL': 'https://somewhere.over.the.rainbow' + }, + context, + 'SUCCESS', + { + 'foo':'bar' + }, + 'SHARRPhysResourceId' + ) + good_body = '{"Status": "SUCCESS", "PhysicalResourceId": "SHARRPhysResourceId", "Reason": "See details in CloudWatch Log Stream: test-stream", "StackId": "foobarbaz", "RequestId": "thisisarequestid", "LogicalResourceId": "SHARR-thingy", "Data": {"foo": "bar"}}' + assert response_obj.response_body == good_body + assert response_obj.response_url == 'https://somewhere.over.the.rainbow' + assert response_obj.response_headers == {'content-length': '247', 'content-type': ''} diff --git a/source/solution_deploy/source/tests/test_data/create.json b/source/solution_deploy/source/test/test_data/create.json similarity index 100% rename from source/solution_deploy/source/tests/test_data/create.json rename to source/solution_deploy/source/test/test_data/create.json diff --git a/source/solution_deploy/source/tests/test_data/delete.json b/source/solution_deploy/source/test/test_data/delete.json similarity index 100% rename from source/solution_deploy/source/tests/test_data/delete.json rename to source/solution_deploy/source/test/test_data/delete.json diff --git a/source/solution_deploy/source/tests/test-createCustomAction.py b/source/solution_deploy/source/tests/test-createCustomAction.py deleted file mode 100644 index d94e7161..00000000 --- a/source/solution_deploy/source/tests/test-createCustomAction.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -""" -Unit Test: check_ssm_doc_state.py -Run from /deployment/build/Orchestrator after running build-s3-dist.sh -""" - - -import pytest -import boto3 -from botocore.stub import Stubber, ANY -from check_ssm_doc_state import get_lambda_role, lambda_handler -from awsapi_cached_client import AWSCachedClient - -def test_get_lambda_role(): - assert get_lambda_role('basename', 'AFSBP', 'us-east-1') == 'basename-AFSBP_us-east-1' - -def test_lambda_handler(): - """ - Verifies only that the APIs were called - """ - AWS = AWSCachedClient('us-east-1') - ssm_c = AWS.get_connection('ssm') - sts_c = AWS.get_connection('sts') - testing_account = sts_c.get_caller_identity().get('Account') - stsc_stub = Stubber(sts_c) - stsc_stub.add_response( - 'get_caller_identity', - {} - ) - stsc_stub.add_response( - 'assume_role', - { - # "RoleArn": "arn:aws:iam::" + testing_account + ":role/SO0111-SHARR-Orchestrator-Member_us-east-1" - } - ) - ssmc_stub = Stubber(ssm_c) - ssmc_stub.add_response( - 'describe_document', - {} - ) - event = { - "Finding": { - "AwsAccountId": testing_account - }, - "AutomationDocId": "test-doc-id" - } - # assert lambda_handler(event, {}) == '1234' diff --git a/source/test/__snapshots__/admin_account_parm.test.ts.snap b/source/test/__snapshots__/admin_account_parm.test.ts.snap new file mode 100644 index 00000000..4791a5d7 --- /dev/null +++ b/source/test/__snapshots__/admin_account_parm.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdminParm Test Stack 1`] = ` +Object { + "Parameters": Object { + "SecHubAdminAccount": Object { + "AllowedPattern": "\\\\d{12}", + "Description": "Admin account number", + "Type": "String", + }, + }, +} +`; diff --git a/source/test/__snapshots__/member_stack.test.ts.snap b/source/test/__snapshots__/member_stack.test.ts.snap index dc516694..3ba71988 100644 --- a/source/test/__snapshots__/member_stack.test.ts.snap +++ b/source/test/__snapshots__/member_stack.test.ts.snap @@ -22,6 +22,12 @@ Object { "LogGroupName", ], }, + Object { + "Label": Object { + "default": "Playbooks", + }, + "Parameters": Array [], + }, ], "ParameterLabels": Object { "LogGroupName": Object { @@ -42,232 +48,62 @@ Object { }, }, "Resources": Object { - "OrchestratorMemberRoleMemberAccountRoleBE9AD9D5": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, + "RunbookStackNoRoles": Object { "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "iam:PassRole", - "iam:GetRole", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/SO0111-*", - ], - ], - }, - }, - Object { - "Action": Array [ - "ssm:DescribeAutomationExecutions", - "ssm:DescribeDocument", - "ssm:GetParameters", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":*:*", - ], - ], - }, - }, - Object { - "Action": Array [ - "ssm:StartAutomationExecution", - "ssm:GetAutomationExecution", - ], - "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":document/SHARR-*", - ], - ], - }, - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":automation-definition/*", - ], - ], - }, - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - "::automation-definition/*", - ], - ], - }, - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":automation-execution/*", - ], - ], - }, - ], - }, - Object { - "Action": Array [ - "cloudwatch:PutMetricData", - "securityhub:BatchUpdateFindings", - ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "member_orchestrator", - }, - ], - "RoleName": Object { + "TemplateURL": Object { "Fn::Join": Array [ "", Array [ - "SO0111-SHARR-Orchestrator-Member_", + "https://", Object { - "Ref": "AWS::Region", + "Fn::FindInMap": Array [ + "SourceCode", + "General", + "S3Bucket", + ], }, + "-reference.s3.amazonaws.com/", + Object { + "Fn::FindInMap": Array [ + "SourceCode", + "General", + "KeyPrefix", + ], + }, + "/aws-sharr-remediations.template", ], ], }, }, - "Type": "AWS::IAM::Role", + "Type": "AWS::CloudFormation::Stack", }, - "OrchestratorMemberRoleSHARRKeyAliasAF329C03": Object { + "SHARRKeyAliasEBF509D8": Object { "Properties": Object { "Description": "KMS Customer Managed Key that will encrypt data for remediations", "Name": "/Solutions/SO0111/CMK_REMEDIATION_ARN", "Type": "String", "Value": Object { "Fn::GetAtt": Array [ - "OrchestratorMemberRoleSHARRRemediationKey8F55D497", + "SHARRRemediationKeyE744743D", "Arn", ], }, }, "Type": "AWS::SSM::Parameter", }, - "OrchestratorMemberRoleSHARRRemediationKey8F55D497": Object { + "SHARRRemediationKeyAlias5531874D": Object { + "Properties": Object { + "AliasName": "alias/SO0111-SHARR-Remediation-Key", + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "SHARRRemediationKeyE744743D", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "SHARRRemediationKeyE744743D": Object { "DeletionPolicy": "Retain", "Properties": Object { "EnableKeyRotation": true, @@ -296,10 +132,6 @@ Object { "", Array [ "logs.", - Object { - "Ref": "AWS::Region", - }, - ".", Object { "Ref": "AWS::URLSuffix", }, @@ -310,7 +142,11 @@ Object { "Fn::Join": Array [ "", Array [ - "cloudtrail.", + "logs.", + Object { + "Ref": "AWS::Region", + }, + ".", Object { "Ref": "AWS::URLSuffix", }, @@ -321,13 +157,14 @@ Object { "Fn::Join": Array [ "", Array [ - "cloudwatch.", + "cloudtrail.", Object { "Ref": "AWS::URLSuffix", }, ], ], }, + "cloudwatch.amazonaws.com", ], }, "Resource": "*", @@ -362,52 +199,6 @@ Object { "Type": "AWS::KMS::Key", "UpdateReplacePolicy": "Retain", }, - "OrchestratorMemberRoleSHARRRemediationKeyAlias3BD00A05": Object { - "Properties": Object { - "AliasName": "alias/SO0111-SHARR-Remediation-Key", - "TargetKeyId": Object { - "Fn::GetAtt": Array [ - "OrchestratorMemberRoleSHARRRemediationKey8F55D497", - "Arn", - ], - }, - }, - "Type": "AWS::KMS::Alias", - }, - "RunbookStack": Object { - "Properties": Object { - "Parameters": Object { - "SecHubAdminAccount": Object { - "Ref": "SecHubAdminAccount", - }, - }, - "TemplateURL": Object { - "Fn::Join": Array [ - "", - Array [ - "https://", - Object { - "Fn::FindInMap": Array [ - "SourceCode", - "General", - "S3Bucket", - ], - }, - "-reference.s3.amazonaws.com/", - Object { - "Fn::FindInMap": Array [ - "SourceCode", - "General", - "KeyPrefix", - ], - }, - "/aws-sharr-remediations.template", - ], - ], - }, - }, - "Type": "AWS::CloudFormation::Stack", - }, "SSMParameterLogGroupName47918519": Object { "Properties": Object { "Description": "Parameter to store log group name", diff --git a/source/test/__snapshots__/orchestrator.test.ts.snap b/source/test/__snapshots__/orchestrator.test.ts.snap index 2676e22a..6a0458d0 100644 --- a/source/test/__snapshots__/orchestrator.test.ts.snap +++ b/source/test/__snapshots__/orchestrator.test.ts.snap @@ -55,6 +55,7 @@ Object { "Type": "AWS::CloudFormation::Stack", }, "OrchestratorRole9CF251DB": Object { + "DeletionPolicy": "Retain", "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -169,6 +170,7 @@ Object { ], }, "Type": "AWS::IAM::Role", + "UpdateReplacePolicy": "Retain", }, "OrchestratorSHARROrchestratorArnC8FB076A": Object { "Properties": Object { @@ -191,23 +193,31 @@ Object { "Fn::Join": Array [ "", Array [ - "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Automation Document State\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", + "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Remediation Approval Requirement\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", + Object { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"aaa\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\",\\"ResourceRegion.$\\":\\"$.Payload.resourceregion\\"},\\"Resource\\":\\"arn:", + Object { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"xxx\\",\\"Payload.$\\":\\"$\\"}},\\"Get Remediation Approval Requirement\\":{\\"Next\\":\\"Get Automation Document State\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Determine whether the selected remediation requires manual approval\\",\\"TimeoutSeconds\\":300,\\"ResultPath\\":\\"$.Workflow\\",\\"ResultSelector\\":{\\"WorkflowDocument.$\\":\\"$.Payload.workflowdoc\\",\\"WorkflowAccount.$\\":\\"$.Payload.workflowaccount\\",\\"WorkflowRole.$\\":\\"$.Payload.workflowrole\\",\\"WorkflowConfig.$\\":\\"$.Payload.workflow_data\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"aaa\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\"},\\"Resource\\":\\"arn:", + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"bbb\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"Remediation Queued\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"Account.$\\":\\"$.Payload.executionaccount\\",\\"Region.$\\":\\"$.Payload.executionregion\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"xxx\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\"},\\"Resource\\":\\"arn:", + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"yyy\\",\\"Payload.$\\":\\"$\\"}},\\"Remediation Queued\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)\\",\\"State.$\\":\\"States.Format('QUEUED')\\",\\"ExecId.$\\":\\"$.SSMExecution.ExecId\\"}},\\"Next\\":\\"Queued Notification\\"},\\"Queued Notification\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notification that a remediation has queued\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.notificationResult\\",\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"yyy\\",\\"Payload.$\\":\\"$\\"}},\\"execMonitor\\":{\\"Next\\":\\"Remediation completed?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Monitor the remediation execution until done\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.Remediation\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"RemediationState.$\\":\\"$.Payload.remediation_status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"LogData.$\\":\\"$.Payload.logdata\\",\\"AffectedObject.$\\":\\"$.Payload.affected_object\\"},\\"Resource\\":\\"arn:", + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"aaa\\",\\"Payload.$\\":\\"$\\"}},\\"execMonitor\\":{\\"Next\\":\\"Remediation completed?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Monitor the remediation execution until done\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.Remediation\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"RemediationState.$\\":\\"$.Payload.remediation_status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"LogData.$\\":\\"$.Payload.logdata\\",\\"AffectedObject.$\\":\\"$.Payload.affected_object\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"zzz\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"zzz\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", ], ], }, diff --git a/source/test/__snapshots__/orchestrator_logs.test.ts.snap b/source/test/__snapshots__/orchestrator_logs.test.ts.snap new file mode 100644 index 00000000..2d6b4794 --- /dev/null +++ b/source/test/__snapshots__/orchestrator_logs.test.ts.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Global Roles Stack 1`] = ` +Object { + "Conditions": Object { + "EncryptedLogGroup": Object { + "Fn::And": Array [ + Object { + "Condition": "isNotGovCloud", + }, + Object { + "Fn::Equals": Array [ + Object { + "Ref": "ReuseOrchestratorLogGroup", + }, + "no", + ], + }, + ], + }, + "UnencryptedLogGroup": Object { + "Fn::And": Array [ + Object { + "Fn::Not": Array [ + Object { + "Condition": "isNotGovCloud", + }, + ], + }, + Object { + "Fn::Equals": Array [ + Object { + "Ref": "ReuseOrchestratorLogGroup", + }, + "no", + ], + }, + ], + }, + "isNotGovCloud": Object { + "Fn::Not": Array [ + Object { + "Fn::Equals": Array [ + Object { + "Ref": "AWS::Partition", + }, + "aws-us-gov", + ], + }, + ], + }, + }, + "Description": "test;", + "Parameters": Object { + "KmsKeyArn": Object { + "Description": "ARN of the KMS key to use to encrypt log data.", + "Type": "String", + }, + "ReuseOrchestratorLogGroup": Object { + "AllowedValues": Array [ + "yes", + "no", + ], + "Default": "no", + "Description": "Reuse existing Orchestrator Log Group? Choose \\"yes\\" if the log group already exists, else \\"no\\"", + "Type": "String", + }, + }, + "Resources": Object { + "OrchestratorLogsEFDFFA92": Object { + "Condition": "UnencryptedLogGroup", + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W84", + "reason": "KmsKeyId is not supported in GovCloud.", + }, + ], + }, + }, + "Properties": Object { + "LogGroupName": "TestLogGroup", + "RetentionInDays": 365, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "OrchestratorLogsEncrypted072D6E38": Object { + "Condition": "EncryptedLogGroup", + "DeletionPolicy": "Retain", + "Properties": Object { + "KmsKeyId": Object { + "Ref": "KmsKeyArn", + }, + "LogGroupName": "TestLogGroup", + "RetentionInDays": 365, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/test/__snapshots__/runbook_stack.test.ts.snap b/source/test/__snapshots__/runbook_stack.test.ts.snap index 9e3253f7..ef6a67df 100644 --- a/source/test/__snapshots__/runbook_stack.test.ts.snap +++ b/source/test/__snapshots__/runbook_stack.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`default stack 1`] = ` +exports[`Global Roles Stack 1`] = ` Object { "Description": "test;", "Parameters": Object { @@ -11,174 +11,7 @@ Object { }, }, "Resources": Object { - "EnableVPCFlowLogsremediationrole00848CDF": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "vpc-flow-logs.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:DescribeLogGroups", - "logs:DescribeLogStreams", - "logs:PutLogEvents", - ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "default_lambdaPolicy", - }, - ], - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableVPCFlowLogs-remediationRole_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "Rds6EnhancedMonitoringRole2FD1E9A5": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W28", - "reason": "Static names required to allow use in automated remediation runbooks.", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "monitoring.rds.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-RDSMonitoring-remediationRole_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "Rds6EnhancedMonitoringRoleRDS6EnhancedMonitoringPolicyA2EB4EE9": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:PutRetentionPolicy", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":logs:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:RDS*", - ], - ], - }, - "Sid": "EnableCreationAndManagementOfRDSCloudwatchLogGroups", - }, - Object { - "Action": Array [ - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams", - "logs:GetLogEvents", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":logs:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:RDS*:log-stream:*", - ], - ], - }, - "Sid": "EnableCreationAndManagementOfRDSCloudwatchLogStreams", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "Rds6EnhancedMonitoringRoleRDS6EnhancedMonitoringPolicyA2EB4EE9", - "Roles": Array [ - Object { - "Ref": "Rds6EnhancedMonitoringRole2FD1E9A5", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleConfigureS3BucketLoggingMemberAccountRoleE068390D": Object { + "OrchestratorMemberRoleMemberAccountRoleBE9AD9D5": Object { "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -212,10 +45,7 @@ Object { Object { "Ref": "SecHubAdminAccount", }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, + ":role/SO0111-SHARR-Orchestrator-Admin", ], ], }, @@ -225,4224 +55,763 @@ Object { ], "Version": "2012-10-17", }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-ConfigureS3BucketLogging_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleConfigureS3BucketLoggingSHARRMemberBasePolicyAC4F82A8": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleConfigureS3BucketLoggingSHARRMemberBasePolicyAC4F82A8", - "Roles": Array [ + "Policies": Array [ Object { - "Ref": "RemediationRoleConfigureS3BucketLoggingMemberAccountRoleE068390D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleConfigureS3BucketPublicAccessBlockMemberAccountRoleC78F6EE7": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "iam:PassRole", + "iam:GetRole", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":role/SO0111-*", + ], ], + }, + }, + Object { + "Action": Array [ + "ssm:DescribeAutomationExecutions", + "ssm:DescribeDocument", + "ssm:GetParameters", ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:*:*:*", + ], + ], + }, }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-ConfigureS3BucketPublicAccessBlock_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleConfigureS3BucketPublicAccessBlockSHARRMemberBasePolicyB9DCBD99": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", + Object { + "Action": Array [ + "ssm:StartAutomationExecution", + "ssm:GetAutomationExecution", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:*:", + Object { + "Ref": "AWS::AccountId", + }, + ":document/SHARR-*", + ], + ], + }, Object { - "Ref": "AWS::Partition", + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:*:", + Object { + "Ref": "AWS::AccountId", + }, + ":automation-definition/*", + ], + ], }, - ":ssm:", Object { - "Ref": "AWS::Region", + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:*::automation-definition/*", + ], + ], }, - ":", Object { - "Ref": "AWS::AccountId", + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:*:", + Object { + "Ref": "AWS::AccountId", + }, + ":automation-execution/*", + ], + ], }, - ":parameter/Solutions/SO0111/*", ], - ], - }, + }, + Object { + "Action": Array [ + "cloudwatch:PutMetricData", + "securityhub:BatchUpdateFindings", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleConfigureS3BucketPublicAccessBlockSHARRMemberBasePolicyB9DCBD99", - "Roles": Array [ - Object { - "Ref": "RemediationRoleConfigureS3BucketPublicAccessBlockMemberAccountRoleC78F6EE7", + "PolicyName": "member_orchestrator", }, ], + "RoleName": "SO0111-SHARR-Orchestrator-Member", }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::IAM::Role", }, - "RemediationRoleConfigureS3PublicAccessBlockMemberAccountRole98A4BC1D": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, + }, +} +`; + +exports[`Regional Documents 1`] = ` +Object { + "Description": "test;", + "Resources": Object { + "SHARRConfigureS3BucketPublicAccessBlockAutomationDocumentD9BFB480": Object { + "Properties": Object { + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-ConfigureS3BucketPublicAccessBlock + +## What does this document do? +This document is used to create or modify the PublicAccessBlock configuration for an Amazon S3 bucket. + +## Input Parameters +* BucketName: (Required) Name of the S3 bucket (not the ARN). +* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy. + * Default: \\"true\\" +* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket. + * Default: \\"true\\" +* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket. + * Default: \\"true\\" +* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. + * Default: \\"true\\" +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* GetBucketPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call + +## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. +", + "mainSteps": Array [ Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", + "action": "aws:executeAwsApi", + "description": "## PutBucketPublicAccessBlock +Creates or modifies the PublicAccessBlock configuration for a S3 Bucket. +", + "inputs": Object { + "Api": "PutPublicAccessBlock", + "Bucket": "{{BucketName}}", + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", + }, + "Service": "s3", + }, + "isCritical": true, + "isEnd": false, + "maxAttempts": 2, + "name": "PutBucketPublicAccessBlock", + "timeoutSeconds": 600, }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], + "action": "aws:executeScript", + "description": "## GetBucketPublicAccessBlock +Retrieves the S3 PublicAccessBlock configuration for a S3 Bucket. +## Outputs +* Output: JSON formatted response from the GetPublicAccessBlock API call. +", + "inputs": Object { + "Handler": "validate_s3_bucket_publicaccessblock", + "InputPayload": Object { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "Bucket": "{{BucketName}}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", }, - "Service": "ssm.amazonaws.com", + "Runtime": "python3.7", + "Script": "import boto3 + +def validate_s3_bucket_publicaccessblock(event, context): + s3_client = boto3.client(\\"s3\\") + bucket = event[\\"Bucket\\"] + restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] + block_public_acls = event[\\"BlockPublicAcls\\"] + ignore_public_acls = event[\\"IgnorePublicAcls\\"] + block_public_policy = event[\\"BlockPublicPolicy\\"] + + output = s3_client.get_public_access_block(Bucket=bucket) + updated_block_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicAcls\\"] + updated_ignore_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"IgnorePublicAcls\\"] + updated_block_policy = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicPolicy\\"] + updated_restrict_buckets = output[\\"PublicAccessBlockConfiguration\\"][\\"RestrictPublicBuckets\\"] + + if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ + and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: + return { + \\"output\\": + { + \\"message\\": \\"Bucket public access block configuration successfully set.\\", + \\"configuration\\": output[\\"PublicAccessBlockConfiguration\\"] + } + } + else: + info = \\"CONFIGURATION VALUES DO NOT MATCH WITH PARAMETERS PROVIDED VALUES RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\".format( + restrict_public_buckets, + block_public_acls, + ignore_public_acls, + block_public_policy + ) + raise Exception(info)", }, + "isCritical": true, + "isEnd": true, + "name": "GetBucketPublicAccessBlock", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, }, ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-ConfigureS3PublicAccessBlock_", - Object { - "Ref": "AWS::Region", - }, - ], + "outputs": Array [ + "GetBucketPublicAccessBlock.Output", ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleConfigureS3PublicAccessBlockSHARRMemberBasePolicy26BF29A6": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BlockPublicAcls": Object { + "allowedValues": Array [ + true, + false, ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket.", + "type": "Boolean", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleConfigureS3PublicAccessBlockSHARRMemberBasePolicy26BF29A6", - "Roles": Array [ - Object { - "Ref": "RemediationRoleConfigureS3PublicAccessBlockMemberAccountRole98A4BC1D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleCreateAccessLoggingBucketMemberAccountRole3E1569D8": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", + "BlockPublicPolicy": Object { + "allowedValues": Array [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", + "type": "Boolean", }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", + "BucketName": Object { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\\\d+\\\\.)+\\\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])\\\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])$)", + "description": "(Required) The bucket name (not the ARN).", + "type": "String", }, - ], + "IgnorePublicAcls": Object { + "allowedValues": Array [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket.", + "type": "Boolean", + }, + "RestrictPublicBuckets": Object { + "allowedValues": Array [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy.", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-ConfigureS3BucketPublicAccessBlock", }, + "Type": "AWS::SSM::Document", + }, + "SHARRConfigureS3PublicAccessBlockAutomationDocumentA2255F0A": Object { "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-ConfigureS3PublicAccessBlock + +## What does this document do? +This document is used to create or modify the S3 [PublicAccessBlock](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options) configuration for an AWS account. + +## Input Parameters +* AccountId: (Required) Account ID of the account for which the S3 Account Public Access Block is to be configured. +* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account. + * Default: \\"true\\" +* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account. + * Default: \\"true\\" +* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain. + * Default: \\"true\\" +* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. + * Default: \\"true\\" +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* GetPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call. +", + "mainSteps": Array [ + Object { + "action": "aws:executeAwsApi", + "description": "## PutAccountPublicAccessBlock +Creates or modifies the S3 PublicAccessBlock configuration for an AWS account. +", + "inputs": Object { + "AccountId": "{{ AccountId }}", + "Api": "PutPublicAccessBlock", + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-CreateAccessLoggingBucket_", - Object { - "Ref": "AWS::Region", + "Service": "s3control", }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleCreateAccessLoggingBucketSHARRMemberBasePolicy0B9908F2": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", + "isEnd": false, + "name": "PutAccountPublicAccessBlock", + "outputs": Array [ + Object { + "Name": "PutAccountPublicAccessBlockResponse", + "Selector": "$", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleCreateAccessLoggingBucketSHARRMemberBasePolicy0B9908F2", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateAccessLoggingBucketMemberAccountRole3E1569D8", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleCreateCloudTrailMultiRegionTrailMemberAccountRoleF70577FF": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", + "timeoutSeconds": 600, }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], + "action": "aws:executeScript", + "description": "## GetPublicAccessBlock +Retrieves the S3 PublicAccessBlock configuration for an AWS account. +## Outputs +* Output: JSON formatted response from the GetPublicAccessBlock API call. +", + "inputs": Object { + "Handler": "handler", + "InputPayload": Object { + "AccountId": "{{ AccountId }}", + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", }, - "Service": "ssm.amazonaws.com", + "Runtime": "python3.7", + "Script": "import boto3 +from time import sleep + +def verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy): + s3control_client = boto3.client('s3control') + wait_time = 30 + max_time = 480 + retry_count = 1 + max_retries = max_time/wait_time + while retry_count <= max_retries: + sleep(wait_time) + retry_count = retry_count + 1 + get_public_access_response = s3control_client.get_public_access_block(AccountId=account_id) + updated_block_acl = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicAcls'] + updated_ignore_acl = get_public_access_response['PublicAccessBlockConfiguration']['IgnorePublicAcls'] + updated_block_policy = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicPolicy'] + updated_restrict_buckets = get_public_access_response['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] + if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ + and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: + return { + \\"output\\": { + \\"message\\": \\"Verification successful. S3 Public Access Block Updated.\\", + \\"HTTPResponse\\": get_public_access_response[\\"PublicAccessBlockConfiguration\\"] + }, + } + raise Exception( + \\"VERFICATION FAILED. S3 GetPublicAccessBlock CONFIGURATION VALUES \\" + \\"DO NOT MATCH WITH PARAMETERS PROVIDED VALUES \\" + \\"RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\" + .format(updated_restrict_buckets, updated_block_acl, updated_ignore_acl, updated_block_policy) + ) + +def handler(event, context): + account_id = event[\\"AccountId\\"] + restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] + block_public_acls = event[\\"BlockPublicAcls\\"] + ignore_public_acls = event[\\"IgnorePublicAcls\\"] + block_public_policy = event[\\"BlockPublicPolicy\\"] + return verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy)", }, + "isEnd": true, + "name": "GetPublicAccessBlock", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, }, ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-CreateCloudTrailMultiRegionTrail_", - Object { - "Ref": "AWS::Region", - }, - ], + "outputs": Array [ + "GetPublicAccessBlock.Output", ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleCreateCloudTrailMultiRegionTrailSHARRMemberBasePolicyA86222AF": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, + "parameters": Object { + "AccountId": Object { + "allowedPattern": "^\\\\d{12}$", + "description": "(Required) The account ID for the AWS account whose PublicAccessBlock configuration you want to set.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleCreateCloudTrailMultiRegionTrailSHARRMemberBasePolicyA86222AF", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateCloudTrailMultiRegionTrailMemberAccountRoleF70577FF", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleCreateLogMetricFilterAndAlarmMemberAccountRoleAA3E3C8A": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", + "BlockPublicAcls": Object { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account.", + "type": "Boolean", }, - ], + "BlockPublicPolicy": Object { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", + "type": "Boolean", + }, + "IgnorePublicAcls": Object { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain.", + "type": "Boolean", + }, + "RestrictPublicBuckets": Object { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account.", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-ConfigureS3PublicAccessBlock", }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-CreateLogMetricFilterAndAlarm_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleCreateLogMetricFilterAndAlarmSHARRMemberBasePolicy2AFEEF94": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleCreateLogMetricFilterAndAlarmSHARRMemberBasePolicy2AFEEF94", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateLogMetricFilterAndAlarmMemberAccountRoleAA3E3C8A", - }, - ], - }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::SSM::Document", }, - "RemediationRoleDisablePublicAccessForSecurityGroupMemberAccountRole3BED8BF4": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, + "SHARRCreateAccessLoggingBucketAutomationDocument63C3DE52": Object { "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - SHARR-CreateAccessLoggingBucket + +## What does this document do? +Creates an S3 bucket for access logging. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* BucketName: (Required) Name of the bucket to create +", + "mainSteps": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], + "action": "aws:executeScript", + "inputs": Object { + "Handler": "create_logging_bucket", + "InputPayload": Object { + "AWS_REGION": "{{global:REGION}}", + "BucketName": "{{BucketName}}", }, - "Service": "ssm.amazonaws.com", + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.exceptions import ClientError +from botocore.config import Config + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_logging_bucket(event, context): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + try: + kwargs = { + 'Bucket': event['BucketName'], + 'GrantWrite': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery', + 'GrantReadACP': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery' + } + if event['AWS_REGION'] != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': event['AWS_REGION'] + } + + s3.create_bucket(**kwargs) + + s3.put_bucket_encryption( + Bucket=event['BucketName'], + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'AES256' + } + } + ] + } + ) + return { + \\"output\\": { + \\"Message\\": f'Bucket {event[\\"BucketName\\"]} created' + } + } + except ClientError as error: + if error.response['Error']['Code'] != 'BucketAlreadyExists' and \\\\ + error.response['Error']['Code'] != 'BucketAlreadyOwnedByYou': + exit(str(error)) + else: + return { + \\"output\\": { + \\"Message\\": f'Bucket {event[\\"BucketName\\"]} already exists' + } + } + except Exception as e: + print(e) + exit(str(e))", }, + "isEnd": true, + "name": "CreateAccessLoggingBucket", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], }, ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-DisablePublicAccessForSecurityGroup_", - Object { - "Ref": "AWS::Region", - }, - ], + "outputs": Array [ + "CreateAccessLoggingBucket.Output", ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": Object { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\\\d+\\\\.)+\\\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])\\\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])$)", + "description": "(Required) The bucket name (not the ARN).", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-CreateAccessLoggingBucket", }, - "Type": "AWS::IAM::Role", + "Type": "AWS::SSM::Document", }, - "RemediationRoleDisablePublicAccessForSecurityGroupSHARRMemberBasePolicy3076FC8A": Object { + "SHARRCreateCloudTrailMultiRegionTrailAutomationDocument59B9EAA0": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleDisablePublicAccessForSecurityGroupSHARRMemberBasePolicy3076FC8A", - "Roles": Array [ - Object { - "Ref": "RemediationRoleDisablePublicAccessForSecurityGroupMemberAccountRole3BED8BF4", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableAWSConfigMemberAccountRole3914B25F": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableAWSConfig_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableAWSConfigSHARRMemberBasePolicy535B8C0F": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableAWSConfigSHARRMemberBasePolicy535B8C0F", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableAWSConfigMemberAccountRole3914B25F", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableAutoScalingGroupELBHealthCheckMemberAccountRole03AE4AEA": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableAutoScalingGroupELBHealthCheck_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableAutoScalingGroupELBHealthCheckSHARRMemberBasePolicy3ED01525": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableAutoScalingGroupELBHealthCheckSHARRMemberBasePolicy3ED01525", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableAutoScalingGroupELBHealthCheckMemberAccountRole03AE4AEA", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableCloudTrailEncryptionMemberAccountRoleA936699B": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableCloudTrailEncryption_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableCloudTrailEncryptionSHARRMemberBasePolicy6489774E": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableCloudTrailEncryptionSHARRMemberBasePolicy6489774E", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailEncryptionMemberAccountRoleA936699B", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableCloudTrailLogFileValidationMemberAccountRole3F5F7157": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableCloudTrailLogFileValidation_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableCloudTrailLogFileValidationSHARRMemberBasePolicy85A07C2D": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableCloudTrailLogFileValidationSHARRMemberBasePolicy85A07C2D", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailLogFileValidationMemberAccountRole3F5F7157", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableCloudTrailToCloudWatchLoggingMemberAccountRoleE7E9C206": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableCloudTrailToCloudWatchLogging_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableCloudTrailToCloudWatchLoggingSHARRMemberBasePolicy0E4130D5": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableCloudTrailToCloudWatchLoggingSHARRMemberBasePolicy0E4130D5", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailToCloudWatchLoggingMemberAccountRoleE7E9C206", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableEbsEncryptionByDefaultMemberAccountRoleDF17FF59": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableEbsEncryptionByDefault_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableEbsEncryptionByDefaultSHARRMemberBasePolicy77CF4834": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableEbsEncryptionByDefaultSHARRMemberBasePolicy77CF4834", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableEbsEncryptionByDefaultMemberAccountRoleDF17FF59", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableEnhancedMonitoringOnRDSInstanceMemberAccountRoleB3EFCB99": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableEnhancedMonitoringOnRDSInstance_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableEnhancedMonitoringOnRDSInstanceSHARRMemberBasePolicy4D03FBD0": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableEnhancedMonitoringOnRDSInstanceSHARRMemberBasePolicy4D03FBD0", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableEnhancedMonitoringOnRDSInstanceMemberAccountRoleB3EFCB99", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableKeyRotationMemberAccountRole2366F17F": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableKeyRotation_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableKeyRotationSHARRMemberBasePolicyA6E832D4": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableKeyRotationSHARRMemberBasePolicyA6E832D4", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableKeyRotationMemberAccountRole2366F17F", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableRDSClusterDeletionProtectionMemberAccountRole019A1667": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableRDSClusterDeletionProtection_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableRDSClusterDeletionProtectionSHARRMemberBasePolicy90D2EA44": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableRDSClusterDeletionProtectionSHARRMemberBasePolicy90D2EA44", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableRDSClusterDeletionProtectionMemberAccountRole019A1667", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleEnableVPCFlowLogsMemberAccountRoleB79F3729": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-EnableVPCFlowLogs_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleEnableVPCFlowLogsSHARRMemberBasePolicy0D33A918": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleEnableVPCFlowLogsSHARRMemberBasePolicy0D33A918", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableVPCFlowLogsMemberAccountRoleB79F3729", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleMakeEBSSnapshotsPrivateMemberAccountRoleFA05CFAF": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-MakeEBSSnapshotsPrivate_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleMakeEBSSnapshotsPrivateSHARRMemberBasePolicy7DE85B9C": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleMakeEBSSnapshotsPrivateSHARRMemberBasePolicy7DE85B9C", - "Roles": Array [ - Object { - "Ref": "RemediationRoleMakeEBSSnapshotsPrivateMemberAccountRoleFA05CFAF", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleMakeRDSSnapshotPrivateMemberAccountRole6760FE6D": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-MakeRDSSnapshotPrivate_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleMakeRDSSnapshotPrivateSHARRMemberBasePolicyFF0FBF31": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleMakeRDSSnapshotPrivateSHARRMemberBasePolicyFF0FBF31", - "Roles": Array [ - Object { - "Ref": "RemediationRoleMakeRDSSnapshotPrivateMemberAccountRole6760FE6D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleRemoveLambdaPublicAccessMemberAccountRoleB266862C": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-RemoveLambdaPublicAccess_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleRemoveLambdaPublicAccessSHARRMemberBasePolicy6AACE4BE": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleRemoveLambdaPublicAccessSHARRMemberBasePolicy6AACE4BE", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRemoveLambdaPublicAccessMemberAccountRoleB266862C", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleRemoveVPCDefaultSecurityGroupRulesMemberAccountRole406D320B": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-RemoveVPCDefaultSecurityGroupRules_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleRemoveVPCDefaultSecurityGroupRulesSHARRMemberBasePolicy18B08253": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleRemoveVPCDefaultSecurityGroupRulesSHARRMemberBasePolicy18B08253", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRemoveVPCDefaultSecurityGroupRulesMemberAccountRole406D320B", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleRevokeUnusedIAMUserCredentialsMemberAccountRole5C008B43": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-RevokeUnusedIAMUserCredentials_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleRevokeUnusedIAMUserCredentialsSHARRMemberBasePolicy6519E750": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleRevokeUnusedIAMUserCredentialsSHARRMemberBasePolicy6519E750", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRevokeUnusedIAMUserCredentialsMemberAccountRole5C008B43", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RemediationRoleSetIAMPasswordPolicyMemberAccountRoleA1FF47B4": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Resource * is required due to the administrative nature of the solution.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "SecHubAdminAccount", - }, - ":role/SO0111-SHARR-Orchestrator-Admin_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "Service": "ssm.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-SetIAMPasswordPolicy_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - }, - "Type": "AWS::IAM::Role", - }, - "RemediationRoleSetIAMPasswordPolicySHARRMemberBasePolicy3E89D2C9": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":parameter/Solutions/SO0111/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "RemediationRoleSetIAMPasswordPolicySHARRMemberBasePolicy3E89D2C9", - "Roles": Array [ - Object { - "Ref": "RemediationRoleSetIAMPasswordPolicyMemberAccountRoleA1FF47B4", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRConfigureS3BucketPublicAccessBlockAutomationDocumentD9BFB480": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - AWSConfigRemediation-ConfigureS3BucketPublicAccessBlock - -## What does this document do? -This document is used to create or modify the PublicAccessBlock configuration for an Amazon S3 bucket. - -## Input Parameters -* BucketName: (Required) Name of the S3 bucket (not the ARN). -* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy. - * Default: \\"true\\" -* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket. - * Default: \\"true\\" -* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket. - * Default: \\"true\\" -* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - * Default: \\"true\\" -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - -## Output Parameters -* GetBucketPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call - -## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "## PutBucketPublicAccessBlock -Creates or modifies the PublicAccessBlock configuration for a S3 Bucket. -", - "inputs": Object { - "Api": "PutPublicAccessBlock", - "Bucket": "{{BucketName}}", - "PublicAccessBlockConfiguration": Object { - "BlockPublicAcls": "{{ BlockPublicAcls }}", - "BlockPublicPolicy": "{{ BlockPublicPolicy }}", - "IgnorePublicAcls": "{{ IgnorePublicAcls }}", - "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", - }, - "Service": "s3", - }, - "isCritical": true, - "isEnd": false, - "maxAttempts": 2, - "name": "PutBucketPublicAccessBlock", - "timeoutSeconds": 600, - }, - Object { - "action": "aws:executeScript", - "description": "## GetBucketPublicAccessBlock -Retrieves the S3 PublicAccessBlock configuration for a S3 Bucket. -## Outputs -* Output: JSON formatted response from the GetPublicAccessBlock API call. -", - "inputs": Object { - "Handler": "validate_s3_bucket_publicaccessblock", - "InputPayload": Object { - "BlockPublicAcls": "{{ BlockPublicAcls }}", - "BlockPublicPolicy": "{{ BlockPublicPolicy }}", - "Bucket": "{{BucketName}}", - "IgnorePublicAcls": "{{ IgnorePublicAcls }}", - "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", - }, - "Runtime": "python3.6", - "Script": "import boto3 - -def validate_s3_bucket_publicaccessblock(event, context): - s3_client = boto3.client(\\"s3\\") - bucket = event[\\"Bucket\\"] - restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] - block_public_acls = event[\\"BlockPublicAcls\\"] - ignore_public_acls = event[\\"IgnorePublicAcls\\"] - block_public_policy = event[\\"BlockPublicPolicy\\"] - - output = s3_client.get_public_access_block(Bucket=bucket) - updated_block_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicAcls\\"] - updated_ignore_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"IgnorePublicAcls\\"] - updated_block_policy = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicPolicy\\"] - updated_restrict_buckets = output[\\"PublicAccessBlockConfiguration\\"][\\"RestrictPublicBuckets\\"] - - if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ - and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: - return { - \\"output\\": - { - \\"message\\": \\"Bucket public access block configuration successfully set.\\", - \\"configuration\\": output[\\"PublicAccessBlockConfiguration\\"] - } - } - else: - info = \\"CONFIGURATION VALUES DO NOT MATCH WITH PARAMETERS PROVIDED VALUES RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\".format( - restrict_public_buckets, - block_public_acls, - ignore_public_acls, - block_public_policy - ) - raise Exception(info)", - }, - "isCritical": true, - "isEnd": true, - "name": "GetBucketPublicAccessBlock", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.output", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "GetBucketPublicAccessBlock.Output", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "BlockPublicAcls": Object { - "allowedValues": Array [ - true, - false, - ], - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket.", - "type": "Boolean", - }, - "BlockPublicPolicy": Object { - "allowedValues": Array [ - true, - false, - ], - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", - "type": "Boolean", - }, - "BucketName": Object { - "allowedPattern": "(?=^.{3,63}$)(?!^(\\\\d+\\\\.)+\\\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])\\\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])$)", - "description": "(Required) The bucket name (not the ARN).", - "type": "String", - }, - "IgnorePublicAcls": Object { - "allowedValues": Array [ - true, - false, - ], - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket.", - "type": "Boolean", - }, - "RestrictPublicBuckets": Object { - "allowedValues": Array [ - true, - false, - ], - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy.", - "type": "Boolean", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-ConfigureS3BucketPublicAccessBlock", - }, - "Type": "AWS::SSM::Document", - }, - "SHARRConfigureS3PublicAccessBlockAutomationDocumentA2255F0A": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - AWSConfigRemediation-ConfigureS3PublicAccessBlock - -## What does this document do? -This document is used to create or modify the S3 [PublicAccessBlock](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options) configuration for an AWS account. - -## Input Parameters -* AccountId: (Required) Account ID of the account for which the S3 Account Public Access Block is to be configured. -* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account. - * Default: \\"true\\" -* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account. - * Default: \\"true\\" -* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain. - * Default: \\"true\\" -* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - * Default: \\"true\\" -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - -## Output Parameters -* GetPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call. -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "## PutAccountPublicAccessBlock -Creates or modifies the S3 PublicAccessBlock configuration for an AWS account. -", - "inputs": Object { - "AccountId": "{{ AccountId }}", - "Api": "PutPublicAccessBlock", - "PublicAccessBlockConfiguration": Object { - "BlockPublicAcls": "{{ BlockPublicAcls }}", - "BlockPublicPolicy": "{{ BlockPublicPolicy }}", - "IgnorePublicAcls": "{{ IgnorePublicAcls }}", - "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", - }, - "Service": "s3control", - }, - "isEnd": false, - "name": "PutAccountPublicAccessBlock", - "outputs": Array [ - Object { - "Name": "PutAccountPublicAccessBlockResponse", - "Selector": "$", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:executeScript", - "description": "## GetPublicAccessBlock -Retrieves the S3 PublicAccessBlock configuration for an AWS account. -## Outputs -* Output: JSON formatted response from the GetPublicAccessBlock API call. -", - "inputs": Object { - "Handler": "handler", - "InputPayload": Object { - "AccountId": "{{ AccountId }}", - "BlockPublicAcls": "{{ BlockPublicAcls }}", - "BlockPublicPolicy": "{{ BlockPublicPolicy }}", - "IgnorePublicAcls": "{{ IgnorePublicAcls }}", - "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", - }, - "Runtime": "python3.7", - "Script": "import boto3 -from time import sleep - -def verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy): - s3control_client = boto3.client('s3control') - wait_time = 30 - max_time = 480 - retry_count = 1 - max_retries = max_time/wait_time - while retry_count <= max_retries: - sleep(wait_time) - retry_count = retry_count + 1 - get_public_access_response = s3control_client.get_public_access_block(AccountId=account_id) - updated_block_acl = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicAcls'] - updated_ignore_acl = get_public_access_response['PublicAccessBlockConfiguration']['IgnorePublicAcls'] - updated_block_policy = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicPolicy'] - updated_restrict_buckets = get_public_access_response['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] - if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ - and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: - return { - \\"output\\": { - \\"message\\": \\"Verification successful. S3 Public Access Block Updated.\\", - \\"HTTPResponse\\": get_public_access_response[\\"PublicAccessBlockConfiguration\\"] - }, - } - raise Exception( - \\"VERFICATION FAILED. S3 GetPublicAccessBlock CONFIGURATION VALUES \\" - \\"DO NOT MATCH WITH PARAMETERS PROVIDED VALUES \\" - \\"RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\" - .format(updated_restrict_buckets, updated_block_acl, updated_ignore_acl, updated_block_policy) - ) - -def handler(event, context): - account_id = event[\\"AccountId\\"] - restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] - block_public_acls = event[\\"BlockPublicAcls\\"] - ignore_public_acls = event[\\"IgnorePublicAcls\\"] - block_public_policy = event[\\"BlockPublicPolicy\\"] - return verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy)", - }, - "isEnd": true, - "name": "GetPublicAccessBlock", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.output", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "GetPublicAccessBlock.Output", - ], - "parameters": Object { - "AccountId": Object { - "allowedPattern": "^\\\\d{12}$", - "description": "(Required) The account ID for the AWS account whose PublicAccessBlock configuration you want to set.", - "type": "String", - }, - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "BlockPublicAcls": Object { - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account.", - "type": "Boolean", - }, - "BlockPublicPolicy": Object { - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", - "type": "Boolean", - }, - "IgnorePublicAcls": Object { - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain.", - "type": "Boolean", - }, - "RestrictPublicBuckets": Object { - "default": true, - "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account.", - "type": "Boolean", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-ConfigureS3PublicAccessBlock", - }, - "Type": "AWS::SSM::Document", - }, - "SHARRCreateAccessLoggingBucketAutomationDocument63C3DE52": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CreateAccessLoggingBucket - -## What does this document do? -Creates an S3 bucket for access logging. - -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* BucketName: (Required) Name of the bucket to create -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_logging_bucket", - "InputPayload": Object { - "AWS_REGION": "{{global:REGION}}", - "BucketName": "{{BucketName}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -from botocore.exceptions import ClientError -from botocore.config import Config - -def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - -def create_logging_bucket(event, context): - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - try: - kwargs = { - 'Bucket': event['BucketName'], - 'GrantWrite': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery', - 'GrantReadACP': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery' - } - if event['AWS_REGION'] != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': event['AWS_REGION'] - } - - s3.create_bucket(**kwargs) - - s3.put_bucket_encryption( - Bucket=event['BucketName'], - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'AES256' - } - } - ] - } - ) - return { - \\"output\\": { - \\"Message\\": f'Bucket {event[\\"BucketName\\"]} created' - } - } - except ClientError as error: - if error.response['Error']['Code'] != 'BucketAlreadyExists' and \\\\ - error.response['Error']['Code'] != 'BucketAlreadyOwnedByYou': - exit(str(error)) - else: - return { - \\"output\\": { - \\"Message\\": f'Bucket {event[\\"BucketName\\"]} already exists' - } - } - except Exception as e: - print(e) - exit(str(e))", - }, - "isEnd": true, - "name": "CreateAccessLoggingBucket", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.output", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "CreateAccessLoggingBucket.Output", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "BucketName": Object { - "allowedPattern": "(?=^.{3,63}$)(?!^(\\\\d+\\\\.)+\\\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])\\\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])$)", - "description": "(Required) The bucket name (not the ARN).", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-CreateAccessLoggingBucket", - }, - "Type": "AWS::SSM::Document", - }, - "SHARRCreateCloudTrailMultiRegionTrailAutomationDocument59B9EAA0": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CreateCloudTrailMultiRegionTrail -## What does this document do? -Creates a multi-region trail with KMS encryption and enables CloudTrail -Note: this remediation will create a NEW trail. - -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data - -## Security Standards / Controls -* AFSBP v1.0.0: CloudTrail.1 -* CIS v1.2.0: 2.1 -* PCI: CloudTrail.2 -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_logging_bucket", - "InputPayload": Object { - "account": "{{global:ACCOUNT_ID}}", - "kms_key_arn": "{{KMSKeyArn}}", - "region": "{{global:REGION}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -ERROR_CREATING_BUCKET = 'Error creating bucket ' - -def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - -def create_logging_bucket(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - kms_key_arn = event['kms_key_arn'] - aws_account = event['account'] - aws_region = event['region'] - bucket_name = 'so0111-access-logs-' + aws_region + '-' + aws_account - - if create_bucket(s3, bucket_name, aws_region) == 'bucket_exists': - return {\\"logging_bucket\\": bucket_name} - encrypt_bucket(s3, bucket_name, kms_key_arn) - put_access_block(s3, bucket_name) - put_bucket_acl(s3, bucket_name) - - return {\\"logging_bucket\\": bucket_name} - -def create_bucket(s3, bucket_name, aws_region): - try: - kwargs = { - 'Bucket': bucket_name, - 'ACL': 'private' - } - if aws_region != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': aws_region - } - - s3.create_bucket(**kwargs) - - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - # bucket already exists - return - if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: - print('Bucket ' + bucket_name + ' already exists') - return 'bucket_exists' - else: - print(ex) - exit(ERROR_CREATING_BUCKET + bucket_name) - except Exception as e: - print(e) - exit(ERROR_CREATING_BUCKET + bucket_name) - -def encrypt_bucket(s3, bucket_name, kms_key_arn): - try: - s3.put_bucket_encryption( - Bucket=bucket_name, - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'aws:kms', - 'KMSMasterKeyID': kms_key_arn.split('key/')[1] - } - } - ] - } - ) - except Exception as e: - exit('Error encrypting bucket ' + bucket_name + ': ' + str(e)) - -def put_access_block(s3, bucket_name): - try: - s3.put_public_access_block( - Bucket=bucket_name, - PublicAccessBlockConfiguration={ - 'BlockPublicAcls': True, - 'IgnorePublicAcls': True, - 'BlockPublicPolicy': True, - 'RestrictPublicBuckets': True - } - ) - except Exception as e: - exit('Error setting public access block for bucket ' + bucket_name + ': ' + str(e)) - -def put_bucket_acl(s3, bucket_name): - try: - s3.put_bucket_acl( - Bucket=bucket_name, - GrantReadACP='uri=http://acs.amazonaws.com/groups/s3/LogDelivery', - GrantWrite='uri=http://acs.amazonaws.com/groups/s3/LogDelivery' - ) - except Exception as e: - exit('Error setting ACL for bucket ' + bucket_name + ': ' + str(e))", - }, - "isEnd": false, - "name": "CreateLoggingBucket", - "outputs": Array [ - Object { - "Name": "LoggingBucketName", - "Selector": "$.Payload.logging_bucket", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_encrypted_bucket", - "InputPayload": Object { - "account": "{{global:ACCOUNT_ID}}", - "kms_key_arn": "{{KMSKeyArn}}", - "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", - "region": "{{global:REGION}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0 # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - -def create_encrypted_bucket(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - kms_key_arn = event['kms_key_arn'] - aws_account = event['account'] - aws_region = event['region'] - logging_bucket = event['logging_bucket'] - bucket_name = 'so0111-aws-cloudtrail-' + aws_account - - if create_s3_bucket(s3, bucket_name, aws_region) == 'bucket_exists': - return {\\"cloudtrail_bucket\\": bucket_name} - put_bucket_encryption(s3, bucket_name, kms_key_arn) - put_public_access_block(s3, bucket_name) - put_bucket_logging(s3, bucket_name, logging_bucket) - - return {\\"cloudtrail_bucket\\": bucket_name} - -def create_s3_bucket(s3, bucket_name, aws_region): - try: - kwargs = { - 'Bucket': bucket_name, - 'ACL': 'private' - } - if aws_region != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': aws_region - } - - s3.create_bucket(**kwargs) - - except ClientError as client_ex: - exception_type = client_ex.response['Error']['Code'] - if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: - print('Bucket ' + bucket_name + ' already exists') - return 'bucket_exists' - else: - exit('Error creating bucket ' + bucket_name + ' ' + str(client_ex)) - except Exception as e: - exit('Error creating bucket ' + bucket_name + ' ' + str(e)) - -def put_bucket_encryption(s3, bucket_name, kms_key_arn): - try: - s3.put_bucket_encryption( - Bucket=bucket_name, - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'aws:kms', - 'KMSMasterKeyID': kms_key_arn.split('key/')[1] - } - } - ] - } - ) - except Exception as e: - print(e) - exit('Error applying encryption to bucket ' + bucket_name + ' with key ' + kms_key_arn) - -def put_public_access_block(s3, bucket_name): - try: - s3.put_public_access_block( - Bucket=bucket_name, - PublicAccessBlockConfiguration={ - 'BlockPublicAcls': True, - 'IgnorePublicAcls': True, - 'BlockPublicPolicy': True, - 'RestrictPublicBuckets': True - } - ) - except Exception as e: - exit(f'Error setting public access block for bucket {bucket_name}: {str(e)}') - -def put_bucket_logging(s3, bucket_name, logging_bucket): - try: - s3.put_bucket_logging( - Bucket=bucket_name, - BucketLoggingStatus={ - 'LoggingEnabled': { - 'TargetBucket': logging_bucket, - 'TargetPrefix': 'cloudtrail-access-logs' - } - } - ) - except Exception as e: - print(e) - exit('Error setting public access block for bucket ' + bucket_name)", - }, - "isEnd": false, - "name": "CreateCloudTrailBucket", - "outputs": Array [ - Object { - "Name": "CloudTrailBucketName", - "Selector": "$.Payload.cloudtrail_bucket", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_bucket_policy", - "InputPayload": Object { - "account": "{{global:ACCOUNT_ID}}", - "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", - "partition": "{{AWSPartition}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - -def create_bucket_policy(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - cloudtrail_bucket = event['cloudtrail_bucket'] - aws_partition = event['partition'] - aws_account = event['account'] - try: - bucket_policy = { - \\"Version\\": \\"2012-10-17\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSCloudTrailAclCheck20150319\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"cloudtrail.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:GetBucketAcl\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket - }, - { - \\"Sid\\": \\"AWSCloudTrailWrite20150319\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"cloudtrail.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:PutObject\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket + \\"/AWSLogs/\\" + aws_account + \\"/*\\", - \\"Condition\\": { - \\"StringEquals\\": { - \\"s3:x-amz-acl\\": \\"bucket-owner-full-control\\" - } - } - } - ] - } - s3.put_bucket_policy( - Bucket=cloudtrail_bucket, - Policy=json.dumps(bucket_policy) - ) - return { - \\"output\\": { - \\"Message\\": f'Set bucket policy for bucket {cloudtrail_bucket}' - } - } - except Exception as e: - print(e) - exit('PutBucketPolicy failed: ' + str(e))", - }, - "isEnd": false, - "name": "CreateCloudTrailBucketPolicy", - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "enable_cloudtrail", - "InputPayload": Object { - "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", - "kms_key_arn": "{{KMSKeyArn}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -def connect_to_cloudtrail(boto_config): - return boto3.client('cloudtrail', config=boto_config) - -def enable_cloudtrail(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - ct = connect_to_cloudtrail(boto_config) - - try: - ct.create_trail( - Name='multi-region-cloud-trail', - S3BucketName=event['cloudtrail_bucket'], - IncludeGlobalServiceEvents=True, - EnableLogFileValidation=True, - IsMultiRegionTrail=True, - KmsKeyId=event['kms_key_arn'] - ) - ct.start_logging( - Name='multi-region-cloud-trail' - ) - return { - \\"output\\": { - \\"Message\\": f'CloudTrail Trail multi-region-cloud-trail created' - } - } - except Exception as e: - exit('Error enabling AWS Config: ' + str(e)) - ", - }, - "isEnd": false, - "name": "EnableCloudTrail", - "outputs": Array [ - Object { - "Name": "CloudTrailBucketName", - "Selector": "$.Payload.cloudtrail_bucket", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "process_results", - "InputPayload": Object { - "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", - "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -def process_results(event, context): - print(f'Created encrypted CloudTrail bucket {event[\\"cloudtrail_bucket\\"]}') - print(f'Created access logging for CloudTrail bucket in bucket {event[\\"logging_bucket\\"]}') - print('Enabled multi-region AWS CloudTrail') - return { - \\"response\\": { - \\"message\\": \\"AWS CloudTrail successfully enabled\\", - \\"status\\": \\"Success\\" - } - }", - }, - "isEnd": true, - "name": "Remediation", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "Remediation.Output", - ], - "parameters": Object { - "AWSPartition": Object { - "allowedValues": Array [ - "aws", - "aws-cn", - "aws-us-gov", - ], - "default": "aws", - "description": "Partition for creation of ARNs.", - "type": "String", - }, - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "KMSKeyArn": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", - "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", - "description": "The ARN of the KMS key created by SHARR for this remediation", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-CreateCloudTrailMultiRegionTrail", - }, - "Type": "AWS::SSM::Document", - }, - "SHARRCreateLogMetricFilterAndAlarmAutomationDocument0AE52F3F": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CreateLogMetricFilterAndAlarm -## What does this document do? -Creates a metric filter for a given log group and also creates and alarm for the metric. - -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* CloudWatch Log Group Name: Name of the CloudWatch log group to use to create metric filter -* Alarm Value: Threshhold value for the creating an alarm for the CloudWatch Alarm - -## Security Standards / Controls -* CIS v1.2.0: 3.1-3.14 -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_encrypted_topic", - "InputPayload": Object { - "kms_key_arn": "{{KMSKeyArn}}", - "topic_name": "{{SNSTopicName}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0 # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -boto_config = Config( - retries ={ - 'mode': 'standard' - } -) - -def connect_to_sns(): - return boto3.client('sns', config=boto_config) - -def connect_to_ssm(): - return boto3.client('ssm', config=boto_config) - -def create_encrypted_topic(event, context): - - kms_key_arn = event['kms_key_arn'] - new_topic = False - topic_arn = '' - topic_name = event['topic_name'] - - try: - sns = connect_to_sns() - topic_arn = sns.create_topic( - Name=topic_name, - Attributes={ - 'KmsMasterKeyId': kms_key_arn.split('key/')[1] - } - )['TopicArn'] - new_topic = True - - except ClientError as client_exception: - exception_type = client_exception.response['Error']['Code'] - if exception_type == 'InvalidParameter': - print(f'Topic {topic_name} already exists. This remediation may have been run before.') - print('Ignoring exception - remediation continues.') - topic_arn = sns.create_topic( - Name=topic_name - )['TopicArn'] - else: - exit(f'ERROR: Unhandled client exception: {client_exception}') - - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - if new_topic: - try: - ssm = connect_to_ssm() - ssm.put_parameter( - Name='/Solutions/SO0111/SNS_Topic_CIS3.x', - Description='SNS Topic for AWS Config updates', - Type='String', - Overwrite=True, - Value=topic_arn - ) - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - create_topic_policy(topic_arn) - - return {\\"topic_arn\\": topic_arn} - -def create_topic_policy(topic_arn): - sns = connect_to_sns() - try: - topic_policy = { - \\"Id\\": \\"Policy_ID\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSConfigSNSPolicy\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": \\"cloudwatch.amazonaws.com\\" - }, - \\"Action\\": \\"SNS:Publish\\", - \\"Resource\\": topic_arn, - }] - } - - sns.set_topic_attributes( - TopicArn=topic_arn, - AttributeName='Policy', - AttributeValue=json.dumps(topic_policy) - ) - except Exception as e: - exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", - }, - "name": "CreateTopic", - "outputs": Array [ - Object { - "Name": "TopicArn", - "Selector": "$.Payload.topic_arn", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "verify", - "InputPayload": Object { - "AlarmDesc": "{{AlarmDesc}}", - "AlarmName": "{{AlarmName}}", - "AlarmThreshold": "{{AlarmThreshold}}", - "FilterName": "{{FilterName}}", - "FilterPattern": "{{FilterPattern}}", - "LogGroupName": "{{LogGroupName}}", - "MetricName": "{{MetricName}}", - "MetricNamespace": "{{MetricNamespace}}", - "MetricValue": "{{MetricValue}}", - "TopicArn": "{{CreateTopic.TopicArn}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -import logging -import os -from botocore.config import Config - -boto_config = Config( - retries={ - 'max_attempts': 10, - 'mode': 'standard' - } -) - -log = logging.getLogger() -LOG_LEVEL = str(os.getenv('LogLevel', 'INFO')) -log.setLevel(LOG_LEVEL) - - -def get_service_client(service_name): - \\"\\"\\" - Returns the service client for given the service name - :param service_name: name of the service - :return: service client - \\"\\"\\" - log.debug(\\"Getting the service client for service: {}\\".format(service_name)) - return boto3.client(service_name, config=boto_config) - - -def put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value): - \\"\\"\\" - Puts the metric filter on the CloudWatch log group with provided values - :param cw_log_group: Name of the CloudWatch log group - :param filter_name: Name of the filter - :param filter_pattern: Pattern for the filter - :param metric_name: Name of the metric - :param metric_namespace: Namespace where metric is logged - :param metric_value: Value to be logged for the metric - \\"\\"\\" - logs_client = get_service_client('logs') - log.debug(\\"Putting the metric filter with values: {}\\".format([ - cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value])) - try: - logs_client.put_metric_filter( - logGroupName=cw_log_group, - filterName=filter_name, - filterPattern=filter_pattern, - metricTransformations=[ - { - 'metricName': metric_name, - 'metricNamespace': metric_namespace, - 'metricValue': str(metric_value), - 'unit': 'Count' - } - ] - ) - except Exception as e: - exit(\\"Exception occurred while putting metric filter: \\" + str(e)) - log.debug(\\"Successfully added the metric filter.\\") - - -def put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn): - \\"\\"\\" - Puts the metric alarm for the metric name with provided values - :param alarm_name: Name for the alarm - :param alarm_desc: Description for the alarm - :param alarm_threshold: Threshold value for the alarm - :param metric_name: Name of the metric - :param metric_namespace: Namespace where metric is logged - \\"\\"\\" - cw_client = get_service_client('cloudwatch') - log.debug(\\"Putting the metric alarm with values {}\\".format( - [alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace])) - try: - cw_client.put_metric_alarm( - AlarmName=alarm_name, - AlarmDescription=alarm_desc, - ActionsEnabled=True, - OKActions=[ - topic_arn - ], - AlarmActions=[ - topic_arn - ], - MetricName=metric_name, - Namespace=metric_namespace, - Statistic='Sum', - Period=300, - Unit='Count', - EvaluationPeriods=12, - DatapointsToAlarm=1, - Threshold=alarm_threshold, - ComparisonOperator='GreaterThanOrEqualToThreshold', - TreatMissingData='notBreaching' - ) - except Exception as e: - exit(\\"Exception occurred while putting metric alarm: \\" + str(e)) - log.debug(\\"Successfully added metric alarm.\\") - - -def verify(event, context): - log.info(\\"Begin handler\\") - log.debug(\\"====Print Event====\\") - log.debug(event) - - filter_name = event['FilterName'] - filter_pattern = event['FilterPattern'] - metric_name = event['MetricName'] - metric_namespace = event['MetricNamespace'] - metric_value = event['MetricValue'] - alarm_name = event['AlarmName'] - alarm_desc = event['AlarmDesc'] - alarm_threshold = event['AlarmThreshold'] - cw_log_group = event['LogGroupName'] - topic_arn = event['TopicArn'] - - put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value) - put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn) - return { - \\"response\\": { - \\"message\\": f'Created filter {event[\\"FilterName\\"]} for metric {event[\\"MetricName\\"]}, and alarm {event[\\"AlarmName\\"]}', - \\"status\\": \\"Success\\" - } - }", - }, - "name": "CreateMetricFilerAndAlarm", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.response", - "Type": "StringMap", - }, - ], - }, - ], - "parameters": Object { - "AlarmDesc": Object { - "description": "Description of the Alarm to be created for the metric filter", - "type": "String", - }, - "AlarmName": Object { - "description": "Name of the Alarm to be created for the metric filter", - "type": "String", - }, - "AlarmThreshold": Object { - "description": "Threshold value for the alarm", - "type": "Integer", - }, - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", - "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", - "type": "String", - }, - "FilterName": Object { - "description": "Name for the metric filter", - "type": "String", - }, - "FilterPattern": Object { - "description": "Filter pattern to create metric filter", - "type": "String", - }, - "KMSKeyArn": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", - "description": "The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket", - "type": "String", - }, - "LogGroupName": Object { - "description": "Name of the log group to be used to create metric filter", - "type": "String", - }, - "MetricName": Object { - "description": "Name of the metric for metric filter", - "type": "String", - }, - "MetricNamespace": Object { - "description": "Namespace where the metrics will be sent", - "type": "String", - }, - "MetricValue": Object { - "description": "Value of the metric for metric filter", - "type": "Integer", - }, - "SNSTopicName": Object { - "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-CreateLogMetricFilterAndAlarm", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableAWSConfigAutomationDocument5BFC17F4": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - SHARR-EnableAWSConfig - -## What does this document do? -Enables AWS Config: -* Turns on recording for all resources. -* Creates an encrypted bucket for Config logging. -* Creates a logging bucket for access logs for the config bucket -* Creates an SNS topic for Config notifications -* Creates a service-linked role - -## Input Parameters -* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. -* KMSKeyArn: KMS Customer-managed key to use for encryption of Config log data and SNS Topic -* AWSServiceRoleForConfig: (Optional) The name of the exiting IAM role to use for the Config service. Default: aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig -* SNSTopicName: (Required) Name of the SNS Topic to use to post AWS Config messages. - -## Output Parameters -* Remediation.Output: STDOUT and messages from the remediation steps. -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_encrypted_topic", - "InputPayload": Object { - "kms_key_arn": "{{KMSKeyArn}}", - "topic_name": "{{SNSTopicName}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0 # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -boto_config = Config( - retries ={ - 'mode': 'standard' - } -) - -def connect_to_sns(): - return boto3.client('sns', config=boto_config) - -def connect_to_ssm(): - return boto3.client('ssm', config=boto_config) - -def create_encrypted_topic(event, context): - - kms_key_arn = event['kms_key_arn'] - new_topic = False - topic_arn = '' - topic_name = event['topic_name'] - - try: - sns = connect_to_sns() - topic_arn = sns.create_topic( - Name=topic_name, - Attributes={ - 'KmsMasterKeyId': kms_key_arn.split('key/')[1] - } - )['TopicArn'] - new_topic = True - - except ClientError as client_exception: - exception_type = client_exception.response['Error']['Code'] - if exception_type == 'InvalidParameter': - print(f'Topic {topic_name} already exists. This remediation may have been run before.') - print('Ignoring exception - remediation continues.') - topic_arn = sns.create_topic( - Name=topic_name - )['TopicArn'] - else: - exit(f'ERROR: Unhandled client exception: {client_exception}') - - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - if new_topic: - try: - ssm = connect_to_ssm() - ssm.put_parameter( - Name='/Solutions/SO0111/SNS_Topic_Config.1', - Description='SNS Topic for AWS Config updates', - Type='String', - Overwrite=True, - Value=topic_arn - ) - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - create_topic_policy(topic_arn) - - return {\\"topic_arn\\": topic_arn} - -def create_topic_policy(topic_arn): - sns = connect_to_sns() - try: - topic_policy = { - \\"Id\\": \\"Policy_ID\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSConfigSNSPolicy\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": \\"config.amazonaws.com\\" - }, - \\"Action\\": \\"SNS:Publish\\", - \\"Resource\\": topic_arn, - }] - } - - sns.set_topic_attributes( - TopicArn=topic_arn, - AttributeName='Policy', - AttributeValue=json.dumps(topic_policy) - ) - except Exception as e: - exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", - }, - "isEnd": false, - "name": "CreateTopic", - "outputs": Array [ - Object { - "Name": "TopicArn", - "Selector": "$.Payload.topic_arn", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-CreateAccessLoggingBucket", - "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket_{{global:REGION}}", - "BucketName": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", - }, - }, - "isEnd": false, - "name": "CreateAccessLoggingBucket", - }, - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "create_encrypted_bucket", - "InputPayload": Object { - "account": "{{global:ACCOUNT_ID}}", - "kms_key_arn": "{{KMSKeyArn}}", - "logging_bucket": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", - "partition": "{{global:AWS_PARTITION}}", - "region": "{{global:REGION}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError -from botocore.retries import bucket - -boto_config = Config( - retries ={ - 'mode': 'standard' - } -) - -def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - -def create_bucket(bucket_name, aws_region): - s3 = connect_to_s3(boto_config) - try: - if aws_region == 'us-east-1': - s3.create_bucket( - ACL='private', - Bucket=bucket_name - ) - else: - s3.create_bucket( - ACL='private', - Bucket=bucket_name, - CreateBucketConfiguration={ - 'LocationConstraint': aws_region - } - ) - return \\"created\\" - - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - # bucket already exists - return - if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: - print('Bucket ' + bucket_name + ' already exists') - return \\"already exists\\" - else: - exit(f'ERROR creating bucket {bucket_name}: {str(ex)}') - except Exception as e: - exit(f'ERROR creating bucket {bucket_name}: {str(e)}') - -def encrypt_bucket(bucket_name, kms_key): - s3 = connect_to_s3(boto_config) - try: - s3.put_bucket_encryption( - Bucket=bucket_name, - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'aws:kms', - 'KMSMasterKeyID': kms_key - } - } - ] - } - ) - except Exception as e: - exit(f'ERROR putting bucket encryption for {bucket_name}: {str(e)}') - -def block_public_access(bucket_name): - s3 = connect_to_s3(boto_config) - try: - s3.put_public_access_block( - Bucket=bucket_name, - PublicAccessBlockConfiguration={ - 'BlockPublicAcls': True, - 'IgnorePublicAcls': True, - 'BlockPublicPolicy': True, - 'RestrictPublicBuckets': True - } - ) - except Exception as e: - exit(f'ERROR setting public access block for bucket {bucket_name}: {str(e)}') - -def enable_access_logging(bucket_name, logging_bucket): - s3 = connect_to_s3(boto_config) - try: - s3.put_bucket_logging( - Bucket=bucket_name, - BucketLoggingStatus={ - 'LoggingEnabled': { - 'TargetBucket': logging_bucket, - 'TargetPrefix': f'access-logs/{bucket_name}' - } - } - ) - except Exception as e: - exit(f'Error setting access logging for bucket {bucket_name}: {str(e)}') - -def create_bucket_policy(config_bucket, aws_partition): - s3 = connect_to_s3(boto_config) - try: - bucket_policy = { - \\"Version\\": \\"2012-10-17\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSConfigBucketPermissionsCheck\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"config.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:GetBucketAcl\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket - }, - { - \\"Sid\\": \\"AWSConfigBucketExistenceCheck\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"config.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:ListBucket\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket - }, - { - \\"Sid\\": \\"AWSConfigBucketDelivery\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"config.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:PutObject\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket + \\"/*\\", - \\"Condition\\": { - \\"StringEquals\\": { - \\"s3:x-amz-acl\\": \\"bucket-owner-full-control\\" - } - } - } - ] - } - s3.put_bucket_policy( - Bucket=config_bucket, - Policy=json.dumps(bucket_policy) - ) - except Exception as e: - exit(f'ERROR: PutBucketPolicy failed for {config_bucket}: {str(e)}') - -def create_encrypted_bucket(event, context): - - kms_key_arn = event['kms_key_arn'] - aws_partition = event['partition'] - aws_account = event['account'] - aws_region = event['region'] - logging_bucket = event['logging_bucket'] - bucket_name = 'so0111-aws-config-' + aws_region + '-' + aws_account - - if create_bucket(bucket_name, aws_region) == 'already exists': - return {\\"config_bucket\\": bucket_name} - - encrypt_bucket(bucket_name, kms_key_arn.split('key/')[1]) - block_public_access(bucket_name) - enable_access_logging(bucket_name, logging_bucket) - create_bucket_policy(bucket_name, aws_partition) - - return {\\"config_bucket\\": bucket_name}", - }, - "isEnd": false, - "name": "CreateConfigBucket", - "outputs": Array [ - Object { - "Name": "ConfigBucketName", - "Selector": "$.Payload.config_bucket", - "Type": "String", - }, - ], - }, + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - SHARR-CreateCloudTrailMultiRegionTrail +## What does this document do? +Creates a multi-region trail with KMS encryption and enables CloudTrail +Note: this remediation will create a NEW trail. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data + +## Security Standards / Controls +* AFSBP v1.0.0: CloudTrail.1 +* CIS v1.2.0: 2.1 +* PCI: CloudTrail.2 +", + "mainSteps": Array [ Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "enable_config", + "Handler": "create_logging_bucket", "InputPayload": Object { "account": "{{global:ACCOUNT_ID}}", - "aws_service_role": "{{AWSServiceRoleForConfig}}", - "config_bucket": "{{CreateConfigBucket.ConfigBucketName}}", - "partition": "{{global:AWS_PARTITION}}", + "kms_key_arn": "{{KMSKeyArn}}", "region": "{{global:REGION}}", - "topic_arn": "{{CreateTopic.TopicArn}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0/ # # # # or in the \\"license\\" file accompanying this file. This file is distributed # # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # # or implied. See the License for the specific language governing permis- # # sions and limitations under the License. # ############################################################################### - + import boto3 from botocore.config import Config from botocore.exceptions import ClientError -boto_config = Config( - retries ={ - 'mode': 'standard' - } -) +ERROR_CREATING_BUCKET = 'Error creating bucket ' -def connect_to_config(boto_config): - return boto3.client('config', config=boto_config) +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) -def create_config_recorder(aws_partition, aws_account, aws_service_role): - cfgsvc = connect_to_config(boto_config) +def create_logging_bucket(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + kms_key_arn = event['kms_key_arn'] + aws_account = event['account'] + aws_region = event['region'] + bucket_name = 'so0111-access-logs-' + aws_region + '-' + aws_account + + if create_bucket(s3, bucket_name, aws_region) == 'bucket_exists': + return {\\"logging_bucket\\": bucket_name} + encrypt_bucket(s3, bucket_name, kms_key_arn) + put_access_block(s3, bucket_name) + put_bucket_acl(s3, bucket_name) + + return {\\"logging_bucket\\": bucket_name} + +def create_bucket(s3, bucket_name, aws_region): try: - config_service_role_arn = 'arn:' + aws_partition + ':iam::' + aws_account + ':role/' + aws_service_role - cfgsvc.put_configuration_recorder( - ConfigurationRecorder={ - 'name': 'default', - 'roleARN': config_service_role_arn, - 'recordingGroup': { - 'allSupported': True, - 'includeGlobalResourceTypes': True - } + kwargs = { + 'Bucket': bucket_name, + 'ACL': 'private' + } + if aws_region != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': aws_region } - ) + + s3.create_bucket(**kwargs) + except ClientError as ex: exception_type = ex.response['Error']['Code'] - # recorder already exists - continue - if exception_type in [\\"MaxNumberOfConfigurationRecordersExceededException\\"]: - print('Config Recorder already exists. Continuing.') + # bucket already exists - return + if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: + print('Bucket ' + bucket_name + ' already exists') + return 'bucket_exists' else: - exit(f'ERROR: Boto3 ClientError enabling Config: {exception_type} - {str(ex)}') + print(ex) + exit(ERROR_CREATING_BUCKET + bucket_name) except Exception as e: - exit(f'ERROR enabling AWS Config - create_config_recorder: {str(e)}') + print(e) + exit(ERROR_CREATING_BUCKET + bucket_name) -def create_delivery_channel(config_bucket, aws_account, topic_arn): - cfgsvc = connect_to_config(boto_config) +def encrypt_bucket(s3, bucket_name, kms_key_arn): try: - cfgsvc.put_delivery_channel( - DeliveryChannel={ - 'name': 'default', - 's3BucketName': config_bucket, - 's3KeyPrefix': aws_account, - 'snsTopicARN': topic_arn, - 'configSnapshotDeliveryProperties': { - 'deliveryFrequency': 'Twelve_Hours' - } + s3.put_bucket_encryption( + Bucket=bucket_name, + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'aws:kms', + 'KMSMasterKeyID': kms_key_arn.split('key/')[1] + } + } + ] } ) - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - # delivery channel already exists - return - if exception_type in [\\"MaxNumberOfDeliveryChannelsExceededException\\"]: - print('DeliveryChannel already exists') - else: - exit(f'ERROR: Boto3 ClientError enabling Config: {exception_type} - {str(ex)}') except Exception as e: - exit(f'ERROR enabling AWS Config - create_delivery_channel: {str(e)}') + exit('Error encrypting bucket ' + bucket_name + ': ' + str(e)) -def start_recorder(): - cfgsvc = connect_to_config(boto_config) +def put_access_block(s3, bucket_name): try: - cfgsvc.start_configuration_recorder( - ConfigurationRecorderName='default' + s3.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + 'BlockPublicAcls': True, + 'IgnorePublicAcls': True, + 'BlockPublicPolicy': True, + 'RestrictPublicBuckets': True + } ) except Exception as e: - exit(f'ERROR enabling AWS Config: {str(e)}') - -def enable_config(event, context): - aws_account = event['account'] - aws_partition = event['partition'] - aws_service_role = event['aws_service_role'] - config_bucket = event['config_bucket'] - topic_arn = event['topic_arn'] + exit('Error setting public access block for bucket ' + bucket_name + ': ' + str(e)) - create_config_recorder(aws_partition, aws_account, aws_service_role) - create_delivery_channel(config_bucket, aws_account, topic_arn) - start_recorder()", +def put_bucket_acl(s3, bucket_name): + try: + s3.put_bucket_acl( + Bucket=bucket_name, + GrantReadACP='uri=http://acs.amazonaws.com/groups/s3/LogDelivery', + GrantWrite='uri=http://acs.amazonaws.com/groups/s3/LogDelivery' + ) + except Exception as e: + exit('Error setting ACL for bucket ' + bucket_name + ': ' + str(e))", }, "isEnd": false, - "name": "EnableConfig", + "name": "CreateLoggingBucket", "outputs": Array [ Object { - "Name": "ConfigBucketName", - "Selector": "$.Payload.config_bucket", + "Name": "LoggingBucketName", + "Selector": "$.Payload.logging_bucket", "Type": "String", }, ], @@ -4450,132 +819,149 @@ def enable_config(event, context): Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "process_results", + "Handler": "create_encrypted_bucket", "InputPayload": Object { - "config_bucket": "{{CreateConfigBucket.ConfigBucketName}}", - "logging_bucket": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", - "sns_topic_arn": "{{CreateTopic.TopicArn}}", + "account": "{{global:ACCOUNT_ID}}", + "kms_key_arn": "{{KMSKeyArn}}", + "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", + "region": "{{global:REGION}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0 # # # # or in the \\"license\\" file accompanying this file. This file is distributed # # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # # or implied. See the License for the specific language governing permis- # # sions and limitations under the License. # ############################################################################### - -def process_results(event, context): - print(f'Created encrypted SNS topic {event[\\"sns_topic_arn\\"]}') - print(f'Created encrypted Config bucket {event[\\"config_bucket\\"]}') - print(f'Created access logging for Config bucket in bucket {event[\\"logging_bucket\\"]}') - print('Enabled AWS Config by creating a default recorder') - return { - \\"response\\": { - \\"message\\": \\"AWS Config successfully enabled\\", - \\"status\\": \\"Success\\" + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_encrypted_bucket(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' } - }", - }, - "isEnd": true, - "name": "Remediation", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "Remediation.Output", - ], - "parameters": Object { - "AWSServiceRoleForConfig": Object { - "allowedPattern": "^(:?[\\\\w+=,.@-]+/)+[\\\\w+=,.@-]+$", - "default": "aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", - "type": "String", - }, - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", - "type": "String", - }, - "KMSKeyArn": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", - "description": "The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket", - "type": "String", - }, - "SNSTopicName": Object { - "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableAWSConfig", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableAutoScalingGroupELBHealthCheckAutomationDocumentB72D2528": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - SHARR-EnableAutoScalingGroupELBHealthCheck + ) + s3 = connect_to_s3(boto_config) + + kms_key_arn = event['kms_key_arn'] + aws_account = event['account'] + aws_region = event['region'] + logging_bucket = event['logging_bucket'] + bucket_name = 'so0111-aws-cloudtrail-' + aws_account -## What does this document do? -This runbook enables health checks for the Amazon EC2 Auto Scaling (Auto Scaling) group you specify using the [UpdateAutoScalingGroup](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_UpdateAutoScalingGroup.html) API. + if create_s3_bucket(s3, bucket_name, aws_region) == 'bucket_exists': + return {\\"cloudtrail_bucket\\": bucket_name} + put_bucket_encryption(s3, bucket_name, kms_key_arn) + put_public_access_block(s3, bucket_name) + put_bucket_logging(s3, bucket_name, logging_bucket) -## Input Parameters -* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. -* AutoScalingGroupARN: (Required) The Amazon Resource Name (ARN) of the auto scaling group that you want to enable health checks on. -* HealthCheckGracePeriod: (Optional) The amount of time, in seconds, that Auto Scaling waits before checking the health status of an Amazon Elastic Compute Cloud (Amazon EC2) instance that has come into service. + return {\\"cloudtrail_bucket\\": bucket_name} -## Output Parameters +def create_s3_bucket(s3, bucket_name, aws_region): + try: + kwargs = { + 'Bucket': bucket_name, + 'ACL': 'private' + } + if aws_region != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': aws_region + } -* Remediation.Output - stdout messages from the remediation + s3.create_bucket(**kwargs) -## Security Standards / Controls -* AFSBP v1.0.0: Autoscaling.1 -* CIS v1.2.0: 2.1 -* PCI: Autoscaling.1 -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "Enable ELB health check type on ASG", - "inputs": Object { - "Api": "UpdateAutoScalingGroup", - "AutoScalingGroupName": "{{AutoScalingGroupName}}", - "HealthCheckGracePeriod": "{{HealthCheckGracePeriod}}", - "HealthCheckType": "ELB", - "Service": "autoscaling", + except ClientError as client_ex: + exception_type = client_ex.response['Error']['Code'] + if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: + print('Bucket ' + bucket_name + ' already exists') + return 'bucket_exists' + else: + exit('Error creating bucket ' + bucket_name + ' ' + str(client_ex)) + except Exception as e: + exit('Error creating bucket ' + bucket_name + ' ' + str(e)) + +def put_bucket_encryption(s3, bucket_name, kms_key_arn): + try: + s3.put_bucket_encryption( + Bucket=bucket_name, + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'aws:kms', + 'KMSMasterKeyID': kms_key_arn.split('key/')[1] + } + } + ] + } + ) + except Exception as e: + print(e) + exit('Error applying encryption to bucket ' + bucket_name + ' with key ' + kms_key_arn) + +def put_public_access_block(s3, bucket_name): + try: + s3.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + 'BlockPublicAcls': True, + 'IgnorePublicAcls': True, + 'BlockPublicPolicy': True, + 'RestrictPublicBuckets': True + } + ) + except Exception as e: + exit(f'Error setting public access block for bucket {bucket_name}: {str(e)}') + +def put_bucket_logging(s3, bucket_name, logging_bucket): + try: + s3.put_bucket_logging( + Bucket=bucket_name, + BucketLoggingStatus={ + 'LoggingEnabled': { + 'TargetBucket': logging_bucket, + 'TargetPrefix': 'cloudtrail-access-logs' + } + } + ) + except Exception as e: + print(e) + exit('Error setting public access block for bucket ' + bucket_name)", }, - "name": "EnableELBHealthCheck", + "isEnd": false, + "name": "CreateCloudTrailBucket", "outputs": Array [ Object { - "Name": "Output", - "Selector": "$", - "Type": "StringMap", + "Name": "CloudTrailBucketName", + "Selector": "$.Payload.cloudtrail_bucket", + "Type": "String", }, ], }, Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "verify", + "Handler": "create_bucket_policy", "InputPayload": Object { - "AsgName": "{{AutoScalingGroupName}}", + "account": "{{global:ACCOUNT_ID}}", + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "partition": "{{AWSPartition}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python @@ -4599,112 +985,77 @@ import boto3 from botocore.config import Config from botocore.exceptions import ClientError -def connect_to_autoscaling(boto_config): - return boto3.client('autoscaling', config=boto_config) +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) -def verify(event, context): +def create_bucket_policy(event, context): boto_config = Config( retries ={ 'mode': 'standard' } ) - asg_client = connect_to_autoscaling(boto_config) - asg_name = event['AsgName'] + s3 = connect_to_s3(boto_config) + + cloudtrail_bucket = event['cloudtrail_bucket'] + aws_partition = event['partition'] + aws_account = event['account'] try: - desc_asg = asg_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[asg_name] - ) - if len(desc_asg['AutoScalingGroups']) < 1: - exit(f'No AutoScaling Group found matching {asg_name}') - - health_check = desc_asg['AutoScalingGroups'][0]['HealthCheckType'] - print(json.dumps(desc_asg['AutoScalingGroups'][0], default=str)) - if (health_check == 'ELB'): - return { - \\"response\\": { - \\"message\\": \\"Autoscaling Group health check type updated to ELB\\", - \\"status\\": \\"Success\\" - } - } - else: - return { - \\"response\\": { - \\"message\\": \\"Autoscaling Group health check type is not ELB\\", - \\"status\\": \\"Failed\\" + bucket_policy = { + \\"Version\\": \\"2012-10-17\\", + \\"Statement\\": [ + { + \\"Sid\\": \\"AWSCloudTrailAclCheck20150319\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": [ + \\"cloudtrail.amazonaws.com\\" + ] + }, + \\"Action\\": \\"s3:GetBucketAcl\\", + \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket + }, + { + \\"Sid\\": \\"AWSCloudTrailWrite20150319\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": [ + \\"cloudtrail.amazonaws.com\\" + ] + }, + \\"Action\\": \\"s3:PutObject\\", + \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket + \\"/AWSLogs/\\" + aws_account + \\"/*\\", + \\"Condition\\": { + \\"StringEquals\\": { + \\"s3:x-amz-acl\\": \\"bucket-owner-full-control\\" + } + } } + ] + } + s3.put_bucket_policy( + Bucket=cloudtrail_bucket, + Policy=json.dumps(bucket_policy) + ) + return { + \\"output\\": { + \\"Message\\": f'Set bucket policy for bucket {cloudtrail_bucket}' } + } except Exception as e: - exit(\\"Exception while executing remediation: \\" + str(e))", + print(e) + exit('PutBucketPolicy failed: ' + str(e))", }, - "name": "Remediation", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.response", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "Remediation.Output", - ], - "parameters": Object { - "AutoScalingGroupName": Object { - "allowedPattern": "^[\\\\u0020-\\\\uD7FF\\\\uE000-\\\\uFFFD\\\\uD800\\\\uDC00-\\\\uDBFF\\\\uDFFF]{1,255}$", - "description": "(Required) The Amazon Resource Name (ARN) of the auto scaling group that you want to enable health checks on.", - "type": "String", - }, - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", - "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", - "type": "String", - }, - "HealthCheckGracePeriod": Object { - "allowedPattern": "^[0-9]\\\\d*$", - "default": 300, - "description": "(Optional) The amount of time, in seconds, that Auto Scaling waits before checking the health status of an Amazon Elastic Compute Cloud (Amazon EC2) instance that has come into service.", - "type": "Integer", + "isEnd": false, + "name": "CreateCloudTrailBucketPolicy", }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableAutoScalingGroupELBHealthCheck", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableCloudTrailEncryptionAutomationDocumentE1C28DB6": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-EnableCloudTrailEncryption -## What does this document do? -Enables encryption on a CloudTrail using the provided KMS CMK - -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data -* TrailRegion: region of the CloudTrail to encrypt -* TrailArn: ARN of the CloudTrail to encrypt - -## Security Standards / Controls -* AFSBP v1.0.0: CloudTrail.2 -* CIS v1.2.0: 2.7 -* PCI: CloudTrail.1 -", - "mainSteps": Array [ Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "enable_trail_encryption", + "Handler": "enable_cloudtrail", "InputPayload": Object { - "exec_region": "{{global:REGION}}", - "kms_key_arn": "{{KMSKeyArn}}", - "region": "{{global:REGION}}", - "trail": "{{TrailArn}}", - "trail_region": "{{TrailRegion}}", + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "kms_key_arn": "{{KMSKeyArn}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python @@ -4727,210 +1078,56 @@ import boto3 from botocore.config import Config from botocore.exceptions import ClientError -def connect_to_cloudtrail(region, boto_config): - return boto3.client('cloudtrail', region_name=region, config=boto_config) +def connect_to_cloudtrail(boto_config): + return boto3.client('cloudtrail', config=boto_config) + +def enable_cloudtrail(event, context): -def enable_trail_encryption(event, context): - \\"\\"\\" - remediates CloudTrail.2 by enabling SSE-KMS - On success returns a string map - On failure returns NoneType - \\"\\"\\" boto_config = Config( retries ={ 'mode': 'standard' } ) - - if event['trail_region'] != event['exec_region']: - exit('ERROR: cross-region remediation is not yet supported') - - ctrail_client = connect_to_cloudtrail(event['trail_region'], boto_config) - kms_key_arn = event['kms_key_arn'] + ct = connect_to_cloudtrail(boto_config) try: - ctrail_client.update_trail( - Name=event['trail'], - KmsKeyId=kms_key_arn + ct.create_trail( + Name='multi-region-cloud-trail', + S3BucketName=event['cloudtrail_bucket'], + IncludeGlobalServiceEvents=True, + EnableLogFileValidation=True, + IsMultiRegionTrail=True, + KmsKeyId=event['kms_key_arn'] + ) + ct.start_logging( + Name='multi-region-cloud-trail' ) return { - \\"response\\": { - \\"message\\": f'Enabled KMS CMK encryption on {event[\\"trail\\"]}', - \\"status\\": \\"Success\\" + \\"output\\": { + \\"Message\\": f'CloudTrail Trail multi-region-cloud-trail created' } } except Exception as e: - exit(f'Error enabling SSE-KMS encryption: {str(e)}')", - }, - "isEnd": true, - "name": "Remediation", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.response", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "Remediation.Output", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "KMSKeyArn": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", - "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", - "description": "The ARN of the KMS key created by SHARR for this remediation", - "type": "String", - }, - "TrailArn": Object { - "allowedPattern": "^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:trail/[A-Za-z0-9._-]{3,128}$", - "description": "ARN of the CloudTrail", - "type": "String", - }, - "TrailRegion": Object { - "allowedPattern": "^[a-z]{2}(?:-gov)?-[a-z]+-\\\\d$", - "description": "Region the CloudTrail is in", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailEncryption", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableCloudTrailLogFileValidationAutomationDocumentA3B13B24": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - AWSConfigRemediation-EnableCloudTrailLogFileValidation - -## What does this document do? -This runbook enables log file validation for your AWS CloudTrail trail using the [UpdateTrail](https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_UpdateTrail.html) API. - -## Input Parameters -* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. -* TrailName: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. - -## Output Parameters -* UpdateTrail.Output: The response of the UpdateTrail API call. - -## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "## UpdateTrail -Enables log file validation for the AWS CloudTrail trail you specify in the TrailName parameter. -## Outputs -* Output: Response from the UpdateTrail API call. -", - "inputs": Object { - "Api": "UpdateTrail", - "EnableLogFileValidation": true, - "Name": "{{ TrailName }}", - "Service": "cloudtrail", + exit('Error enabling AWS Config: ' + str(e)) + ", }, "isEnd": false, - "name": "UpdateTrail", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyTrail -Verifies log file validation is enabled for your trail. -", - "inputs": Object { - "Api": "GetTrail", - "DesiredValues": Array [ - "True", - ], - "Name": "{{ TrailName }}", - "PropertySelector": "$.Trail.LogFileValidationEnabled", - "Service": "cloudtrail", - }, - "isEnd": true, - "name": "VerifyTrail", - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "UpdateTrail.Output", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", - "type": "String", - }, - "TrailName": Object { - "allowedPattern": "(^arn:(aws[a-zA-Z-]*)?:cloudtrail:[a-z0-9-]+:\\\\d{12}:trail\\\\/(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$)|(^(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$)", - "description": "(Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for.", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailLogFileValidation", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableCloudTrailToCloudWatchLoggingAutomationDocument39CFB27F": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-EnableCloudTrailToCloudWatchLogging -## What does this document do? -Creates a CloudWatch logs group for CloudTrail data. - -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data - -## Security Standards / Controls -* AFSBP v1.0.0: N/A -* CIS v1.2.0: 2.4 -* PCI: CloudTrail.4 -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "Create the log group", - "inputs": Object { - "Api": "CreateLogGroup", - "Service": "logs", - "logGroupName": "{{LogGroupName}}", - }, - "name": "CreateLogGroup", + "name": "EnableCloudTrail", "outputs": Array [ Object { - "Name": "Output", - "Selector": "$", - "Type": "StringMap", + "Name": "CloudTrailBucketName", + "Selector": "$.Payload.cloudtrail_bucket", + "Type": "String", }, ], }, Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "wait_for_loggroup", + "Handler": "process_results", "InputPayload": Object { - "LogGroup": "{{LogGroupName}}", + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python @@ -4946,64 +1143,22 @@ Creates a CloudWatch logs group for CloudTrail data. # or in the \\"license\\" file accompanying this file. This file is distributed # # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # # or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import boto3 -from botocore.config import Config -import time - -def connect_to_logs(boto_config): - return boto3.client('logs', config=boto_config) - -def wait_for_loggroup(event, context): - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - cwl_client = connect_to_logs(boto_config) - - max_retries = 3 - attempts = 0 - while attempts < max_retries: - try: - describe_group = cwl_client.describe_log_groups(logGroupNamePrefix=event['LogGroup']) - print(len(describe_group['logGroups'])) - if len(describe_group['logGroups']) == 1: - return str(describe_group['logGroups'][0]['arn']) - elif len(describe_group['logGroups']) > 1: - exit(f'More than one Log Group matches {event[\\"LogGroup\\"]}') - else: - time.sleep(2) - attempts += 1 - - except Exception as e: - exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: {str(e)}') - - exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: Timed out')", - }, - "isEnd": false, - "name": "WaitForCreation", - "outputs": Array [ - Object { - "Name": "CloudWatchLogsGroupArn", - "Selector": "$.Payload", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeAwsApi", - "description": "Enable logging to CloudWatch Logs", - "inputs": Object { - "Api": "UpdateTrail", - "CloudWatchLogsLogGroupArn": "{{WaitForCreation.CloudWatchLogsGroupArn}}", - "CloudWatchLogsRoleArn": "{{CloudWatchLogsRole}}", - "Name": "{{TrailName}}", - "Service": "cloudtrail", +# sions and limitations under the License. # +############################################################################### + +def process_results(event, context): + print(f'Created encrypted CloudTrail bucket {event[\\"cloudtrail_bucket\\"]}') + print(f'Created access logging for CloudTrail bucket in bucket {event[\\"logging_bucket\\"]}') + print('Enabled multi-region AWS CloudTrail') + return { + \\"response\\": { + \\"message\\": \\"AWS CloudTrail successfully enabled\\", + \\"status\\": \\"Success\\" + } + }", }, - "name": "UpdateTrailToCWLogs", + "isEnd": true, + "name": "Remediation", "outputs": Array [ Object { "Name": "Output", @@ -5014,697 +1169,922 @@ def wait_for_loggroup(event, context): }, ], "outputs": Array [ - "UpdateTrailToCWLogs.Output", + "Remediation.Output", ], "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "AWSPartition": Object { + "allowedValues": Array [ + "aws", + "aws-cn", + "aws-us-gov", + ], + "default": "aws", + "description": "Partition for creation of ARNs.", "type": "String", }, - "CloudWatchLogsRole": Object { + "AutomationAssumeRole": Object { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows CloudTrail to log to CloudWatch.", - "type": "String", - }, - "LogGroupName": Object { - "allowedPattern": "^[a-zA-Z0-9-_./]{1,512}$", - "description": "(Required) The name of the Log Group for CloudTrail logs.", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, - "TrailName": Object { - "allowedPattern": "^[A-Za-z0-9._-]{3,128}$", - "description": "(Required) The name of the CloudTrail.", + "KMSKeyArn": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", + "description": "The ARN of the KMS key created by SHARR for this remediation", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailToCloudWatchLogging", + "Name": "SHARR-CreateCloudTrailMultiRegionTrail", }, "Type": "AWS::SSM::Document", }, - "SHARREnableEbsEncryptionByDefaultAutomationDocumentE19C88DF": Object { + "SHARRCreateLogMetricFilterAndAlarmAutomationDocument0AE52F3F": Object { "Properties": Object { "Content": Object { "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - AWSConfigRemediation-EnableEbsEncryptionByDefault - + "description": "### Document Name - SHARR-CreateLogMetricFilterAndAlarm ## What does this document do? -This document enables EBS encryption by default for an AWS account in the current region using the [EnableEbsEncryptionByDefault](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableEbsEncryptionByDefault.html) API. +Creates a metric filter for a given log group and also creates and alarm for the metric. ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* CloudWatch Log Group Name: Name of the CloudWatch log group to use to create metric filter +* Alarm Value: Threshhold value for the creating an alarm for the CloudWatch Alarm -## Output Parameters -* ModifyAccount.EnableEbsEncryptionByDefaultResponse: JSON formatted response from the EnableEbsEncryptionByDefault API. +## Security Standards / Controls +* CIS v1.2.0: 3.1-3.14 ", "mainSteps": Array [ Object { - "action": "aws:executeAwsApi", - "description": "## ModifyAccount -Enables EBS encryption by default for the account in the current region. -## Outputs -* EnableEbsEncryptionByDefaultResponse: Response from the EnableEbsEncryptionByDefault API. -", + "action": "aws:executeScript", "inputs": Object { - "Api": "EnableEbsEncryptionByDefault", - "Service": "ec2", - }, - "isEnd": false, - "name": "ModifyAccount", - "outputs": Array [ - Object { - "Name": "EnableEbsEncryptionByDefaultResponse", - "Selector": "$", - "Type": "StringMap", + "Handler": "create_encrypted_topic", + "InputPayload": Object { + "kms_key_arn": "{{KMSKeyArn}}", + "topic_name": "{{SNSTopicName}}", }, - ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyEbsEncryptionByDefault -Checks if EbsEncryptionByDefault is enabled correctly from the previous step. -", - "inputs": Object { - "Api": "GetEbsEncryptionByDefault", - "DesiredValues": Array [ - "True", - ], - "PropertySelector": "$.EbsEncryptionByDefault", - "Service": "ec2", - }, - "isEnd": true, - "name": "VerifyEbsEncryptionByDefault", - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "ModifyAccount.EnableEbsEncryptionByDefaultResponse", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+$", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableEbsEncryptionByDefault", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableEnhancedMonitoringOnRDSInstanceAutomationDocumentF85DD852": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - AWSConfigRemediation-EnableEnhancedMonitoringOnRDSInstance + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### -## What does this document do? -This document is used to enable enhanced monitoring on an RDS Instance using the input parameter DB Instance resourceId. +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError -## Input Parameters -* ResourceId: (Required) Resource ID of the RDS DB Instance. -* MonitoringInterval: (Optional) - * The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. - * If MonitoringRoleArn is specified, then you must also set MonitoringInterval to a value other than 0. - * Valid Values: 1, 5, 10, 15, 30, 60 - * Default: 60 -* MonitoringRoleArn: (Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) -## Output Parameters -* EnableEnhancedMonitoring.DbInstance - The standard HTTP response from the ModifyDBInstance API. -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "## DescribeDBInstances - Makes describeDBInstances API call using RDS Instance DbiResourceId to get DBInstanceId. -## Outputs -* DbInstanceIdentifier: DBInstance Identifier of the RDS Instance. -", - "inputs": Object { - "Api": "DescribeDBInstances", - "Filters": Array [ - Object { - "Name": "dbi-resource-id", - "Values": Array [ - "{{ ResourceId }}", - ], - }, - ], - "Service": "rds", +def connect_to_sns(): + return boto3.client('sns', config=boto_config) + +def connect_to_ssm(): + return boto3.client('ssm', config=boto_config) + +def create_encrypted_topic(event, context): + + kms_key_arn = event['kms_key_arn'] + new_topic = False + topic_arn = '' + topic_name = event['topic_name'] + + try: + sns = connect_to_sns() + topic_arn = sns.create_topic( + Name=topic_name, + Attributes={ + 'KmsMasterKeyId': kms_key_arn.split('key/')[1] + } + )['TopicArn'] + new_topic = True + + except ClientError as client_exception: + exception_type = client_exception.response['Error']['Code'] + if exception_type == 'InvalidParameter': + print(f'Topic {topic_name} already exists. This remediation may have been run before.') + print('Ignoring exception - remediation continues.') + topic_arn = sns.create_topic( + Name=topic_name + )['TopicArn'] + else: + exit(f'ERROR: Unhandled client exception: {client_exception}') + + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + if new_topic: + try: + ssm = connect_to_ssm() + ssm.put_parameter( + Name='/Solutions/SO0111/SNS_Topic_CIS3.x', + Description='SNS Topic for AWS Config updates', + Type='String', + Overwrite=True, + Value=topic_arn + ) + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + create_topic_policy(topic_arn) + + return {\\"topic_arn\\": topic_arn} + +def create_topic_policy(topic_arn): + sns = connect_to_sns() + try: + topic_policy = { + \\"Id\\": \\"Policy_ID\\", + \\"Statement\\": [ + { + \\"Sid\\": \\"AWSConfigSNSPolicy\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": \\"cloudwatch.amazonaws.com\\" + }, + \\"Action\\": \\"SNS:Publish\\", + \\"Resource\\": topic_arn, + }] + } + + sns.set_topic_attributes( + TopicArn=topic_arn, + AttributeName='Policy', + AttributeValue=json.dumps(topic_policy) + ) + except Exception as e: + exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", }, - "isEnd": false, - "name": "DescribeDBInstances", + "name": "CreateTopic", "outputs": Array [ Object { - "Name": "DbInstanceIdentifier", - "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Name": "TopicArn", + "Selector": "$.Payload.topic_arn", "Type": "String", }, ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyDBInstanceStatus -Verifies if DB Instance status is available before enabling enhanced monitoring. -", - "inputs": Object { - "Api": "DescribeDBInstances", - "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", - "DesiredValues": Array [ - "available", - ], - "PropertySelector": "$.DBInstances[0].DBInstanceStatus", - "Service": "rds", - }, - "isEnd": false, - "name": "VerifyDBInstanceStatus", - "timeoutSeconds": 600, - }, - Object { - "action": "aws:executeAwsApi", - "description": "## EnableEnhancedMonitoring - Makes ModifyDBInstance API call to enable Enhanced Monitoring on the RDS Instance - using the DBInstanceId from the previous action. -## Outputs - * DbInstance: The standard HTTP response from the ModifyDBInstance API. -", - "inputs": Object { - "Api": "ModifyDBInstance", - "ApplyImmediately": false, - "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", - "MonitoringInterval": "{{ MonitoringInterval }}", - "MonitoringRoleArn": "{{ MonitoringRoleArn }}", - "Service": "rds", - }, - "isEnd": false, - "name": "EnableEnhancedMonitoring", - "outputs": Array [ - Object { - "Name": "DbInstance", - "Selector": "$", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, }, Object { "action": "aws:executeScript", - "description": "## VerifyEnhancedMonitoringEnabled -Checks that the enhanced monitoring is enabled on RDS Instance in the previous step exists. -## Outputs -* Output: The standard HTTP response from the ModifyDBInstance API. -", "inputs": Object { - "Handler": "handler", + "Handler": "verify", "InputPayload": Object { - "DBIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", - "MonitoringInterval": "{{ MonitoringInterval }}", + "AlarmDesc": "{{AlarmDesc}}", + "AlarmName": "{{AlarmName}}", + "AlarmThreshold": "{{AlarmThreshold}}", + "FilterName": "{{FilterName}}", + "FilterPattern": "{{FilterPattern}}", + "LogGroupName": "{{LogGroupName}}", + "MetricName": "{{MetricName}}", + "MetricNamespace": "{{MetricNamespace}}", + "MetricValue": "{{MetricValue}}", + "TopicArn": "{{CreateTopic.TopicArn}}", }, - "Runtime": "python3.6", - "Script": "import boto3 -import time + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### -def handler(event, context): - rds_client = boto3.client(\\"rds\\") - db_instance_id = event[\\"DBIdentifier\\"] - monitoring_interval = event[\\"MonitoringInterval\\"] +import boto3 +import logging +import os +from botocore.config import Config - try: - rds_waiter = rds_client.get_waiter(\\"db_instance_available\\") - rds_waiter.wait(DBInstanceIdentifier=db_instance_id) +boto_config = Config( + retries={ + 'max_attempts': 10, + 'mode': 'standard' + } +) - db_instances = rds_client.describe_db_instances( - DBInstanceIdentifier=db_instance_id) +log = logging.getLogger() +LOG_LEVEL = str(os.getenv('LogLevel', 'INFO')) +log.setLevel(LOG_LEVEL) - for db_instance in db_instances.get(\\"DBInstances\\", [{}]): - db_monitoring_interval = db_instance.get(\\"MonitoringInterval\\") - if db_monitoring_interval == monitoring_interval: - return { - \\"output\\": db_instances[\\"ResponseMetadata\\"] - } - else: - info = \\"VERIFICATION FAILED. RDS INSTANCE MONITORING INTERVAL {} IS NOT ENABLED WITH THE REQUIRED VALUE {}\\".format( - db_monitoring_interval, monitoring_interval) - raise Exception(info) +def get_service_client(service_name): + \\"\\"\\" + Returns the service client for given the service name + :param service_name: name of the service + :return: service client + \\"\\"\\" + log.debug(\\"Getting the service client for service: {}\\".format(service_name)) + return boto3.client(service_name, config=boto_config) + + +def put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value): + \\"\\"\\" + Puts the metric filter on the CloudWatch log group with provided values + :param cw_log_group: Name of the CloudWatch log group + :param filter_name: Name of the filter + :param filter_pattern: Pattern for the filter + :param metric_name: Name of the metric + :param metric_namespace: Namespace where metric is logged + :param metric_value: Value to be logged for the metric + \\"\\"\\" + logs_client = get_service_client('logs') + log.debug(\\"Putting the metric filter with values: {}\\".format([ + cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value])) + try: + logs_client.put_metric_filter( + logGroupName=cw_log_group, + filterName=filter_name, + filterPattern=filter_pattern, + metricTransformations=[ + { + 'metricName': metric_name, + 'metricNamespace': metric_namespace, + 'metricValue': str(metric_value), + 'unit': 'Count' + } + ] + ) except Exception as e: - raise e", - }, - "isEnd": true, - "name": "VerifyEnhancedMonitoringEnabled", - "outputs": Array [ - Object { - "Name": "Output", - "Selector": "$.Payload.output", - "Type": "StringMap", - }, - ], - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "EnableEnhancedMonitoring.DbInstance", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "MonitoringInterval": Object { - "allowedValues": Array [ - 1, - 5, - 10, - 15, - 30, - 60, - ], - "default": 60, - "description": "(Optional) The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance.", - "type": "Integer", - }, - "MonitoringRoleArn": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$", - "description": "(Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs.", - "type": "String", - }, - "ResourceId": Object { - "allowedPattern": "db-[A-Z0-9]{26}", - "description": "(Required) Resource ID of the Amazon RDS instance for which Enhanced Monitoring needs to be enabled.", - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableEnhancedMonitoringOnRDSInstance", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableKeyRotationAutomationDocumentAFF68728": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - AWSConfigRemediation-EnableKeyRotation + exit(\\"Exception occurred while putting metric filter: \\" + str(e)) + log.debug(\\"Successfully added the metric filter.\\") + + +def put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn): + \\"\\"\\" + Puts the metric alarm for the metric name with provided values + :param alarm_name: Name for the alarm + :param alarm_desc: Description for the alarm + :param alarm_threshold: Threshold value for the alarm + :param metric_name: Name of the metric + :param metric_namespace: Namespace where metric is logged + \\"\\"\\" + cw_client = get_service_client('cloudwatch') + log.debug(\\"Putting the metric alarm with values {}\\".format( + [alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace])) + try: + cw_client.put_metric_alarm( + AlarmName=alarm_name, + AlarmDescription=alarm_desc, + ActionsEnabled=True, + OKActions=[ + topic_arn + ], + AlarmActions=[ + topic_arn + ], + MetricName=metric_name, + Namespace=metric_namespace, + Statistic='Sum', + Period=300, + Unit='Count', + EvaluationPeriods=12, + DatapointsToAlarm=1, + Threshold=alarm_threshold, + ComparisonOperator='GreaterThanOrEqualToThreshold', + TreatMissingData='notBreaching' + ) + except Exception as e: + exit(\\"Exception occurred while putting metric alarm: \\" + str(e)) + log.debug(\\"Successfully added metric alarm.\\") -## What does this document do? -This document enables automatic key rotation for the given AWS Key Management Service (KMS) symmetric customer master key(CMK) using [EnableKeyRotation](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html) API. -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* KeyId: (Required) The Key ID of the AWS KMS symmetric CMK. +def verify(event, context): + log.info(\\"Begin handler\\") + log.debug(\\"====Print Event====\\") + log.debug(event) -## Output Parameters -* EnableKeyRotation.EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. -", - "mainSteps": Array [ - Object { - "action": "aws:executeAwsApi", - "description": "## EnableKeyRotation -Enables automatic key rotation for the given AWS KMS CMK. -## Outputs -* EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. -", - "inputs": Object { - "Api": "EnableKeyRotation", - "KeyId": "{{ KeyId }}", - "Service": "kms", + filter_name = event['FilterName'] + filter_pattern = event['FilterPattern'] + metric_name = event['MetricName'] + metric_namespace = event['MetricNamespace'] + metric_value = event['MetricValue'] + alarm_name = event['AlarmName'] + alarm_desc = event['AlarmDesc'] + alarm_threshold = event['AlarmThreshold'] + cw_log_group = event['LogGroupName'] + topic_arn = event['TopicArn'] + + put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value) + put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn) + return { + \\"response\\": { + \\"message\\": f'Created filter {event[\\"FilterName\\"]} for metric {event[\\"MetricName\\"]}, and alarm {event[\\"AlarmName\\"]}', + \\"status\\": \\"Success\\" + } + }", }, - "isEnd": false, - "name": "EnableKeyRotation", + "name": "CreateMetricFilerAndAlarm", "outputs": Array [ Object { - "Name": "EnableKeyRotationResponse", - "Selector": "$", + "Name": "Output", + "Selector": "$.Payload.response", "Type": "StringMap", }, ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyKeyRotation -Verifies that the KeyRotationEnabled is set to true for the given AWS KMS CMK. -", - "inputs": Object { - "Api": "GetKeyRotationStatus", - "DesiredValues": Array [ - "True", - ], - "KeyId": "{{ KeyId }}", - "PropertySelector": "$.KeyRotationEnabled", - "Service": "kms", - }, - "isEnd": true, - "name": "VerifyKeyRotation", - "timeoutSeconds": 600, }, ], - "outputs": Array [ - "EnableKeyRotation.EnableKeyRotationResponse", - ], "parameters": Object { + "AlarmDesc": Object { + "description": "Description of the Alarm to be created for the metric filter", + "type": "String", + }, + "AlarmName": Object { + "description": "Name of the Alarm to be created for the metric filter", + "type": "String", + }, + "AlarmThreshold": Object { + "description": "Threshold value for the alarm", + "type": "Integer", + }, "AutomationAssumeRole": Object { "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", "type": "String", }, - "KeyId": Object { - "allowedPattern": "[a-z0-9-]{1,2048}", - "description": "(Required) The Key ID of the AWS KMS symmetric CMK.", + "FilterName": Object { + "description": "Name for the metric filter", + "type": "String", + }, + "FilterPattern": Object { + "description": "Filter pattern to create metric filter", + "type": "String", + }, + "KMSKeyArn": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", + "description": "The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket", + "type": "String", + }, + "LogGroupName": Object { + "description": "Name of the log group to be used to create metric filter", + "type": "String", + }, + "MetricName": Object { + "description": "Name of the metric for metric filter", + "type": "String", + }, + "MetricNamespace": Object { + "description": "Namespace where the metrics will be sent", + "type": "String", + }, + "MetricValue": Object { + "description": "Value of the metric for metric filter", + "type": "Integer", + }, + "SNSTopicName": Object { + "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentType": "Automation", - "Name": "SHARR-EnableKeyRotation", + "Name": "SHARR-CreateLogMetricFilterAndAlarm", }, "Type": "AWS::SSM::Document", }, - "SHARREnableRDSClusterDeletionProtectionAutomationDocumentBA07C167": Object { + "SHARREnableAWSConfigAutomationDocument5BFC17F4": Object { "Properties": Object { "Content": Object { "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - AWSConfigRemediation-EnableRDSClusterDeletionProtection + "description": "### Document name - SHARR-EnableAWSConfig ## What does this document do? -This document enables \`Deletion Protection\` on a given Amazon RDS cluster using the [ModifyDBCluster](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBCluster.html) API. -Please note, AWS Config is required to be enabled in this region for this document to work as it requires the resource ID recorded by the AWS Config service. +Enables AWS Config: +* Turns on recording for all resources. +* Creates an encrypted bucket for Config logging. +* Creates a logging bucket for access logs for the config bucket +* Creates an SNS topic for Config notifications +* Creates a service-linked role ## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* ClusterId: (Required) Resource ID of the Amazon RDS cluster. +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* KMSKeyArn: KMS Customer-managed key to use for encryption of Config log data and SNS Topic +* AWSServiceRoleForConfig: (Optional) The name of the exiting IAM role to use for the Config service. Default: aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig +* SNSTopicName: (Required) Name of the SNS Topic to use to post AWS Config messages. ## Output Parameters -* EnableRDSClusterDeletionProtection.ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. +* Remediation.Output: STDOUT and messages from the remediation steps. ", "mainSteps": Array [ Object { - "action": "aws:executeAwsApi", - "description": "## GetRDSClusterIdentifer -Accepts the resource ID of the Amazon RDS Cluster as input and returns the cluster name. -## Outputs -* DbClusterIdentifier: The ID of the DB cluster for which the input parameter matches DbClusterResourceId element from the output of the DescribeDBClusters API call. -", + "action": "aws:executeScript", "inputs": Object { - "Api": "GetResourceConfigHistory", - "Service": "config", - "limit": 1, - "resourceId": "{{ ClusterId }}", - "resourceType": "AWS::RDS::DBCluster", + "Handler": "create_encrypted_topic", + "InputPayload": Object { + "kms_key_arn": "{{KMSKeyArn}}", + "topic_name": "{{SNSTopicName}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) + +def connect_to_sns(): + return boto3.client('sns', config=boto_config) + +def connect_to_ssm(): + return boto3.client('ssm', config=boto_config) + +def create_encrypted_topic(event, context): + + kms_key_arn = event['kms_key_arn'] + new_topic = False + topic_arn = '' + topic_name = event['topic_name'] + + try: + sns = connect_to_sns() + topic_arn = sns.create_topic( + Name=topic_name, + Attributes={ + 'KmsMasterKeyId': kms_key_arn.split('key/')[1] + } + )['TopicArn'] + new_topic = True + + except ClientError as client_exception: + exception_type = client_exception.response['Error']['Code'] + if exception_type == 'InvalidParameter': + print(f'Topic {topic_name} already exists. This remediation may have been run before.') + print('Ignoring exception - remediation continues.') + topic_arn = sns.create_topic( + Name=topic_name + )['TopicArn'] + else: + exit(f'ERROR: Unhandled client exception: {client_exception}') + + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + if new_topic: + try: + ssm = connect_to_ssm() + ssm.put_parameter( + Name='/Solutions/SO0111/SNS_Topic_Config.1', + Description='SNS Topic for AWS Config updates', + Type='String', + Overwrite=True, + Value=topic_arn + ) + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + create_topic_policy(topic_arn) + + return {\\"topic_arn\\": topic_arn} + +def create_topic_policy(topic_arn): + sns = connect_to_sns() + try: + topic_policy = { + \\"Id\\": \\"Policy_ID\\", + \\"Statement\\": [ + { + \\"Sid\\": \\"AWSConfigSNSPolicy\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": \\"config.amazonaws.com\\" + }, + \\"Action\\": \\"SNS:Publish\\", + \\"Resource\\": topic_arn, + }] + } + + sns.set_topic_attributes( + TopicArn=topic_arn, + AttributeName='Policy', + AttributeValue=json.dumps(topic_policy) + ) + except Exception as e: + exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", }, "isEnd": false, - "name": "GetRDSClusterIdentifer", + "name": "CreateTopic", "outputs": Array [ Object { - "Name": "DbClusterIdentifier", - "Selector": "$.configurationItems[0].resourceName", + "Name": "TopicArn", + "Selector": "$.Payload.topic_arn", "Type": "String", }, ], - "timeoutSeconds": 600, }, Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyDBClusterStatus -Verifies if the DB Cluster status is available before enabling cluster deletion protection. -", + "action": "aws:executeAutomation", "inputs": Object { - "Api": "DescribeDBClusters", - "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", - "DesiredValues": Array [ - "available", - ], - "PropertySelector": "$.DBClusters[0].Status", - "Service": "rds", + "DocumentName": "SHARR-CreateAccessLoggingBucket", + "RuntimeParameters": Object { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket", + "BucketName": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", + }, }, "isEnd": false, - "name": "VerifyDBClusterStatus", - "timeoutSeconds": 600, + "name": "CreateAccessLoggingBucket", }, Object { - "action": "aws:executeAwsApi", - "description": "## EnableRDSClusterDeletionProtection -Enables deletion protection on the Amazon RDS Cluster. -## Outputs -* ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. -", + "action": "aws:executeScript", "inputs": Object { - "Api": "ModifyDBCluster", - "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", - "DeletionProtection": true, - "Service": "rds", - }, - "isEnd": false, - "name": "EnableRDSClusterDeletionProtection", - "outputs": Array [ - Object { - "Name": "ModifyDBClusterResponse", - "Selector": "$", - "Type": "StringMap", + "Handler": "create_encrypted_bucket", + "InputPayload": Object { + "account": "{{global:ACCOUNT_ID}}", + "kms_key_arn": "{{KMSKeyArn}}", + "logging_bucket": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", + "partition": "{{global:AWS_PARTITION}}", + "region": "{{global:REGION}}", }, - ], - "timeoutSeconds": 600, - }, - Object { - "action": "aws:assertAwsResourceProperty", - "description": "## VerifyDBClusterModification -Verifies that deletion protection has been enabled for the given Amazon RDS database cluster. -", - "inputs": Object { - "Api": "DescribeDBClusters", - "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", - "DesiredValues": Array [ - "True", - ], - "PropertySelector": "$.DBClusters[0].DeletionProtection", - "Service": "rds", - }, - "isEnd": true, - "name": "VerifyDBClusterModification", - "timeoutSeconds": 600, - }, - ], - "outputs": Array [ - "EnableRDSClusterDeletionProtection.ModifyDBClusterResponse", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError +from botocore.retries import bucket + +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_bucket(bucket_name, aws_region): + s3 = connect_to_s3(boto_config) + try: + if aws_region == 'us-east-1': + s3.create_bucket( + ACL='private', + Bucket=bucket_name + ) + else: + s3.create_bucket( + ACL='private', + Bucket=bucket_name, + CreateBucketConfiguration={ + 'LocationConstraint': aws_region + } + ) + return \\"created\\" + + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # bucket already exists - return + if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: + print('Bucket ' + bucket_name + ' already exists') + return \\"already exists\\" + else: + exit(f'ERROR creating bucket {bucket_name}: {str(ex)}') + except Exception as e: + exit(f'ERROR creating bucket {bucket_name}: {str(e)}') + +def encrypt_bucket(bucket_name, kms_key): + s3 = connect_to_s3(boto_config) + try: + s3.put_bucket_encryption( + Bucket=bucket_name, + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'aws:kms', + 'KMSMasterKeyID': kms_key + } + } + ] + } + ) + except Exception as e: + exit(f'ERROR putting bucket encryption for {bucket_name}: {str(e)}') + +def block_public_access(bucket_name): + s3 = connect_to_s3(boto_config) + try: + s3.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + 'BlockPublicAcls': True, + 'IgnorePublicAcls': True, + 'BlockPublicPolicy': True, + 'RestrictPublicBuckets': True + } + ) + except Exception as e: + exit(f'ERROR setting public access block for bucket {bucket_name}: {str(e)}') + +def enable_access_logging(bucket_name, logging_bucket): + s3 = connect_to_s3(boto_config) + try: + s3.put_bucket_logging( + Bucket=bucket_name, + BucketLoggingStatus={ + 'LoggingEnabled': { + 'TargetBucket': logging_bucket, + 'TargetPrefix': f'access-logs/{bucket_name}' + } + } + ) + except Exception as e: + exit(f'Error setting access logging for bucket {bucket_name}: {str(e)}') + +def create_bucket_policy(config_bucket, aws_partition): + s3 = connect_to_s3(boto_config) + try: + bucket_policy = { + \\"Version\\": \\"2012-10-17\\", + \\"Statement\\": [ + { + \\"Sid\\": \\"AWSConfigBucketPermissionsCheck\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": [ + \\"config.amazonaws.com\\" + ] + }, + \\"Action\\": \\"s3:GetBucketAcl\\", + \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket }, - "ClusterId": Object { - "allowedPattern": "^[a-zA-Z0-9-]{1,35}$", - "description": "(Required) Amazon RDS cluster resourceId for which deletion protection needs to be enabled.", - "type": "String", + { + \\"Sid\\": \\"AWSConfigBucketExistenceCheck\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": [ + \\"config.amazonaws.com\\" + ] + }, + \\"Action\\": \\"s3:ListBucket\\", + \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-EnableRDSClusterDeletionProtection", - }, - "Type": "AWS::SSM::Document", - }, - "SHARREnableVPCFlowLogsAutomationDocument450A4332": Object { - "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-EnableVPCFlowLogs -## What does this document do? -Enables VPC Flow Logs for a given VPC + { + \\"Sid\\": \\"AWSConfigBucketDelivery\\", + \\"Effect\\": \\"Allow\\", + \\"Principal\\": { + \\"Service\\": [ + \\"config.amazonaws.com\\" + ] + }, + \\"Action\\": \\"s3:PutObject\\", + \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + config_bucket + \\"/*\\", + \\"Condition\\": { + \\"StringEquals\\": { + \\"s3:x-amz-acl\\": \\"bucket-owner-full-control\\" + } + } + } + ] + } + s3.put_bucket_policy( + Bucket=config_bucket, + Policy=json.dumps(bucket_policy) + ) + except Exception as e: + exit(f'ERROR: PutBucketPolicy failed for {config_bucket}: {str(e)}') -## Input Parameters -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -* VPC: VPC Id of the VPC for which logs are to be enabled -* RemediationRole: role arn of the role to use for logging -* KMSKeyArn: Amazon Resource Name (ARN) of the KMS Customer-Managed Key to use to encrypt the log group +def create_encrypted_bucket(event, context): + + kms_key_arn = event['kms_key_arn'] + aws_partition = event['partition'] + aws_account = event['account'] + aws_region = event['region'] + logging_bucket = event['logging_bucket'] + bucket_name = 'so0111-aws-config-' + aws_region + '-' + aws_account -## Security Standards / Controls -* AFSBP v1.0.0: CloudTrail.2 -* CIS v1.2.0: 2.7 -* PCI: CloudTrail.1 -", - "mainSteps": Array [ + if create_bucket(bucket_name, aws_region) == 'already exists': + return {\\"config_bucket\\": bucket_name} + + encrypt_bucket(bucket_name, kms_key_arn.split('key/')[1]) + block_public_access(bucket_name) + enable_access_logging(bucket_name, logging_bucket) + create_bucket_policy(bucket_name, aws_partition) + + return {\\"config_bucket\\": bucket_name}", + }, + "isEnd": false, + "name": "CreateConfigBucket", + "outputs": Array [ + Object { + "Name": "ConfigBucketName", + "Selector": "$.Payload.config_bucket", + "Type": "String", + }, + ], + }, Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "enable_flow_logs", + "Handler": "enable_config", "InputPayload": Object { - "kms_key_arn": "{{KMSKeyArn}}", - "remediation_role": "{{RemediationRole}}", - "vpc": "{{VPC}}", + "account": "{{global:ACCOUNT_ID}}", + "aws_service_role": "{{AWSServiceRoleForConfig}}", + "config_bucket": "{{CreateConfigBucket.ConfigBucketName}}", + "partition": "{{global:AWS_PARTITION}}", + "region": "{{global:REGION}}", + "topic_arn": "{{CreateTopic.TopicArn}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0/ # # # # or in the \\"license\\" file accompanying this file. This file is distributed # # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # # or implied. See the License for the specific language governing permis- # # sions and limitations under the License. # ############################################################################### - + import boto3 -import time from botocore.config import Config from botocore.exceptions import ClientError -def connect_to_logs(boto_config): - return boto3.client('logs', config=boto_config) - -def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - -def log_group_exists(client, group): - try: - log_group_verification = client.describe_log_groups( - logGroupNamePrefix=group - )['logGroups'] - if len(log_group_verification) >= 1: - for existing_loggroup in log_group_verification: - if existing_loggroup['logGroupName'] == group: - return 1 - return 0 - - except Exception as e: - exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) -def wait_for_loggroup(client, wait_interval, max_retries, loggroup): - attempts = 1 - while not log_group_exists(client, loggroup): - time.sleep(wait_interval) - attempts += 1 - if attempts > max_retries: - exit(f'Timeout waiting for log group {loggroup} to become active') +def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) -def flowlogs_active(client, loggroup): - # searches for flow log status, filtered on unique CW Log Group created earlier +def create_config_recorder(aws_partition, aws_account, aws_service_role): + cfgsvc = connect_to_config(boto_config) try: - flow_status = client.describe_flow_logs( - DryRun=False, - Filters=[ - { - 'Name': 'log-group-name', - 'Values': [loggroup] - }, - ] - )['FlowLogs'] - if len(flow_status) == 1 and flow_status[0]['FlowLogStatus'] == 'ACTIVE': - return 1 + config_service_role_arn = 'arn:' + aws_partition + ':iam::' + aws_account + ':role/' + aws_service_role + cfgsvc.put_configuration_recorder( + ConfigurationRecorder={ + 'name': 'default', + 'roleARN': config_service_role_arn, + 'recordingGroup': { + 'allSupported': True, + 'includeGlobalResourceTypes': True + } + } + ) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # recorder already exists - continue + if exception_type in [\\"MaxNumberOfConfigurationRecordersExceededException\\"]: + print('Config Recorder already exists. Continuing.') else: - return 0 - + exit(f'ERROR: Boto3 ClientError enabling Config: {exception_type} - {str(ex)}') except Exception as e: - exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') - -def wait_for_flowlogs(client, wait_interval, max_retries, loggroup): - attempts = 1 - while not flowlogs_active(client, loggroup): - time.sleep(wait_interval) - attempts += 1 - if attempts > max_retries: - exit(f'Timeout waiting for flowlogs to log group {loggroup} to become active') - -def enable_flow_logs(event, context): - \\"\\"\\" - remediates CloudTrail.2 by enabling SSE-KMS - On success returns a string map - On failure returns NoneType - \\"\\"\\" - max_retries = event.get('retries', 12) # max number of waits for actions to complete. - wait_interval = event.get('wait', 5) # how many seconds between attempts - - boto_config_args = { - 'retries': { - 'mode': 'standard' - } - } - - boto_config = Config(**boto_config_args) - - if 'vpc' not in event or 'remediation_role' not in event or 'kms_key_arn' not in event: - exit('Error: missing vpc from input') + exit(f'ERROR enabling AWS Config - create_config_recorder: {str(e)}') - logs_client = connect_to_logs(boto_config) - ec2_client = connect_to_ec2(boto_config) - - kms_key_arn = event['kms_key_arn'] # for logs encryption at rest - - # set dynamic variable for CW Log Group for VPC Flow Logs - vpc_flow_loggroup = \\"VPCFlowLogs/\\" + event['vpc'] - # create cloudwatch log group +def create_delivery_channel(config_bucket, aws_account, topic_arn): + cfgsvc = connect_to_config(boto_config) try: - logs_client.create_log_group( - logGroupName=vpc_flow_loggroup, - kmsKeyId=kms_key_arn + cfgsvc.put_delivery_channel( + DeliveryChannel={ + 'name': 'default', + 's3BucketName': config_bucket, + 's3KeyPrefix': aws_account, + 'snsTopicARN': topic_arn, + 'configSnapshotDeliveryProperties': { + 'deliveryFrequency': 'Twelve_Hours' + } + } ) - except ClientError as client_error: - exception_type = client_error.response['Error']['Code'] - - if exception_type in [\\"ResourceAlreadyExistsException\\"]: - print(f'CloudWatch Logs group {vpc_flow_loggroup} already exists') + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # delivery channel already exists - return + if exception_type in [\\"MaxNumberOfDeliveryChannelsExceededException\\"]: + print('DeliveryChannel already exists') else: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') - + exit(f'ERROR: Boto3 ClientError enabling Config: {exception_type} - {str(ex)}') except Exception as e: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(e)}') - - # wait for CWL creation to propagate - wait_for_loggroup(logs_client, wait_interval, max_retries, vpc_flow_loggroup) + exit(f'ERROR enabling AWS Config - create_delivery_channel: {str(e)}') - # create VPC Flow Logging +def start_recorder(): + cfgsvc = connect_to_config(boto_config) try: - ec2_client.create_flow_logs( - DryRun=False, - DeliverLogsPermissionArn=event['remediation_role'], - LogGroupName=vpc_flow_loggroup, - ResourceIds=[event['vpc']], - ResourceType='VPC', - TrafficType='REJECT', - LogDestinationType='cloud-watch-logs' + cfgsvc.start_configuration_recorder( + ConfigurationRecorderName='default' ) - except ClientError as client_error: - exception_type = client_error.response['Error']['Code'] - - if exception_type in [\\"FlowLogAlreadyExists\\"]: - return { - \\"response\\": { - \\"message\\": f'VPC Flow Logs for {event[\\"vpc\\"]} already enabled', - \\"status\\": \\"Success\\" - } - } - else: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') except Exception as e: - exit(f'create_flow_logs failed {str(e)}') + exit(f'ERROR enabling AWS Config: {str(e)}') - # wait for Flow Log creation to propagate. Exits on timeout (no need to check results) - wait_for_flowlogs(ec2_client, wait_interval, max_retries, vpc_flow_loggroup) +def enable_config(event, context): + aws_account = event['account'] + aws_partition = event['partition'] + aws_service_role = event['aws_service_role'] + config_bucket = event['config_bucket'] + topic_arn = event['topic_arn'] - # wait_for_flowlogs will exit if unsuccessful after max_retries * wait_interval (60 seconds by default) + create_config_recorder(aws_partition, aws_account, aws_service_role) + create_delivery_channel(config_bucket, aws_account, topic_arn) + start_recorder()", + }, + "isEnd": false, + "name": "EnableConfig", + "outputs": Array [ + Object { + "Name": "ConfigBucketName", + "Selector": "$.Payload.config_bucket", + "Type": "String", + }, + ], + }, + Object { + "action": "aws:executeScript", + "inputs": Object { + "Handler": "process_results", + "InputPayload": Object { + "config_bucket": "{{CreateConfigBucket.ConfigBucketName}}", + "logging_bucket": "so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}", + "sns_topic_arn": "{{CreateTopic.TopicArn}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +def process_results(event, context): + print(f'Created encrypted SNS topic {event[\\"sns_topic_arn\\"]}') + print(f'Created encrypted Config bucket {event[\\"config_bucket\\"]}') + print(f'Created access logging for Config bucket in bucket {event[\\"logging_bucket\\"]}') + print('Enabled AWS Config by creating a default recorder') return { \\"response\\": { - \\"message\\": f'VPC Flow Logs enabled for {event[\\"vpc\\"]} to {vpc_flow_loggroup}', + \\"message\\": \\"AWS Config successfully enabled\\", \\"status\\": \\"Success\\" } }", @@ -5714,7 +2094,7 @@ def enable_flow_logs(event, context): "outputs": Array [ Object { "Name": "Output", - "Selector": "$.Payload.response", + "Selector": "$", "Type": "StringMap", }, ], @@ -5724,352 +2104,142 @@ def enable_flow_logs(event, context): "Remediation.Output", ], "parameters": Object { + "AWSServiceRoleForConfig": Object { + "allowedPattern": "^(:?[\\\\w+=,.@-]+/)+[\\\\w+=,.@-]+$", + "default": "aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "type": "String", + }, "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", "type": "String", }, "KMSKeyArn": Object { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", - "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", - "description": "The ARN of the KMS key created by SHARR for remediations requiring encryption", - "type": "String", - }, - "RemediationRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "description": "The ARN of the role that will allow VPC Flow Logs to log to CloudWatch logs", + "description": "The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket", "type": "String", }, - "VPC": Object { - "allowedPattern": "^vpc-[0-9a-f]{8,17}", - "description": "The VPC ID of the VPC", + "SNSTopicName": Object { + "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentType": "Automation", - "Name": "SHARR-EnableVPCFlowLogs", + "Name": "SHARR-EnableAWSConfig", }, "Type": "AWS::SSM::Document", }, - "SHARRMakeEBSSnapshotsPrivateAutomationDocumentE34DB88E": Object { + "SHARREnableAutoScalingGroupELBHealthCheckAutomationDocumentB72D2528": Object { "Properties": Object { "Content": Object { "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - SHARR-MakeEBSSnapshotPrivate + "description": "### Document name - SHARR-EnableAutoScalingGroupELBHealthCheck ## What does this document do? -This runbook works an the account level to remove public share on all EBS snapshots +This runbook enables health checks for the Amazon EC2 Auto Scaling (Auto Scaling) group you specify using the [UpdateAutoScalingGroup](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_UpdateAutoScalingGroup.html) API. ## Input Parameters * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* AutoScalingGroupARN: (Required) The Amazon Resource Name (ARN) of the auto scaling group that you want to enable health checks on. +* HealthCheckGracePeriod: (Optional) The amount of time, in seconds, that Auto Scaling waits before checking the health status of an Amazon Elastic Compute Cloud (Amazon EC2) instance that has come into service. ## Output Parameters * Remediation.Output - stdout messages from the remediation ## Security Standards / Controls -* AFSBP v1.0.0: EC2.1 -* CIS v1.2.0: n/a -* PCI: EC2.1 +* AFSBP v1.0.0: Autoscaling.1 +* CIS v1.2.0: 2.1 +* PCI: Autoscaling.1 ", "mainSteps": Array [ Object { - "action": "aws:executeScript", + "action": "aws:executeAwsApi", + "description": "Enable ELB health check type on ASG", "inputs": Object { - "Handler": "get_public_snapshots", - "InputPayload": Object { - "account_id": "{{AccountId}}", - "region": "{{global:REGION}}", - "testmode": "{{TestMode}}", - }, - "Runtime": "python3.7", - "Script": "#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - -def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - -def get_public_snapshots(event, context): - account_id = event['account_id'] - - if 'testmode' in event and event['testmode']: - return [ - { - \\"Description\\": \\"Snapshot of idle volume before deletion\\", - \\"Encrypted\\": False, - \\"OwnerId\\": \\"111111111111\\", - \\"Progress\\": \\"100%\\", - \\"SnapshotId\\": \\"snap-12341234123412345\\", - \\"StartTime\\": \\"2021-03-11T08:23:02.785Z\\", - \\"State\\": \\"completed\\", - \\"VolumeId\\": \\"vol-12341234123412345\\", - \\"VolumeSize\\": 4, - \\"Tags\\": [ - { - \\"Key\\": \\"SnapshotDate\\", - \\"Value\\": \\"2021-03-11 08:23:02.376859\\" - }, - { - \\"Key\\": \\"DeleteEBSVolOnCompletion\\", - \\"Value\\": \\"False\\" - }, - { - \\"Key\\": \\"SnapshotReason\\", - \\"Value\\": \\"Idle Volume\\" - } - ] - }, - { - \\"Description\\": \\"Snapshot of idle volume before deletion\\", - \\"Encrypted\\": False, - \\"OwnerId\\": \\"111111111111\\", - \\"Progress\\": \\"100%\\", - \\"SnapshotId\\": \\"snap-12341234123412345\\", - \\"StartTime\\": \\"2021-03-11T08:20:37.399Z\\", - \\"State\\": \\"completed\\", - \\"VolumeId\\": \\"vol-12341234123412345\\", - \\"VolumeSize\\": 4, - \\"Tags\\": [ - { - \\"Key\\": \\"DeleteEBSVolOnCompletion\\", - \\"Value\\": \\"False\\" - }, - { - \\"Key\\": \\"SnapshotDate\\", - \\"Value\\": \\"2021-03-11 08:20:37.224101\\" - }, - { - \\"Key\\": \\"SnapshotReason\\", - \\"Value\\": \\"Idle Volume\\" - } - ] - }, - { - \\"Description\\": \\"Snapshot of idle volume before deletion\\", - \\"Encrypted\\": False, - \\"OwnerId\\": \\"111111111111\\", - \\"Progress\\": \\"100%\\", - \\"SnapshotId\\": \\"snap-12341234123412345\\", - \\"StartTime\\": \\"2021-03-11T08:22:48.936Z\\", - \\"State\\": \\"completed\\", - \\"VolumeId\\": \\"vol-12341234123412345\\", - \\"VolumeSize\\": 4, - \\"Tags\\": [ - { - \\"Key\\": \\"SnapshotReason\\", - \\"Value\\": \\"Idle Volume\\" - }, - { - \\"Key\\": \\"SnapshotDate\\", - \\"Value\\": \\"2021-03-11 08:22:48.714893\\" - }, - { - \\"Key\\": \\"DeleteEBSVolOnCompletion\\", - \\"Value\\": \\"False\\" - } - ] - }, - { - \\"Description\\": \\"Snapshot of idle volume before deletion\\", - \\"Encrypted\\": False, - \\"OwnerId\\": \\"111111111111\\", - \\"Progress\\": \\"100%\\", - \\"SnapshotId\\": \\"snap-12341234123412345\\", - \\"StartTime\\": \\"2021-03-11T08:23:05.156Z\\", - \\"State\\": \\"completed\\", - \\"VolumeId\\": \\"vol-12341234123412345\\", - \\"VolumeSize\\": 4, - \\"Tags\\": [ - { - \\"Key\\": \\"DeleteEBSVolOnCompletion\\", - \\"Value\\": \\"False\\" - }, - { - \\"Key\\": \\"SnapshotReason\\", - \\"Value\\": \\"Idle Volume\\" - }, - { - \\"Key\\": \\"SnapshotDate\\", - \\"Value\\": \\"2021-03-11 08:23:04.876640\\" - } - ] - }, - { - \\"Description\\": \\"Snapshot of idle volume before deletion\\", - \\"Encrypted\\": False, - \\"OwnerId\\": \\"111111111111\\", - \\"Progress\\": \\"100%\\", - \\"SnapshotId\\": \\"snap-12341234123412345\\", - \\"StartTime\\": \\"2021-03-11T08:22:34.850Z\\", - \\"State\\": \\"completed\\", - \\"VolumeId\\": \\"vol-12341234123412345\\", - \\"VolumeSize\\": 4, - \\"Tags\\": [ - { - \\"Key\\": \\"DeleteEBSVolOnCompletion\\", - \\"Value\\": \\"False\\" - }, - { - \\"Key\\": \\"SnapshotReason\\", - \\"Value\\": \\"Idle Volume\\" - }, - { - \\"Key\\": \\"SnapshotDate\\", - \\"Value\\": \\"2021-03-11 08:22:34.671355\\" - } - ] - } - ] - - return list_public_snapshots(account_id) - -def list_public_snapshots(account_id): - ec2 = connect_to_ec2(boto_config) - control_token = 'start' - try: - - buffer = [] - - while control_token: - - if control_token == 'start': # needed a value to start the loop. Now reset it - control_token = '' - - kwargs = { - 'MaxResults': 100, - 'OwnerIds': [ account_id ], - 'RestorableByUserIds': [ 'all' ] - } - if control_token: - kwargs['NextToken'] = control_token - - response = ec2.describe_snapshots( - **kwargs - ) - - if 'NextToken' in response: - control_token = response['NextToken'] - else: - control_token = '' - - buffer += response['Snapshots'] - - return buffer - except Exception as e: - print(e) - exit('Failed to describe_snapshots')", + "Api": "UpdateAutoScalingGroup", + "AutoScalingGroupName": "{{AutoScalingGroupName}}", + "HealthCheckGracePeriod": "{{HealthCheckGracePeriod}}", + "HealthCheckType": "ELB", + "Service": "autoscaling", }, - "name": "GetPublicSnapshotIds", + "name": "EnableELBHealthCheck", "outputs": Array [ Object { - "Name": "Snapshots", - "Selector": "$.Payload", - "Type": "MapList", + "Name": "Output", + "Selector": "$", + "Type": "StringMap", }, ], }, Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "make_snapshots_private", + "Handler": "verify", "InputPayload": Object { - "region": "{{global:REGION}}", - "snapshots": "{{GetPublicSnapshotIds.Snapshots}}", + "AsgName": "{{AutoScalingGroupName}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the \\"license\\" file accompanying this file. This file is distributed # -# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### - -import json -import boto3 -from botocore.config import Config -from botocore.exceptions import ClientError - -def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - -def make_snapshots_private(event, context): - boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - ec2 = connect_to_ec2(boto_config) - - remediated = [] - snapshots = event['snapshots'] +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### - success_count = 0 - - for snapshot in snapshots: - try: - ec2.modify_snapshot_attribute( - Attribute='CreateVolumePermission', - CreateVolumePermission={ - 'Remove': [{'Group': 'all'}] - }, - SnapshotId=snapshot['SnapshotId'] - ) - print('Snapshot ' + snapshot['SnapshotId'] + ' permissions set to private') +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError - remediated.append(snapshot['SnapshotId']) - success_count += 1 - except Exception as e: - print(e) - print('FAILED to remediate Snapshot ' + snapshot['SnapshotId']) +def connect_to_autoscaling(boto_config): + return boto3.client('autoscaling', config=boto_config) - result=json.dumps(ec2.describe_snapshots( - SnapshotIds=remediated - ), indent=2, default=str) - print(result) +def verify(event, context): - return { - \\"response\\": { - \\"message\\": f'{success_count} of {len(snapshots)} Snapshot permissions set to private', - \\"status\\": \\"Success\\" + boto_config = Config( + retries ={ + 'mode': 'standard' } - }", + ) + asg_client = connect_to_autoscaling(boto_config) + asg_name = event['AsgName'] + try: + desc_asg = asg_client.describe_auto_scaling_groups( + AutoScalingGroupNames=[asg_name] + ) + if len(desc_asg['AutoScalingGroups']) < 1: + exit(f'No AutoScaling Group found matching {asg_name}') + + health_check = desc_asg['AutoScalingGroups'][0]['HealthCheckType'] + print(json.dumps(desc_asg['AutoScalingGroups'][0], default=str)) + if (health_check == 'ELB'): + return { + \\"response\\": { + \\"message\\": \\"Autoscaling Group health check type updated to ELB\\", + \\"status\\": \\"Success\\" + } + } + else: + return { + \\"response\\": { + \\"message\\": \\"Autoscaling Group health check type is not ELB\\", + \\"status\\": \\"Failed\\" + } + } + except Exception as e: + exit(\\"Exception while executing remediation: \\" + str(e))", }, "name": "Remediation", "outputs": Array [ @@ -6085,9 +2255,9 @@ def make_snapshots_private(event, context): "Remediation.Output", ], "parameters": Object { - "AccountId": Object { - "allowedPattern": "^[0-9]{12}$", - "description": "Account ID of the account for which snapshots are to be checked.", + "AutoScalingGroupName": Object { + "allowedPattern": "^[\\\\u0020-\\\\uD7FF\\\\uE000-\\\\uFFFD\\\\uD800\\\\uDC00-\\\\uDBFF\\\\uDFFF]{1,255}$", + "description": "(Required) The Amazon Resource Name (ARN) of the auto scaling group that you want to enable health checks on.", "type": "String", }, "AutomationAssumeRole": Object { @@ -6095,61 +2265,61 @@ def make_snapshots_private(event, context): "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", "type": "String", }, - "TestMode": Object { - "default": false, - "description": "Enables test mode, which generates a list of fake volume Ids", - "type": "Boolean", + "HealthCheckGracePeriod": Object { + "allowedPattern": "^[0-9]\\\\d*$", + "default": 300, + "description": "(Optional) The amount of time, in seconds, that Auto Scaling waits before checking the health status of an Amazon Elastic Compute Cloud (Amazon EC2) instance that has come into service.", + "type": "Integer", }, }, "schemaVersion": "0.3", }, "DocumentType": "Automation", - "Name": "SHARR-MakeEBSSnapshotsPrivate", + "Name": "SHARR-EnableAutoScalingGroupELBHealthCheck", }, "Type": "AWS::SSM::Document", }, - "SHARRMakeRDSSnapshotPrivateAutomationDocumentD9111EA7": Object { + "SHARREnableCloudTrailEncryptionAutomationDocumentE1C28DB6": Object { "Properties": Object { "Content": Object { "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document name - SHARR-MakeRDSSnapshotPrivate - + "description": "### Document Name - SHARR-EnableCloudTrailEncryption ## What does this document do? -This runbook removes public access to an RDS Snapshot +Enables encryption on a CloudTrail using the provided KMS CMK ## Input Parameters -* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. -* DBSnapshotId: identifier of the public snapshot -* DBSnapshotType: snapshot or cluster-snapshot - -## Output Parameters - -* Remediation.Output - stdout messages from the remediation +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data +* TrailRegion: region of the CloudTrail to encrypt +* TrailArn: ARN of the CloudTrail to encrypt ## Security Standards / Controls -* AFSBP v1.0.0: RDS.1 -* CIS v1.2.0: n/a -* PCI: RDS.1 +* AFSBP v1.0.0: CloudTrail.2 +* CIS v1.2.0: 2.7 +* PCI: CloudTrail.1 ", "mainSteps": Array [ Object { "action": "aws:executeScript", "inputs": Object { - "Handler": "make_snapshot_private", + "Handler": "enable_trail_encryption", "InputPayload": Object { - "DBSnapshotId": "{{DBSnapshotId}}", - "DBSnapshotType": "{{DBSnapshotType}}", + "exec_region": "{{global:REGION}}", + "kms_key_arn": "{{KMSKeyArn}}", + "region": "{{global:REGION}}", + "trail": "{{TrailArn}}", + "trail_region": "{{TrailRegion}}", }, "Runtime": "python3.7", "Script": "#!/usr/bin/python ############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # # use this file except in compliance with the License. A copy of the License # # is located at # # # -# http://www.apache.org/licenses/LICENSE-2.0/ # +# http://www.apache.org/licenses/LICENSE-2.0/ # # # # or in the \\"license\\" file accompanying this file. This file is distributed # # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # @@ -6157,1277 +2327,1402 @@ This runbook removes public access to an RDS Snapshot # sions and limitations under the License. # ############################################################################### -import json import boto3 from botocore.config import Config from botocore.exceptions import ClientError -def connect_to_rds(boto_config): - return boto3.client('rds', config=boto_config) +def connect_to_cloudtrail(region, boto_config): + return boto3.client('cloudtrail', region_name=region, config=boto_config) -def make_snapshot_private(event, context): +def enable_trail_encryption(event, context): + \\"\\"\\" + remediates CloudTrail.2 by enabling SSE-KMS + On success returns a string map + On failure returns NoneType + \\"\\"\\" boto_config = Config( retries ={ - 'mode': 'standard' + 'mode': 'standard' } ) - rds_client = connect_to_rds(boto_config) - snapshot_id = event['DBSnapshotId'] - snapshot_type = event['DBSnapshotType'] + + if event['trail_region'] != event['exec_region']: + exit('ERROR: cross-region remediation is not yet supported') + + ctrail_client = connect_to_cloudtrail(event['trail_region'], boto_config) + kms_key_arn = event['kms_key_arn'] + try: - if (snapshot_type == 'snapshot'): - rds_client.modify_db_snapshot_attribute( - DBSnapshotIdentifier=snapshot_id, - AttributeName='restore', - ValuesToRemove=['all'] - ) - elif (snapshot_type == 'cluster-snapshot'): - rds_client.modify_db_cluster_snapshot_attribute( - DBClusterSnapshotIdentifier=snapshot_id, - AttributeName='restore', - ValuesToRemove=['all'] - ) - print(f'Remediation completed: {snapshot_id} public access removed.') + ctrail_client.update_trail( + Name=event['trail'], + KmsKeyId=kms_key_arn + ) return { \\"response\\": { - \\"message\\": f'Snapshot {snapshot_id} permissions set to private', + \\"message\\": f'Enabled KMS CMK encryption on {event[\\"trail\\"]}', \\"status\\": \\"Success\\" } } except Exception as e: - exit(f'Remediation failed for {snapshot_id}: {str(e)}')", + exit(f'Error enabling SSE-KMS encryption: {str(e)}')", }, - "name": "MakeRDSSnapshotPrivate", + "isEnd": true, + "name": "Remediation", "outputs": Array [ Object { "Name": "Output", - "Selector": "$.Payload.response", - "Type": "StringMap", - }, - ], - }, - ], - "outputs": Array [ - "MakeRDSSnapshotPrivate.Output", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", - "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", - "type": "String", - }, - "DBSnapshotId": Object { - "allowedPattern": "^[a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,}$", - "type": "String", - }, - "DBSnapshotType": Object { - "allowedValues": Array [ - "cluster-snapshot", - "snapshot", - ], - "type": "String", - }, - }, - "schemaVersion": "0.3", - }, - "DocumentType": "Automation", - "Name": "SHARR-MakeRDSSnapshotPrivate", - }, - "Type": "AWS::SSM::Document", - }, - "SHARRRemediationPolicyConfigureS3BucketLogging9F85EEE2": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", - }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "s3:PutBucketLogging", - "s3:CreateBucket", - "s3:PutEncryptionConfiguration", - "s3:PutBucketAcl", - ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyConfigureS3BucketLogging9F85EEE2", - "Roles": Array [ - Object { - "Ref": "RemediationRoleConfigureS3BucketLoggingMemberAccountRoleE068390D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyConfigureS3BucketPublicAccessBlock2E4EF13D": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", - }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "s3:PutBucketPublicAccessBlock", - "s3:GetBucketPublicAccessBlock", - ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyConfigureS3BucketPublicAccessBlock2E4EF13D", - "Roles": Array [ - Object { - "Ref": "RemediationRoleConfigureS3BucketPublicAccessBlockMemberAccountRoleC78F6EE7", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyConfigureS3PublicAccessBlockEAD9CA55": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", - }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "s3:PutAccountPublicAccessBlock", - "s3:GetAccountPublicAccessBlock", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyConfigureS3PublicAccessBlockEAD9CA55", - "Roles": Array [ - Object { - "Ref": "RemediationRoleConfigureS3PublicAccessBlockMemberAccountRole98A4BC1D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyCreateAccessLoggingBucket4A1677D6": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resources.", }, ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "s3:CreateBucket", - "s3:PutEncryptionConfiguration", - "s3:PutBucketAcl", - ], - "Effect": "Allow", - "Resource": "*", - }, + "outputs": Array [ + "Remediation.Output", ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyCreateAccessLoggingBucket4A1677D6", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateAccessLoggingBucketMemberAccountRole3E1569D8", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyCreateCloudTrailMultiRegionTrail59B12044": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation.", + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions.", + "KMSKeyArn": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", + "description": "The ARN of the KMS key created by SHARR for this remediation", + "type": "String", }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "cloudtrail:CreateTrail", - "cloudtrail:UpdateTrail", - "cloudtrail:StartLogging", - ], - "Effect": "Allow", - "Resource": "*", + "TrailArn": Object { + "allowedPattern": "^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:trail/[A-Za-z0-9._-]{3,128}$", + "description": "ARN of the CloudTrail", + "type": "String", }, - Object { - "Action": Array [ - "s3:CreateBucket", - "s3:PutEncryptionConfiguration", - "s3:PutBucketPublicAccessBlock", - "s3:PutBucketLogging", - "s3:PutBucketAcl", - "s3:PutBucketPolicy", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":s3:::so0111-*", - ], - ], - }, + "TrailRegion": Object { + "allowedPattern": "^[a-z]{2}(?:-gov)?-[a-z]+-\\\\d$", + "description": "Region the CloudTrail is in", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyCreateCloudTrailMultiRegionTrail59B12044", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateCloudTrailMultiRegionTrailMemberAccountRoleF70577FF", }, - ], + "schemaVersion": "0.3", + }, + "DocumentType": "Automation", + "Name": "SHARR-EnableCloudTrailEncryption", }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::SSM::Document", }, - "SHARRRemediationPolicyCreateLogMetricFilterAndAlarm102AC980": Object { + "SHARREnableCloudTrailLogFileValidationAutomationDocumentA3B13B24": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableCloudTrailLogFileValidation + +## What does this document do? +This runbook enables log file validation for your AWS CloudTrail trail using the [UpdateTrail](https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_UpdateTrail.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* TrailName: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. + +## Output Parameters +* UpdateTrail.Output: The response of the UpdateTrail API call. + +## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. +", + "mainSteps": Array [ Object { - "Action": Array [ - "logs:PutMetricFilter", - "cloudwatch:PutMetricAlarm", - ], - "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":logs:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:*", - ], - ], - }, + "action": "aws:executeAwsApi", + "description": "## UpdateTrail +Enables log file validation for the AWS CloudTrail trail you specify in the TrailName parameter. +## Outputs +* Output: Response from the UpdateTrail API call. +", + "inputs": Object { + "Api": "UpdateTrail", + "EnableLogFileValidation": true, + "Name": "{{ TrailName }}", + "Service": "cloudtrail", + }, + "isEnd": false, + "name": "UpdateTrail", + "outputs": Array [ Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":cloudwatch:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":alarm:*", - ], - ], + "Name": "Output", + "Selector": "$", + "Type": "StringMap", }, ], + "timeoutSeconds": 600, }, Object { - "Action": Array [ - "sns:CreateTopic", - "sns:SetTopicAttributes", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":sns:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":SO0111-SHARR-LocalAlarmNotification", - ], + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyTrail +Verifies log file validation is enabled for your trail. +", + "inputs": Object { + "Api": "GetTrail", + "DesiredValues": Array [ + "True", ], + "Name": "{{ TrailName }}", + "PropertySelector": "$.Trail.LogFileValidationEnabled", + "Service": "cloudtrail", }, + "isEnd": true, + "name": "VerifyTrail", + "timeoutSeconds": 600, }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyCreateLogMetricFilterAndAlarm102AC980", - "Roles": Array [ - Object { - "Ref": "RemediationRoleCreateLogMetricFilterAndAlarmMemberAccountRoleAA3E3C8A", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyDisablePublicAccessForSecurityGroupE214EF9A": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", - }, + "outputs": Array [ + "UpdateTrail.Output", ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ec2:DescribeSecurityGroupReferences", - "ec2:DescribeSecurityGroups", - "ec2:UpdateSecurityGroupRuleDescriptionsEgress", - "ec2:UpdateSecurityGroupRuleDescriptionsIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:RevokeSecurityGroupEgress", - ], - "Effect": "Allow", - "Resource": "*", + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyDisablePublicAccessForSecurityGroupE214EF9A", - "Roles": Array [ - Object { - "Ref": "RemediationRoleDisablePublicAccessForSecurityGroupMemberAccountRole3BED8BF4", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableAWSConfig8A0259D3": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", + "TrailName": Object { + "allowedPattern": "(^arn:(aws[a-zA-Z-]*)?:cloudtrail:[a-z0-9-]+:\\\\d{12}:trail\\\\/(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$)|(^(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$)", + "description": "(Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for.", + "type": "String", }, - ], + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableCloudTrailLogFileValidation", }, + "Type": "AWS::SSM::Document", + }, + "SHARREnableCloudTrailToCloudWatchLoggingAutomationDocument39CFB27F": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - SHARR-EnableCloudTrailToCloudWatchLogging +## What does this document do? +Creates a CloudWatch logs group for CloudTrail data. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data + +## Security Standards / Controls +* AFSBP v1.0.0: N/A +* CIS v1.2.0: 2.4 +* PCI: CloudTrail.4 +", + "mainSteps": Array [ Object { - "Action": Array [ - "iam:GetRole", - "iam:PassRole", - ], - "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", - ], - ], - }, + "action": "aws:executeAwsApi", + "description": "Create the log group", + "inputs": Object { + "Api": "CreateLogGroup", + "Service": "logs", + "logGroupName": "{{LogGroupName}}", + }, + "name": "CreateLogGroup", + "outputs": Array [ Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/SO0111-CreateAccessLoggingBucket_*", - ], - ], + "Name": "Output", + "Selector": "$", + "Type": "StringMap", }, ], }, Object { - "Action": Array [ - "sns:CreateTopic", - "sns:SetTopicAttributes", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":sns:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":SO0111-SHARR-AWSConfigNotification", - ], - ], - }, - }, - Object { - "Action": "ssm:StartAutomationExecution", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ssm:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":automation-definition/SHARR-CreateAccessLoggingBucket:*", - ], - ], + "action": "aws:executeScript", + "inputs": Object { + "Handler": "wait_for_loggroup", + "InputPayload": Object { + "LogGroup": "{{LogGroupName}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +import time + +def connect_to_logs(boto_config): + return boto3.client('logs', config=boto_config) + +def wait_for_loggroup(event, context): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + cwl_client = connect_to_logs(boto_config) + + max_retries = 3 + attempts = 0 + while attempts < max_retries: + try: + describe_group = cwl_client.describe_log_groups(logGroupNamePrefix=event['LogGroup']) + print(len(describe_group['logGroups'])) + for group in describe_group['logGroups']: + if group['logGroupName'] == event['LogGroup']: + return str(group['arn']) + # no match - wait and retry + time.sleep(2) + attempts += 1 + + except Exception as e: + exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: {str(e)}') + + exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: Timed out')", }, - }, - Object { - "Action": Array [ - "ssm:GetAutomationExecution", - "config:PutConfigurationRecorder", - "config:PutDeliveryChannel", - "config:DescribeConfigurationRecorders", - "config:StartConfigurationRecorder", + "isEnd": false, + "name": "WaitForCreation", + "outputs": Array [ + Object { + "Name": "CloudWatchLogsGroupArn", + "Selector": "$.Payload", + "Type": "String", + }, ], - "Effect": "Allow", - "Resource": "*", }, Object { - "Action": Array [ - "s3:CreateBucket", - "s3:PutEncryptionConfiguration", - "s3:PutBucketPublicAccessBlock", - "s3:PutBucketLogging", - "s3:PutBucketAcl", - "s3:PutBucketPolicy", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":s3:::so0111-*", - ], - ], + "action": "aws:executeAwsApi", + "description": "Enable logging to CloudWatch Logs", + "inputs": Object { + "Api": "UpdateTrail", + "CloudWatchLogsLogGroupArn": "{{WaitForCreation.CloudWatchLogsGroupArn}}", + "CloudWatchLogsRoleArn": "{{CloudWatchLogsRole}}", + "Name": "{{TrailName}}", + "Service": "cloudtrail", }, + "name": "UpdateTrailToCWLogs", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableAWSConfig8A0259D3", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableAWSConfigMemberAccountRole3914B25F", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableAutoScalingGroupELBHealthCheckD6F46CE8": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* ASG.", - }, + "outputs": Array [ + "UpdateTrailToCWLogs.Output", ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "autoscaling:UpdateAutoScalingGroup", - "autoscaling:DescribeAutoScalingGroups", - ], - "Effect": "Allow", - "Resource": "*", + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableAutoScalingGroupELBHealthCheckD6F46CE8", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableAutoScalingGroupELBHealthCheckMemberAccountRole03AE4AEA", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableCloudTrailEncryption5715DA83": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation.", + "CloudWatchLogsRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows CloudTrail to log to CloudWatch.", + "type": "String", }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions.", + "LogGroupName": Object { + "allowedPattern": "^[a-zA-Z0-9-_./]{1,512}$", + "description": "(Required) The name of the Log Group for CloudTrail logs.", + "type": "String", }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "cloudtrail:UpdateTrail", - "Effect": "Allow", - "Resource": "*", + "TrailName": Object { + "allowedPattern": "^[A-Za-z0-9._-]{3,128}$", + "description": "(Required) The name of the CloudTrail.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableCloudTrailEncryption5715DA83", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailEncryptionMemberAccountRoleA936699B", }, - ], + "schemaVersion": "0.3", + }, + "DocumentType": "Automation", + "Name": "SHARR-EnableCloudTrailToCloudWatchLogging", }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::SSM::Document", }, - "SHARRRemediationPolicyEnableCloudTrailLogFileValidation00359A88": Object { + "SHARREnableEbsEncryptionByDefaultAutomationDocumentE19C88DF": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-EnableEbsEncryptionByDefault + +## What does this document do? +This document enables EBS encryption by default for an AWS account in the current region using the [EnableEbsEncryptionByDefault](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableEbsEncryptionByDefault.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* ModifyAccount.EnableEbsEncryptionByDefaultResponse: JSON formatted response from the EnableEbsEncryptionByDefault API. +", + "mainSteps": Array [ Object { - "Action": Array [ - "cloudtrail:UpdateTrail", - "cloudtrail:GetTrail", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":cloudtrail:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":trail/*", - ], - ], + "action": "aws:executeAwsApi", + "description": "## ModifyAccount +Enables EBS encryption by default for the account in the current region. +## Outputs +* EnableEbsEncryptionByDefaultResponse: Response from the EnableEbsEncryptionByDefault API. +", + "inputs": Object { + "Api": "EnableEbsEncryptionByDefault", + "Service": "ec2", }, - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableCloudTrailLogFileValidation00359A88", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailLogFileValidationMemberAccountRole3F5F7157", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableCloudTrailToCloudWatchLoggingA9BBB945": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow creation and description of any log group", + "isEnd": false, + "name": "ModifyAccount", + "outputs": Array [ + Object { + "Name": "EnableEbsEncryptionByDefaultResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, }, Object { - "id": "W28", - "reason": "Static resource names are required to enable cross-account functionality", + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyEbsEncryptionByDefault +Checks if EbsEncryptionByDefault is enabled correctly from the previous step. +", + "inputs": Object { + "Api": "GetEbsEncryptionByDefault", + "DesiredValues": Array [ + "True", + ], + "PropertySelector": "$.EbsEncryptionByDefault", + "Service": "ec2", + }, + "isEnd": true, + "name": "VerifyEbsEncryptionByDefault", + "timeoutSeconds": 600, }, ], + "outputs": Array [ + "ModifyAccount.EnableEbsEncryptionByDefaultResponse", + ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableEbsEncryptionByDefault", }, + "Type": "AWS::SSM::Document", + }, + "SHARREnableEnhancedMonitoringOnRDSInstanceAutomationDocumentF85DD852": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-EnableEnhancedMonitoringOnRDSInstance + +## What does this document do? +This document is used to enable enhanced monitoring on an RDS Instance using the input parameter DB Instance resourceId. + +## Input Parameters +* ResourceId: (Required) Resource ID of the RDS DB Instance. +* MonitoringInterval: (Optional) + * The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. + * If MonitoringRoleArn is specified, then you must also set MonitoringInterval to a value other than 0. + * Valid Values: 1, 5, 10, 15, 30, 60 + * Default: 60 +* MonitoringRoleArn: (Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* EnableEnhancedMonitoring.DbInstance - The standard HTTP response from the ModifyDBInstance API. +", + "mainSteps": Array [ Object { - "Action": "cloudtrail:UpdateTrail", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":cloudtrail:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":trail/*", - ], + "action": "aws:executeAwsApi", + "description": "## DescribeDBInstances + Makes describeDBInstances API call using RDS Instance DbiResourceId to get DBInstanceId. +## Outputs +* DbInstanceIdentifier: DBInstance Identifier of the RDS Instance. +", + "inputs": Object { + "Api": "DescribeDBInstances", + "Filters": Array [ + Object { + "Name": "dbi-resource-id", + "Values": Array [ + "{{ ResourceId }}", + ], + }, ], + "Service": "rds", }, + "isEnd": false, + "name": "DescribeDBInstances", + "outputs": Array [ + Object { + "Name": "DbInstanceIdentifier", + "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Type": "String", + }, + ], + "timeoutSeconds": 600, }, Object { - "Action": "iam:PassRole", - "Effect": "Allow", - "Resource": Object { - "Fn::GetAtt": Array [ - "ctcwremediationrole7AB69D0B", - "Arn", + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceStatus +Verifies if DB Instance status is available before enabling enhanced monitoring. +", + "inputs": Object { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "DesiredValues": Array [ + "available", ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", }, + "isEnd": false, + "name": "VerifyDBInstanceStatus", + "timeoutSeconds": 600, }, Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:DescribeLogGroups", + "action": "aws:executeAwsApi", + "description": "## EnableEnhancedMonitoring + Makes ModifyDBInstance API call to enable Enhanced Monitoring on the RDS Instance + using the DBInstanceId from the previous action. +## Outputs + * DbInstance: The standard HTTP response from the ModifyDBInstance API. +", + "inputs": Object { + "Api": "ModifyDBInstance", + "ApplyImmediately": false, + "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "MonitoringInterval": "{{ MonitoringInterval }}", + "MonitoringRoleArn": "{{ MonitoringRoleArn }}", + "Service": "rds", + }, + "isEnd": false, + "name": "EnableEnhancedMonitoring", + "outputs": Array [ + Object { + "Name": "DbInstance", + "Selector": "$", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableCloudTrailToCloudWatchLoggingA9BBB945", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableCloudTrailToCloudWatchLoggingMemberAccountRoleE7E9C206", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableEbsEncryptionByDefaultED8BC775": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", + "timeoutSeconds": 600, }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ Object { - "Action": Array [ - "ec2:EnableEBSEncryptionByDefault", - "ec2:GetEbsEncryptionByDefault", + "action": "aws:executeScript", + "description": "## VerifyEnhancedMonitoringEnabled +Checks that the enhanced monitoring is enabled on RDS Instance in the previous step exists. +## Outputs +* Output: The standard HTTP response from the ModifyDBInstance API. +", + "inputs": Object { + "Handler": "handler", + "InputPayload": Object { + "DBIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "MonitoringInterval": "{{ MonitoringInterval }}", + }, + "Runtime": "python3.7", + "Script": "import boto3 +import time + +def handler(event, context): + rds_client = boto3.client(\\"rds\\") + db_instance_id = event[\\"DBIdentifier\\"] + monitoring_interval = event[\\"MonitoringInterval\\"] + + try: + rds_waiter = rds_client.get_waiter(\\"db_instance_available\\") + rds_waiter.wait(DBInstanceIdentifier=db_instance_id) + + db_instances = rds_client.describe_db_instances( + DBInstanceIdentifier=db_instance_id) + + for db_instance in db_instances.get(\\"DBInstances\\", [{}]): + db_monitoring_interval = db_instance.get(\\"MonitoringInterval\\") + + if db_monitoring_interval == monitoring_interval: + return { + \\"output\\": db_instances[\\"ResponseMetadata\\"] + } + else: + info = \\"VERIFICATION FAILED. RDS INSTANCE MONITORING INTERVAL {} IS NOT ENABLED WITH THE REQUIRED VALUE {}\\".format( + db_monitoring_interval, monitoring_interval) + raise Exception(info) + except Exception as e: + raise e", + }, + "isEnd": true, + "name": "VerifyEnhancedMonitoringEnabled", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", + "timeoutSeconds": 600, }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableEbsEncryptionByDefaultED8BC775", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableEbsEncryptionByDefaultMemberAccountRoleDF17FF59", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableEnhancedMonitoringOnRDSInstance6E7C63B0": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* RDS database.", - }, + "outputs": Array [ + "EnableEnhancedMonitoring.DbInstance", ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "iam:GetRole", - "iam:PassRole", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/SO0111-RDSMonitoring-remediationRole_", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "Action": Array [ - "rds:DescribeDBInstances", - "rds:ModifyDBInstance", + "MonitoringInterval": Object { + "allowedValues": Array [ + 1, + 5, + 10, + 15, + 30, + 60, ], - "Effect": "Allow", - "Resource": "*", + "default": 60, + "description": "(Optional) The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance.", + "type": "Integer", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableEnhancedMonitoringOnRDSInstance6E7C63B0", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableEnhancedMonitoringOnRDSInstanceMemberAccountRoleB3EFCB99", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableKeyRotation7DBFDFE8": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resource.", + "MonitoringRoleArn": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$", + "description": "(Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs.", + "type": "String", + }, + "ResourceId": Object { + "allowedPattern": "db-[A-Z0-9]{26}", + "description": "(Required) Resource ID of the Amazon RDS instance for which Enhanced Monitoring needs to be enabled.", + "type": "String", }, - ], + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableEnhancedMonitoringOnRDSInstance", }, + "Type": "AWS::SSM::Document", + }, + "SHARREnableKeyRotationAutomationDocumentAFF68728": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableKeyRotation + +## What does this document do? +This document enables automatic key rotation for the given AWS Key Management Service (KMS) symmetric customer master key(CMK) using [EnableKeyRotation](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KeyId: (Required) The Key ID of the AWS KMS symmetric CMK. + +## Output Parameters +* EnableKeyRotation.EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. +", + "mainSteps": Array [ Object { - "Action": Array [ - "kms:EnableKeyRotation", - "kms:GetKeyRotationStatus", + "action": "aws:executeAwsApi", + "description": "## EnableKeyRotation +Enables automatic key rotation for the given AWS KMS CMK. +## Outputs +* EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. +", + "inputs": Object { + "Api": "EnableKeyRotation", + "KeyId": "{{ KeyId }}", + "Service": "kms", + }, + "isEnd": false, + "name": "EnableKeyRotation", + "outputs": Array [ + Object { + "Name": "EnableKeyRotationResponse", + "Selector": "$", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableKeyRotation7DBFDFE8", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableKeyRotationMemberAccountRole2366F17F", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableRDSClusterDeletionProtectionBC66754B": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* RDS database.", + "timeoutSeconds": 600, }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ Object { - "Action": "iam:GetRole", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/RDSEnhancedMonitoringRole", - ], + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyKeyRotation +Verifies that the KeyRotationEnabled is set to true for the given AWS KMS CMK. +", + "inputs": Object { + "Api": "GetKeyRotationStatus", + "DesiredValues": Array [ + "True", ], + "KeyId": "{{ KeyId }}", + "PropertySelector": "$.KeyRotationEnabled", + "Service": "kms", }, + "isEnd": true, + "name": "VerifyKeyRotation", + "timeoutSeconds": 600, }, - Object { - "Action": "config:GetResourceConfigHistory", - "Effect": "Allow", - "Resource": "*", + ], + "outputs": Array [ + "EnableKeyRotation.EnableKeyRotationResponse", + ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "Action": Array [ - "rds:DescribeDBClusters", - "rds:ModifyDBCluster", - ], - "Effect": "Allow", - "Resource": "*", + "KeyId": Object { + "allowedPattern": "[a-z0-9-]{1,2048}", + "description": "(Required) The Key ID of the AWS KMS symmetric CMK.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableRDSClusterDeletionProtectionBC66754B", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableRDSClusterDeletionProtectionMemberAccountRole019A1667", }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyEnableVPCFlowLogs22F36069": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* resources.", - }, - ], + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableKeyRotation", }, + "Type": "AWS::SSM::Document", + }, + "SHARREnableRDSClusterDeletionProtectionAutomationDocumentBA07C167": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableRDSClusterDeletionProtection + +## What does this document do? +This document enables \`Deletion Protection\` on a given Amazon RDS cluster using the [ModifyDBCluster](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBCluster.html) API. +Please note, AWS Config is required to be enabled in this region for this document to work as it requires the resource ID recorded by the AWS Config service. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* ClusterId: (Required) Resource ID of the Amazon RDS cluster. + +## Output Parameters +* EnableRDSClusterDeletionProtection.ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. +", + "mainSteps": Array [ Object { - "Action": "ec2:CreateFlowLogs", - "Effect": "Allow", - "Resource": Array [ + "action": "aws:executeAwsApi", + "description": "## GetRDSClusterIdentifer +Accepts the resource ID of the Amazon RDS Cluster as input and returns the cluster name. +## Outputs +* DbClusterIdentifier: The ID of the DB cluster for which the input parameter matches DbClusterResourceId element from the output of the DescribeDBClusters API call. +", + "inputs": Object { + "Api": "GetResourceConfigHistory", + "Service": "config", + "limit": 1, + "resourceId": "{{ ClusterId }}", + "resourceType": "AWS::RDS::DBCluster", + }, + "isEnd": false, + "name": "GetRDSClusterIdentifer", + "outputs": Array [ Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ec2:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":vpc/*", - ], - ], + "Name": "DbClusterIdentifier", + "Selector": "$.configurationItems[0].resourceName", + "Type": "String", }, + ], + "timeoutSeconds": 600, + }, + Object { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBClusterStatus +Verifies if the DB Cluster status is available before enabling cluster deletion protection. +", + "inputs": Object { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DesiredValues": Array [ + "available", + ], + "PropertySelector": "$.DBClusters[0].Status", + "Service": "rds", + }, + "isEnd": false, + "name": "VerifyDBClusterStatus", + "timeoutSeconds": 600, + }, + Object { + "action": "aws:executeAwsApi", + "description": "## EnableRDSClusterDeletionProtection +Enables deletion protection on the Amazon RDS Cluster. +## Outputs +* ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. +", + "inputs": Object { + "Api": "ModifyDBCluster", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DeletionProtection": true, + "Service": "rds", + }, + "isEnd": false, + "name": "EnableRDSClusterDeletionProtection", + "outputs": Array [ Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ec2:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":vpc-flow-log/*", - ], - ], + "Name": "ModifyDBClusterResponse", + "Selector": "$", + "Type": "StringMap", }, ], + "timeoutSeconds": 600, }, Object { - "Action": "iam:PassRole", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":role/SO0111-EnableVPCFlowLogs-remediationRole_", - Object { - "Ref": "AWS::Region", - }, - ], + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBClusterModification +Verifies that deletion protection has been enabled for the given Amazon RDS database cluster. +", + "inputs": Object { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DesiredValues": Array [ + "True", ], + "PropertySelector": "$.DBClusters[0].DeletionProtection", + "Service": "rds", }, + "isEnd": true, + "name": "VerifyDBClusterModification", + "timeoutSeconds": 600, + }, + ], + "outputs": Array [ + "EnableRDSClusterDeletionProtection.ModifyDBClusterResponse", + ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "Action": Array [ - "ec2:DescribeFlowLogs", - "logs:CreateLogGroup", - "logs:DescribeLogGroups", - ], - "Effect": "Allow", - "Resource": "*", + "ClusterId": Object { + "allowedPattern": "^[a-zA-Z0-9-]{1,35}$", + "description": "(Required) Amazon RDS cluster resourceId for which deletion protection needs to be enabled.", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyEnableVPCFlowLogs22F36069", - "Roles": Array [ - Object { - "Ref": "RemediationRoleEnableVPCFlowLogsMemberAccountRoleB79F3729", }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyMakeEBSSnapshotsPrivate64D2F13D": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* snapshot.", - }, - ], + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableRDSClusterDeletionProtection", }, + "Type": "AWS::SSM::Document", + }, + "SHARREnableVPCFlowLogsAutomationDocument450A4332": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - SHARR-EnableVPCFlowLogs +## What does this document do? +Enables VPC Flow Logs for a given VPC + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* VPC: VPC Id of the VPC for which logs are to be enabled +* RemediationRole: role arn of the role to use for logging +* KMSKeyArn: Amazon Resource Name (ARN) of the KMS Customer-Managed Key to use to encrypt the log group + +## Security Standards / Controls +* AFSBP v1.0.0: CloudTrail.2 +* CIS v1.2.0: 2.7 +* PCI: CloudTrail.1 +", + "mainSteps": Array [ Object { - "Action": Array [ - "ec2:ModifySnapshotAttribute", - "ec2:DescribeSnapshots", + "action": "aws:executeScript", + "inputs": Object { + "Handler": "enable_flow_logs", + "InputPayload": Object { + "kms_key_arn": "{{KMSKeyArn}}", + "remediation_role": "{{RemediationRole}}", + "vpc": "{{VPC}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +import time +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_logs(boto_config): + return boto3.client('logs', config=boto_config) + +def connect_to_ec2(boto_config): + return boto3.client('ec2', config=boto_config) + +def log_group_exists(client, group): + try: + log_group_verification = client.describe_log_groups( + logGroupNamePrefix=group + )['logGroups'] + if len(log_group_verification) >= 1: + for existing_loggroup in log_group_verification: + if existing_loggroup['logGroupName'] == group: + return 1 + return 0 + + except Exception as e: + exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') + +def wait_for_loggroup(client, wait_interval, max_retries, loggroup): + attempts = 1 + while not log_group_exists(client, loggroup): + time.sleep(wait_interval) + attempts += 1 + if attempts > max_retries: + exit(f'Timeout waiting for log group {loggroup} to become active') + +def flowlogs_active(client, loggroup): + # searches for flow log status, filtered on unique CW Log Group created earlier + try: + flow_status = client.describe_flow_logs( + DryRun=False, + Filters=[ + { + 'Name': 'log-group-name', + 'Values': [loggroup] + }, + ] + )['FlowLogs'] + if len(flow_status) == 1 and flow_status[0]['FlowLogStatus'] == 'ACTIVE': + return 1 + else: + return 0 + + except Exception as e: + exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') + +def wait_for_flowlogs(client, wait_interval, max_retries, loggroup): + attempts = 1 + while not flowlogs_active(client, loggroup): + time.sleep(wait_interval) + attempts += 1 + if attempts > max_retries: + exit(f'Timeout waiting for flowlogs to log group {loggroup} to become active') + +def enable_flow_logs(event, context): + \\"\\"\\" + remediates CloudTrail.2 by enabling SSE-KMS + On success returns a string map + On failure returns NoneType + \\"\\"\\" + max_retries = event.get('retries', 12) # max number of waits for actions to complete. + wait_interval = event.get('wait', 5) # how many seconds between attempts + + boto_config_args = { + 'retries': { + 'mode': 'standard' + } + } + + boto_config = Config(**boto_config_args) + + if 'vpc' not in event or 'remediation_role' not in event or 'kms_key_arn' not in event: + exit('Error: missing vpc from input') + + logs_client = connect_to_logs(boto_config) + ec2_client = connect_to_ec2(boto_config) + + kms_key_arn = event['kms_key_arn'] # for logs encryption at rest + + # set dynamic variable for CW Log Group for VPC Flow Logs + vpc_flow_loggroup = \\"VPCFlowLogs/\\" + event['vpc'] + # create cloudwatch log group + try: + logs_client.create_log_group( + logGroupName=vpc_flow_loggroup, + kmsKeyId=kms_key_arn + ) + except ClientError as client_error: + exception_type = client_error.response['Error']['Code'] + + if exception_type in [\\"ResourceAlreadyExistsException\\"]: + print(f'CloudWatch Logs group {vpc_flow_loggroup} already exists') + else: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') + + except Exception as e: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(e)}') + + # wait for CWL creation to propagate + wait_for_loggroup(logs_client, wait_interval, max_retries, vpc_flow_loggroup) + + # create VPC Flow Logging + try: + ec2_client.create_flow_logs( + DryRun=False, + DeliverLogsPermissionArn=event['remediation_role'], + LogGroupName=vpc_flow_loggroup, + ResourceIds=[event['vpc']], + ResourceType='VPC', + TrafficType='REJECT', + LogDestinationType='cloud-watch-logs' + ) + except ClientError as client_error: + exception_type = client_error.response['Error']['Code'] + + if exception_type in [\\"FlowLogAlreadyExists\\"]: + return { + \\"response\\": { + \\"message\\": f'VPC Flow Logs for {event[\\"vpc\\"]} already enabled', + \\"status\\": \\"Success\\" + } + } + else: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') + except Exception as e: + exit(f'create_flow_logs failed {str(e)}') + + # wait for Flow Log creation to propagate. Exits on timeout (no need to check results) + wait_for_flowlogs(ec2_client, wait_interval, max_retries, vpc_flow_loggroup) + + # wait_for_flowlogs will exit if unsuccessful after max_retries * wait_interval (60 seconds by default) + return { + \\"response\\": { + \\"message\\": f'VPC Flow Logs enabled for {event[\\"vpc\\"]} to {vpc_flow_loggroup}', + \\"status\\": \\"Success\\" + } + }", + }, + "isEnd": true, + "name": "Remediation", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyMakeEBSSnapshotsPrivate64D2F13D", - "Roles": Array [ - Object { - "Ref": "RemediationRoleMakeEBSSnapshotsPrivateMemberAccountRoleFA05CFAF", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyMakeRDSSnapshotPrivate26FDF037": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for *any* snapshot.", + "outputs": Array [ + "Remediation.Output", + ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "KMSKeyArn": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", + "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", + "description": "The ARN of the KMS key created by SHARR for remediations requiring encryption", + "type": "String", + }, + "RemediationRole": Object { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", + "description": "The ARN of the role that will allow VPC Flow Logs to log to CloudWatch logs", + "type": "String", + }, + "VPC": Object { + "allowedPattern": "^vpc-[0-9a-f]{8,17}", + "description": "The VPC ID of the VPC", + "type": "String", }, - ], + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-EnableVPCFlowLogs", }, + "Type": "AWS::SSM::Document", + }, + "SHARRMakeEBSSnapshotsPrivateAutomationDocumentE34DB88E": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - SHARR-MakeEBSSnapshotPrivate + +## What does this document do? +This runbook works an the account level to remove public share on all EBS snapshots + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: EC2.1 +* CIS v1.2.0: n/a +* PCI: EC2.1 +", + "mainSteps": Array [ Object { - "Action": Array [ - "rds:ModifyDBSnapshotAttribute", - "rds:ModifyDBClusterSnapshotAttribute", + "action": "aws:executeScript", + "inputs": Object { + "Handler": "get_public_snapshots", + "InputPayload": Object { + "account_id": "{{AccountId}}", + "region": "{{global:REGION}}", + "testmode": "{{TestMode}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + +def connect_to_ec2(boto_config): + return boto3.client('ec2', config=boto_config) + +def get_public_snapshots(event, context): + account_id = event['account_id'] + + if 'testmode' in event and event['testmode']: + return [ + \\"snap-12341234123412345\\", + \\"snap-12341234123412345\\", + \\"snap-12341234123412345\\", + \\"snap-12341234123412345\\", + \\"snap-12341234123412345\\" + ] + + return list_public_snapshots(account_id) + +def list_public_snapshots(account_id): + ec2 = connect_to_ec2(boto_config) + control_token = 'start' + try: + + public_snapshot_ids = [] + + while control_token: + + if control_token == 'start': # needed a value to start the loop. Now reset it + control_token = '' + + kwargs = { + 'MaxResults': 100, + 'OwnerIds': [ account_id ], + 'RestorableByUserIds': [ 'all' ] + } + if control_token: + kwargs['NextToken'] = control_token + + response = ec2.describe_snapshots( + **kwargs + ) + + for snapshot in response['Snapshots']: + public_snapshot_ids.append(snapshot['SnapshotId']) + + if 'NextToken' in response: + control_token = response['NextToken'] + else: + control_token = '' + + return public_snapshot_ids + + except Exception as e: + print(e) + exit('Failed to describe_snapshots')", + }, + "name": "GetPublicSnapshotIds", + "outputs": Array [ + Object { + "Name": "Snapshots", + "Selector": "$.Payload", + "Type": "StringList", + }, ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyMakeRDSSnapshotPrivate26FDF037", - "Roles": Array [ - Object { - "Ref": "RemediationRoleMakeRDSSnapshotPrivateMemberAccountRole6760FE6D", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyRemoveLambdaPublicAccessDD6213D0": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for any resource.", }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ Object { - "Action": Array [ - "lambda:GetPolicy", - "lambda:RemovePermission", + "action": "aws:executeScript", + "inputs": Object { + "Handler": "make_snapshots_private", + "InputPayload": Object { + "region": "{{global:REGION}}", + "snapshots": "{{GetPublicSnapshotIds.Snapshots}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_ec2(boto_config): + return boto3.client('ec2', config=boto_config) + +def make_snapshots_private(event, context): + boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + ec2 = connect_to_ec2(boto_config) + + remediated = [] + snapshots = event['snapshots'] + + success_count = 0 + + for snapshot_id in snapshots: + try: + ec2.modify_snapshot_attribute( + Attribute='CreateVolumePermission', + CreateVolumePermission={ + 'Remove': [{'Group': 'all'}] + }, + SnapshotId=snapshot_id + ) + print(f'Snapshot {snapshot_id} permissions set to private') + + remediated.append(snapshot_id) + success_count += 1 + except Exception as e: + print(e) + print(f'FAILED to remediate Snapshot {snapshot_id}') + + result=json.dumps(ec2.describe_snapshots( + SnapshotIds=remediated + ), indent=2, default=str) + print(result) + + return { + \\"response\\": { + \\"message\\": f'{success_count} of {len(snapshots)} Snapshot permissions set to private', + \\"status\\": \\"Success\\" + } + }", + }, + "name": "Remediation", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, ], - "Effect": "Allow", - "Resource": "*", }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyRemoveLambdaPublicAccessDD6213D0", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRemoveLambdaPublicAccessMemberAccountRoleB266862C", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyRemoveVPCDefaultSecurityGroupRules94040124": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for any resource.", - }, - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, + "outputs": Array [ + "Remediation.Output", ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "ec2:UpdateSecurityGroupRuleDescriptionsEgress", - "ec2:UpdateSecurityGroupRuleDescriptionsIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:RevokeSecurityGroupEgress", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":ec2:*:", - Object { - "Ref": "AWS::AccountId", - }, - ":security-group/*", - ], - ], - }, + "parameters": Object { + "AccountId": Object { + "allowedPattern": "^[0-9]{12}$", + "description": "Account ID of the account for which snapshots are to be checked.", + "type": "String", + }, + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", + "type": "String", }, - Object { - "Action": Array [ - "ec2:DescribeSecurityGroupReferences", - "ec2:DescribeSecurityGroups", - ], - "Effect": "Allow", - "Resource": "*", + "TestMode": Object { + "default": false, + "description": "Enables test mode, which generates a list of fake volume Ids", + "type": "Boolean", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyRemoveVPCDefaultSecurityGroupRules94040124", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRemoveVPCDefaultSecurityGroupRulesMemberAccountRole406D320B", }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicyRevokeUnusedIAMUserCredentialsEEF45939": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for any resource.", - }, - ], + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-MakeEBSSnapshotsPrivate", }, + "Type": "AWS::SSM::Document", + }, + "SHARRMakeRDSSnapshotPrivateAutomationDocumentD9111EA7": Object { "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - SHARR-MakeRDSSnapshotPrivate + +## What does this document do? +This runbook removes public access to an RDS Snapshot + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* DBSnapshotId: identifier of the public snapshot +* DBSnapshotType: snapshot or cluster-snapshot + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: RDS.1 +* CIS v1.2.0: n/a +* PCI: RDS.1 +", + "mainSteps": Array [ Object { - "Action": Array [ - "iam:UpdateAccessKey", - "iam:ListAccessKeys", - "iam:GetAccessKeyLastUsed", - "iam:GetUser", - "iam:GetLoginProfile", - "iam:DeleteLoginProfile", - ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::", - Object { - "Ref": "AWS::AccountId", - }, - ":user/*", - ], - ], + "action": "aws:executeScript", + "inputs": Object { + "Handler": "make_snapshot_private", + "InputPayload": Object { + "DBSnapshotId": "{{DBSnapshotId}}", + "DBSnapshotType": "{{DBSnapshotType}}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_rds(): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + return boto3.client('rds', config=boto_config) + +def make_snapshot_private(event, context): + + rds_client = connect_to_rds() + snapshot_id = event['DBSnapshotId'] + snapshot_type = event['DBSnapshotType'] + try: + if (snapshot_type == 'snapshot'): + rds_client.modify_db_snapshot_attribute( + DBSnapshotIdentifier=snapshot_id, + AttributeName='restore', + ValuesToRemove=['all'] + ) + elif (snapshot_type == 'cluster-snapshot'): + rds_client.modify_db_cluster_snapshot_attribute( + DBClusterSnapshotIdentifier=snapshot_id, + AttributeName='restore', + ValuesToRemove=['all'] + ) + else: + exit(f'Unrecognized snapshot_type {snapshot_type}') + + print(f'Remediation completed: {snapshot_id} public access removed.') + return { + \\"response\\": { + \\"message\\": f'Snapshot {snapshot_id} permissions set to private', + \\"status\\": \\"Success\\" + } + } + except Exception as e: + exit(f'Remediation failed for {snapshot_id}: {str(e)}')", }, - }, - Object { - "Action": "config:ListDiscoveredResources", - "Effect": "Allow", - "Resource": "*", + "name": "MakeRDSSnapshotPrivate", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], }, ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicyRevokeUnusedIAMUserCredentialsEEF45939", - "Roles": Array [ - Object { - "Ref": "RemediationRoleRevokeUnusedIAMUserCredentialsMemberAccountRole5C008B43", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "SHARRRemediationPolicySetIAMPasswordPolicy044BED0C": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required for to allow remediation for any resource.", - }, + "outputs": Array [ + "MakeRDSSnapshotPrivate.Output", ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ - "iam:UpdateAccountPasswordPolicy", - "iam:GetAccountPasswordPolicy", - "ec2:UpdateSecurityGroupRuleDescriptionsIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:RevokeSecurityGroupEgress", + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", + "type": "String", + }, + "DBSnapshotId": Object { + "allowedPattern": "^[a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,}$", + "type": "String", + }, + "DBSnapshotType": Object { + "allowedValues": Array [ + "cluster-snapshot", + "snapshot", ], - "Effect": "Allow", - "Resource": "*", + "type": "String", }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SHARRRemediationPolicySetIAMPasswordPolicy044BED0C", - "Roles": Array [ - Object { - "Ref": "RemediationRoleSetIAMPasswordPolicyMemberAccountRoleA1FF47B4", }, - ], + "schemaVersion": "0.3", + }, + "DocumentType": "Automation", + "Name": "SHARR-MakeRDSSnapshotPrivate", }, - "Type": "AWS::IAM::Policy", + "Type": "AWS::SSM::Document", }, "SHARRRemoveLambdaPublicAccessAutomationDocumentB33C7F4F": Object { "Properties": Object { @@ -7507,6 +3802,13 @@ def remove_resource_policy(functionname, sid, client): except Exception as e: exit(f'FAILED: SID {sid} was NOT removed from Lambda function {functionname} - {str(e)}') +def remove_public_statement(client, functionname, statement, principal_source): + for principal in list(principal_source): + if principal == \\"*\\" or (isinstance(principal, dict) and principal.get(\\"AWS\\",\\"\\") == \\"*\\"): + print_policy_before(statement) + remove_resource_policy(functionname, statement['Sid'], client) + break # there will only be one that matches + def remove_lambda_public_access(event, context): client = connect_to_lambda(boto_config) @@ -7521,20 +3823,9 @@ def remove_lambda_public_access(event, context): print('Scanning for public resource policies in ' + functionname) for statement in statements: - principal_statements = [] - - if isinstance(statement['Principal'], list): - principal_statements = statement['Principal'] - else: - principal_statements = [statement['Principal']] - - for principal in principal_statements: - if principal == \\"*\\" or (isinstance(principal, dict) and principal.get(\\"AWS\\",\\"\\") == \\"*\\"): - print_policy_before(statement) - remove_resource_policy(functionname, statement['Sid'], client) - break + remove_public_statement(client, functionname, statement, list(statement['Principal'])) - result = client.get_policy(FunctionName=functionname) + client.get_policy(FunctionName=functionname) verify(functionname) except ClientError as ex: @@ -7662,40 +3953,162 @@ def get_permissions(group_id): return default_group.get(\\"IpPermissions\\"), default_group.get(\\"IpPermissionsEgress\\") -def handler(event, context): - group_id = event.get(\\"GroupId\\") - ingress_permissions, egress_permissions = get_permissions(group_id) +def handler(event, context): + group_id = event.get(\\"GroupId\\") + ingress_permissions, egress_permissions = get_permissions(group_id) + + if ingress_permissions: + ec2_client.revoke_security_group_ingress(GroupId=group_id, IpPermissions=ingress_permissions) + if egress_permissions: + ec2_client.revoke_security_group_egress(GroupId=group_id, IpPermissions=egress_permissions) + + ingress_permissions, egress_permissions = get_permissions(group_id) + if ingress_permissions or egress_permissions: + raise Exception(f\\"VERIFICATION FAILED. SECURITY GROUP {group_id} NOT CLOSED.\\") + + return { + \\"output\\": \\"Security group closed successfully.\\" + }", + }, + "isCritical": true, + "isEnd": true, + "maxAttempts": 3, + "name": "RemoveRulesAndVerify", + "onFailure": "Abort", + "outputs": Array [ + Object { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "String", + }, + ], + "timeoutSeconds": 180, + }, + ], + "outputs": Array [ + "RemoveRulesAndVerify.Output", + ], + "parameters": Object { + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "GroupId": Object { + "allowedPattern": "sg-[a-z0-9]+$", + "description": "(Required) The unique ID of the security group.", + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, + "DocumentType": "Automation", + "Name": "SHARR-RemoveVPCDefaultSecurityGroupRules", + }, + "Type": "AWS::SSM::Document", + }, + "SHARRRevokeUnrotatedKeysAutomationDocumentB68F3672": Object { + "Properties": Object { + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - SHARR-RevokeUnrotatedKeys + +## What does this document do? +This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. It will disabled keys that have been used within the previous 90 days by have not been rotated by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html). Please note, this automation document requires AWS Config to be enabled. + +## Input Parameters +* Finding: (Required) Security Hub finding details JSON +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* MaxCredentialUsageAge: (Optional) Maximum number of days a key is allowed to be unrotated before revoking it. DEFAULT: 90 + +## Output Parameters +* RevokeUnrotatedKeys.Output +", + "mainSteps": Array [ + Object { + "action": "aws:executeScript", + "description": "## RevokeUnrotatedKeys + +This step deactivates IAM user access keys that have not been rotated in more than MaxCredentialUsageAge days +## Outputs +* Output: Success message or failure Exception. +", + "inputs": Object { + "Handler": "unrotated_key_handler", + "InputPayload": Object { + "IAMResourceId": "{{ IAMResourceId }}", + "MaxCredentialUsageAge": "{{ MaxCredentialUsageAge }}", + }, + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_cloudtrail(region, boto_config): + return boto3.client('cloudtrail', region_name=region, config=boto_config) - if ingress_permissions: - ec2_client.revoke_security_group_ingress(GroupId=group_id, IpPermissions=ingress_permissions) - if egress_permissions: - ec2_client.revoke_security_group_egress(GroupId=group_id, IpPermissions=egress_permissions) +def enable_trail_encryption(event, context): + \\"\\"\\" + remediates CloudTrail.2 by enabling SSE-KMS + On success returns a string map + On failure returns NoneType + \\"\\"\\" + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + + if event['trail_region'] != event['exec_region']: + exit('ERROR: cross-region remediation is not yet supported') - ingress_permissions, egress_permissions = get_permissions(group_id) - if ingress_permissions or egress_permissions: - raise Exception(f\\"VERIFICATION FAILED. SECURITY GROUP {group_id} NOT CLOSED.\\") + ctrail_client = connect_to_cloudtrail(event['trail_region'], boto_config) + kms_key_arn = event['kms_key_arn'] - return { - \\"output\\": \\"Security group closed successfully.\\" - }", + try: + ctrail_client.update_trail( + Name=event['trail'], + KmsKeyId=kms_key_arn + ) + return { + \\"response\\": { + \\"message\\": f'Enabled KMS CMK encryption on {event[\\"trail\\"]}', + \\"status\\": \\"Success\\" + } + } + except Exception as e: + exit(f'Error enabling SSE-KMS encryption: {str(e)}')", }, - "isCritical": true, "isEnd": true, - "maxAttempts": 3, - "name": "RemoveRulesAndVerify", - "onFailure": "Abort", + "name": "RevokeUnrotatedKeys", "outputs": Array [ Object { "Name": "Output", - "Selector": "$.Payload.output", - "Type": "String", + "Selector": "$.Payload", + "Type": "StringMap", }, ], - "timeoutSeconds": 180, + "timeoutSeconds": 600, }, ], "outputs": Array [ - "RemoveRulesAndVerify.Output", + "RevokeUnrotatedKeys.Output", ], "parameters": Object { "AutomationAssumeRole": Object { @@ -7703,16 +4116,22 @@ def handler(event, context): "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, - "GroupId": Object { - "allowedPattern": "sg-[a-z0-9]+$", - "description": "(Required) The unique ID of the security group.", + "IAMResourceId": Object { + "allowedPattern": "^[\\\\w+=,.@_-]{1,128}$", + "description": "(Required) IAM resource unique identifier.", + "type": "String", + }, + "MaxCredentialUsageAge": Object { + "allowedPattern": "^(\\\\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\\\\b)$", + "default": "90", + "description": "(Required) Maximum number of days within which a credential must be used. The default value is 90 days.", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentType": "Automation", - "Name": "SHARR-RemoveVPCDefaultSecurityGroupRules", + "Name": "SHARR-RevokeUnrotatedKeys", }, "Type": "AWS::SSM::Document", }, @@ -7749,7 +4168,7 @@ This step deactivates expired IAM User access keys, deletes expired login profil "IAMResourceId": "{{ IAMResourceId }}", "MaxCredentialUsageAge": "{{ MaxCredentialUsageAge }}", }, - "Runtime": "python3.6", + "Runtime": "python3.7", "Script": "import boto3 from datetime import datetime from datetime import timedelta @@ -8043,95 +4462,169 @@ def update_and_verify_iam_user_password_policy(event, context): }, "Type": "AWS::SSM::Document", }, - "ctcwremediationrole7AB69D0B": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W28", - "reason": "Static names chosen intentionally to provide integration in cross-account permissions", - }, - ], - }, - }, + "SHARRSetSSLBucketPolicyAutomationDocument5C7D5BF3": Object { "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ + "Content": Object { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - SHARR-SetSSLBucketPolicy + +## What does this document do? +This document adds a bucket policy to require transmission over HTTPS for the given S3 bucket by adding a policy statement to the bucket policy. + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* BucketName: (Required) Name of the bucket to modify. +* AccountId: (Required) Account to which the bucket belongs + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: S3.5 +* CIS v1.2.0: n/a +* PCI: S3.5 +", + "mainSteps": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": Object { - "Fn::Join": Array [ - "", - Array [ - "cloudtrail.", - Object { - "Ref": "AWS::URLSuffix", - }, - ], - ], + "action": "aws:executeScript", + "inputs": Object { + "Handler": "add_ssl_bucket_policy", + "InputPayload": Object { + "accountid": "{{AccountId}}", + "bucket": "{{BucketName}}", }, - }, - }, - ], - "Version": "2012-10-17", + "Runtime": "python3.7", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the \\"license\\" file accompanying this file. This file is distributed # +# on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + +def connect_to_s3(): + return boto3.client('s3', config=boto_config) + +def policy_to_add(bucket): + return { + \\"Sid\\": \\"AllowSSLRequestsOnly\\", + \\"Action\\": \\"s3:*\\", + \\"Effect\\": \\"Deny\\", + \\"Resource\\": [ + f'arn:aws:s3:::{bucket}', + f'arn:aws:s3:::{bucket}/*' + ], + \\"Condition\\": { + \\"Bool\\": { + \\"aws:SecureTransport\\": \\"false\\" + } }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "logs:CreateLogStream", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":logs:*:*:log-group:*", - ], - ], - }, - }, + \\"Principal\\": \\"*\\" + } +def new_policy(): + return { + \\"Id\\": \\"BucketPolicy\\", + \\"Version\\": \\"2012-10-17\\", + \\"Statement\\": [] + } + +def add_ssl_bucket_policy(event, context): + bucket_name = event['bucket'] + account_id = event['accountid'] + s3 = connect_to_s3() + bucket_policy = {} + try: + existing_policy = s3.get_bucket_policy( + Bucket=bucket_name, + ExpectedBucketOwner=account_id + ) + bucket_policy = json.loads(existing_policy['Policy']) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # delivery channel already exists - return + if exception_type not in [\\"NoSuchBucketPolicy\\"]: + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR getting bucket policy for {bucket_name}: {str(e)}') + + if not bucket_policy: + bucket_policy = new_policy() + + print(f'Existing policy: {bucket_policy}') + bucket_policy['Statement'].append(policy_to_add(bucket_name)) + + try: + result = s3.put_bucket_policy( + Bucket=bucket_name, + Policy=json.dumps(bucket_policy, indent=4, default=str), + ExpectedBucketOwner=account_id + ) + print(result) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR putting bucket policy for {bucket_name}: {str(e)}') + + print(f'New policy: {bucket_policy}')", + }, + "name": "Remediation", + "outputs": Array [ Object { - "Action": "logs:PutLogEvents", - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":logs:*:*:log-group:*:log-stream:*", - ], - ], - }, + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", }, ], - "Version": "2012-10-17", }, - "PolicyName": "default_lambdaPolicy", - }, - ], - "RoleName": Object { - "Fn::Join": Array [ - "", - Array [ - "SO0111-CloudTrailToCloudWatchLogs_", - Object { - "Ref": "AWS::Region", - }, - ], ], + "outputs": Array [ + "Remediation.Output", + ], + "parameters": Object { + "AccountId": Object { + "allowedPattern": "^[0-9]{12}$", + "description": "Account ID of the account for the finding", + "type": "String", + }, + "AutomationAssumeRole": Object { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[\\\\w+=,.@/-]+$", + "description": "(Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": Object { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\\\d+\\\\.)+\\\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])\\\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\\\-]*[a-z0-9])$)", + "description": "Name of the bucket to have a policy added", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentType": "Automation", + "Name": "SHARR-SetSSLBucketPolicy", }, - "Type": "AWS::IAM::Role", + "Type": "AWS::SSM::Document", }, }, } diff --git a/source/test/__snapshots__/solution_deploy.test.ts.snap b/source/test/__snapshots__/solution_deploy.test.ts.snap index 7c019d5b..5a6e6926 100644 --- a/source/test/__snapshots__/solution_deploy.test.ts.snap +++ b/source/test/__snapshots__/solution_deploy.test.ts.snap @@ -107,21 +107,16 @@ Object { "DeletionPolicy": "Delete", "DependsOn": Array [ "CreateCustomActionE7A973F5", + "createCustomActionPolicyE424E925", ], "Properties": Object { "Description": "Submit the finding to AWS Security Hub Automated Response and Remediation", "Id": "SHARRRemediation", "Name": "Remediate with SHARR", "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:eu-west-1:111111111111:function:SO0111-SHARR-CustomAction", - ], + "Fn::GetAtt": Array [ + "CreateCustomActionE7A973F5", + "Arn", ], }, }, @@ -591,6 +586,64 @@ Object { }, "Type": "AWS::Lambda::Function", }, + "getApprovalRequirementE7F50E54": Object { + "DependsOn": Array [ + "orchestratorRole46A9F242", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "False positive. Access is provided via a policy", + }, + Object { + "id": "W89", + "reason": "There is no need to run this lambda in a VPC", + }, + Object { + "id": "W92", + "reason": "There is no need for Reserved Concurrency", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": "solutions-eu-west-1", + "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/get_approval_requirement.py.zip", + }, + "Description": "Determines if a manual approval is required for remediation", + "Environment": Object { + "Variables": Object { + "AWS_PARTITION": Object { + "Ref": "AWS::Partition", + }, + "SOLUTION_ID": "SO0111", + "SOLUTION_VERSION": "v1.0.0", + "WORKFLOW_RUNBOOK": "", + "log_level": "info", + }, + }, + "FunctionName": "SO0111-SHARR-getApprovalRequirement", + "Handler": "get_approval_requirement.lambda_handler", + "Layers": Array [ + Object { + "Ref": "SharrLambdaLayer5BF8F147", + }, + ], + "MemorySize": 256, + "Role": Object { + "Fn::GetAtt": Array [ + "orchestratorRole46A9F242", + "Arn", + ], + }, + "Runtime": "python3.8", + "Timeout": 600, + }, + "Type": "AWS::Lambda::Function", + }, "monitorSSMExecStateB496B8AF": Object { "DependsOn": Array [ "orchestratorRole46A9F242", @@ -691,7 +744,7 @@ Object { Object { "Ref": "AWS::Partition", }, - ":ssm:*:111111111111:parameter/Solutions/SO0111/*", + ":ssm:eu-west-1:111111111111:parameter/Solutions/SO0111/*", ], ], }, @@ -731,6 +784,9 @@ Object { }, "PolicyName": "SO0111-SHARR_Orchestrator_Notifier", "Roles": Array [ + Object { + "Ref": "orchestratorRole46A9F242", + }, Object { "Ref": "notifyRole40298120", }, @@ -861,7 +917,7 @@ Object { Object { "Ref": "AWS::Partition", }, - ":iam::*:role/SO0111-SHARR-Orchestrator-Member_eu-west-1", + ":iam::*:role/SO0111-SHARR-Orchestrator-Member", ], ], }, @@ -892,6 +948,7 @@ Object { "Type": "AWS::IAM::Policy", }, "orchestratorRole12B410FD": Object { + "DeletionPolicy": "Retain", "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -1049,6 +1106,34 @@ Object { ], ], }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:eu-west-1:111111111111:function:", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::GetAtt": Array [ + "getApprovalRequirementE7F50E54", + "Arn", + ], + }, + ], + }, + ], + }, + ], + ], + }, ], }, Object { @@ -1079,6 +1164,7 @@ Object { ], }, "Type": "AWS::IAM::Role", + "UpdateReplacePolicy": "Retain", }, "orchestratorRole46A9F242": Object { "Metadata": Object { @@ -1105,7 +1191,7 @@ Object { "Version": "2012-10-17", }, "Description": "Lambda role to allow cross account read-only SHARR orchestrator functions", - "RoleName": "SO0111-SHARR-Orchestrator-Admin_eu-west-1", + "RoleName": "SO0111-SHARR-Orchestrator-Admin", }, "Type": "AWS::IAM::Role", }, @@ -1130,7 +1216,7 @@ Object { "Fn::Join": Array [ "", Array [ - "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Automation Document State\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", + "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Remediation Approval Requirement\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, @@ -1141,7 +1227,7 @@ Object { "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\"},\\"Resource\\":\\"arn:", + "\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\",\\"ResourceRegion.$\\":\\"$.Payload.resourceregion\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, @@ -1152,7 +1238,18 @@ Object { "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\"},\\"Resource\\":\\"arn:", + "\\",\\"Payload.$\\":\\"$\\"}},\\"Get Remediation Approval Requirement\\":{\\"Next\\":\\"Get Automation Document State\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Determine whether the selected remediation requires manual approval\\",\\"TimeoutSeconds\\":300,\\"ResultPath\\":\\"$.Workflow\\",\\"ResultSelector\\":{\\"WorkflowDocument.$\\":\\"$.Payload.workflowdoc\\",\\"WorkflowAccount.$\\":\\"$.Payload.workflowaccount\\",\\"WorkflowRole.$\\":\\"$.Payload.workflowrole\\",\\"WorkflowConfig.$\\":\\"$.Payload.workflow_data\\"},\\"Resource\\":\\"arn:", + Object { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", + Object { + "Fn::GetAtt": Array [ + "getApprovalRequirementE7F50E54", + "Arn", + ], + }, + "\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"Remediation Queued\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"Account.$\\":\\"$.Payload.executionaccount\\",\\"Region.$\\":\\"$.Payload.executionregion\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", }, @@ -1163,6 +1260,17 @@ Object { "Arn", ], }, + "\\",\\"Payload.$\\":\\"$\\"}},\\"Remediation Queued\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)\\",\\"State.$\\":\\"States.Format('QUEUED')\\",\\"ExecId.$\\":\\"$.SSMExecution.ExecId\\"}},\\"Next\\":\\"Queued Notification\\"},\\"Queued Notification\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notification that a remediation has queued\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.notificationResult\\",\\"Resource\\":\\"arn:", + Object { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", + Object { + "Fn::GetAtt": Array [ + "sendNotifications1367638A", + "Arn", + ], + }, "\\",\\"Payload.$\\":\\"$\\"}},\\"execMonitor\\":{\\"Next\\":\\"Remediation completed?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Monitor the remediation execution until done\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.Remediation\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"RemediationState.$\\":\\"$.Payload.remediation_status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"LogData.$\\":\\"$.Payload.logdata\\",\\"AffectedObject.$\\":\\"$.Payload.affected_object\\"},\\"Resource\\":\\"arn:", Object { "Ref": "AWS::Partition", @@ -1174,7 +1282,7 @@ Object { "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", + "\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", ], ], }, diff --git a/source/test/admin_account_parm.test.ts b/source/test/admin_account_parm.test.ts new file mode 100644 index 00000000..5f11aaa2 --- /dev/null +++ b/source/test/admin_account_parm.test.ts @@ -0,0 +1,34 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + +import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { App, Stack } from '@aws-cdk/core'; +import { AdminAccountParm } from '../lib/admin_account_parm-construct'; + +function createAdminAccountParm(): Stack { + const app = new App(); + const stack = new Stack(app, 'testStack', { + stackName: 'testStack' + }); + const admin = new AdminAccountParm(stack, 'roles', { + solutionId: 'SO0111' + }) + return stack; +} +test('AdminParm Test Stack', () => { + expect(SynthUtils.toCloudFormation(createAdminAccountParm())).toMatchSnapshot(); +}); + \ No newline at end of file diff --git a/source/test/member_stack.test.ts b/source/test/member_stack.test.ts index 3dab1560..e802ec35 100644 --- a/source/test/member_stack.test.ts +++ b/source/test/member_stack.test.ts @@ -1,3 +1,18 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { MemberStack } from '../solution_deploy/lib/sharr_member-stack'; diff --git a/source/test/orchestrator.test.ts b/source/test/orchestrator.test.ts index a0701366..6c01eba8 100644 --- a/source/test/orchestrator.test.ts +++ b/source/test/orchestrator.test.ts @@ -1,3 +1,18 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + import { SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { App, Stack } from '@aws-cdk/core'; @@ -70,6 +85,7 @@ test('test App Orchestrator Construct', () => { ssmExecDocLambda: 'yyy', ssmExecMonitorLambda: 'zzz', notifyLambda: 'aaa', + getApprovalRequirementLambda: 'bbb', solutionId: 'bbb', solutionName: 'This is a test', solutionVersion: '1.1.1', diff --git a/source/test/orchestrator_logs.test.ts b/source/test/orchestrator_logs.test.ts new file mode 100644 index 00000000..9d5b7e4c --- /dev/null +++ b/source/test/orchestrator_logs.test.ts @@ -0,0 +1,34 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + + import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; + import * as cdk from '@aws-cdk/core'; + import { OrchLogStack } from '../solution_deploy/lib/orchestrator-log-stack'; + + const app = new cdk.App(); + + function getTestStack(): cdk.Stack { + const app = new cdk.App(); + const stack = new OrchLogStack(app, 'roles', { + description: 'test;', + solutionId: 'SO0111', + logGroupName: 'TestLogGroup' + }) + return stack; + } + test('Global Roles Stack', () => { + expect(SynthUtils.toCloudFormation(getTestStack())).toMatchSnapshot(); + }); + \ No newline at end of file diff --git a/source/test/runbook_stack.test.ts b/source/test/runbook_stack.test.ts index fd1f5ea0..45234872 100644 --- a/source/test/runbook_stack.test.ts +++ b/source/test/runbook_stack.test.ts @@ -1,19 +1,51 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; -import { RemediationRunbookStack } from '../solution_deploy/lib/remediation_runbook-stack'; +import { MemberRoleStack, RemediationRunbookStack } from '../solution_deploy/lib/remediation_runbook-stack'; + +const app = new cdk.App(); + +function getRoleTestStack(): cdk.Stack { + const app = new cdk.App(); + const stack = new MemberRoleStack(app, 'roles', { + description: 'test;', + solutionId: 'SO0111', + solutionVersion: 'v1.1.1', + solutionDistBucket: 'sharrbukkit' + }) + return stack; +} +test('Global Roles Stack', () => { + expect(SynthUtils.toCloudFormation(getRoleTestStack())).toMatchSnapshot(); +}); -function getTestStack(): cdk.Stack { +function getSsmTestStack(): cdk.Stack { const app = new cdk.App(); const stack = new RemediationRunbookStack(app, 'stack', { description: 'test;', solutionId: 'SO0111', solutionVersion: 'v1.1.1', solutionDistBucket: 'sharrbukkit', - ssmdocs: 'remediation_runbooks' + ssmdocs: 'remediation_runbooks', + roleStack: getRoleTestStack() }) return stack; } -test('default stack', () => { - expect(SynthUtils.toCloudFormation(getTestStack())).toMatchSnapshot(); +test('Regional Documents', () => { + expect(SynthUtils.toCloudFormation(getSsmTestStack())).toMatchSnapshot(); }); \ No newline at end of file diff --git a/source/test/solution_deploy.test.ts b/source/test/solution_deploy.test.ts index b5874655..59d35933 100644 --- a/source/test/solution_deploy.test.ts +++ b/source/test/solution_deploy.test.ts @@ -1,3 +1,18 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + import { expect as expectCDK, matchTemplate, MatchStyle, SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; diff --git a/source/test/ssmplaybook.test.ts b/source/test/ssmplaybook.test.ts index 0a332ec6..5ef8c2ce 100644 --- a/source/test/ssmplaybook.test.ts +++ b/source/test/ssmplaybook.test.ts @@ -1,3 +1,18 @@ +/***************************************************************************** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. A copy of the * + * License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed * + * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * + * express or implied. See the License for the specific language governing * + * permissions and limitations under the License. * + *****************************************************************************/ + import {expect as expectCDK, haveResourceLike, ResourcePart, SynthUtils} from '@aws-cdk/assert'; import { App, Stack } from '@aws-cdk/core'; import { @@ -6,6 +21,8 @@ import { Effect } from '@aws-cdk/aws-iam'; import { SsmPlaybook, Trigger, SsmRole, SsmRemediationRunbook } from '../lib/ssmplaybook'; +import { AdminAccountParm } from '../lib/admin_account_parm-construct'; +import { MemberRoleStack } from '../solution_deploy/lib/remediation_runbook-stack'; // ---------------------------- // SsmPlaybook - Parse Runbook @@ -75,9 +92,15 @@ function getTriggerStack(): Stack { // --------------------- function getSsmRemediationRunbook(): Stack { const app = new App(); - const stack = new Stack(app, 'MyTestStack', { + const stack = new Stack(app, 'MyTestStack', { stackName: 'testStack' }); + const roleStack = new MemberRoleStack(app, 'roles', { + description: 'test;', + solutionId: 'SO0111', + solutionVersion: 'v1.1.1', + solutionDistBucket: 'sharrbukkit' + }) new SsmRemediationRunbook(stack, 'Playbook', { ssmDocName: 'blahblahblah', ssmDocPath: 'test/test_data/', @@ -114,59 +137,61 @@ test('Test Shared Remediation Generation', () => { }); // ------------------ -// SsmRole +// SsmRole // ------------------ function getSsmRemediationRoleCis(): Stack { - const app = new App(); - const stack = new Stack(app, 'MyTestStack', { - stackName: 'testStack' - }); - let inlinePolicy = new Policy(stack, 'Policy') - let rdsPerms = new PolicyStatement(); - rdsPerms.addActions("rds:ModifyDBSnapshotAttribute") - rdsPerms.effect = Effect.ALLOW - rdsPerms.addResources("*"); - inlinePolicy.addStatements(rdsPerms) - - new SsmRole(stack, 'Role', { - solutionId: "SO0111", - ssmDocName: "foobar", - adminAccountNumber: "111111111111", - remediationPolicy: inlinePolicy, - remediationRoleName: "SHARR-RemediationRoleName" - }) - return stack; + const app = new App(); + const stack = new Stack(app, 'MyTestStack', { + stackName: 'testStack' + }); + let inlinePolicy = new Policy(stack, 'Policy') + let rdsPerms = new PolicyStatement(); + rdsPerms.addActions("rds:ModifyDBSnapshotAttribute") + rdsPerms.effect = Effect.ALLOW + rdsPerms.addResources("*"); + inlinePolicy.addStatements(rdsPerms) + const adminAccount = new AdminAccountParm(stack, 'AdminAccountParameter', { + solutionId: 'SO0111' + }) + new SsmRole(stack, 'Role', { + solutionId: "SO0111", + ssmDocName: "foobar", + remediationPolicy: inlinePolicy, + remediationRoleName: "SHARR-RemediationRoleName" + }) + return stack; } test('Test SsmRole Generation', () => { - expectCDK(getSsmRemediationRoleCis()).to(haveResourceLike("AWS::IAM::Role", { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::111111111111:role/SO0111-SHARR-Orchestrator-Admin_", - { - "Ref": "AWS::Region" - } - ] +expectCDK(getSsmRemediationRoleCis()).to(haveResourceLike("AWS::IAM::Role", { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "SecHubAdminAccount" + }, + ":role/SO0111-SHARR-Orchestrator-Admin" ] - }, - "Service": "ssm.amazonaws.com" - } + ] + }, + "Service": "ssm.amazonaws.com" } - ], - "Version": "2012-10-17" - }, - "RoleName": "SHARR-RemediationRoleName" - }, ResourcePart.Properties)); + } + ], + "Version": "2012-10-17" + }, + "RoleName": "SHARR-RemediationRoleName" +}, ResourcePart.Properties)); }); diff --git a/source/version.txt b/source/version.txt deleted file mode 100644 index a58941b0..00000000 --- a/source/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.3 \ No newline at end of file