From 8cfa35d4f30fd5298af80d5c77dcb31153f9c447 Mon Sep 17 00:00:00 2001 From: garvit singh Date: Thu, 2 Jun 2022 11:22:31 -0400 Subject: [PATCH] update to v1.5.0 --- .DS_Store | Bin 0 -> 6148 bytes CHANGELOG.md | 14 +- README.md | 10 +- deployment/build-s3-dist.sh | 41 +- deployment/requirements.txt | 2 +- deployment/run-unit-tests.sh | 14 +- deployment/testing_requirements.txt | 2 + source/.DS_Store | Bin 0 -> 6148 bytes source/LambdaLayers/.DS_Store | Bin 0 -> 6148 bytes source/LambdaLayers/cfnresponse.py | 44 + source/LambdaLayers/test/.DS_Store | Bin 0 -> 6148 bytes source/LambdaLayers/test/test_cfnresponse.py | 88 + source/Orchestrator/.DS_Store | Bin 0 -> 6148 bytes .../lib/common-orchestrator-construct.ts | 38 +- .../test/test_check_ssm_doc_state.py | 2 +- source/lib/orchestrator_roles-construct.ts | 70 +- source/lib/sharrplaybook-construct.ts | 73 +- source/lib/ssmplaybook.ts | 169 +- source/package.json | 47 +- source/playbooks/.DS_Store | Bin 0 -> 6148 bytes source/playbooks/AFSBP/README.md | 9 +- source/playbooks/AFSBP/bin/afsbp.ts | 135 +- .../AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml | 30 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml | 17 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml | 18 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml | 97 + .../AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml | 98 + .../AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml | 75 + .../AFSBP/ssmdocs/AFSBP_Config.1.yaml | 21 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml | 20 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml | 34 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml | 36 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml | 21 +- .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml | 93 + .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml | 15 +- .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml | 20 +- .../AFSBP/ssmdocs/AFSBP_Lambda.1.yaml | 39 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml | 33 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml | 92 + .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml | 98 + .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml | 91 + .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml | 109 + .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml | 94 + .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml | 33 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml | 35 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml | 94 + .../AFSBP/ssmdocs/AFSBP_Redshift.1.yaml | 93 + .../AFSBP/ssmdocs/AFSBP_Redshift.3.yaml | 110 + .../AFSBP/ssmdocs/AFSBP_Redshift.4.yaml | 186 + .../AFSBP/ssmdocs/AFSBP_Redshift.6.yaml | 110 + .../playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml | 21 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml | 26 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml | 95 + .../playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml | 21 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml | 108 + .../ssmdocs/scripts/deserializeApiList.py | 15 + .../test/test_s3-6_deserialize_api_list.py | 28 + .../__snapshots__/afsbp_stack.test.ts.snap | 1542 ++- .../playbooks/AFSBP/test/afsbp_stack.test.ts | 1 + source/playbooks/CIS120/README.md | 1 + source/playbooks/CIS120/bin/cis120.ts | 259 +- source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml | 24 +- source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml | 35 +- source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml | 17 +- source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml | 13 +- source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml | 42 +- source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml | 26 +- source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml | 36 +- source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml | 21 +- source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml | 20 +- source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml | 18 +- source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml | 38 +- source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml | 17 +- source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml | 21 +- source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml | 36 +- source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml | 36 +- .../test/__snapshots__/cis_stack.test.ts.snap | 1543 ++- .../playbooks/CIS120/test/cis_stack.test.ts | 3 +- source/playbooks/NEWPLAYBOOK/README.md | 6 + .../playbooks/NEWPLAYBOOK/bin/newplaybook.ts | 74 +- .../NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml | 49 +- source/playbooks/PCI321/.DS_Store | Bin 0 -> 6148 bytes source/playbooks/PCI321/README.md | 5 + source/playbooks/PCI321/bin/pci321.ts | 123 +- .../PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml | 31 +- .../PCI321/ssmdocs/PCI_PCI.CW.1.yaml | 21 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml | 18 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml | 19 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml | 42 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml | 34 +- .../PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml | 75 + .../PCI321/ssmdocs/PCI_PCI.Config.1.yaml | 19 +- .../PCI321/ssmdocs/PCI_PCI.EC2.1.yaml | 20 +- .../PCI321/ssmdocs/PCI_PCI.EC2.2.yaml | 30 +- .../PCI321/ssmdocs/PCI_PCI.EC2.5.yaml | 80 + .../PCI321/ssmdocs/PCI_PCI.EC2.6.yaml | 38 +- .../PCI321/ssmdocs/PCI_PCI.IAM.7.yaml | 20 +- .../PCI321/ssmdocs/PCI_PCI.IAM.8.yaml | 15 +- .../PCI321/ssmdocs/PCI_PCI.KMS.1.yaml | 36 +- .../PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml | 40 +- .../PCI321/ssmdocs/PCI_PCI.RDS.1.yaml | 33 +- .../PCI321/ssmdocs/PCI_PCI.RDS.2.yaml | 91 + .../PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml | 92 + .../PCI321/ssmdocs/PCI_PCI.S3.1.yaml | 21 +- .../PCI321/ssmdocs/PCI_PCI.S3.4.yaml | 95 + .../PCI321/ssmdocs/PCI_PCI.S3.5.yaml | 21 +- .../PCI321/ssmdocs/PCI_PCI.S3.6.yaml | 21 +- .../__snapshots__/pci321_stack.test.ts.snap | 1543 ++- .../PCI321/test/pci321_stack.test.ts | 3 +- source/playbooks/common/deserialize_json.py | 11 + source/playbooks/common/parse_input.py | 197 + .../playbooks/common/test/test_afsbp_parse.py | 267 + .../common/test/test_cis120_parse.py | 424 + .../common/test/test_deserialize_json.py | 14 + .../common/test/test_pci321_parse.py | 307 + .../ConfigureS3BucketPublicAccessBlock.yaml | 6 +- .../ConfigureS3PublicAccessBlock.yaml | 8 +- .../CreateAccessLoggingBucket.yaml | 4 +- .../CreateCloudTrailMultiRegionTrail.yaml | 12 +- .../CreateLogMetricFilterAndAlarm.yaml | 27 +- .../DisablePublicAccessToRDSInstance.yaml | 128 + .../DisablePublicAccessToRedshiftCluster.yaml | 77 + .../remediation_runbooks/EnableAWSConfig.yaml | 24 +- .../EnableAutoScalingGroupELBHealthCheck.yaml | 8 +- ...leAutomaticSnapshotsOnRedshiftCluster.yaml | 125 + ...omaticVersionUpgradeOnRedshiftCluster.yaml | 79 + .../EnableCloudTrailEncryption.yaml | 4 +- .../EnableCloudTrailLogFileValidation.yaml | 4 +- .../EnableCloudTrailToCloudWatchLogging.yaml | 6 +- .../EnableCopyTagsToSnapshotOnRDSCluster.yaml | 101 + .../EnableDefaultEncryptionS3.yaml | 80 + .../EnableEbsEncryptionByDefault.yaml | 4 +- ...EnableEnhancedMonitoringOnRDSInstance.yaml | 4 +- .../EnableKeyRotation.yaml | 2 +- ...bleMinorVersionUpgradeOnRDSDBInstance.yaml | 93 + .../EnableMultiAZOnRDSInstance.yaml | 127 + .../EnableRDSClusterDeletionProtection.yaml | 2 +- .../EnableRDSInstanceDeletionProtection.yaml | 90 + .../EnableRedshiftClusterAuditLogging.yaml | 122 + .../EnableVPCFlowLogs.yaml | 6 +- .../EncryptRDSSnapshot.yaml | 143 + .../MakeEBSSnapshotsPrivate.yaml | 10 +- .../MakeRDSSnapshotPrivate.yaml | 10 +- .../RemoveLambdaPublicAccess.yaml | 12 +- .../RemoveVPCDefaultSecurityGroupRules.yaml | 4 +- .../ReplaceCodeBuildClearTextCredentials.yaml | 95 + .../RevokeUnrotatedKeys.yaml | 6 +- .../RevokeUnusedIAMUserCredentials.yaml | 26 +- .../remediation_runbooks/S3BlockDenylist.yaml | 52 + .../SetIAMPasswordPolicy.yaml | 6 +- .../SetSSLBucketPolicy.yaml | 8 +- .../scripts/PutS3BucketPolicyDeny.py | 150 + .../ReplaceCodeBuildClearTextCredentials.py | 175 + .../scripts/RevokeUnrotatedKeys.py | 18 +- .../test/test_puts3bucketpolicydeny.py | 433 + ...st_replacecodebuildcleartextcredentials.py | 679 + .../scripts/test/test_revokeunrotatedkeys.py | 81 +- source/solution_deploy/.DS_Store | Bin 0 -> 6148 bytes source/solution_deploy/bin/solution_deploy.ts | 99 +- .../lib/remediation_runbook-stack.ts | 807 +- source/solution_deploy/lib/runbook_factory.ts | 273 + .../solution_deploy/lib/sharr_member-stack.ts | 327 +- .../lib/solution_deploy-stack.ts | 56 +- source/solution_deploy/source/bin/normalizer | 8 + .../test/test_updatableRunbookProvider.py | 436 + .../source/updatableRunbookProvider.py | 198 + .../__snapshots__/member_stack.test.ts.snap | 322 + .../__snapshots__/orchestrator.test.ts.snap | 182 +- .../__snapshots__/runbook_stack.test.ts.snap | 10926 ++++++++++------ .../solution_deploy.test.ts.snap | 122 +- source/test/member_stack.test.ts | 15 +- source/test/orchestrator.test.ts | 17 +- source/test/orchestrator_logs.test.ts | 5 +- source/test/regex_registry.test.ts | 9 + source/test/regex_registry.ts | 623 + source/test/runbook_stack.test.ts | 7 +- source/test/runbook_validator.test.ts | 525 + source/test/solution_deploy.test.ts | 5 +- source/test/ssmplaybook.test.ts | 37 +- source/test/test_data/tstest-runbook.yaml | 6 +- 180 files changed, 21011 insertions(+), 7613 deletions(-) create mode 100644 .DS_Store create mode 100644 source/.DS_Store create mode 100644 source/LambdaLayers/.DS_Store create mode 100644 source/LambdaLayers/cfnresponse.py create mode 100644 source/LambdaLayers/test/.DS_Store create mode 100644 source/LambdaLayers/test/test_cfnresponse.py create mode 100644 source/Orchestrator/.DS_Store create mode 100644 source/playbooks/.DS_Store create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.3.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.4.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.6.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml create mode 100644 source/playbooks/AFSBP/ssmdocs/scripts/deserializeApiList.py create mode 100644 source/playbooks/AFSBP/ssmdocs/scripts/test/test_s3-6_deserialize_api_list.py create mode 100644 source/playbooks/PCI321/.DS_Store create mode 100644 source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml create mode 100644 source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml create mode 100644 source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml create mode 100644 source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml create mode 100644 source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.4.yaml create mode 100644 source/playbooks/common/deserialize_json.py create mode 100644 source/playbooks/common/parse_input.py create mode 100644 source/playbooks/common/test/test_afsbp_parse.py create mode 100644 source/playbooks/common/test/test_cis120_parse.py create mode 100644 source/playbooks/common/test/test_deserialize_json.py create mode 100644 source/playbooks/common/test/test_pci321_parse.py create mode 100644 source/remediation_runbooks/DisablePublicAccessToRDSInstance.yaml create mode 100644 source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml create mode 100644 source/remediation_runbooks/EnableAutomaticSnapshotsOnRedshiftCluster.yaml create mode 100644 source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml create mode 100644 source/remediation_runbooks/EnableCopyTagsToSnapshotOnRDSCluster.yaml create mode 100644 source/remediation_runbooks/EnableDefaultEncryptionS3.yaml create mode 100644 source/remediation_runbooks/EnableMinorVersionUpgradeOnRDSDBInstance.yaml create mode 100644 source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml create mode 100644 source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml create mode 100644 source/remediation_runbooks/EnableRedshiftClusterAuditLogging.yaml create mode 100644 source/remediation_runbooks/EncryptRDSSnapshot.yaml create mode 100644 source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml create mode 100644 source/remediation_runbooks/S3BlockDenylist.yaml create mode 100644 source/remediation_runbooks/scripts/PutS3BucketPolicyDeny.py create mode 100644 source/remediation_runbooks/scripts/ReplaceCodeBuildClearTextCredentials.py create mode 100644 source/remediation_runbooks/scripts/test/test_puts3bucketpolicydeny.py create mode 100644 source/remediation_runbooks/scripts/test/test_replacecodebuildcleartextcredentials.py create mode 100644 source/solution_deploy/.DS_Store create mode 100644 source/solution_deploy/lib/runbook_factory.ts create mode 100755 source/solution_deploy/source/bin/normalizer create mode 100644 source/solution_deploy/source/test/test_updatableRunbookProvider.py create mode 100644 source/solution_deploy/source/updatableRunbookProvider.py create mode 100644 source/test/regex_registry.test.ts create mode 100644 source/test/regex_registry.ts create mode 100644 source/test/runbook_validator.test.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1404b5720778f133f9b97da93324aa995f72196a GIT binary patch literal 6148 zcmeHKyH3ME5S)VuMbMUZip`nbF+Q(nO$YZu=p7Rj(EimZ}^;5pHCR~79Bb7=rQ4U`)&U+ z3_TCV4ffnR-~(IirW@zaR8|T|0VyB_q<|E-tbnyv+VUbtaSRMPir>t0^5&8gO7Yv7 zuU?MQ0u`lz6gXAjI+iQz|Bv(w^ZzMHD=8oa{*?k{v3c68`AXSaXD?^Hw$h*JKgL=w mXYf``^j6G;wc`6pUA1TKx5P2f>C8Kws2>5-MJ5IQLV<4;d>Zut literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md index d53dc054..acea2c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,20 @@ 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.5.0] - 2022-05-31 + +### Added +- New remediations - see Implementation Guide + +### Changed +- Improved cross-region remediation using resource region from Resources[0].Id +- Added custom resource provider for SSM documents to allow in-place stack upgrades + ## [1.4.2] - 2022-01-14 ### Changed - Fix to correct the generator id pattern for CIS 1.2.0 Ruleset. - ## [1.4.1] - 2022-01-05 ### Changed @@ -52,14 +60,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New PCI-DSS v3.2.1 Playbook support for 17 controls (see IG for details) - Library of remediation SSM Automation runbooks - NEWPLAYBOOK as a template for custom playbook creation - + ### Changed - Updated to CDK v1.117.0 - Reduced duplicate code - Updated CIS playbook to Orchestrator architecture - Single Orchestrator deployment to enable multi-standard remediation with a single click - Custom Actions now consolidated to one: "Remediate with SHARR" - + ### Removed - AWS Service Catalog for Playbook deployment diff --git a/README.md b/README.md index daa62d06..dfd7bc43 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,11 @@ Detailed instructions for creating a new automated remediation in an existing Pl ### Prerequisites for Customization -- a Linux client with the AWS CLI v2 installed and python 3.7+, AWS CDK 1.114.0+ +- a Linux client with the following software + - AWS CLI v2 + - Python 3.7+ with pip + - AWS CDK 1.155.0+ + - Node.js with npm - source code downloaded from GitHub - two S3 buckets (minimum): 1 global and 1 for each region where you will deploy - An Amazon S3 Bucket for solution templates - accessed globally via https. @@ -179,6 +183,10 @@ build-s3-dist.sh -b -v #### Run Unit Tests +Some Python unit tests execute AWS API calls. The calls that create, read, or modify resources are stubbed, but some +calls to APIs that do not require any permissions execute against the real AWS APIs (e.g. STS GetCallerIdentity). The +recommended way to run the unit tests is to configure your credentials for a no-access console role. + ```bash cd ./deployment chmod +x ./run-unit-tests.sh diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index f2ee27a8..d04f2dfd 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -18,9 +18,9 @@ # Important: CDK global version number # This controls the CDK and AWS Solutions Constructs version. Solutions -# Constructs versions map 1:1 to CDK versions. When setting this value, +# Constructs versions map 1:1 to CDK versions. When setting this value, # choose the latest AWS Solutions Constructs version. -required_cdk_version=1.132.0 +required_cdk_version=1.155.0 # Get reference for all important folders template_dir="$PWD" @@ -74,17 +74,17 @@ do b ) bucket=${OPTARG};; v ) version=${OPTARG};; t ) devtest=1;; - c) + 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 "Version must be provided via a parameter or ../version.txt. Others are optional." 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" - exit 1 + echo "Dev example: ./build-s3-dist.sh -b solutions -v v1.0.0 -t" + exit 1 ;; esac done @@ -92,7 +92,7 @@ done #------------------------------------------------------------------------------ # DISABLE OVERRIDE WARNINGS #------------------------------------------------------------------------------ -# Use with care: disables the warning for overridden properties on +# Use with care: disables the warning for overridden properties on # AWS Solutions Constructs export overrideWarningsEnabled=false @@ -127,7 +127,7 @@ echo "export DIST_VERSION=$version" >> ./setenv.sh # # It takes precedence over the command line (oddly backwards, but to prevent # errors) -# +# # Ex: # #!/bin/bash # SOLUTION_ID='SO0111' @@ -162,7 +162,7 @@ fi if [[ -z "$SOLUTION_TRADEMARKEDNAME" ]]; then echo "SOLUTION_TRADEMARKEDNAME is missing from ../solution_env.sh" exit 1 -else +else export SOLUTION_TRADEMARKEDNAME echo "export DIST_SOLUTION_NAME=$SOLUTION_TRADEMARKEDNAME" >> ./setenv.sh fi @@ -196,7 +196,7 @@ export PATH=$(npm bin):$PATH # Check cdk version cdkver=`cdk --version | grep -Eo '^[0-9]{1,2}\.[0-9]+\.[0-9]+'` echo CDK version $cdkver -if [[ $cdkver != $required_cdk_version ]]; then +if [[ $cdkver != $required_cdk_version ]]; then echo Required CDK version is $required_cdk_version, found $cdkver exit 255 fi @@ -215,10 +215,19 @@ find . -name package-lock.json | while read file;do rm $file; done mkdir -p $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 +do_cmd 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 +echo "------------------------------------------------------------------------------" +echo "[Pack] Member Stack Lambda Layer (used by custom resources)" +echo "------------------------------------------------------------------------------" +do_cmd mkdir -p $temp_work_dir/source/solution_deploy/memberlambdalayer/python +do_cmd cp ${template_dir}/${source_dir}/LambdaLayers/cfnresponse.py $temp_work_dir/source/solution_deploy/memberlambdalayer/python +do_cmd cp ${template_dir}/${source_dir}/LambdaLayers/logger.py $temp_work_dir/source/solution_deploy/memberlambdalayer/python +do_cmd cd $temp_work_dir/source/solution_deploy/memberlambdalayer +do_cmd zip --recurse-paths ${build_dist_dir}/lambda/memberLayer.zip python + echo "------------------------------------------------------------------------------" echo "[Pack] Custom Action Lambda" echo "------------------------------------------------------------------------------" @@ -229,12 +238,18 @@ zip ${build_dist_dir}/lambda/createCustomAction.py.zip createCustomAction.py # These are not packaged with the Lambda do_cmd cp ../../LambdaLayers/*.py . +echo "------------------------------------------------------------------------------" +echo "[Pack] Updatable Runbook Provider Lambda" +echo "------------------------------------------------------------------------------" +do_cmd cd $temp_work_dir/source/solution_deploy/source +do_cmd zip ${build_dist_dir}/lambda/updatableRunbookProvider.py.zip updatableRunbookProvider.py + echo "------------------------------------------------------------------------------" echo "[Pack] Orchestrator Lambdas" echo "------------------------------------------------------------------------------" # cd $template_dir cd $temp_work_dir/source/Orchestrator -ls | while read file; do +ls | while read file; do if [ ! -d $file ]; then zip ${build_dist_dir}/lambda/${file}.zip ${file} fi @@ -247,7 +262,7 @@ echo "-------------------------------------------------------------------------- echo "[Create] Playbooks" echo "------------------------------------------------------------------------------" for playbook in `ls ${template_dir}/${source_dir}/playbooks`; do - if [ $playbook == 'NEWPLAYBOOK' ] || [ $playbook == '.coverage' ]; then + if [ $playbook == 'NEWPLAYBOOK' ] || [ $playbook == '.coverage' ] || [ $playbook == 'common' ]; then continue fi echo Create $playbook playbook diff --git a/deployment/requirements.txt b/deployment/requirements.txt index d5c2bc9c..4a5625c7 100644 --- a/deployment/requirements.txt +++ b/deployment/requirements.txt @@ -1 +1 @@ -requests>=2.22.0 +requests>=2.25.0 diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index b1c65667..a09e4af1 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -115,13 +115,17 @@ echo "-------------------------------------------------------------------------- run_pytest "${source_dir}/remediation_runbooks/scripts" "RemediationRunbooks" echo "------------------------------------------------------------------------------" -echo "[Test] Python Scripts for Playbook Scripts" +echo "[Test] Python Scripts for Playbook common scripts" +echo "------------------------------------------------------------------------------" +run_pytest "${source_dir}/playbooks/common" "PlaybookCommon" + +echo "------------------------------------------------------------------------------" +echo "[Test] Python Scripts for Playbooks" echo "------------------------------------------------------------------------------" for playbook in `ls ${source_dir}/playbooks`; do - # if [ $playbook == 'NEWPLAYBOOK' ]; then - # continue - # fi - run_pytest "${source_dir}/playbooks/${playbook}/ssmdocs/scripts" "Playbook${playbook}" + if [ -d ${source_dir}/playbooks/${playbook}/ssmdocs/scripts/tests ]; then + run_pytest "${source_dir}/playbooks/${playbook}/ssmdocs/scripts" "Playbook${playbook}" + fi done # The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list diff --git a/deployment/testing_requirements.txt b/deployment/testing_requirements.txt index 24090165..221eeb30 100644 --- a/deployment/testing_requirements.txt +++ b/deployment/testing_requirements.txt @@ -3,3 +3,5 @@ pytest>=4.2.1 pytest-cov pytest-env bandit +boto3==1.23.9 +requests==2.27.1 diff --git a/source/.DS_Store b/source/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..25109c131e610579ab4dbc6834b3c9d558e3c96a GIT binary patch literal 6148 zcmeHK!A`?441Hlk8e$SVa?CIA2i3IQkT~!Gu&$GsC}e8cVYhxd&#^(R=ot-UOUZK_ z+fAO-BnA+c$LbDP0GP1}22n;t%%f8m=6p#EIY)&J?%7V*jU+lpldOH7*ZhVO{q;9! zt9DzLDb?H@CaAGT**DGcfCgKx5BXW*YP5T>ig)smaCyYf4mh3n7#7gpu zObo#Gk7);t0M>LTwjO3?%m>_Y#trxB_H+5ZUN2sxtvcW-eZ*`(w*@I61*Cu!kOER* zK?>wCzTPb8ne-@9Kng5F0slS}y0a!*XM8#sVgw)umczJ?S%NHHAZxO9vO=?*9xPie z#t^SZJ6ZC&nrxlD9hSp~<(h|QedXQN6$w;|Bv*)=KooXQYjz> z{+R+c98QNlUn~F$)La1`&c2Z~;_UkSZ}C_8gt(pM}DVD)cPbU+mOs`-Y|# z5#2wpJCR;QR&b+iElf<2FJ+XQ4A;kH9PangN^Xm!72v&0_H&z{0#twsPys4H1!kl` z9^|X_jGl>)LItS6JQT3+LxCG>vIYIqf#4$m*rDu(wa*e@u>x3=Er<$CqZN!+^)bZi z-VT<$t|nVB+C_8t(7dzS6a&*}7cEF&S{)2jfC@|$SVrF4`G16eoBt;*OsN1B_%j8x zJDd)Cyi}g8AFpTib5?EL;GkcQ@b(jc#E#+(+ztE17GO=bASy8a2)GOkRN$uyyZ{;| B5o7=W literal 0 HcmV?d00001 diff --git a/source/Orchestrator/lib/common-orchestrator-construct.ts b/source/Orchestrator/lib/common-orchestrator-construct.ts index d5bb8426..ef50df71 100644 --- a/source/Orchestrator/lib/common-orchestrator-construct.ts +++ b/source/Orchestrator/lib/common-orchestrator-construct.ts @@ -14,17 +14,18 @@ * permissions and limitations under the License. * *****************************************************************************/ +import * as cdk_nag from 'cdk-nag'; import * as cdk from '@aws-cdk/core'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { LambdaInvoke } from '@aws-cdk/aws-stepfunctions-tasks'; import * as lambda from '@aws-cdk/aws-lambda'; -import { - PolicyDocument, - PolicyStatement, - Role, - Effect, - ServicePrincipal, - CfnRole +import { + PolicyDocument, + PolicyStatement, + Role, + Effect, + ServicePrincipal, + CfnRole } from '@aws-cdk/aws-iam'; import { StringParameter } from '@aws-cdk/aws-ssm'; @@ -355,18 +356,18 @@ export class OrchestratorConstruct extends cdk.Construct { checkWorkflowNew.when( sfn.Condition.or( sfn.Condition.stringEquals( - '$.EventType', + '$.EventType', 'Security Hub Findings - Custom Action' - ), + ), sfn.Condition.and( sfn.Condition.stringEquals( - '$.Finding.Workflow.Status', + '$.Finding.Workflow.Status', 'NEW' ), sfn.Condition.stringEquals( - '$.EventType', + '$.EventType', 'Security Hub Findings - Imported' - ), + ), ) ), getApprovalRequirement @@ -507,7 +508,7 @@ export class OrchestratorConstruct extends cdk.Construct { ) orchestratorPolicy.addStatements( new PolicyStatement({ - actions: [ + actions: [ "kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey" @@ -518,7 +519,7 @@ export class OrchestratorConstruct extends cdk.Construct { ] }) ) - + const principal = new ServicePrincipal(`states.amazonaws.com`); const orchestratorRole = new Role(this, 'Role', { assumedBy: principal, @@ -540,6 +541,10 @@ export class OrchestratorConstruct extends cdk.Construct { }; } + cdk_nag.NagSuppressions.addResourceSuppressions(orchestratorRole, [ + {id: 'AwsSolutions-IAM5', reason: 'CloudWatch Logs permissions require resource * except for DescribeLogGroups, except for GovCloud, which only works with resource *'} + ]); + const orchestratorStateMachine = new sfn.StateMachine(this, 'StateMachine', { definition: extractFindings, stateMachineName: `${RESOURCE_PREFIX}-SHARR-Orchestrator`, @@ -577,5 +582,10 @@ export class OrchestratorConstruct extends cdk.Construct { if (roleToModify) { roleToModify.node.tryRemoveChild('DefaultPolicy') } + + cdk_nag.NagSuppressions.addResourceSuppressions(orchestratorStateMachine, [ + {id: 'AwsSolutions-SF1', reason: 'False alarm. Logging configuration is overridden to log ALL.'}, + {id: 'AwsSolutions-SF2', reason: 'X-Ray is not needed for this use case.'} + ]); } } \ No newline at end of file diff --git a/source/Orchestrator/test/test_check_ssm_doc_state.py b/source/Orchestrator/test/test_check_ssm_doc_state.py index 8b95d8b2..352900f0 100644 --- a/source/Orchestrator/test/test_check_ssm_doc_state.py +++ b/source/Orchestrator/test/test_check_ssm_doc_state.py @@ -82,7 +82,7 @@ def workflow_doc(): "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" + "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* RemediationDoc: (Required) remediation runbook to execute after approval\n* SSMExec: (Required) json-formatted data for decision support in determining approval requirement\n" } } diff --git a/source/lib/orchestrator_roles-construct.ts b/source/lib/orchestrator_roles-construct.ts index b1810c0e..ac2f12f7 100644 --- a/source/lib/orchestrator_roles-construct.ts +++ b/source/lib/orchestrator_roles-construct.ts @@ -14,7 +14,10 @@ * permissions and limitations under the License. * *****************************************************************************/ -import * as cdk from '@aws-cdk/core'; +import { + Stack, + Construct, + ArnFormat } from '@aws-cdk/core'; import { PolicyStatement, Effect, @@ -32,11 +35,11 @@ export interface OrchRoleProps { adminRoleName: string; } -export class OrchestratorMemberRole extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: OrchRoleProps) { +export class OrchestratorMemberRole extends Construct { + constructor(scope: 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 stack = Stack.of(this); const memberPolicy = new PolicyDocument(); /** @@ -53,40 +56,25 @@ export class OrchestratorMemberRole extends cdk.Construct { `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" + "ssm:StartAutomationExecution" ) 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: '/' + arnFormat: ArnFormat.SLASH_RESOURCE_NAME }), stack.formatArn({ service: 'ssm', region: '*', resource: 'automation-definition', resourceName: '*', - sep: '/' + arnFormat: ArnFormat.SLASH_RESOURCE_NAME }), stack.formatArn({ service: 'ssm', @@ -94,18 +82,52 @@ export class OrchestratorMemberRole extends cdk.Construct { resource: 'automation-definition', account:'', resourceName: '*', - sep: '/' + arnFormat: ArnFormat.SLASH_RESOURCE_NAME }), stack.formatArn({ service: 'ssm', region: '*', resource: 'automation-execution', resourceName: '*', - sep: '/' + arnFormat: ArnFormat.SLASH_RESOURCE_NAME }) ); memberPolicy.addStatements(ssmRWPerms) + memberPolicy.addStatements( + // The actions in your policy do not support resource-level permissions and require you to choose All resources + new PolicyStatement({ + actions: [ + 'ssm:DescribeAutomationExecutions', + 'ssm:GetAutomationExecution' + ], + resources: [ '*' ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + 'ssm:DescribeDocument' + ], + resources: [ `arn:${stack.partition}:ssm:*:*:document/*` ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + 'ssm:GetParameters', + 'ssm:GetParameter' + ], + resources: [ `arn:${stack.partition}:ssm:*:*:parameter/Solutions/SO0111/*` ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + "config:DescribeConfigRules" + ], + resources: [ "*" ], + effect: Effect.ALLOW + }) + ) + const sechubPerms = new PolicyStatement(); sechubPerms.addActions("cloudwatch:PutMetricData") sechubPerms.addActions("securityhub:BatchUpdateFindings") diff --git a/source/lib/sharrplaybook-construct.ts b/source/lib/sharrplaybook-construct.ts index 714916a4..c11d53ae 100644 --- a/source/lib/sharrplaybook-construct.ts +++ b/source/lib/sharrplaybook-construct.ts @@ -23,6 +23,7 @@ import * as cdk from '@aws-cdk/core'; import { StringParameter } from '@aws-cdk/aws-ssm'; import { Trigger, SsmPlaybook } from './ssmplaybook'; import { AdminAccountParm } from './admin_account_parm-construct'; +import { RunbookFactory } from '../solution_deploy/lib/runbook_factory'; export interface IControl { control: string; @@ -44,7 +45,7 @@ export class PlaybookPrimaryStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props: PlaybookProps) { super(scope, id, props); - + const stack = cdk.Stack.of(this) const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name const orchestratorArn = StringParameter.valueForStringParameter(this, `/Solutions/${RESOURCE_PREFIX}/OrchestratorArn`) @@ -62,12 +63,12 @@ export class PlaybookPrimaryStack extends cdk.Stack { }); new cdk.CfnMapping(this, 'SourceCode', { - mapping: { "General": { + mapping: { "General": { "S3Bucket": props.solutionDistBucket, "KeyPrefix": props.solutionDistName + '/' + props.solutionVersion } } }) - + const processRemediation = function(controlSpec: IControl): void { if ((controlSpec.executes != undefined) && (controlSpec.control != controlSpec.executes)) { @@ -93,58 +94,58 @@ export class PlaybookPrimaryStack extends cdk.Stack { targetArn: orchestratorArn }) } - + props.remediations.forEach(processRemediation) } } export interface MemberStackProps { - description: string; - solutionId: string; - solutionVersion: string; - solutionDistBucket: string; - securityStandard: string; - securityStandardVersion: string; - securityStandardLongName: string; - ssmdocs?: string; - remediations: IControl[]; + description: string; + solutionId: string; + solutionVersion: string; + solutionDistBucket: string; + securityStandard: string; + securityStandardVersion: string; + securityStandardLongName: string; + ssmdocs?: string; + commonScripts?: string; + remediations: IControl[]; } export class PlaybookMemberStack extends cdk.Stack { - constructor(scope: cdk.App, id: string, props: MemberStackProps) { super(scope, id, props); - const stack = cdk.Stack.of(this) + const stack = cdk.Stack.of(this); - let ssmdocs = '' + let ssmdocs = ''; if (props.ssmdocs == undefined) { - ssmdocs = './ssmdocs' + ssmdocs = './ssmdocs'; } else { - ssmdocs = props.ssmdocs + ssmdocs = props.ssmdocs; } new AdminAccountParm(this, 'AdminAccountParameter', { - solutionId: props.solutionId - }) + solutionId: props.solutionId + }); const processRemediation = function(controlSpec: IControl): void { - // Create the ssm automation document only if this is not a remapped control - if (!(controlSpec.executes && controlSpec.control != controlSpec.executes)) { - new SsmPlaybook(stack, `${props.securityStandard} ${controlSpec.control}`, { - securityStandard: props.securityStandard, - securityStandardVersion: props.securityStandardVersion, - controlId: controlSpec.control, - ssmDocPath: ssmdocs, - ssmDocFileName: `${props.securityStandard}_${controlSpec.control}.yaml`, - solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket - }) - } - } - - props.remediations.forEach(processRemediation) + // Create the ssm automation document only if this is not a remapped control + if (!(controlSpec.executes && controlSpec.control != controlSpec.executes)) { + RunbookFactory.createControlRunbook(stack, `${props.securityStandard} ${controlSpec.control}`, { + securityStandard: props.securityStandard, + securityStandardVersion: props.securityStandardVersion, + controlId: controlSpec.control, + ssmDocPath: ssmdocs, + ssmDocFileName: `${props.securityStandard}_${controlSpec.control}.yaml`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId, + commonScripts: props.commonScripts + }); + } + }; + props.remediations.forEach(processRemediation); } } - diff --git a/source/lib/ssmplaybook.ts b/source/lib/ssmplaybook.ts index e9bac9ed..905f5f99 100644 --- a/source/lib/ssmplaybook.ts +++ b/source/lib/ssmplaybook.ts @@ -21,14 +21,15 @@ import * as yaml from 'js-yaml'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as events from '@aws-cdk/aws-events'; import { - Effect, - PolicyStatement, - ServicePrincipal, - Policy, - Role, + Effect, + PolicyStatement, + ServicePrincipal, + Policy, + Role, CfnRole, ArnPrincipal, - CompositePrincipal + CompositePrincipal, + AccountPrincipal } from '@aws-cdk/aws-iam'; import { StateMachine } from '@aws-cdk/aws-stepfunctions'; import { IRuleTarget, EventPattern, Rule } from '@aws-cdk/aws-events'; @@ -41,25 +42,26 @@ import { MemberRoleStack } from '../solution_deploy/lib/remediation_runbook-stac */ export interface IssmPlaybookProps { - securityStandard: string; // ex. AFSBP - securityStandardVersion: string; - controlId: string; - ssmDocPath: string; - ssmDocFileName: string; - solutionVersion: string; - solutionDistBucket: string; - adminRoleName?: string; - remediationPolicy?: Policy; - adminAccountNumber?: string; - solutionId?: string; - scriptPath?: string; + securityStandard: string; // ex. AFSBP + securityStandardVersion: string; + controlId: string; + ssmDocPath: string; + ssmDocFileName: string; + solutionVersion: string; + solutionDistBucket: string; + adminRoleName?: string; + remediationPolicy?: Policy; + adminAccountNumber?: string; + solutionId: string; + scriptPath?: string; + commonScripts?: string; } export class SsmPlaybook extends cdk.Construct { - + constructor(scope: cdk.Construct, id: string, props: IssmPlaybookProps) { super(scope, id); - + let scriptPath = '' if (props.scriptPath == undefined ) { scriptPath = `${props.ssmDocPath}/scripts` @@ -67,7 +69,12 @@ export class SsmPlaybook extends cdk.Construct { scriptPath = props.scriptPath } - let illegalChars = /[\.]/g; + let commonScripts = '' + if (props.commonScripts == undefined ) { + commonScripts = '../common' + } else { + commonScripts = props.commonScripts + } const enableParam = new cdk.CfnParameter(this, 'Enable ' + props.controlId, { type: "String", @@ -75,7 +82,6 @@ export class SsmPlaybook extends cdk.Construct { default: "Available", allowedValues: ["Available", "NOT Available"] }) - enableParam.overrideLogicalId(`${props.securityStandard}${props.controlId.replace(illegalChars, '')}Active`) const installSsmDoc = new cdk.CfnCondition(this, 'Enable ' + props.controlId + ' Condition', { expression: cdk.Fn.conditionEquals(enableParam, "Available") @@ -83,7 +89,7 @@ export class SsmPlaybook extends cdk.Construct { let ssmDocName = `SHARR-${props.securityStandard}_${props.securityStandardVersion}_${props.controlId}` let ssmDocFQFileName = `${props.ssmDocPath}/${props.ssmDocFileName}` - let ssmDocType = props.ssmDocFileName.substr(props.ssmDocFileName.length - 4).toLowerCase() + let ssmDocType = props.ssmDocFileName.substring(props.ssmDocFileName.length - 4).toLowerCase() let ssmDocIn = fs.readFileSync(ssmDocFQFileName, 'utf8') @@ -93,7 +99,14 @@ export class SsmPlaybook extends cdk.Construct { for (let line of ssmDocIn.split('\n')) { let foundMatch = re.exec(line) if (foundMatch && foundMatch.groups && foundMatch.groups.script) { - let scriptIn = fs.readFileSync(`${scriptPath}/${foundMatch.groups.script}`, 'utf8') + let pathAndFileToInsert = foundMatch.groups.script + // If a relative path is provided then use it + if (pathAndFileToInsert.substring(0,7) === 'common/') { + pathAndFileToInsert = `${commonScripts}/${pathAndFileToInsert.substring(7)}` + } else { + pathAndFileToInsert = `${scriptPath}/${pathAndFileToInsert}` + } + let scriptIn = fs.readFileSync(pathAndFileToInsert, 'utf8') for (let scriptLine of scriptIn.split('\n')) { ssmDocOut += foundMatch.groups.padding + scriptLine + '\n' } @@ -112,7 +125,8 @@ export class SsmPlaybook extends cdk.Construct { const AutoDoc = new ssm.CfnDocument(this, 'Automation Document', { content: ssmDocSource, documentType: 'Automation', - name: ssmDocName + name: ssmDocName, + versionName: props.solutionVersion }) AutoDoc.cfnOptions.condition = installSsmDoc } @@ -132,7 +146,7 @@ export interface ITriggerProps { } export class Trigger extends cdk.Construct { - + constructor(scope: cdk.Construct, id: string, props: ITriggerProps) { super(scope, id); let illegalChars = /[\.]/g; @@ -153,6 +167,9 @@ export class Trigger extends cdk.Construct { let complianceStatusFilter = { "Status": [ "FAILED", "WARNING" ] } + const recordStateFilter: string[] = [ + 'ACTIVE' + ]; const stateMachine = sfn.StateMachine.fromStateMachineArn(this, 'orchestrator', props.targetArn); @@ -181,7 +198,7 @@ export class Trigger extends cdk.Construct { }); enable_auto_remediation_param.overrideLogicalId(`${props.securityStandard}${props.controlId.replace(illegalChars, '')}AutoTrigger`) - + interface IPattern { source: any, detailType: any @@ -195,13 +212,14 @@ export class Trigger extends cdk.Construct { // GeneratorId includes both standard and control/rule ID GeneratorId: [props.generatorId], Workflow: workflowStatusFilter, - Compliance: complianceStatusFilter + Compliance: complianceStatusFilter, + RecordState: recordStateFilter } } } 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.', @@ -209,7 +227,7 @@ export class Trigger extends cdk.Construct { targets: [stateMachineTarget], eventPattern: triggerPattern }); - + const cfnEventRule_auto = eventRule_auto.node.defaultChild as events.CfnRule; cfnEventRule_auto.addPropertyOverride('State', enable_auto_remediation_param.valueAsString); } @@ -222,7 +240,7 @@ export interface IOneTriggerProps { prereq: cdk.CfnResource[]; } export class OneTrigger extends cdk.Construct { -// used in place of Trigger. Sends all finding events for which the +// used in place of Trigger. Sends all finding events for which the // SHARR custom action is initiated to the Step Function constructor(scope: cdk.Construct, id: string, props: IOneTriggerProps) { @@ -284,7 +302,7 @@ export class OneTrigger extends cdk.Construct { detailType: ["Security Hub Findings - Custom Action"], resources: [ customAction.getAttString('Arn') ], detail: { - findings: { + findings: { Compliance: complianceStatusFilter } } @@ -305,39 +323,71 @@ export interface RoleProps { readonly ssmDocName: string; readonly remediationPolicy: Policy; readonly remediationRoleName: string; -} +}; 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` + const roleStack = MemberRoleStack.of(this) as MemberRoleStack; 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( - "ssm:GetParameters", - "ssm:GetParameter", - "ssm:PutParameter" + basePolicy.addStatements( + new PolicyStatement({ + actions: [ + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:PutParameter" + ], + resources: [ + `arn:${stack.partition}:ssm:*:${stack.account}:parameter/Solutions/SO0111/*` + ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + "iam:PassRole" + ], + resources: [ + `arn:${stack.partition}:iam::${stack.account}:role/${props.remediationRoleName}` + ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + "ssm:StartAutomationExecution", + "ssm:GetAutomationExecution", + "ssm:DescribeAutomationStepExecutions" + ], + resources: [ + `arn:${stack.partition}:ssm:*:${stack.account}:document/Solutions/SHARR-${props.remediationRoleName}`, + `arn:${stack.partition}:ssm:*:${stack.account}:automation-definition/*`, + `arn:${stack.partition}:ssm:*::automation-definition/*`, + `arn:${stack.partition}:ssm:*:${stack.account}:automation-execution/*` + ], + effect: Effect.ALLOW + }), + new PolicyStatement({ + actions: [ + "sts:AssumeRole" + ], + resources: [ + `arn:${stack.partition}:iam::${stack.account}:role/${props.remediationRoleName}` + ], + effect: Effect.ALLOW + }) ) - ssmParmPerms.effect = Effect.ALLOW - ssmParmPerms.addResources( - `arn:${stack.partition}:ssm:*:${stack.account}:parameter/Solutions/SO0111/*` - ); - basePolicy.addStatements(ssmParmPerms) // AssumeRole Policy let principalPolicyStatement = new PolicyStatement(); principalPolicyStatement.addActions("sts:AssumeRole"); principalPolicyStatement.effect = Effect.ALLOW; + const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); let roleprincipal = new ArnPrincipal( - 'arn:' + stack.partition + ':iam::' + adminAccount.valueAsString + - ':role/' + adminRoleName + `arn:${stack.partition}:iam::${stack.account}:role/${RESOURCE_PREFIX}-SHARR-Orchestrator-Member` ); let principals = new CompositePrincipal(roleprincipal); @@ -346,6 +396,10 @@ export class SsmRole extends cdk.Construct { let serviceprincipal = new ServicePrincipal('ssm.amazonaws.com') principals.addPrincipals(serviceprincipal); + // Multi-account/region automations must be able to assume the remediation role + const accountPrincipal = new AccountPrincipal(stack.account); + principals.addPrincipals(accountPrincipal); + let memberRole = new Role(this, 'MemberAccountRole', { assumedBy: principals, roleName: props.remediationRoleName @@ -354,6 +408,7 @@ export class SsmRole extends cdk.Construct { memberRole.attachInlinePolicy(basePolicy) memberRole.attachInlinePolicy(props.remediationPolicy) memberRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN) + memberRole.node.addDependency(roleStack.getOrchestratorMemberRole()); const memberRoleResource = memberRole.node.findChild('Resource') as CfnRole; @@ -372,18 +427,18 @@ export class SsmRole extends cdk.Construct { } export interface RemediationRunbookProps { - ssmDocName: string; - ssmDocPath: string; - ssmDocFileName: string; - solutionVersion: string; - solutionDistBucket: string; - remediationPolicy?: Policy; - solutionId?: string; - scriptPath?: string; + ssmDocName: string; + ssmDocPath: string; + ssmDocFileName: string; + solutionVersion: string; + solutionDistBucket: string; + remediationPolicy?: Policy; + solutionId: string; + scriptPath?: string; } export class SsmRemediationRunbook extends cdk.Construct { - + constructor(scope: cdk.Construct, id: string, props: RemediationRunbookProps) { super(scope, id); diff --git a/source/package.json b/source/package.json index d8b9f486..58466181 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "1.4.2", + "version": "1.5.0", "description": "Automated remediation for AWS Security Hub (SO0111)", "bin": { "solution_deploy": "bin/solution_deploy.js" @@ -18,29 +18,30 @@ "cdk": "cdk" }, "devDependencies": { - "@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": "^1.132.0", - "cdk": "~1.132.0", + "@aws-cdk/assert": "~1.155.0", + "@aws-cdk/aws-events": "~1.155.0", + "@aws-cdk/aws-iam": "~1.155.0", + "@aws-cdk/aws-kms": "~1.155.0", + "@aws-cdk/aws-lambda": "~1.155.0", + "@aws-cdk/aws-logs": "~1.155.0", + "@aws-cdk/aws-s3": "~1.155.0", + "@aws-cdk/aws-sns": "~1.155.0", + "@aws-cdk/aws-ssm": "~1.155.0", + "@aws-cdk/aws-stepfunctions": "~1.155.0", + "@aws-cdk/aws-stepfunctions-tasks": "~1.155.0", + "@aws-cdk/core": "~1.155.0", + "@types/jest": "^27.5.0", + "@types/js-yaml": "^4.0.5", + "@types/node": "17.0.31", + "aws-cdk": "^1.155.0", + "cdk": "~1.155.0", + "cdk-nag": "^1.0.0", "fs": "^0.0.1-security", - "jest": "^27.3.1", + "jest": "^28.1.0", "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "ts-jest": "^27.0.7", - "ts-node": "^10.4.0", - "typescript": "^4.5.2" + "source-map-support": "^0.5.21", + "ts-jest": "^28.0.2", + "ts-node": "^10.7.0", + "typescript": "^4.6.4" } } diff --git a/source/playbooks/.DS_Store b/source/playbooks/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5250ab3b4c4655613cdbf3b44ffa875689df38d0 GIT binary patch literal 6148 zcmeHKJFWsT477O&A-#r@GFJdD5G}ld4v7mOphTiT^jmQ*j>h;?Jhae30UArrB#tLh zrdY22?dHgwJQ%{$i1hyvj_V=9yVX?NHjkB3h7`3&Ps-Zbd7RbS0^h(b=L$E&+$k8m90R=^V`1fZ>`9SV ZY>xe!*akWsai;_MGhn*VsKC1wxB<;G6b=9Y literal 0 HcmV?d00001 diff --git a/source/playbooks/AFSBP/README.md b/source/playbooks/AFSBP/README.md index f1a5be60..e437d88c 100644 --- a/source/playbooks/AFSBP/README.md +++ b/source/playbooks/AFSBP/README.md @@ -4,13 +4,16 @@ The AWS Foundational Security Best Practices (AFSBP) playbook is part of the AWS * AutoScaling.1 * CloudTrail.1 -* Config.1 -* CloudTrail.1 * CloudTrail.2 +* CloudTrail.4 +* CloudTrail.5 +* Config.1 +* CodeBuild.2 * EC2.1 * EC2.2 * EC2.6 * EC2.7 +* IAM.3 * IAM.7 * IAM.8 * Lambda.1 @@ -20,6 +23,8 @@ The AWS Foundational Security Best Practices (AFSBP) playbook is part of the AWS * S3.1 * S3.2 * S3.3 +* S3.4 +* S3.5 See the [AWS Security Hub Automated Response and Remediation Implementation Guide](https://docs.aws.amazon.com/solutions/latest/aws-security-hub-automated-response-and-remediation/welcome.html) for more information on this Playbook. diff --git a/source/playbooks/AFSBP/bin/afsbp.ts b/source/playbooks/AFSBP/bin/afsbp.ts index cd84fcb4..9cfb6251 100644 --- a/source/playbooks/AFSBP/bin/afsbp.ts +++ b/source/playbooks/AFSBP/bin/afsbp.ts @@ -1,21 +1,14 @@ #!/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 'source-map-support/register'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + PlaybookPrimaryStack, + PlaybookMemberStack, + IControl +} from '../../../lib/sharrplaybook-construct'; +import * as cdk_nag from 'cdk-nag'; import * as cdk from '@aws-cdk/core'; -import { PlaybookPrimaryStack, PlaybookMemberStack, IControl } from '../../../lib/sharrplaybook-construct'; +import 'source-map-support/register'; // SOLUTION_* - set by solution_env.sh const SOLUTION_ID = process.env['SOLUTION_ID'] || 'undefined'; @@ -25,61 +18,81 @@ const DIST_VERSION = process.env['DIST_VERSION'] || '%%VERSION%%'; const DIST_OUTPUT_BUCKET = process.env['DIST_OUTPUT_BUCKET'] || '%%BUCKET%%'; const DIST_SOLUTION_NAME = process.env['DIST_SOLUTION_NAME'] || '%%SOLUTION%%'; -const standardShortName = 'AFSBP' -const standardLongName = 'aws-foundational-security-best-practices' -const standardVersion = '1.0.0' // DO NOT INCLUDE 'V' -const RESOURCE_PREFIX = SOLUTION_ID.replace(/^DEV-/,''); // prefix on every resource name +const standardShortName = 'AFSBP'; +const standardLongName = 'aws-foundational-security-best-practices'; +const standardVersion = '1.0.0'; // DO NOT INCLUDE 'V' const app = new cdk.App(); +cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks()); // Creates one rule per control Id. The Step Function determines what document to run based on // Security Standard and Control Id. See afsbp-member-stack const remediations: IControl[] = [ - { "control": 'AutoScaling.1' }, - { "control": 'CloudTrail.1' }, - { "control": 'CloudTrail.2' }, - { "control": 'Config.1' }, - { "control": 'EC2.1' }, - { "control": 'EC2.2' }, - { "control": 'EC2.6' }, - { "control": 'EC2.7' }, - { "control": 'IAM.7' }, - { "control": 'IAM.8' }, - { "control": 'Lambda.1' }, - { "control": 'RDS.1' }, - { "control": 'RDS.6' }, - { "control": 'RDS.7' }, - { "control": 'S3.1' }, - { "control": 'S3.2' }, - { - "control": 'S3.3', - "executes": 'S3.2' - }, - { "control": 'S3.5' } -] + { "control": 'AutoScaling.1' }, + { "control": 'CloudTrail.1' }, + { "control": 'CloudTrail.2' }, + { "control": 'CloudTrail.4' }, + { "control": 'CloudTrail.5' }, + { "control": 'CodeBuild.2' }, + { "control": 'Config.1' }, + { "control": 'EC2.1' }, + { "control": 'EC2.2' }, + { "control": 'EC2.6' }, + { "control": 'EC2.7' }, + { "control": 'IAM.3' }, + { "control": 'IAM.7' }, + { "control": 'IAM.8' }, + { "control": 'Lambda.1' }, + { "control": 'RDS.1' }, + { "control": 'RDS.2' }, + { "control": 'RDS.4' }, + { "control": 'RDS.5' }, + { "control": 'RDS.6' }, + { "control": 'RDS.7' }, + { "control": 'RDS.8' }, + { "control": 'RDS.13' }, + { "control": 'RDS.16' }, + { "control": 'Redshift.1' }, + { "control": 'Redshift.3' }, + { "control": 'Redshift.4' }, + { "control": 'Redshift.6' }, + { "control": 'S3.1' }, + { "control": 'S3.2' }, + { + "control": 'S3.3', + "executes": 'S3.2' + }, + { "control": 'S3.4' }, + { "control": 'S3.5' }, + { "control": 'S3.6' }, + { + "control": 'S3.8', + "executes": 'S3.2' + } +]; const adminStack = new PlaybookPrimaryStack(app, 'AFSBPStack', { - description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - solutionDistName: DIST_SOLUTION_NAME, - remediations: remediations, - securityStandardLongName: standardLongName, - securityStandard: standardShortName, - securityStandardVersion: standardVersion + description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + solutionDistName: DIST_SOLUTION_NAME, + remediations: remediations, + securityStandardLongName: standardLongName, + securityStandard: standardShortName, + securityStandardVersion: standardVersion }); const memberStack = new PlaybookMemberStack(app, 'AFSBPMemberStack', { - description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - securityStandard: standardShortName, - securityStandardVersion: standardVersion, - securityStandardLongName: standardLongName, - remediations: remediations + description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + securityStandard: standardShortName, + securityStandardVersion: standardVersion, + securityStandardLongName: standardLongName, + remediations: remediations }); -adminStack.templateOptions.templateFormatVersion = "2010-09-09" -memberStack.templateOptions.templateFormatVersion = "2010-09-09" +adminStack.templateOptions.templateFormatVersion = "2010-09-09"; +memberStack.templateOptions.templateFormatVersion = "2010-09-09"; diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml index 331a70d0..160c1f31 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml @@ -15,8 +15,8 @@ description: | ## Documentation Links * [AFSBP AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-autoscaling-1) - - + + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: @@ -25,16 +25,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for ASG1 finding + description: The input from the Orchestrator Step function for the AutoScaling.1 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput action: 'aws:executeScript' @@ -51,22 +50,33 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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 + expected_control_id: + - 'AutoScaling.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - + - name: Remediation action: 'aws:executeAutomation' isEnd: false inputs: DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: 'SO0111-EnableAutoScalingGroupELBHealthCheck' RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck' AutoScalingGroupName: '{{ParseInput.AutoScalingGroupName}}' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml index b780a757..b67f6543 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml @@ -3,24 +3,24 @@ description: | ## 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 * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP CloudTrail.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-1) - + 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the CloudTrail.1 finding KMSKeyArn: type: String default: >- @@ -49,11 +49,12 @@ mainSteps: Finding: '{{Finding}}' region: '{{global:REGION}}' parse_id_pattern: '' - expected_control_id: 'CloudTrail.1' - Runtime: python3.7 + expected_control_id: + - 'CloudTrail.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -64,7 +65,7 @@ mainSteps: RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml index 40857d84..af107a6a 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml @@ -20,17 +20,18 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the CloudTrail.2 finding KMSKeyArn: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} + 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' outputs: @@ -47,19 +48,20 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.resource.Region + Selector: $.Payload.resource_region Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'CloudTrail.2' - Runtime: python3.7 + expected_control_id: + - 'CloudTrail.2' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% - - + - name: Remediation action: 'aws:executeAutomation' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml new file mode 100644 index 00000000..d0eee1a3 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml @@ -0,0 +1,97 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.4 + + ## What does this document do? + This document enables CloudTrail log file validation. + + ## 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 CloudTrail.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-4) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the CloudTrail.4 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailLogFileValidation" + allowedPattern: '^[\w+=,.@-]+' + +mainSteps: + - + name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: TrailName + 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: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' + expected_control_id: + - 'CloudTrail.4' + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + isEnd: false + - + name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableCloudTrailLogFileValidation + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + TrailName: '{{ParseInput.TrailName}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Enabled CloudTrail log file validation.' + UpdatedBy: 'SHARR-AFSBP_1.0.0_CloudTrail.2.4' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml new file mode 100644 index 00000000..5201deff --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml @@ -0,0 +1,98 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.5 + + ## What does this document do? + This document configures CloudTrail to log to CloudWatch Logs. + + ## 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 + * [AFSBP v1.0.0 CloudTrail.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-5) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the CloudTrail.5 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailToCloudWatchLogging" + allowedPattern: '^[\w+=,.@-]+' + +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: TrailName + 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: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' + expected_control_id: + - 'CloudTrail.5' + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + isEnd: false + + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + TrailName: '{{ ParseInput.TrailName }}' + 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/{{RemediationRoleName}}' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Configured CloudTrail logging to CloudWatch Logs Group CloudTrail/{{ParseInput.TrailName}}' + UpdatedBy: 'SHARR-AFSBP_1.0.0_CloudTrail.5' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml new file mode 100644 index 00000000..eaa58e02 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml @@ -0,0 +1,75 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_CodeBuild.2 + + ## What does this document do? + This document removes CodeBuild project environment variables containing clear text credentials and replaces them with Amazon EC2 Systems Manager Parameters. + + ## 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 CodeBuild.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-codebuild-2) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the CodeBuild.2 finding + 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+=,.@-]+$' +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: ProjectName + 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):codebuild:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:project/([A-Za-z0-9][A-Za-z0-9\-_]{1,254})$' + expected_control_id: [ 'CodeBuild.2' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + - name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-ReplaceCodeBuildClearTextCredentials + RuntimeParameters: + ProjectName: '{{ ParseInput.ProjectName }}' + AutomationAssumeRole: 'arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-ReplaceCodeBuildClearTextCredentials' + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ ParseInput.FindingId }}' + ProductArn: '{{ ParseInput.ProductArn }}' + Note: + Text: 'Replaced clear text credentials with SSM parameters.' + UpdatedBy: 'SHARR-AFSBP_1.0.0_CodeBuild.2' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml index 73ecb6cd..5748fdd4 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml @@ -14,17 +14,17 @@ description: | ## Documentation Links * [AFSBP Config.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-config-1) - + 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the Config.1 finding KMSKeyArn: type: String default: >- @@ -53,14 +53,15 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'Config.1' - Runtime: python3.7 + expected_control_id: + - 'Config.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - - + + - name: Remediation action: 'aws:executeAutomation' isEnd: false @@ -70,8 +71,8 @@ mainSteps: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - - - + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml index f653022b..f4b58c7d 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml @@ -15,19 +15,18 @@ assumeRole: '{{ AutomationAssumeRole }}' parameters: Finding: type: StringMap - description: The input from Step function for EC2.1 finding + description: The input from the Orchestrator Step function for the 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+=,.@-]+' + 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+=,.@-]+$' outputs: - Remediation.Output - ParseInput.AffectedObject mainSteps: - - + - name: ParseInput action: 'aws:executeScript' outputs: @@ -51,15 +50,16 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' resource_index: 2 - expected_control_id: 'EC2.1' - Runtime: python3.7 + expected_control_id: + - 'EC2.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' inputs: @@ -70,7 +70,7 @@ mainSteps: TestMode: '{{ParseInput.TestMode}}' isEnd: false - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml index d2785056..3eb28f85 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml @@ -2,7 +2,7 @@ description: | ### Document Name - SHARR-AFSBP_1.0.0_EC2.2 ## What does this document do? - This document deletes ingress and egress rules from default security + This document deletes ingress and egress rules from default security group using the AWS SSM Runbook AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules ## Input Parameters @@ -14,7 +14,7 @@ description: | ## Documentation Links * [AFSBP EC2.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-2) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: @@ -24,10 +24,14 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the EC2.2 finding + RemediationRoleName: + type: String + default: "SO0111-RemoveVPCDefaultSecurityGroupRules" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,25 +49,35 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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 + expected_control_id: + - 'EC2.2' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% - isEnd: false - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: GroupId: '{{ParseInput.GroupId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + - 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 81945997..bc317a8e 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml @@ -7,27 +7,30 @@ description: | ## 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 * [AFSBP EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-6) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for ASG1 finding + description: The input from the Orchestrator Step function for the EC2.6 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableVPCFlowLogs" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,15 +48,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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 + expected_control_id: + - 'EC2.6' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -61,10 +71,14 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-EnableVPCFlowLogs + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' 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' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 0fe5341b..04828c43 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml @@ -16,10 +16,14 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the EC2.7 finding + RemediationRoleName: + type: String + default: "SO0111-EnableEbsEncryptionByDefault" + allowedPattern: '^[\w+=,.@-]+' outputs: - ExecRemediation.Output @@ -42,13 +46,14 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'EC2.7' - Runtime: python3.7 + expected_control_id: + - 'EC2.7' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - + - name: ExecRemediation action: 'aws:executeAutomation' @@ -56,9 +61,9 @@ mainSteps: inputs: DocumentName: SHARR-EnableEbsEncryptionByDefault RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEbsEncryptionByDefault' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml new file mode 100644 index 00000000..a06f9b70 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml @@ -0,0 +1,93 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_IAM.3 + + ## 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 + * [AFSBP v1.0.0 IAM.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-3) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the IAM.3 finding + 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+=,.@-]+$' + MaxCredentialUsageAge: + type: String + description: (Required) Maximum number of days a key can be unrotated. The default value is 90 days. + allowedPattern: ^[1-9][0-9]{0,3}|10000$ + default: "90" + RemediationRoleName: + type: String + default: "SO0111-RevokeUnrotatedKeys" + allowedPattern: '^[\w+=,.@-]+' +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - 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 + - 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(?:(?:\u002F)|(?:\u002F[\u0021-\u007F]{1,510}\u002F))([\w+=,.@-]{1,64})$' + expected_control_id: + - 'IAM.3' + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/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/{{RemediationRoleName}}' + 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-AFSBP_1.0.0_IAM.3' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml index 6dba0bd7..ec5e2c89 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml @@ -15,21 +15,20 @@ description: | ## Documentation Links * [AFSBP IAM.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-7) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the IAM.7 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -49,10 +48,10 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' expected_control_id: [ 'IAM.7' ] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml index 8003db72..5e507947 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml @@ -15,22 +15,21 @@ description: | ## Documentation Links * [AFSBP IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-8) - - + + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for ASG1 finding + description: The input from the Orchestrator Step function for the IAM.8 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -52,11 +51,12 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'IAM.8' - Runtime: python3.7 + expected_control_id: + - 'IAM.8' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml index 49b40562..023916cd 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml @@ -1,9 +1,9 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_Lambda.1 + ### Document Name - SHARR-AFSBP_1.0.0_Lambda.1 ## What does this document do? This document removes the public resource policy. A public resource policy - contains a principal "*" or AWS: "*", which allows public access to the + contains a principal "*" or AWS: "*", which allows public access to the function. The remediation is to remove the SID of the public policy. ## Input Parameters @@ -21,12 +21,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for the finding + description: The input from the Orchestrator Step function for the Lambda.1 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-RemoveLambdaPublicAccess" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -44,27 +47,37 @@ mainSteps: - Name: FunctionName Selector: $.Payload.resource_id Type: String + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_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 + expected_control_id: + - 'Lambda.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: SHARR-RemoveLambdaPublicAccess + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: FunctionName: '{{ ParseInput.FunctionName }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess' - - - + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml index 56fc9810..a90d88a0 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml @@ -17,12 +17,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for RDS.1 finding + description: The input from the Orchestrator Step function for the 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-MakeRDSSnapshotPrivate" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -46,29 +49,39 @@ mainSteps: - Name: Type Selector: $.Payload.type Type: String + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + 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 + expected_control_id: + - 'RDS.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% nextStep: Remediation - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: SHARR-MakeRDSSnapshotPrivate + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: DBSnapshotId: '{{ParseInput.DBSnapshotId}}' DBSnapshotType: '{{ParseInput.DBSnapshotType}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' nextStep: UpdateFinding - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml new file mode 100644 index 00000000..06521f20 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml @@ -0,0 +1,92 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_RDS.13 + + ## What does this document do? + This document enables `Auto minor version upgrade` on a given Amazon RDS instance by calling another SSM document. + + ## 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 - The standard HTTP response from the ModifyDBInstance API. + + ## Documentation Links + * [AFSBP RDS.13](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-13) + +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 the RDS.13 finding + RemediationRoleName: + type: String + default: "SO0111-EnableMinorVersionUpgradeOnRDSDBInstance" + allowedPattern: '^[\w+=,.@-]+' + +outputs: + - Remediation.Output + - ParseInput.AffectedObject +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: 'DbiResourceId' + Selector: '$.Payload.resource.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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '' + expected_control_id: + - 'RDS.13' + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + - name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-EnableMinorVersionUpgradeOnRDSDBInstance + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbiResourceId: '{{ ParseInput.DbiResourceId }}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Minor Version enabled on the RDS Instance.' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.13' + Workflow: + Status: 'RESOLVED' + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml new file mode 100644 index 00000000..aa5c1255 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml @@ -0,0 +1,98 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_RDS.16 + + ## What does this document do? + This document enables `Copy tags to snapshots` on a given Amazon RDS cluster by calling another SSM document. + + ## 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 - The standard HTTP response from the ModifyDBCluster API. + + ## Documentation Links + * [AFSBP RDS.16](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-16) + +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 the RDS.16 finding + RemediationRoleName: + type: String + default: "SO0111-EnableCopyTagsToSnapshotOnRDSCluster" + allowedPattern: '^[\w+=,.@-]+' + +outputs: + - Remediation.Output + - ParseInput.AffectedObject +mainSteps: + - + name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: DbClusterResourceId + 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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '' + expected_control_id: + - 'RDS.16' + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + + - + name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-EnableCopyTagsToSnapshotOnRDSCluster + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbClusterResourceId: '{{ ParseInput.DbClusterResourceId }}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + ApplyImmediately: true + + - + name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Copy Tags to Snapshots enabled on RDS DB cluster' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.16' + Workflow: + Status: 'RESOLVED' + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml new file mode 100644 index 00000000..d4f57a94 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml @@ -0,0 +1,91 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +description: | + ### Document Name - SHARR-AFSBP_1.0.0_RDS.2 + ## What does this document do? + This document disables public access to RDS instances by calling another SSM document + + ## 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. + + ## Documentation Links + * [AFSBP RDS.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-2) +schemaVersion: '0.3' +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the RDS.2 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-DisablePublicAccessToRDSInstance' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:db:((?!.*--.*)(?!.*-$)[a-z][a-z0-9-]{0,62})$' + expected_control_id: + - 'RDS.2' + Runtime: 'python3.8' + Handler: 'parse_event' + Script: |- + %%SCRIPT=common/parse_input.py%% + outputs: + - Name: 'DbiResourceId' + Selector: '$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId' + Type: 'String' + - Name: 'AffectedObject' + Selector: '$.Payload.object' + Type: 'StringMap' + - Name: 'FindingId' + Selector: '$.Payload.finding.Id' + Type: 'String' + - Name: 'ProductArn' + Selector: '$.Payload.finding.ProductArn' + Type: 'String' + - Name: 'RemediationRegion' + Selector: '$.Payload.resource_region' + Type: 'String' + - Name: 'RemediationAccount' + Selector: '$.Payload.account_id' + Type: 'String' +- name: 'Remediation' + action: 'aws:executeAutomation' + inputs: + DocumentName: 'SHARR-DisablePublicAccessToRDSInstance' + TargetLocations: + - Accounts: + - '{{ParseInput.RemediationAccount}}' + Regions: + - '{{ParseInput.RemediationRegion}}' + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbiResourceId: '{{ParseInput.DbiResourceId}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' +- name: 'UpdateFinding' + action: 'aws:executeAwsApi' + inputs: + Service: 'securityhub' + Api: 'BatchUpdateFindings' + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Disabled public access to RDS instance' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.2' + Workflow: + Status: 'RESOLVED' + description: 'Update finding' + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml new file mode 100644 index 00000000..5177e2e4 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml @@ -0,0 +1,109 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document Name - SHARR-AFSBP_1.0.0_RDS.4 + + ## What does this document do? + This document encrypts an unencrypted RDS snapshot by calling another SSM document + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. + * KMSKeyId: (Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use to encrypt the snapshot. + + ## Documentation Links + * [AFSBP RDS.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-4) +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the RDS.4 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-EncryptRDSSnapshot' + allowedPattern: '^[\w+=,.@/-]+' + KMSKeyId: + type: 'String' + default: 'alias/aws/rds' + description: '(Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use to encrypt the snapshot.' + 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' + outputs: + - Name: 'SourceDBSnapshotIdentifier' + Selector: '$.Payload.matches[1]' + Type: 'String' + - Name: 'SourceDBSnapshotIdentifierNoPrefix' + Selector: '$.Payload.matches[2]' + Type: 'String' + - Name: 'DBSnapshotType' + Selector: '$.Payload.matches[0]' + Type: 'String' + - Name: 'AffectedObject' + Selector: '$.Payload.object' + Type: 'StringMap' + - Name: 'FindingId' + Selector: '$.Payload.finding.Id' + Type: 'String' + - Name: 'ProductArn' + Selector: '$.Payload.finding.ProductArn' + Type: 'String' + - Name: 'RemediationRegion' + Selector: '$.Payload.resource_region' + Type: 'String' + - Name: 'RemediationAccount' + Selector: '$.Payload.account_id' + 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|dbclustersnapshot):((?:rds:)?((?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}))$' + resource_index: 2 + expected_control_id: + - 'RDS.4' + Runtime: 'python3.8' + Handler: 'parse_event' + Script: |- + %%SCRIPT=common/parse_input.py%% +- name: 'Remediation' + action: 'aws:executeAutomation' + inputs: + DocumentName: 'SHARR-EncryptRDSSnapshot' + TargetLocations: + - Accounts: + - '{{ParseInput.RemediationAccount}}' + Regions: + - '{{ParseInput.RemediationRegion}}' + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + SourceDBSnapshotIdentifier: '{{ParseInput.SourceDBSnapshotIdentifier}}' + TargetDBSnapshotIdentifier: '{{ParseInput.SourceDBSnapshotIdentifierNoPrefix}}-encrypted' + DBSnapshotType: '{{ParseInput.DBSnapshotType}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + KmsKeyId: '{{KMSKeyId}}' +- name: 'UpdateFinding' + action: 'aws:executeAwsApi' + inputs: + Service: 'securityhub' + Api: 'BatchUpdateFindings' + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Encrypted RDS snapshot' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.4' + Workflow: + Status: 'RESOLVED' + description: 'Update finding' + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml new file mode 100644 index 00000000..936bc418 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml @@ -0,0 +1,94 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document Name - SHARR-AFSBP_1.0.RDS.5 + + ## What does this document do? + This document configures an RDS DB instance for multiple Availability Zones by calling another SSM document. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. + + ## Documentation Links + * [AFSBP RDS.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-5) +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the RDS.5 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-EnableMultiAZOnRDSInstance' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '' + expected_control_id: + - 'RDS.5' + Runtime: 'python3.8' + Handler: 'parse_event' + Script: |- + %%SCRIPT=common/parse_input.py%% + outputs: + - Name: 'DbInstanceResourceId' + Selector: '$.Payload.details.AwsRdsDbInstance.DbiResourceId' + Type: 'String' + - Name: 'AffectedObject' + Selector: '$.Payload.object' + Type: 'StringMap' + - Name: 'FindingId' + Selector: '$.Payload.finding.Id' + Type: 'String' + - Name: 'ProductArn' + Selector: '$.Payload.finding.ProductArn' + Type: 'String' + - Name: 'RemediationRegion' + Selector: '$.Payload.resource_region' + Type: 'String' + - Name: 'RemediationAccount' + Selector: '$.Payload.account_id' + Type: 'String' +- name: 'Remediation' + action: 'aws:executeAutomation' + inputs: + DocumentName: 'SHARR-EnableMultiAZOnRDSInstance' + TargetLocations: + - Accounts: + - '{{ParseInput.RemediationAccount}}' + Regions: + - '{{ParseInput.RemediationRegion}}' + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbiResourceId: '{{ParseInput.DbInstanceResourceId}}' + ApplyImmediately: true + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' +- name: 'UpdateFinding' + action: 'aws:executeAwsApi' + inputs: + Service: 'securityhub' + Api: 'BatchUpdateFindings' + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Configured RDS cluster for multiple Availability Zones' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.5' + Workflow: + Status: 'RESOLVED' + description: 'Update finding' + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml index 6e35ee15..bd8b4bfc 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml @@ -19,10 +19,14 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the RDS.6 finding + RemediationRoleName: + type: String + default: "SO0111-EnableEnhancedMonitoringOnRDSInstance" + allowedPattern: '^[\w+=,.@-]+' outputs: - Remediation.Output @@ -44,18 +48,25 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: - Finding: '{{Finding}}' + Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'RDS.6' - Runtime: python3.7 + expected_control_id: + - 'RDS.6' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: GetMonitoringRoleArn action: aws:executeAwsApi description: | @@ -77,12 +88,16 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-EnableEnhancedMonitoringOnRDSInstance + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: ResourceId: '{{ ParseInput.ResourceId }}' MonitoringRoleArn: '{{GetMonitoringRoleArn.Arn}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableEnhancedMonitoringOnRDSInstance' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml index 4211fe52..f39bd8d7 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml @@ -9,7 +9,7 @@ description: | * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters - * Remediation.Output - The standard HTTP response from the ModifyDBInstance API. + * Remediation.Output - The standard HTTP response from the ModifyDBCluster API. ## Documentation Links * [AFSBP RDS.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-7) @@ -20,10 +20,14 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the RDS.7 finding + RemediationRoleName: + type: String + default: "SO0111-EnableRDSClusterDeletionProtection" + allowedPattern: '^[\w+=,.@-]+' outputs: - Remediation.Output @@ -45,28 +49,37 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'RDS.7' - Runtime: python3.7 + expected_control_id: + - 'RDS.7' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% - isEnd: false - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: SHARR-EnableRDSClusterDeletionProtection + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: ClusterId: '{{ ParseInput.ResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableRDSClusterDeletionProtection' - - - + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml new file mode 100644 index 00000000..e1c88ad4 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml @@ -0,0 +1,94 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document Name - SHARR-AFSBP_1.0.RDS.8 + + ## What does this document do? + This document enables `Deletion Protection` on a given Amazon RDS cluster by calling another SSM document. + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. + + ## Documentation Links + * [AFSBP RDS.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-8) +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the RDS.8 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-EnableRDSInstanceDeletionProtection' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '' + expected_control_id: + - 'RDS.8' + Runtime: 'python3.8' + Handler: 'parse_event' + Script: |- + %%SCRIPT=common/parse_input.py%% + outputs: + - Name: 'DbInstanceResourceId' + Selector: '$.Payload.details.AwsRdsDbInstance.DbiResourceId' + Type: 'String' + - Name: 'AffectedObject' + Selector: '$.Payload.object' + Type: 'StringMap' + - Name: 'FindingId' + Selector: '$.Payload.finding.Id' + Type: 'String' + - Name: 'ProductArn' + Selector: '$.Payload.finding.ProductArn' + Type: 'String' + - Name: 'RemediationRegion' + Selector: '$.Payload.resource_region' + Type: 'String' + - Name: 'RemediationAccount' + Selector: '$.Payload.account_id' + Type: 'String' +- name: 'Remediation' + action: 'aws:executeAutomation' + inputs: + DocumentName: 'SHARR-EnableRDSInstanceDeletionProtection' + TargetLocations: + - Accounts: + - '{{ParseInput.RemediationAccount}}' + Regions: + - '{{ParseInput.RemediationRegion}}' + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbInstanceResourceId: '{{ParseInput.DbInstanceResourceId}}' + ApplyImmediately: true + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' +- name: 'UpdateFinding' + action: 'aws:executeAwsApi' + inputs: + Service: 'securityhub' + Api: 'BatchUpdateFindings' + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Enabled deletion protection on RDS instance' + UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.8' + Workflow: + Status: 'RESOLVED' + description: 'Update finding' + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml new file mode 100644 index 00000000..4c1aa690 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml @@ -0,0 +1,93 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document Name - SHARR-AFSBP_1.0.0_Redshift.1 + + ## What does this document do? + This document disables public access to a Redshift cluster by calling another SSM document + + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. + + ## Documentation Links + * [AFSBP Redshift.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-redshift-1) +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the Redshift.1 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-DisablePublicAccessToRedshiftCluster' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):redshift:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:cluster:(?!.*--)([a-z][a-z0-9-]{0,62})(?- + {{ssm:/Solutions/SO0111/afsbp/1.0.0/S3.4/KmsKeyAlias}} + allowedPattern: '^$|^[a-zA-Z0-9/_-]{1,256}$' + +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 + - Name: BucketName + Selector: $.Payload.resource_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.4' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableDefaultEncryptionS3 + RuntimeParameters: + AccountId: '{{ParseInput.AccountId}}' + BucketName: '{{ParseInput.BucketName}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + KmsKeyAlias: '{{KmsKeyAlias}}' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Enabled default encryption for {{ParseInput.BucketName}}' + UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.4' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml index 786b5130..d8e7914e 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -18,21 +18,20 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the S3.5 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: BucketName Selector: $.Payload.resource_id @@ -54,12 +53,12 @@ mainSteps: 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 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=afsbp_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml new file mode 100644 index 00000000..a3e74080 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml @@ -0,0 +1,108 @@ +description: | + ### Document Name - SHARR-AFSBP_1.0.0_S3.6 + + ## What does this document do? + This document restricts cross-account access to a bucket in the local account. + + ## 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.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-s3-6) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the S3.6 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-S3BlockDenylist" + allowedPattern: '^[\w+=,.@-]+' + +mainSteps: + - + name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: BucketName + Selector: $.Payload.resource_id + Type: String + - Name: AffectedObject + Selector: $.Payload.object + Type: StringMap + - Name: FindingId + Selector: $.Payload.finding.Id + Type: String + - Name: ProductArn + Selector: $.Payload.finding.ProductArn + Type: String + - Name: ConfigRuleName + Selector: $.Payload.aws_config_rule.ConfigRuleName + Type: String + - Name: DenyListSerialized + Selector: $.Payload.aws_config_rule.InputParameters + 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.6' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + + - + name: ExtractSensitiveApis + action: 'aws:executeScript' + inputs: + InputPayload: + SerializedList: '{{ ParseInput.DenyListSerialized }}' + Runtime: python3.8 + Handler: runbook_handler + Script: |- + %%SCRIPT=deserializeApiList.py%% + outputs: + - Name: ListOfApis + Selector: $.Payload + Type: String + + - + name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-S3BlockDenylist + RuntimeParameters: + BucketName: '{{ParseInput.BucketName}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + DenyList: '{{ExtractSensitiveApis.ListOfApis}}' + + - + name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Added explicit deny for sensitive bucket access from another account.' + UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.6' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/deserializeApiList.py b/source/playbooks/AFSBP/ssmdocs/scripts/deserializeApiList.py new file mode 100644 index 00000000..34c2a836 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/scripts/deserializeApiList.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json + +def runbook_handler(event, context): + try: + deserialized = json.loads(event['SerializedList']) + if 'blacklistedActionPattern' in deserialized: + return deserialized['blacklistedActionPattern'] # Returns comma-delimited list in a string + else: + exit('Missing blacklistedActionPattern in AWS Config data') + except Exception as e: + print(e) + exit('Failed getting comma-delimited string list of sensitive API calls input data') diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/test/test_s3-6_deserialize_api_list.py b/source/playbooks/AFSBP/ssmdocs/scripts/test/test_s3-6_deserialize_api_list.py new file mode 100644 index 00000000..8c19bec9 --- /dev/null +++ b/source/playbooks/AFSBP/ssmdocs/scripts/test/test_s3-6_deserialize_api_list.py @@ -0,0 +1,28 @@ +#!/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 +import deserializeApiList as script + +def event(): + return { + "SerializedList": "{\"blacklistedActionPattern\":\"s3:DeleteBucketPolicy,s3:PutBucketAcl,s3:PutBucketPolicy,s3:PutObjectAcl,s3:PutEncryptionConfiguration\"}" + } + +def expected(): + return "s3:DeleteBucketPolicy,s3:PutBucketAcl,s3:PutBucketPolicy,s3:PutObjectAcl,s3:PutEncryptionConfiguration" + +def test_extract_list(): + assert script.runbook_handler(event(), {}) == expected() 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 6a3935db..7e08a132 100644 --- a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap +++ b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap @@ -3,26 +3,26 @@ exports[`Member Stack - AFSBP 1`] = ` Object { "Conditions": Object { - "AFSBPEC21EnableEC21ConditionA4D0F59B": Object { + "EnableEC21Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "AFSBPEC21Active", + "Ref": "EnableEC21", }, "Available", ], }, - "AFSBPLambda1EnableLambda1Condition4E1A1855": Object { + "EnableLambda1Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "AFSBPLambda1Active", + "Ref": "EnableLambda1", }, "Available", ], }, - "AFSBPRDS1EnableRDS1ConditionB553606B": Object { + "EnableRDS1Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "AFSBPRDS1Active", + "Ref": "EnableRDS1", }, "Available", ], @@ -30,7 +30,7 @@ Object { }, "Description": "test;", "Parameters": Object { - "AFSBPEC21Active": Object { + "EnableEC21": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -39,7 +39,7 @@ Object { "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.", "Type": "String", }, - "AFSBPLambda1Active": Object { + "EnableLambda1": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -48,7 +48,7 @@ Object { "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.", "Type": "String", }, - "AFSBPRDS1Active": Object { + "EnableRDS1": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -64,645 +64,974 @@ Object { }, }, "Resources": Object { - "AFSBPEC21AutomationDocument39E9DD5A": Object { - "Condition": "AFSBPEC21EnableEC21ConditionA4D0F59B", + "AFSBPEC21": Object { + "Condition": "EnableEC21Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-AFSBP_1.0.0_EC2.1 -## What does this document do? -This document changes all public EC2 snapshots to private + "Content": "description: | + ### Document Name - SHARR-AFSBP_1.0.0_EC2.1 + ## What does this document do? + This document changes all public EC2 snapshots to private -## 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. + ## 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. -## Documentation Links -* [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "EC2.1", - "parse_id_pattern": "", - "resource_index": 2, - }, - "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 + ## Documentation Links + * [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1) -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}') +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the EC2.1 finding + 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+=,.@-]+$' -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 = bool('testmode' in finding) +outputs: + - Remediation.Output + - ParseInput.AffectedObject - finding_id = finding['Id'] +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.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + isEnd: false - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - + 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 - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", + - + 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 +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_EC2.1", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - Object { - "Name": "AccountId", - "Selector": "$.Payload.account_id", - "Type": "String", - }, - Object { - "Name": "TestMode", - "Selector": "$.Payload.testmode", - "Type": "Boolean", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-MakeEBSSnapshotsPrivate", - "RuntimeParameters": Object { - "AccountId": "{{ParseInput.AccountId}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate", - "TestMode": "{{ParseInput.TestMode}}", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "EBS Snapshot modified to private", - "UpdatedBy": "SHARR-AFSBP_1.0.0_EC2.1", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "outputs": Array [ - "Remediation.Output", - "ParseInput.AffectedObject", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for EC2.1 finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-AFSBP_1.0.0_EC2.1", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "AFSBPLambda1AutomationDocumentB7954EC2": Object { - "Condition": "AFSBPLambda1EnableLambda1Condition4E1A1855", + "AFSBPLambda1": Object { + "Condition": "EnableLambda1Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-AFSBP_1.0.0_Lambda.1 - -## What does this document do? -This document removes the public resource policy. A public resource policy -contains a principal \\"*\\" or AWS: \\"*\\", which allows public access to the -function. The remediation is to remove the SID of the public policy. + "Content": "description: | + ### Document Name - SHARR-AFSBP_1.0.0_Lambda.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. + ## What does this document do? + This document removes the public resource policy. A public resource policy + contains a principal \\"*\\" or AWS: \\"*\\", which allows public access to the + function. The remediation is to remove the SID of the public policy. -## Documentation Links -* [AFSBP Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-1) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "Lambda.1", - "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 + ## 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. -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}') + ## Documentation Links + * [AFSBP Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-1) -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 = bool('testmode' in finding) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - Remediation.Output + - ParseInput.AffectedObject +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the Lambda.1 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: \\"SO0111-RemoveLambdaPublicAccess\\" + allowedPattern: '^[\\\\w+=,.@-]+' - finding_id = finding['Id'] +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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_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.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - + name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-RemoveLambdaPublicAccess + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + FunctionName: '{{ ParseInput.FunctionName }}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", + - + 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 +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_Lambda.1", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - Object { - "Name": "FunctionName", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-RemoveLambdaPublicAccess", - "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess", - "FunctionName": "{{ ParseInput.FunctionName }}", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Lamdba {{ParseInput.FunctionName}} policy updated to remove public access", - "UpdatedBy": "SHARR-AFSBP_1.0.0_Lambda.1", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "Remediation.Output", - "ParseInput.AffectedObject", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for the finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-AFSBP_1.0.0_Lambda.1", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "AFSBPRDS1AutomationDocumentF363990A": Object { - "Condition": "AFSBPRDS1EnableRDS1ConditionB553606B", + "AFSBPRDS1": Object { + "Condition": "EnableRDS1Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-AFSBP_1.0.0_RDS.1 -## What does this document do? -This document changes public RDS snapshot to private + "Content": "description: | + ### Document Name - SHARR-AFSBP_1.0.0_RDS.1 + ## What does this document do? + This document changes public RDS snapshot to private -## 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. + ## 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. -## Documentation Links -* [AFSBP RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-1) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "RDS.1", - "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, - }, - "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 + ## Documentation Links + * [AFSBP RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-1) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - Remediation.Output + - ParseInput.AffectedObject +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the RDS.1 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: \\"SO0111-MakeRDSSnapshotPrivate\\" + allowedPattern: '^[\\\\w+=,.@-]+' -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 = bool('testmode' in finding) - - finding_id = finding['Id'] +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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + 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.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } + nextStep: Remediation - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-MakeRDSSnapshotPrivate + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DBSnapshotId: '{{ParseInput.DBSnapshotId}}' + DBSnapshotType: '{{ParseInput.DBSnapshotType}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + nextStep: UpdateFinding - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", + - 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 +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-AFSBP_1.0.0_RDS.1", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "name": "ParseInput", - "nextStep": "Remediation", - "outputs": Array [ - Object { - "Name": "DBSnapshotId", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - Object { - "Name": "DBSnapshotType", - "Selector": "$.Payload.matches[0]", - "Type": "String", - }, - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - Object { - "Name": "Type", - "Selector": "$.Payload.type", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-MakeRDSSnapshotPrivate", - "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate", - "DBSnapshotId": "{{ParseInput.DBSnapshotId}}", - "DBSnapshotType": "{{ParseInput.DBSnapshotType}}", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - "nextStep": "UpdateFinding", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "RDS DB Snapshot modified to private", - "UpdatedBy": "SHARR-AFSBP_1.0.0_RDS.1", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "Remediation.Output", - "ParseInput.AffectedObject", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for RDS.1 finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-AFSBP_1.0.0_RDS.1", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, }, } @@ -768,6 +1097,9 @@ Object { "GeneratorId": Array [ "aws-foundational-security-best-practices/v/1.0.0/Example.1", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -858,6 +1190,9 @@ Object { "GeneratorId": Array [ "aws-foundational-security-best-practices/v/1.0.0/Example.3", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -948,6 +1283,9 @@ Object { "GeneratorId": Array [ "aws-foundational-security-best-practices/v/1.0.0/Example.5", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", diff --git a/source/playbooks/AFSBP/test/afsbp_stack.test.ts b/source/playbooks/AFSBP/test/afsbp_stack.test.ts index c94ea8ba..8a0c14ed 100644 --- a/source/playbooks/AFSBP/test/afsbp_stack.test.ts +++ b/source/playbooks/AFSBP/test/afsbp_stack.test.ts @@ -35,6 +35,7 @@ function getMemberStack(): cdk.Stack { securityStandardLongName: 'aws-foundational-security-best-practices', securityStandardVersion: '1.0.0', ssmdocs: 'playbooks/AFSBP/ssmdocs', + commonScripts: 'playbooks/common', remediations: [ { "control": 'EC2.1'}, {"control": 'RDS.1'}, {"control":'Lambda.1'} ] }) return stack; diff --git a/source/playbooks/CIS120/README.md b/source/playbooks/CIS120/README.md index 13ab3166..9caddf1f 100644 --- a/source/playbooks/CIS120/README.md +++ b/source/playbooks/CIS120/README.md @@ -3,6 +3,7 @@ The Center for Internet Security AWS Foundations Benchmark v1.2.0 (CIS) playbook is part of the AWS Security Hub Automated Response and Remediation solution. It creates the necessary AWS resources for remediating the following Controls: * 1.3 +* 1.4 * 1.5 * 1.6 * 1.7 diff --git a/source/playbooks/CIS120/bin/cis120.ts b/source/playbooks/CIS120/bin/cis120.ts index 4da720ed..71d13cc0 100644 --- a/source/playbooks/CIS120/bin/cis120.ts +++ b/source/playbooks/CIS120/bin/cis120.ts @@ -1,21 +1,14 @@ #!/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 'source-map-support/register'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + PlaybookPrimaryStack, + PlaybookMemberStack, + IControl +} from '../../../lib/sharrplaybook-construct'; +import * as cdk_nag from 'cdk-nag'; import * as cdk from '@aws-cdk/core'; -import { PlaybookPrimaryStack, PlaybookMemberStack, IControl } from '../../../lib/sharrplaybook-construct'; +import 'source-map-support/register'; // SOLUTION_* - set by solution_env.sh const SOLUTION_ID = process.env['SOLUTION_ID'] || 'undefined'; @@ -25,134 +18,134 @@ const DIST_VERSION = process.env['DIST_VERSION'] || '%%VERSION%%'; const DIST_OUTPUT_BUCKET = process.env['DIST_OUTPUT_BUCKET'] || '%%BUCKET%%'; const DIST_SOLUTION_NAME = process.env['DIST_SOLUTION_NAME'] || '%%SOLUTION%%'; -const standardShortName = 'CIS' -const standardLongName = 'cis-aws-foundations-benchmark' -const standardVersion = '1.2.0' // DO NOT INCLUDE 'V' +const standardShortName = 'CIS'; +const standardLongName = 'cis-aws-foundations-benchmark'; +const standardVersion = '1.2.0'; // DO NOT INCLUDE 'V' const app = new cdk.App(); +cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks()); // Creates one rule per control Id. The Step Function determines what document to run based on // Security Standard and Control Id. See cis-member-stack let remediations: IControl[] = [ - { "control": "1.3" }, - { "control": "1.4" }, - { "control": "1.5" }, - { - "control": "1.6", - "executes": "1.5" - }, - { - "control": "1.7", - "executes": "1.5" - }, - { - "control": "1.8", - "executes": "1.5" - }, - { - "control": "1.9", - "executes": "1.5" - }, - { - "control": "1.10", - "executes": "1.5" - }, - { - "control": "1.11", - "executes": "1.5" - }, - // { "control": "1.20" }, - { "control": "2.1" }, - { "control": "2.2" }, - { "control": "2.3" }, - { "control": "2.4" }, - { "control": "2.5" }, - { "control": "2.6" }, - { "control": "2.7" }, - { "control": "2.8" }, - { "control": "2.9" }, - { "control": "3.1" }, - { - "control": "3.2", - "executes": "3.1" - }, - { - "control": "3.3", - "executes": "3.1" - }, - { - "control": "3.4", - "executes": "3.1" - }, - { - "control": "3.5", - "executes": "3.1" - }, - { - "control": "3.6", - "executes": "3.1" - }, - { - "control": "3.7", - "executes": "3.1" - }, - { - "control": "3.8", - "executes": "3.1" - }, - { - "control": "3.9", - "executes": "3.1" - }, - { - "control": "3.10", - "executes": "3.1" - }, - { - "control": "3.11", - "executes": "3.1" - }, - { - "control": "3.12", - "executes": "3.1" - }, - { - "control": "3.13", - "executes": "3.1" - }, - { - "control": "3.14", - "executes": "3.1" - }, - { "control": "4.1" }, - { - "control": "4.2", - "executes": "4.1" - }, - { "control": "4.3" } -] + { "control": "1.3" }, + { "control": "1.4" }, + { "control": "1.5" }, + { + "control": "1.6", + "executes": "1.5" + }, + { + "control": "1.7", + "executes": "1.5" + }, + { + "control": "1.8", + "executes": "1.5" + }, + { + "control": "1.9", + "executes": "1.5" + }, + { + "control": "1.10", + "executes": "1.5" + }, + { + "control": "1.11", + "executes": "1.5" + }, + { "control": "2.1" }, + { "control": "2.2" }, + { "control": "2.3" }, + { "control": "2.4" }, + { "control": "2.5" }, + { "control": "2.6" }, + { "control": "2.7" }, + { "control": "2.8" }, + { "control": "2.9" }, + { "control": "3.1" }, + { + "control": "3.2", + "executes": "3.1" + }, + { + "control": "3.3", + "executes": "3.1" + }, + { + "control": "3.4", + "executes": "3.1" + }, + { + "control": "3.5", + "executes": "3.1" + }, + { + "control": "3.6", + "executes": "3.1" + }, + { + "control": "3.7", + "executes": "3.1" + }, + { + "control": "3.8", + "executes": "3.1" + }, + { + "control": "3.9", + "executes": "3.1" + }, + { + "control": "3.10", + "executes": "3.1" + }, + { + "control": "3.11", + "executes": "3.1" + }, + { + "control": "3.12", + "executes": "3.1" + }, + { + "control": "3.13", + "executes": "3.1" + }, + { + "control": "3.14", + "executes": "3.1" + }, + { "control": "4.1" }, + { + "control": "4.2", + "executes": "4.1" + }, + { "control": "4.3" } +]; const adminStack = new PlaybookPrimaryStack(app, 'CIS120Stack', { - description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - solutionDistName: DIST_SOLUTION_NAME, - remediations: remediations, - securityStandardLongName: standardLongName, - securityStandard: standardShortName, - securityStandardVersion: standardVersion + description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + solutionDistName: DIST_SOLUTION_NAME, + remediations: remediations, + securityStandardLongName: standardLongName, + securityStandard: standardShortName, + securityStandardVersion: standardVersion }); const memberStack = new PlaybookMemberStack(app, 'CIS120MemberStack', { - description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - securityStandard: standardShortName, - securityStandardVersion: standardVersion, - securityStandardLongName: standardLongName, - remediations: remediations + description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + securityStandard: standardShortName, + securityStandardVersion: standardVersion, + securityStandardLongName: standardLongName, + remediations: remediations }); adminStack.templateOptions.templateFormatVersion = "2010-09-09" diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml index e5d41920..5f0dad27 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml @@ -7,28 +7,27 @@ description: | ## 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 - Output of DescribeAutoScalingGroups API. ## Documentation Links * [CIS v1.2.0 1.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.3) - - + + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 1.3 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput action: 'aws:executeScript' @@ -51,12 +50,13 @@ mainSteps: 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.3' - Runtime: python3.7 + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):iam::\d{12}:user(?:(?:\u002F)|(?:\u002F[\u0021-\u007F]{1,510}\u002F))([\w+=,.@-]{1,64})$' + expected_control_id: + - '1.3' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml index 6821110d..365adf01 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml @@ -7,39 +7,45 @@ description: | ## 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 + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 1.4 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+=,.@-]+' + 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+=,.@-]+$' 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)$ + allowedPattern: ^[1-9][0-9]{0,3}|10000$ default: "90" + RemediationRoleName: + type: String + default: "SO0111-RevokeUnrotatedKeys" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput action: 'aws:executeScript' outputs: - - Name: IAMResourceId + - 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 @@ -52,12 +58,13 @@ mainSteps: 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 + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):iam::\d{12}:user(?:(?:\u002F)|(?:\u002F[\u0021-\u007F]{1,510}\u002F))([\w+=,.@-]{1,64})$' + expected_control_id: + - '1.4' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' @@ -66,7 +73,7 @@ mainSteps: DocumentName: SHARR-RevokeUnrotatedKeys RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnrotatedKeys' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' MaxCredentialUsageAge: '{{MaxCredentialUsageAge}}' - name: UpdateFinding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml index e31258c0..90dc1827 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml @@ -21,22 +21,21 @@ description: | * [CIS v1.2.0 1.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.9) * [CIS v1.2.0 1.10](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.10) * [CIS v1.2.0 1.11](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.11) - - + + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 1.5 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -56,10 +55,10 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' expected_control_id: [ '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11' ] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml index cb143f75..17aac947 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml @@ -4,7 +4,7 @@ description: | ## 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 * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. @@ -18,10 +18,10 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the 2.1 finding KMSKeyArn: type: String default: >- @@ -55,11 +55,12 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: '2.1' - Runtime: python3.7 + expected_control_id: + - '2.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml index c9883d17..0b07f975 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml @@ -7,32 +7,35 @@ description: | ## 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 2.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.2) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.2 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailLogFileValidation" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: TrailName Selector: $.Payload.resource_id @@ -46,25 +49,36 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' - expected_control_id: '2.2' - Runtime: python3.7 + expected_control_id: + - '2.2' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false inputs: DocumentName: SHARR-EnableCloudTrailLogFileValidation + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: TrailName: '{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 9b4ba970..afbdba4d 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml @@ -7,32 +7,31 @@ description: | ## 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 2.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.3) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.3 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: BucketName Selector: $.Payload.resource_id @@ -50,13 +49,14 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$' - expected_control_id: '2.3' - Runtime: python3.7 + expected_control_id: + - '2.3' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml index 63c95448..9e895e5e 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml @@ -7,27 +7,30 @@ description: | ## 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 * [CIS v1.2.0 2.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.4) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.4 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailToCloudWatchLogging" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,15 +48,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' - expected_control_id: '2.4' - Runtime: python3.7 + expected_control_id: + - '2.4' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -61,11 +71,15 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: TrailName: '{{ ParseInput.TrailName }}' 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' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 b62a0025..10ce9e83 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml @@ -14,17 +14,17 @@ description: | ## Documentation Links * [CIS v1.2.0 2.5](https://docs.aws.amazon.com/console/securityhub/standards-cis-2.5/remediation) - + 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the 2.5 finding KMSKeyArn: type: String default: >- @@ -53,14 +53,15 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: '2.5' - Runtime: python3.7 + expected_control_id: + - '2.5' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - - + + - name: Remediation action: 'aws:executeAutomation' isEnd: false @@ -70,8 +71,8 @@ mainSteps: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - - - + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml index bb57c5f7..415a3f31 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml @@ -7,27 +7,26 @@ description: | ## 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 * [CIS v1.2.0 2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.6) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.6 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -49,11 +48,12 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):s3:::([a-z0-9.-]{3,63})$' - expected_control_id: '2.6' - Runtime: python3.7 + expected_control_id: + - '2.6' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: CreateAccessLoggingBucket diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml index cd7500f0..34140bbd 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml @@ -18,17 +18,18 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the 2.7 finding KMSKeyArn: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} + 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' outputs: @@ -45,19 +46,20 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.resource.Region + Selector: $.Payload.resource_region Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: '2.7' - Runtime: python3.7 + expected_control_id: + - '2.7' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% - - + - name: Remediation action: 'aws:executeAutomation' inputs: diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml index 580cf679..b3a2314b 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml @@ -7,26 +7,29 @@ description: | ## 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 * [CIS v1.2.0 2.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.8) schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.8 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableKeyRotation" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -44,25 +47,34 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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: '2.8' - Runtime: python3.7 + expected_control_id: + - '2.8' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% - isEnd: false + %%SCRIPT=common/parse_input.py%% - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: SHARR-EnableKeyRotation + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: KeyId: '{{ParseInput.KMSKeyId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableKeyRotation' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 6844aa3e..afd2941c 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml @@ -7,26 +7,26 @@ description: | ## 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 * [CIS v1.2.0 2.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.9) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 2.9 finding 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+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' mainSteps: - name: ParseInput @@ -48,11 +48,12 @@ mainSteps: 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: '2.9' - Runtime: python3.7 + expected_control_id: + - '2.9' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation diff --git a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml index 5f115d57..72534d8f 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml @@ -3,7 +3,7 @@ description: | ## What does this document do? Remediates the following CIS findings: - + 3.1 - Creates a log metric filter and alarm for unauthorized API calls 3.2 - Creates a log metric filter and alarm for AWS Management Console sign-in without MFA 3.3 - Creates a log metric filter and alarm for usage of "root" account @@ -23,7 +23,7 @@ description: | ## 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 - Output of remediation runbook. @@ -47,25 +47,26 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 3.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+=,.@-]+' + 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+=,.@-]+$' LogGroupName: type: String default: >- {{ssm:/Solutions/SO0111/Metrics_LogGroupName}} description: The name of the Log group to be used to create filters and metric alarms + allowedPattern: '.*' MetricNamespace: type: String default: 'LogMetrics' description: The name of the metric namespace where the metrics will be logged + allowedPattern: '.*' KMSKeyArn: type: String default: >- @@ -97,10 +98,10 @@ mainSteps: parse_id_pattern: '' Finding: '{{Finding}}' expected_control_id: [ '3.1', '3.2', '3.3', '3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13','3.14'] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: GetMetricFilterAndAlarmInputValue @@ -130,7 +131,7 @@ mainSteps: inputs: InputPayload: ControlId: '{{ParseInput.ControlId}}' - Runtime: python3.7 + Runtime: python3.8 Handler: verify Script: |- %%SCRIPT=cis_get_input_values.py%% diff --git a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml index adbcc43e..b4360b00 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml @@ -7,28 +7,31 @@ description: | ## 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 - Output of AWS-DisablePublicAccessForSecurityGroup runbook. ## Documentation Links * [CIS v1.2.0 4.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-4.1) * [CIS v1.2.0 4.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-4.2) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 4.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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-DisablePublicAccessForSecurityGroup" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -46,24 +49,33 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):ec2:(?:[a-z]{2}(?:-gov)?-[a-z]+-[0-9]):[0-9]{12}:security-group/(sg-[a-f0-9]{8,17})$' expected_control_id: ['4.1', '4.2'] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% - isEnd: false + %%SCRIPT=common/parse_input.py%% + - name: Remediation action: 'aws:executeAutomation' - isEnd: false inputs: DocumentName: AWS-DisablePublicAccessForSecurityGroup + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: GroupId: '{{ ParseInput.GroupId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 84969417..6565a083 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml @@ -7,27 +7,30 @@ description: | ## 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 - Output of remediation runbook. ## Documentation Links [CIS v1.2.0 4.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-4.3) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the 4.3 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-RemoveVPCDefaultSecurityGroupRules" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,15 +48,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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-[a-f0-9]{8,17})$' - expected_control_id: '4.3' - Runtime: python3.7 + expected_control_id: + - '4.3' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=cis_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -61,9 +71,13 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: GroupId: '{{ ParseInput.GroupId }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - name: UpdateFinding action: 'aws:executeAwsApi' 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 416e0527..48871d20 100644 --- a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap +++ b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap @@ -71,6 +71,9 @@ Object { ], }, ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -172,6 +175,9 @@ Object { ], }, ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -273,6 +279,9 @@ Object { ], }, ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -373,26 +382,26 @@ Object { exports[`default stack 2`] = ` Object { "Conditions": Object { - "CIS13Enable13Condition31A4889D": Object { + "Enable13Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "CIS13Active", + "Ref": "Enable13", }, "Available", ], }, - "CIS15Enable15ConditionB99E94E1": Object { + "Enable15Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "CIS15Active", + "Ref": "Enable15", }, "Available", ], }, - "CIS21Enable21ConditionBEEACA68": Object { + "Enable21Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "CIS21Active", + "Ref": "Enable21", }, "Available", ], @@ -400,7 +409,7 @@ Object { }, "Description": "test;", "Parameters": Object { - "CIS13Active": Object { + "Enable13": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -409,7 +418,7 @@ Object { "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 1.3 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.", "Type": "String", }, - "CIS15Active": Object { + "Enable15": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -418,7 +427,7 @@ Object { "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 1.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.", "Type": "String", }, - "CIS21Active": Object { + "Enable21": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -434,667 +443,963 @@ Object { }, }, "Resources": Object { - "CIS13AutomationDocumentBF2E2E96": Object { - "Condition": "CIS13Enable13Condition31A4889D", + "CIS13": Object { + "Condition": "Enable13Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CIS_1.2.0_1.3 + "Content": "description: | + ### Document Name - SHARR-CIS_1.2.0_1.3 -## What does this document do? -This document ensures that credentials unused for 90 days or greater are disabled. + ## What does this document do? + This document ensures that credentials unused for 90 days or greater are disabled. -## 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. + ## 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 - Output of DescribeAutoScalingGroups API. - -## Documentation Links -* [CIS v1.2.0 1.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.3) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "1.3", - "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):iam::\\\\d{12}:user/([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 + ## Output Parameters + * Remediation.Output - Output of DescribeAutoScalingGroups API. -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}') + ## Documentation Links + * [CIS v1.2.0 1.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.3) -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 = bool('testmode' in finding) - finding_id = finding['Id'] +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the 1.3 finding + 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+=,.@-]+$' +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - 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 + - 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(?:(?:\\\\u002F)|(?:\\\\u002F[\\\\u0021-\\\\u007F]{1,510}\\\\u002F))([\\\\w+=,.@-]{1,64})$' + expected_control_id: + - '1.3' + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } + 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' - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - 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 for {{ ParseInput.IAMUser }}.' + UpdatedBy: 'SHARR-CIS_1.2.0_1.3' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-CIS_1.2.0_1.3", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "IAMUser", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - Object { - "Name": "IAMResourceId", - "Selector": "$.Payload.details.AwsIamUser.UserId", - "Type": "String", - }, - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-RevokeUnusedIAMUserCredentials", - "RuntimeParameters": Object { - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials", - "IAMResourceId": "{{ ParseInput.IAMResourceId }}", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Deactivated unused keys and expired logins for {{ ParseInput.IAMUser }}.", - "UpdatedBy": "SHARR-CIS_1.2.0_1.3", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "ParseInput.AffectedObject", - "Remediation.Output", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-CIS_1.2.0_1.3", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "CIS15AutomationDocument03646554": Object { - "Condition": "CIS15Enable15ConditionB99E94E1", + "CIS15": Object { + "Condition": "Enable15Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CIS_1.2.0_1.5 + "Content": "description: | + ### Document Name - SHARR-CIS_1.2.0_1.5 -## What does this document do? -This document establishes a default password policy. + ## What does this document do? + This document establishes a default password policy. -## Security Standards and Controls -* CIS 1.5 - 1.11 + ## Security Standards and Controls + * CIS 1.5 - 1.11 -## 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 + ## 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.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.5) -* [CIS v1.2.0 1.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.6) -* [CIS v1.2.0 1.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.7) -* [CIS v1.2.0 1.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.8) -* [CIS v1.2.0 1.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.9) -* [CIS v1.2.0 1.10](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.10) -* [CIS v1.2.0 1.11](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.11) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": Array [ - "1.5", - "1.6", - "1.7", - "1.8", - "1.9", - "1.10", - "1.11", - ], - "parse_id_pattern": "", - }, - "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 + ## Documentation Links + * [CIS v1.2.0 1.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.5) + * [CIS v1.2.0 1.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.6) + * [CIS v1.2.0 1.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.7) + * [CIS v1.2.0 1.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.8) + * [CIS v1.2.0 1.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.9) + * [CIS v1.2.0 1.10](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.10) + * [CIS v1.2.0 1.11](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.11) -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 = bool('testmode' in finding) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the 1.5 finding + 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_id = finding['Id'] +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: [ '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } + 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' - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - 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-CIS_1.2.0_1.5' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-CIS_1.2.0_1.5", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-SetIAMPasswordPolicy", - "RuntimeParameters": Object { - "AllowUsersToChangePassword": true, - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy", - "HardExpiry": true, - "MaxPasswordAge": 90, - "MinimumPasswordLength": 14, - "PasswordReusePrevention": 24, - "RequireLowercaseCharacters": true, - "RequireNumbers": true, - "RequireSymbols": true, - "RequireUppercaseCharacters": true, - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", - "UpdatedBy": "SHARR-CIS_1.2.0_1.5", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "ParseInput.AffectedObject", - "Remediation.Output", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-CIS_1.2.0_1.5", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "CIS21AutomationDocument7655FE02": Object { - "Condition": "CIS21Enable21ConditionBEEACA68", + "CIS21": Object { + "Condition": "Enable21Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-CIS_1.2.0_2.1 - -## 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 -* Finding: (Required) Security Hub finding details JSON -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + "Content": "description: | + ### Document Name - SHARR-CIS_1.2.0_2.1 -## Documentation Links -* [CIS v1.2.0 2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.1) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "2.1", - "parse_id_pattern": "", - }, - "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 + ## What does this document do? + Creates a multi-region trail with KMS encryption and enables CloudTrail + Note: this remediation will create a NEW trail. -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}') + ## 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. -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 = bool('testmode' in finding) + ## Documentation Links + * [CIS v1.2.0 2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.1) - finding_id = finding['Id'] +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 the 2.1 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: ResourceId + 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: AWSPartition + Selector: $.Payload.partition + Type: String + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '' + expected_control_id: + - '2.1' + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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 + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } - if parse_id_pattern: - identifier_match = re.match( - parse_id_pattern, - identifier_raw - ) + isEnd: false - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + - 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}}' - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - 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-CIS_1.2.0_2.11' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-CIS_1.2.0_2.1", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "ResourceId", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - Object { - "Name": "AWSPartition", - "Selector": "$.Payload.partition", - "Type": "String", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-CreateCloudTrailMultiRegionTrail", - "RuntimeParameters": Object { - "AWSPartition": "{{global:AWS_PARTITION}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Multi-region, encrypted AWS CloudTrail successfully created", - "UpdatedBy": "SHARR-CIS_1.2.0_2.11", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "Remediation.Output", - "ParseInput.AffectedObject", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "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", - }, - "Finding": Object { - "description": "The input from Step function for the finding", - "type": "StringMap", - }, - "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-CIS_1.2.0_2.1", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, "RemapCIS4245EB49A0": Object { "Properties": Object { diff --git a/source/playbooks/CIS120/test/cis_stack.test.ts b/source/playbooks/CIS120/test/cis_stack.test.ts index b85bccc1..ee09c273 100644 --- a/source/playbooks/CIS120/test/cis_stack.test.ts +++ b/source/playbooks/CIS120/test/cis_stack.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; +import { SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { StringParameter } from '@aws-cdk/aws-ssm'; import { PlaybookPrimaryStack, PlaybookMemberStack } from '../../../lib/sharrplaybook-construct'; @@ -36,6 +36,7 @@ function getMemberStack(): cdk.Stack { securityStandardVersion: '1.2.0', securityStandardLongName: 'cis-aws-foundations-benchmark', ssmdocs: 'playbooks/CIS120/ssmdocs', + commonScripts: 'playbooks/common', remediations: [ {"control":'1.3'}, {"control":'1.5'}, {"control":'2.1'} ] }) diff --git a/source/playbooks/NEWPLAYBOOK/README.md b/source/playbooks/NEWPLAYBOOK/README.md index 2b881f9e..04a3ae6c 100644 --- a/source/playbooks/NEWPLAYBOOK/README.md +++ b/source/playbooks/NEWPLAYBOOK/README.md @@ -5,4 +5,10 @@ The NEWPLAYBOOK (NEWPB) playbook is part of the AWS Security Hub Automated Respo * Example.1 * Example.2 +Note that in the example remediation, ssmdocs/AFSBP_RDS.6.yaml, the line: +``` +%%SCRIPT=common/parse_input.py%% +``` +...loads parse_input.py from playbooks/common. This same parse code is used in all the the current playbooks. + See the README.md in the root of this archive and the [AWS Security Hub Automated Response and Remediation Implementation Guide](https://docs.aws.amazon.com/solutions/latest/aws-security-hub-automated-response-and-remediation/welcome.html) for more information. diff --git a/source/playbooks/NEWPLAYBOOK/bin/newplaybook.ts b/source/playbooks/NEWPLAYBOOK/bin/newplaybook.ts index 1659cd6d..a28a72fb 100644 --- a/source/playbooks/NEWPLAYBOOK/bin/newplaybook.ts +++ b/source/playbooks/NEWPLAYBOOK/bin/newplaybook.ts @@ -1,21 +1,14 @@ #!/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 'source-map-support/register'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + PlaybookPrimaryStack, + PlaybookMemberStack, + IControl +} from '../../../lib/sharrplaybook-construct'; import * as cdk from '@aws-cdk/core'; -import { PlaybookPrimaryStack, PlaybookMemberStack, IControl } from '../../../lib/sharrplaybook-construct'; +import * as cdk_nag from 'cdk-nag'; +import 'source-map-support/register'; // SOLUTION_* - set by solution_env.sh const SOLUTION_ID = process.env['SOLUTION_ID'] || 'undefined'; @@ -25,40 +18,41 @@ const DIST_VERSION = process.env['DIST_VERSION'] || '%%VERSION%%'; const DIST_OUTPUT_BUCKET = process.env['DIST_OUTPUT_BUCKET'] || '%%BUCKET%%'; const DIST_SOLUTION_NAME = process.env['DIST_SOLUTION_NAME'] || '%%SOLUTION%%'; -const standardShortName = 'NPB' -const standardLongName = 'New Playbook' -const standardVersion = '1.1.1' // DO NOT INCLUDE 'V' +const standardShortName = 'NPB'; +const standardLongName = 'New Playbook'; +const standardVersion = '1.1.1'; // DO NOT INCLUDE 'V' const app = new cdk.App(); +cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks()); // Creates one rule per control Id. The Step Function determines what document to run based on // Security Standard and Control Id. See cis-member-stack const remediations: IControl[] = [ - { "control": "RDS.6" } -] + { "control": "RDS.6" } +]; const adminStack = new PlaybookPrimaryStack(app, 'NPBStack', { - description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - solutionDistName: DIST_SOLUTION_NAME, - remediations: remediations, - securityStandardLongName: standardLongName, - securityStandard: standardShortName, - securityStandardVersion: standardVersion + description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + solutionDistName: DIST_SOLUTION_NAME, + remediations: remediations, + securityStandardLongName: standardLongName, + securityStandard: standardShortName, + securityStandardVersion: standardVersion }); const memberStack = new PlaybookMemberStack(app, 'NPBMemberStack', { - description: `(${SOLUTION_ID}M) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - securityStandard: standardShortName, - securityStandardVersion: standardVersion, - securityStandardLongName: standardLongName, - remediations: remediations + description: `(${SOLUTION_ID}M) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + securityStandard: standardShortName, + securityStandardVersion: standardVersion, + securityStandardLongName: standardLongName, + remediations: remediations }); -adminStack.templateOptions.templateFormatVersion = "2010-09-09" -memberStack.templateOptions.templateFormatVersion = "2010-09-09" +adminStack.templateOptions.templateFormatVersion = "2010-09-09"; +memberStack.templateOptions.templateFormatVersion = "2010-09-09"; diff --git a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml index a9815984..5b41bf23 100644 --- a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml @@ -16,7 +16,7 @@ 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+=,.@-]+' + 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 @@ -45,51 +45,10 @@ mainSteps: InputPayload: Finding: '{{Finding}}' - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - import re - def parse_event(event, context): - - my_control_id = 'RDS.6' - finding = event['Finding'] - - finding_id = finding['Id'] - control_id = '' - 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) - - if not control_id: - exit(f'ERROR: Finding Id is invalid: {finding_id} - missing Control Id') - - if control_id != my_control_id: - exit(f'ERROR: Control Id from input ({control_id}) does not match {my_control_id}') - - 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}') - - account_id = finding['AwsAccountId'] - if not re.match('^\d{12}$', account_id): - exit(f'ERROR: AwsAccountId is invalid: {account_id}') - - resource_id = finding['Resources'][0]['Details']['AwsRdsDbInstance']['DbiResourceId'] - if not re.match( - '^db-[0-9A-Z]{16,26}$', - resource_id - ): - exit('ERROR: DbiResourceId {resource_id} is not valid') - - object = {'Type': 'RDSDBInstance', 'Id': resource_id, 'OutputKey': 'VerifyRemediation.Output'} - return { - "resource_id": resource_id, - "finding_id": finding_id, - "product_arn": product_arn, - "object": object - } + %%SCRIPT=common/parse_input.py%% isEnd: false - name: GetMonitoringRoleArn @@ -128,7 +87,7 @@ mainSteps: inputs: InputPayload: remediation_output: '{{ExecRemediation.Output}}' - Runtime: python3.7 + Runtime: python3.8 Handler: verify_remediation Script: |- import json diff --git a/source/playbooks/PCI321/.DS_Store b/source/playbooks/PCI321/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..747d43d2ee45983a88ea823b64e2966c0e5e5e39 GIT binary patch literal 6148 zcmeH~K~BR!3`M_bBr37#lI2{18;q)Q0xp2sus|wgk+RRO*XG+}P$0^d1^Sjef9kO( z%3H)W0Na1`55NwLs|bjI2z(Rp??a)x*3_wK zd^)(q2te%^4&y#%32N~GwWdx@8KGG#rM6Pb5yM(J<0bQI>eRHA!*ckre6r<)V)1m| zUm_h=n`#vS5%@{KXK&ih`~M?-W&ZaZnFxr$KP6z@;pwpF%Vlp}yr%csLVu=z8FM3@ mOSEEYv||47R(wCpE57G>O`V!XIpa}I)gJ-pA`^kXAn*wWcO6my literal 0 HcmV?d00001 diff --git a/source/playbooks/PCI321/README.md b/source/playbooks/PCI321/README.md index b06a69fd..e50138bd 100644 --- a/source/playbooks/PCI321/README.md +++ b/source/playbooks/PCI321/README.md @@ -7,17 +7,22 @@ The Payment Card Industry Data Security Standard (PCI-DSS) playbook is part of t * CloudTrail.2 * CloudTrail.3 * CloudTrail.4 +* CodeBuild.2 * Config.1 * CW.1 * EC2.1 * EC2.2 +* EC2.5 * EC2.6 * IAM.7 * IAM.8 +* KMS.1 * Lambda.1 * RDS.1 * S3.1 * S3.2 +* S3.4 +* S3.5 * S3.6 See the [AWS Security Hub Automated Response and Remediation Implementation Guide](https://docs.aws.amazon.com/solutions/latest/aws-security-hub-automated-response-and-remediation/welcome.html) for more information on this Playbook. diff --git a/source/playbooks/PCI321/bin/pci321.ts b/source/playbooks/PCI321/bin/pci321.ts index 6c4e6e55..7b8b7344 100644 --- a/source/playbooks/PCI321/bin/pci321.ts +++ b/source/playbooks/PCI321/bin/pci321.ts @@ -1,22 +1,14 @@ #!/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 'source-map-support/register'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + PlaybookPrimaryStack, + PlaybookMemberStack, + IControl +} from '../../../lib/sharrplaybook-construct'; +import * as cdk_nag from 'cdk-nag'; import * as cdk from '@aws-cdk/core'; -import { StringParameter } from '@aws-cdk/aws-ssm'; -import { PlaybookPrimaryStack, PlaybookMemberStack, IControl } from '../../../lib/sharrplaybook-construct'; +import 'source-map-support/register'; // SOLUTION_* - set by solution_env.sh const SOLUTION_ID = process.env['SOLUTION_ID'] || 'undefined'; @@ -26,62 +18,67 @@ const DIST_VERSION = process.env['DIST_VERSION'] || '%%VERSION%%'; const DIST_OUTPUT_BUCKET = process.env['DIST_OUTPUT_BUCKET'] || '%%BUCKET%%'; const DIST_SOLUTION_NAME = process.env['DIST_SOLUTION_NAME'] || '%%SOLUTION%%'; -const standardShortName = 'PCI' -const standardLongName = 'pci-dss' -const standardVersion = '3.2.1' // DO NOT INCLUDE 'V' -const RESOURCE_PREFIX = SOLUTION_ID.replace(/^DEV-/,''); // prefix on every resource name +const standardShortName = 'PCI'; +const standardLongName = 'pci-dss'; +const standardVersion = '3.2.1'; // DO NOT INCLUDE 'V' const app = new cdk.App(); +cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks()); // Creates one rule per control Id. The Step Function determines what document to run based on // Security Standard and Control Id. See cis-member-stack const remediations: IControl[] = [ - { "control": "PCI.AutoScaling.1" }, - { "control": "PCI.IAM.7" }, - { "control": "PCI.CloudTrail.2" }, - { "control": "PCI.CW.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" }, - { "control": "PCI.EC2.6" }, - { "control": "PCI.CloudTrail.3" }, - { "control": "PCI.CloudTrail.4" }, - { "control": "PCI.Config.1" }, - { "control": "PCI.S3.1" }, - { - "control": "PCI.S3.2", - "executes": "PCI.S3.1" - }, - { "control": "PCI.S3.5" }, - { "control": "PCI.S3.6" } -] + { "control": "PCI.AutoScaling.1" }, + { "control": "PCI.IAM.7" }, + { "control": "PCI.CloudTrail.2" }, + { "control": "PCI.CodeBuild.2" }, + { "control": "PCI.CW.1" }, + { "control": "PCI.EC2.1" }, + { "control": "PCI.EC2.2" }, + { "control": "PCI.EC2.5" }, + { "control": "PCI.IAM.8" }, + { "control": "PCI.KMS.1" }, + { "control": "PCI.Lambda.1" }, + { "control": "PCI.RDS.1" }, + { "control": "PCI.RDS.2" }, + { "control": "PCI.Redshift.1" }, + { "control": "PCI.CloudTrail.1" }, + { "control": "PCI.EC2.6" }, + { "control": "PCI.CloudTrail.3" }, + { "control": "PCI.CloudTrail.4" }, + { "control": "PCI.Config.1" }, + { "control": "PCI.S3.1" }, + { + "control": "PCI.S3.2", + "executes": "PCI.S3.1" + }, + { "control": "PCI.S3.4" }, + { "control": "PCI.S3.5" }, + { "control": "PCI.S3.6" } +]; const adminStack = new PlaybookPrimaryStack(app, 'PCI321Stack', { - description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - solutionDistName: DIST_SOLUTION_NAME, - remediations: remediations, - securityStandardLongName: standardLongName, - securityStandard: standardShortName, - securityStandardVersion: standardVersion + description: `(${SOLUTION_ID}P) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Admin Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + solutionDistName: DIST_SOLUTION_NAME, + remediations: remediations, + securityStandardLongName: standardLongName, + securityStandard: standardShortName, + securityStandardVersion: standardVersion }); const memberStack = new PlaybookMemberStack(app, 'PCI321MemberStack', { - description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, - solutionId: SOLUTION_ID, - solutionVersion: DIST_VERSION, - solutionDistBucket: DIST_OUTPUT_BUCKET, - securityStandard: standardShortName, - securityStandardVersion: standardVersion, - securityStandardLongName: standardLongName, - remediations: remediations + description: `(${SOLUTION_ID}C) ${SOLUTION_NAME} ${standardShortName} ${standardVersion} Compliance Pack - Member Account, ${DIST_VERSION}`, + solutionId: SOLUTION_ID, + solutionVersion: DIST_VERSION, + solutionDistBucket: DIST_OUTPUT_BUCKET, + securityStandard: standardShortName, + securityStandardVersion: standardVersion, + securityStandardLongName: standardLongName, + remediations: remediations }); -adminStack.templateOptions.templateFormatVersion = "2010-09-09" -memberStack.templateOptions.templateFormatVersion = "2010-09-09" +adminStack.templateOptions.templateFormatVersion = "2010-09-09"; +memberStack.templateOptions.templateFormatVersion = "2010-09-09"; diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml index 7c657015..bfabfa43 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml @@ -15,8 +15,8 @@ description: | ## Documentation Links * [PCI AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-autoscaling-1) - - + + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: @@ -25,16 +25,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for ASG1 finding + description: The input from the Orchestrator Step function for the PCI.AutoScaling.1 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -52,26 +51,36 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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: 'PCI.AutoScaling.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.AutoScaling.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - + - name: Remediation action: 'aws:executeAutomation' isEnd: false inputs: DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: 'SO0111-SHARR-Orchestrator-Member' RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck' AutoScalingGroupName: '{{ParseInput.AutoScalingGroupName}}' - - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml index 075d8a02..d0d89a05 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml @@ -6,36 +6,37 @@ description: | ## 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 - Output of remediation runbook. ## Documentation Links [PCI v3.2.1 PCI.CW.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-cw-1-remediation) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.CW.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+=,.@-]+' + 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+=,.@-]+$' LogGroupName: type: String default: >- {{ssm:/Solutions/SO0111/Metrics_LogGroupName}} description: The name of the Log group to be used to create filters and metric alarms + allowedPattern: '.*' MetricNamespace: type: String default: 'LogMetrics' description: The name of the metric namespace where the metrics will be logged + allowedPattern: '.*' KMSKeyArn: type: String default: >- @@ -67,10 +68,10 @@ mainSteps: parse_id_pattern: '' Finding: '{{Finding}}' expected_control_id: [ 'PCI.CW.1' ] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: GetMetricFilterAndAlarmInputValue @@ -100,7 +101,7 @@ mainSteps: inputs: InputPayload: ControlId: '{{ParseInput.ControlId}}' - Runtime: python3.7 + Runtime: python3.8 Handler: verify Script: |- %%SCRIPT=pci_get_input_values.py%% diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml index ba759ca8..905097de 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml @@ -20,17 +20,18 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the PCI.CloudTrail.1 finding KMSKeyArn: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} + 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' outputs: @@ -47,20 +48,21 @@ mainSteps: Selector: $.Payload.resource_id Type: String - Name: TrailRegion - Selector: $.Payload.resource.Region + Selector: $.Payload.resource_region Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'PCI.CloudTrail.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.CloudTrail.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml index fa57435b..18e48602 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml @@ -3,14 +3,14 @@ description: | ## 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 * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - + ## Documentation Links * [PCI CloudTrail.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-cloudtrail-2) - + schemaVersion: "0.3" assumeRole: "{{ AutomationAssumeRole }}" @@ -18,10 +18,10 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the PCI.CloudTrail.2 finding KMSKeyArn: type: String default: >- @@ -54,11 +54,12 @@ mainSteps: Finding: '{{Finding}}' region: '{{global:REGION}}' parse_id_pattern: '' - expected_control_id: 'PCI.CloudTrail.2' - Runtime: python3.7 + expected_control_id: + - 'PCI.CloudTrail.2' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -70,7 +71,7 @@ mainSteps: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml index c0627f66..6c2e06b4 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -18,21 +18,24 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.CloudTrail.3 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+=,.@-]+' - + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailLogFileValidation" + allowedPattern: '^[\w+=,.@-]+' + mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: TrailName Selector: $.Payload.resource_id @@ -46,25 +49,36 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' - expected_control_id: 'PCI.CloudTrail.3' - Runtime: python3.7 + expected_control_id: + - 'PCI.CloudTrail.3' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false inputs: DocumentName: SHARR-EnableCloudTrailLogFileValidation + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: TrailName: '{{ParseInput.TrailName}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableCloudTrailLogFileValidation' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - 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 f0f91366..aabe1e8a 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -18,16 +18,19 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.CloudTrail.4 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableCloudTrailToCloudWatchLogging" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,15 +48,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String inputs: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:trail/([A-Za-z0-9._-]{3,128})$' - expected_control_id: 'PCI.CloudTrail.4' - Runtime: python3.7 + expected_control_id: + - 'PCI.CloudTrail.4' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -61,11 +71,15 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: TrailName: '{{ ParseInput.TrailName }}' 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' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml new file mode 100644 index 00000000..b3367a3e --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml @@ -0,0 +1,75 @@ +description: | + ### Document Name - SHARR-PCI_3.2.1_CodeBuild.2 + + ## What does this document do? + This document removes CodeBuild project environment variables containing clear text credentials and replaces them with Amazon EC2 Systems Manager Parameters. + + ## 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 CodeBuild.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-codebuild-2) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the PCI.CodeBuild.2 finding + 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+=,.@-]+$' +mainSteps: + - name: ParseInput + action: 'aws:executeScript' + outputs: + - Name: ProjectName + 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):codebuild:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:project/([A-Za-z0-9][A-Za-z0-9\-_]{1,254})$' + expected_control_id: [ 'PCI.CodeBuild.2' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + - name: Remediation + action: 'aws:executeAutomation' + inputs: + DocumentName: SHARR-ReplaceCodeBuildClearTextCredentials + RuntimeParameters: + ProjectName: '{{ ParseInput.ProjectName }}' + AutomationAssumeRole: 'arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-ReplaceCodeBuildClearTextCredentials' + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ ParseInput.FindingId }}' + ProductArn: '{{ ParseInput.ProductArn }}' + Note: + Text: 'Replaced clear text credentials with SSM parameters.' + UpdatedBy: 'SHARR-PCI_3.2.1_CodeBuild.2' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml index 5fec0a1c..cf3a33d0 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml @@ -21,10 +21,10 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the PCI.Config.1 finding KMSKeyArn: type: String default: >- @@ -56,14 +56,15 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'PCI.Config.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.Config.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - - + + - name: Remediation action: 'aws:executeAutomation' isEnd: false @@ -73,8 +74,8 @@ mainSteps: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAWSConfig' - - - + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml index a2c4e9a4..46a555c5 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml @@ -15,19 +15,18 @@ assumeRole: '{{ AutomationAssumeRole }}' parameters: Finding: type: StringMap - description: The input from Step function for EC2.1 finding + description: The input from the Orchestrator Step function for the PCI.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+=,.@-]+' + 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+=,.@-]+$' outputs: - Remediation.Output - ParseInput.AffectedObject mainSteps: - - + - name: ParseInput action: 'aws:executeScript' outputs: @@ -51,15 +50,16 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' resource_index: 2 - expected_control_id: 'PCI.EC2.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.EC2.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' inputs: @@ -70,7 +70,7 @@ mainSteps: TestMode: '{{ParseInput.TestMode}}' isEnd: false - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml index 466fb102..836f526e 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml @@ -2,7 +2,7 @@ description: | ### Document Name - SHARR-PCI_3.2.1_EC2.2 ## What does this document do? - This document deletes ingress and egress rules from default security + This document deletes ingress and egress rules from default security group using the AWS SSM Runbook AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules ## Input Parameters @@ -24,10 +24,14 @@ 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+=,.@-]+' + 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 + description: The input from the Orchestrator Step function for the PCI.EC2.2 finding + RemediationRoleName: + type: String + default: "SO0111-RemoveVPCDefaultSecurityGroupRules" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,15 +49,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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: 'PCI.EC2.2' - Runtime: python3.7 + expected_control_id: + - 'PCI.EC2.2' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -61,9 +72,14 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: GroupId: '{{ParseInput.GroupId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveVPCDefaultSecurityGroupRules' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml new file mode 100644 index 00000000..50d51806 --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml @@ -0,0 +1,80 @@ +description: | + ### Document Name - SHARR-PCI_3.2.1_EC2.5 + + ## What does this document do? + Removes public access to remove server administrative ports from an EC2 Security Group + + ## 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 - Output of AWS-DisablePublicAccessForSecurityGroup runbook. + + ## Documentation Links + * [PCI v3.2.1 EC2.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-ec2-5) + +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the PCI.EC2.5 finding + 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+=,.@-]+$' + +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]+-[0-9]):[0-9]{12}:security-group/(sg-[a-f0-9]{8,17})$' + expected_control_id: [ 'PCI.EC2.5' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + isEnd: false + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: AWS-DisablePublicAccessForSecurityGroup + RuntimeParameters: + GroupId: '{{ ParseInput.GroupId }}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-DisablePublicAccessForSecurityGroup' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Disabled public access to administrative ports in the security group {{ ParseInput.GroupId }}.' + UpdatedBy: 'SHARR-PCI_3.2.1_EC2.5' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml index a347751d..accd3fc5 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml @@ -7,27 +7,30 @@ description: | ## 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 EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-ec2-6) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.EC2.6 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableVPCFlowLogs" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -45,27 +48,36 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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: 'PCI.EC2.6' - Runtime: python3.7 + expected_control_id: + - 'PCI.EC2.6' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% - isEnd: false + %%SCRIPT=common/parse_input.py%% - name: Remediation action: 'aws:executeAutomation' isEnd: false inputs: DocumentName: SHARR-EnableVPCFlowLogs + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' 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' - + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml index 6c5bdaa6..96c271e7 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml @@ -12,28 +12,27 @@ description: | * Remediation.Output - Output of remediation runbook SEE AWSConfigRemediation-RevokeUnusedIAMUserCredentials - + ## Documentation Links * [PCI IAM.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-iam-7) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.IAM.7 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput @@ -55,11 +54,12 @@ mainSteps: InputPayload: Finding: '{{Finding}}' parse_id_pattern: '' - expected_control_id: 'PCI.IAM.7' - Runtime: python3.7 + expected_control_id: + - 'PCI.IAM.7' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml index 05bc8b5c..30a2b2a5 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml @@ -17,21 +17,20 @@ description: | ## Documentation Links * [PCI IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-iam-8) - + schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.IAM.8 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - name: ParseInput action: 'aws:executeScript' @@ -50,10 +49,10 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' expected_control_id: [ 'PCI.IAM.8' ] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation action: 'aws:executeAutomation' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml index b67e2f01..99374356 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml @@ -11,26 +11,29 @@ description: | ## 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 + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.KMS.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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-EnableKeyRotation" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -48,15 +51,22 @@ mainSteps: - Name: AffectedObject Selector: $.Payload.object Type: StringMap + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String 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 + expected_control_id: + - 'PCI.KMS.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - name: Remediation @@ -64,9 +74,13 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-EnableKeyRotation + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: KeyId: '{{ParseInput.KMSKeyId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableKeyRotation' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' - name: UpdateFinding action: 'aws:executeAwsApi' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml index b5f44db1..4c3cb489 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml @@ -1,9 +1,9 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_Lambda.1 + ### Document Name - SHARR-PCI_3.2.1_Lambda.1 ## What does this document do? This document removes the public resource policy. A public resource policy - contains a principal "*" or AWS: "*", which allows public access to the + contains a principal "*" or AWS: "*", which allows public access to the function. The remediation is to remove the SID of the public policy. ## Input Parameters @@ -21,12 +21,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for the finding + description: The input from the Orchestrator Step function for the PCI.Lambda.1 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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-RemoveLambdaPublicAccess" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -44,16 +47,23 @@ mainSteps: - Name: FunctionName Selector: $.Payload.resource_id Type: String + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_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: 'PCI.Lambda.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.Lambda.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% - isEnd: false + %%SCRIPT=common/parse_input.py%% + isEnd: false - name: Remediation @@ -61,11 +71,15 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-RemoveLambdaPublicAccess + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: FunctionName: '{{ ParseInput.FunctionName }}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RemoveLambdaPublicAccess' - - - + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml index db8e3a8f..979b7e21 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml @@ -6,6 +6,7 @@ description: | ## 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. + ## Documentation Links * [PCI RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-rds-1) @@ -17,12 +18,15 @@ outputs: parameters: Finding: type: StringMap - description: The input from Step function for RDS.1 finding + description: The input from the Orchestrator Step function for the PCI.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+=,.@-]+' + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: "SO0111-MakeRDSSnapshotPrivate" + allowedPattern: '^[\w+=,.@-]+' mainSteps: - name: ParseInput @@ -46,16 +50,23 @@ mainSteps: - Name: Type Selector: $.Payload.type Type: String + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + 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: 'PCI.RDS.1' - Runtime: python3.7 + expected_control_id: + - 'PCI.RDS.1' + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% nextStep: Remediation - name: Remediation @@ -63,12 +74,16 @@ mainSteps: isEnd: false inputs: DocumentName: SHARR-MakeRDSSnapshotPrivate + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' RuntimeParameters: DBSnapshotId: '{{ParseInput.DBSnapshotId}}' DBSnapshotType: '{{ParseInput.DBSnapshotType}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeRDSSnapshotPrivate' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' nextStep: UpdateFinding - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml new file mode 100644 index 00000000..2dfbde36 --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml @@ -0,0 +1,91 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +description: | + ### Document Name - SHARR-PCI_3.2.1_RDS.2 + ## What does this document do? + This document disables public access to RDS instances by calling another SSM document + + ## 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. + + ## Documentation Links + * [PCI RDS.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-rds-2) +schemaVersion: '0.3' +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the PCI.RDS.2 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-DisablePublicAccessToRDSInstance' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:db:((?!.*--.*)(?!.*-$)[a-z][a-z0-9-]{0,62})$' + expected_control_id: + - 'PCI.RDS.2' + Runtime: 'python3.8' + Handler: 'parse_event' + Script: |- + %%SCRIPT=common/parse_input.py%% + outputs: + - Name: 'DbiResourceId' + Selector: '$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId' + Type: 'String' + - Name: 'AffectedObject' + Selector: '$.Payload.object' + Type: 'StringMap' + - Name: 'FindingId' + Selector: '$.Payload.finding.Id' + Type: 'String' + - Name: 'ProductArn' + Selector: '$.Payload.finding.ProductArn' + Type: 'String' + - Name: 'RemediationRegion' + Selector: '$.Payload.resource_region' + Type: 'String' + - Name: 'RemediationAccount' + Selector: '$.Payload.account_id' + Type: 'String' +- name: 'Remediation' + action: 'aws:executeAutomation' + inputs: + DocumentName: 'SHARR-DisablePublicAccessToRDSInstance' + TargetLocations: + - Accounts: + - '{{ParseInput.RemediationAccount}}' + Regions: + - '{{ParseInput.RemediationRegion}}' + ExecutionRoleName: '{{RemediationRoleName}}' + RuntimeParameters: + DbiResourceId: '{{ParseInput.DbiResourceId}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' +- name: 'UpdateFinding' + action: 'aws:executeAwsApi' + inputs: + Service: 'securityhub' + Api: 'BatchUpdateFindings' + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Disabled public access to RDS instance' + UpdatedBy: 'SHARR-PCI_3.2.1_RDS.2' + Workflow: + Status: 'RESOLVED' + description: 'Update finding' + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml new file mode 100644 index 00000000..fd3579f6 --- /dev/null +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml @@ -0,0 +1,92 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +description: | + ### Document Name - SHARR-PCI_3.2.1_Redshift.1 + ## What does this document do? + This document disables public access to a Redshift cluster by calling another SSM document + + ## 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. + * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. + + ## Documentation Links + * [PCI Redshift.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-redshift-1) +schemaVersion: '0.3' +assumeRole: '{{AutomationAssumeRole}}' +outputs: +- 'Remediation.Output' +- 'ParseInput.AffectedObject' +parameters: + Finding: + type: 'StringMap' + description: 'The input from the Orchestrator Step function for the PCI.Redshift.1 finding' + 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+=,.@-]+$' + RemediationRoleName: + type: 'String' + default: 'SO0111-DisablePublicAccessToRedshiftCluster' + allowedPattern: '^[\w+=,.@/-]+' +mainSteps: +- name: 'ParseInput' + action: 'aws:executeScript' + inputs: + InputPayload: + Finding: '{{Finding}}' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):redshift:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:cluster:(?!.*--)([a-z][a-z0-9-]{0,62})(?- + {{ssm:/Solutions/SO0111/afsbp/1.0.0/S3.4/KmsKeyAlias}} + allowedPattern: '^$|^[a-zA-Z0-9/_-]{1,256}$' + +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 + - Name: BucketName + Selector: $.Payload.resource_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.4' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + %%SCRIPT=common/parse_input.py%% + + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableDefaultEncryptionS3 + RuntimeParameters: + AccountId: '{{ParseInput.AccountId}}' + BucketName: '{{ParseInput.BucketName}}' + AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' + KmsKeyAlias: '{{KmsKeyAlias}}' + + - name: UpdateFinding + action: 'aws:executeAwsApi' + inputs: + Service: securityhub + Api: BatchUpdateFindings + FindingIdentifiers: + - Id: '{{ParseInput.FindingId}}' + ProductArn: '{{ParseInput.ProductArn}}' + Note: + Text: 'Enabled default encryption for {{ParseInput.BucketName}}' + UpdatedBy: 'SHARR-PCI_3.2.1_PCI.S3.4' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml index ee8e5558..950f518e 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -18,21 +18,20 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.S3.5 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: BucketName Selector: $.Payload.resource_id @@ -54,12 +53,12 @@ mainSteps: 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 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml index 6ee186f0..2d8080ad 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -18,21 +18,20 @@ schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: - ParseInput.AffectedObject - - Remediation.Output + - Remediation.Output parameters: Finding: type: StringMap - description: The input from Step function for finding + description: The input from the Orchestrator Step function for the PCI.S3.6 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+=,.@-]+' + 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+=,.@-]+$' mainSteps: - - + - name: ParseInput - action: 'aws:executeScript' + action: 'aws:executeScript' outputs: - Name: AccountId Selector: $.Payload.account_id @@ -51,12 +50,12 @@ mainSteps: Finding: '{{Finding}}' parse_id_pattern: '' expected_control_id: [ 'PCI.S3.6' ] - Runtime: python3.7 + Runtime: python3.8 Handler: parse_event Script: |- - %%SCRIPT=pci_parse_input.py%% + %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: Remediation action: 'aws:executeAutomation' isEnd: false 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 d61b25d8..b555c447 100644 --- a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap +++ b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap @@ -60,6 +60,9 @@ Object { "GeneratorId": Array [ "pci-dss/v/3.2.1/PCI.AutoScaling.1", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -150,6 +153,9 @@ Object { "GeneratorId": Array [ "pci-dss/v/3.2.1/PCI.EC2.6", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -240,6 +246,9 @@ Object { "GeneratorId": Array [ "pci-dss/v/3.2.1/PCI.IAM.8", ], + "RecordState": Array [ + "ACTIVE", + ], "Workflow": Object { "Status": Array [ "NEW", @@ -340,26 +349,26 @@ Object { exports[`default stack 2`] = ` Object { "Conditions": Object { - "PCIPCIAutoScaling1EnablePCIAutoScaling1Condition12A40ED8": Object { + "EnablePCIAutoScaling1Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "PCIPCIAutoScaling1Active", + "Ref": "EnablePCIAutoScaling1", }, "Available", ], }, - "PCIPCIEC26EnablePCIEC26Condition32AA9B01": Object { + "EnablePCIEC26Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "PCIPCIEC26Active", + "Ref": "EnablePCIEC26", }, "Available", ], }, - "PCIPCIIAM8EnablePCIIAM8Condition9362A373": Object { + "EnablePCIIAM8Condition": Object { "Fn::Equals": Array [ Object { - "Ref": "PCIPCIIAM8Active", + "Ref": "EnablePCIIAM8", }, "Available", ], @@ -367,7 +376,7 @@ Object { }, "Description": "test;", "Parameters": Object { - "PCIPCIAutoScaling1Active": Object { + "EnablePCIAutoScaling1": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -376,7 +385,7 @@ Object { "Description": "Enable/disable availability of remediation for PCI version 3.2.1 Control PCI.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.", "Type": "String", }, - "PCIPCIEC26Active": Object { + "EnablePCIEC26": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -385,7 +394,7 @@ Object { "Description": "Enable/disable availability of remediation for PCI version 3.2.1 Control PCI.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.", "Type": "String", }, - "PCIPCIIAM8Active": Object { + "EnablePCIIAM8": Object { "AllowedValues": Array [ "Available", "NOT Available", @@ -401,652 +410,976 @@ Object { }, }, "Resources": Object { - "PCIPCIAutoScaling1AutomationDocument6FD68329": Object { - "Condition": "PCIPCIAutoScaling1EnablePCIAutoScaling1Condition12A40ED8", + "PCIPCIAutoScaling1": Object { + "Condition": "EnablePCIAutoScaling1Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-PCI_3.2.1_PCI.AutoScaling.1 + "Content": "description: | + ### Document Name - SHARR-PCI_3.2.1_PCI.AutoScaling.1 -## What does this document do? -This document enables ELB healthcheck on a given AutoScaling Group using the [UpdateAutoScalingGroup] API. + ## What does this document do? + This document enables ELB healthcheck on a given AutoScaling Group using the [UpdateAutoScalingGroup] API. -## Input Parameters -* Finding: (Required) Security Hub finding details JSON -* HealthCheckGracePeriod: (Optional) Health check grace period when ELB health check is Enabled -Default: 30 seconds -* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + ## Input Parameters + * Finding: (Required) Security Hub finding details JSON + * HealthCheckGracePeriod: (Optional) Health check grace period when ELB health check is Enabled + Default: 30 seconds + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. -## Output Parameters -* Remediation.Output + ## Output Parameters + * Remediation.Output -## Documentation Links -* [PCI AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-autoscaling-1) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "PCI.AutoScaling.1", - "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": "#!/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 + ## Documentation Links + * [PCI AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-autoscaling-1) -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): - expected_control_id = event['expected_control_id'] - parse_id_pattern = event['parse_id_pattern'] - resource_id_matches = [] - finding = event['Finding'] - testmode = bool('testmode' in finding) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - Remediation.Output + - ParseInput.AffectedObject +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the PCI.AutoScaling.1 finding + HealthCheckGracePeriod: + type: Integer + default: 30 + description: ELB Health Check Grace Period + 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_id = finding['Id'] +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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + 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: + - 'PCI.AutoScaling.1' + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } + isEnd: false - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: 'SO0111-SHARR-Orchestrator-Member' + 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-PCI_3.2.1_AutoScaling.1' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-PCI_3.2.1_PCI.AutoScaling.1", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "AutoScalingGroupName", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-EnableAutoScalingGroupELBHealthCheck", - "RuntimeParameters": Object { - "AutoScalingGroupName": "{{ParseInput.AutoScalingGroupName}}", - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "ASG health check type updated to ELB", - "UpdatedBy": "SHARR-PCI_3.2.1_AutoScaling.1", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "outputs": Array [ - "Remediation.Output", - "ParseInput.AffectedObject", - ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for ASG1 finding", - "type": "StringMap", - }, - "HealthCheckGracePeriod": Object { - "default": 30, - "description": "ELB Health Check Grace Period", - "type": "Integer", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.AutoScaling.1", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "PCIPCIEC26AutomationDocumentF88E2FFC": Object { - "Condition": "PCIPCIEC26EnablePCIEC26Condition32AA9B01", + "PCIPCIEC26": Object { + "Condition": "EnablePCIEC26Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-PCI_3.2.1_PCI.EC2.6 + "Content": "description: | + ### Document Name - SHARR-PCI_3.2.1_PCI.EC2.6 -## What does this document do? -Enables VPC Flow Logs for a VPC + ## What does this document do? + Enables VPC Flow Logs for a VPC -## 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. + ## 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 + ## Output Parameters + * Remediation.Output - Remediation results -## Documentation Links -* [PCI EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-ec2-6) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": "PCI.EC2.6", - "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:.*:\\\\d{12}:vpc/(vpc-[0-9a-f]{8,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 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}') + ## Documentation Links + * [PCI EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-ec2-6) -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 = bool('testmode' in finding) +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the PCI.EC2.6 finding + 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+=,.@-]+$' + RemediationRoleName: + type: String + default: \\"SO0111-EnableVPCFlowLogs\\" + allowedPattern: '^[\\\\w+=,.@-]+' - finding_id = finding['Id'] +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 + - Name: RemediationRegion + Selector: $.Payload.resource_region + Type: String + - Name: RemediationAccount + Selector: $.Payload.account_id + Type: String + 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: + - 'PCI.EC2.6' + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - name: Remediation + action: 'aws:executeAutomation' + isEnd: false + inputs: + DocumentName: SHARR-EnableVPCFlowLogs + TargetLocations: + - Accounts: [ '{{ParseInput.RemediationAccount}}' ] + Regions: [ '{{ParseInput.RemediationRegion}}' ] + ExecutionRoleName: '{{RemediationRoleName}}' + 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/{{RemediationRoleName}}' + - 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-PCI_3.2.1_PCI.EC2.6' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-PCI_3.2.1_PCI.EC2.6", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "VPC", - "Selector": "$.Payload.resource_id", - "Type": "String", - }, - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-EnableVPCFlowLogs", - "RuntimeParameters": Object { - "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}}", - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Enabled VPC Flow Logs for {{ParseInput.VPC}}", - "UpdatedBy": "SHARR-PCI_3.2.1_PCI.EC2.6", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "ParseInput.AffectedObject", - "Remediation.Output", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.EC2.6", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, - "PCIPCIIAM8AutomationDocument45FA0101": Object { - "Condition": "PCIPCIIAM8EnablePCIIAM8Condition9362A373", + "PCIPCIIAM8": Object { + "Condition": "EnablePCIIAM8Condition", + "DeletionPolicy": "Delete", "Properties": Object { - "Content": Object { - "assumeRole": "{{ AutomationAssumeRole }}", - "description": "### Document Name - SHARR-PCI_3.2.1_PCI.IAM.8 - -## What does this document do? -This document establishes a default password policy. + "Content": "description: | + ### Document Name - SHARR-PCI_3.2.1_PCI.IAM.8 -## Security Standards and Controls -* CIS 1.5 - 1.11 -* AFSBP IAM.7 -* PCI IAM.8 + ## What does this document do? + This document establishes a default password policy. -## 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 IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-iam-8) -", - "mainSteps": Array [ - Object { - "action": "aws:executeScript", - "inputs": Object { - "Handler": "parse_event", - "InputPayload": Object { - "Finding": "{{Finding}}", - "expected_control_id": Array [ - "PCI.IAM.8", - ], - "parse_id_pattern": "", - }, - "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 + ## Security Standards and Controls + * CIS 1.5 - 1.11 + * AFSBP IAM.7 + * PCI IAM.8 -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}') + ## 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 -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 = bool('testmode' in finding) + ## Documentation Links + * [PCI IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-iam-8) - finding_id = finding['Id'] +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +outputs: + - ParseInput.AffectedObject + - Remediation.Output +parameters: + Finding: + type: StringMap + description: The input from the Orchestrator Step function for the PCI.IAM.8 finding + 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+=,.@-]+$' +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: [ 'PCI.IAM.8' ] + Runtime: python3.8 + Handler: parse_event + Script: |- + #!/usr/bin/python + ## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + ## SPDX-License-Identifier: Apache-2.0 - 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') - - # ControlId is the expected value - 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}') - - resource = 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( - parse_id_pattern, - identifier_raw - ) - - if identifier_match: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - resource_id = identifier_match.group(event.get('resource_index', 1)) - else: - exit(f'ERROR: Invalid resource Id {identifier_raw}') + import re + import json + import boto3 + from botocore.config import Config + + def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + + def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + + def get_solution_id(): + return 'SO0111' + + def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + + def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + + def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + + class FindingEvent: + \\"\\"\\" + Finding object returns the parse fields from an input finding json object + \\"\\"\\" + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:subscription/(.*?)/v/(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if \\"RelatedAWSResources:0/type\\" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + \\"\\"\\"Populate fields\\"\\"\\" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\\\\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json[\\"Id\\"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + + ''' + MAIN + ''' + def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + \\"account_id\\": finding_event.account_id, + \\"resource_id\\": finding_event.resource_id, + \\"finding_id\\": finding_event.finding_id, # Deprecate v1.5.0+ + \\"control_id\\": finding_event.control_id, + \\"product_arn\\": finding_event.product_arn, # Deprecate v1.5.0+ + \\"object\\": finding_event.affected_object, + \\"matches\\": finding_event.resource_id_matches, + \\"details\\": finding_event.details, # Deprecate v1.5.0+ + \\"testmode\\": finding_event.testmode, # Deprecate v1.5.0+ + \\"resource\\": finding_event.resource, + \\"resource_region\\": finding_event.resource_region, + \\"finding\\": finding_event.finding_json, + \\"aws_config_rule\\": finding_event.aws_config_rule + } + 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' - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') + - 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-PCI_3.2.1_IAM.8' + Workflow: + Status: RESOLVED + description: Update finding + isEnd: true - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - return { - \\"account_id\\": account_id, - \\"resource_id\\": resource_id, - \\"finding_id\\": finding_id, - \\"control_id\\": control_id, - \\"product_arn\\": product_arn, - \\"object\\": affected_object, - \\"matches\\": resource_id_matches, - \\"details\\": details, - \\"testmode\\": testmode, - \\"resource\\": resource - }", +", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "SHARR-PCI_3.2.1_PCI.IAM.8", + "ServiceToken": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", }, - "isEnd": false, - "name": "ParseInput", - "outputs": Array [ - Object { - "Name": "FindingId", - "Selector": "$.Payload.finding_id", - "Type": "String", - }, - Object { - "Name": "ProductArn", - "Selector": "$.Payload.product_arn", - "Type": "String", - }, - Object { - "Name": "AffectedObject", - "Selector": "$.Payload.object", - "Type": "StringMap", - }, - ], - }, - Object { - "action": "aws:executeAutomation", - "inputs": Object { - "DocumentName": "SHARR-SetIAMPasswordPolicy", - "RuntimeParameters": Object { - "AllowUsersToChangePassword": true, - "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy", - "HardExpiry": true, - "MaxPasswordAge": 90, - "MinimumPasswordLength": 14, - "PasswordReusePrevention": 24, - "RequireLowercaseCharacters": true, - "RequireNumbers": true, - "RequireSymbols": true, - "RequireUppercaseCharacters": true, - }, + ":lambda:", + Object { + "Ref": "AWS::Region", }, - "isEnd": false, - "name": "Remediation", - }, - Object { - "action": "aws:executeAwsApi", - "description": "Update finding", - "inputs": Object { - "Api": "BatchUpdateFindings", - "FindingIdentifiers": Array [ - Object { - "Id": "{{ParseInput.FindingId}}", - "ProductArn": "{{ParseInput.ProductArn}}", - }, - ], - "Note": Object { - "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", - "UpdatedBy": "SHARR-PCI_3.2.1_IAM.8", - }, - "Service": "securityhub", - "Workflow": Object { - "Status": "RESOLVED", - }, + ":", + Object { + "Ref": "AWS::AccountId", }, - "isEnd": true, - "name": "UpdateFinding", - }, - ], - "outputs": Array [ - "ParseInput.AffectedObject", - "Remediation.Output", + ":function:SO0111-SHARR-updatableRunbookProvider", + ], ], - "parameters": Object { - "AutomationAssumeRole": Object { - "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+", - "default": "", - "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.", - "type": "String", - }, - "Finding": Object { - "description": "The input from Step function for finding", - "type": "StringMap", - }, - }, - "schemaVersion": "0.3", }, - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.IAM.8", + "VersionName": "v1.1.1", }, - "Type": "AWS::SSM::Document", + "Type": "Custom::UpdatableRunbook", + "UpdateReplacePolicy": "Delete", }, }, } diff --git a/source/playbooks/PCI321/test/pci321_stack.test.ts b/source/playbooks/PCI321/test/pci321_stack.test.ts index a687cb8a..b8cb68f4 100644 --- a/source/playbooks/PCI321/test/pci321_stack.test.ts +++ b/source/playbooks/PCI321/test/pci321_stack.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, matchTemplate, SynthUtils } from '@aws-cdk/assert'; +import { SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { PlaybookPrimaryStack, PlaybookMemberStack } from '../../../lib/sharrplaybook-construct'; @@ -33,6 +33,7 @@ function getMemberStack(): cdk.Stack { securityStandardVersion: '3.2.1', securityStandardLongName: 'pci-dss', ssmdocs: 'playbooks/PCI321/ssmdocs', + commonScripts: 'playbooks/common', remediations: [ {"control":'PCI.AutoScaling.1'}, {"control":'PCI.EC2.6'}, {"control":'PCI.IAM.8'} ] }) return stack; diff --git a/source/playbooks/common/deserialize_json.py b/source/playbooks/common/deserialize_json.py new file mode 100644 index 00000000..6aa77773 --- /dev/null +++ b/source/playbooks/common/deserialize_json.py @@ -0,0 +1,11 @@ +#!/usr/bin/python +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json + +def event_handler(event, context): + try: + return json.loads(event['SerializedJson']) + except Exception as e: + print(e) + exit('Failed to deserialize data') diff --git a/source/playbooks/common/parse_input.py b/source/playbooks/common/parse_input.py new file mode 100644 index 00000000..5ffbd1ca --- /dev/null +++ b/source/playbooks/common/parse_input.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +## SPDX-License-Identifier: Apache-2.0 + +import re +import json +import boto3 +from botocore.config import Config + +def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + +def connect_to_ssm(boto_config): + return boto3.client('ssm', config=boto_config) + +def get_solution_id(): + return 'SO0111' + +def get_solution_version(): + ssm = connect_to_ssm( + Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' + ) + ) + solution_version = 'unknown' + try: + ssm_parm_value = ssm.get_parameter( + Name=f'/Solutions/{get_solution_id()}/member-version' + )['Parameter'].get('Value', 'unknown') + solution_version = ssm_parm_value + except Exception as e: + print(e) + print(f'ERROR getting solution version') + return solution_version + +def get_shortname(long_name): + short_name = { + 'aws-foundational-security-best-practices': 'AFSBP', + 'cis-aws-foundations-benchmark': 'CIS', + 'pci-dss': 'PCI' + } + return short_name.get(long_name, None) + +def get_config_rule(rule_name): + boto_config = Config( + retries = { + 'mode': 'standard' + }, + user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + ) + config_rule = None + try: + configsvc = connect_to_config(boto_config) + config_rule = configsvc.describe_config_rules( + ConfigRuleNames=[ rule_name ] + ).get('ConfigRules', [])[0] + except Exception as e: + print(e) + exit(f'ERROR getting config rule {rule_name}') + return config_rule + +class FindingEvent: + """ + Finding object returns the parse fields from an input finding json object + """ + def _get_resource_id(self, parse_id_pattern, resource_index): + identifier_raw = self.finding_json['Resources'][0]['Id'] + self.resource_id = identifier_raw + self.resource_id_matches = [] + + if parse_id_pattern: + identifier_match = re.match( + parse_id_pattern, + identifier_raw + ) + + if identifier_match: + for group in range(1, len(identifier_match.groups())+1): + self.resource_id_matches.append(identifier_match.group(group)) + self.resource_id = identifier_match.group(resource_index) + else: + exit(f'ERROR: Invalid resource Id {identifier_raw}') + return + + def _get_standard_info(self): + match_finding_id = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:subscription/(.*?)/v/(\d+\.\d+\.\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', + self.finding_json['Id'] + ) + if match_finding_id: + self.standard_id = get_shortname(match_finding_id.group(1)) + self.standard_version = match_finding_id.group(2) + self.control_id = match_finding_id.group(3) + else: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' + + def _get_aws_config_rule(self): + # config_rule_id refers to the AWS Config Rule that produced the finding + if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': + self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] + self.aws_config_rule = get_config_rule(self.aws_config_rule_id) + return + + def _get_region_from_resource_id(self): + check_for_region = re.match( + r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:((?:[a-z]{2}(?:-gov)?-[a-z]+-\d)):.*:.*$', + self.finding_json['Resources'][0]['Id'] + ) + if check_for_region: + self.resource_region = check_for_region.group(1) + + def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): + self.valid_finding = True + self.resource_region = None + self.control_id = None + self.aws_config_rule_id = None + self.aws_config_rule = {} + + """Populate fields""" + # v1.5 + self.finding_json = finding_json + self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches + self._get_standard_info() # self.standard_id, self.standard_version, self.control_id + + # V1.4 + self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId + if not re.match(r'^\d{12}$', self.account_id): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' + self.finding_id = self.finding_json.get('Id', None) # deprecate + self.product_arn = self.finding_json.get('ProductArn', None) + if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d)::product/aws/securityhub$', self.product_arn): + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' + self.details = self.finding_json['Resources'][0].get('Details', {}) + # Test mode is used with fabricated finding data to tell the + # remediation runbook to run in test more (where supported) + # Currently not widely-used and perhaps should be deprecated. + self.testmode = bool('testmode' in self.finding_json) + self.resource = self.finding_json['Resources'][0] + self._get_region_from_resource_id() + self._get_aws_config_rule() + self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} + + # Validate control_id + if not self.control_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' + elif self.control_id not in expected_control_id: # ControlId is the expected value + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' + + if not self.resource_id: + if self.valid_finding: + self.valid_finding = False + self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' + + if not self.valid_finding: + # Error message and return error data + msg = f'ERROR: {self.invalid_finding_reason}' + exit(msg) + + def __str__(self): + return json.dumps(self.__dict__) + +''' +MAIN +''' +def parse_event(event, context): + finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) + + if not finding_event.valid_finding: + exit('ERROR: Finding is not valid') + + return { + "account_id": finding_event.account_id, + "resource_id": finding_event.resource_id, + "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ + "control_id": finding_event.control_id, + "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ + "object": finding_event.affected_object, + "matches": finding_event.resource_id_matches, + "details": finding_event.details, # Deprecate v1.5.0+ + "testmode": finding_event.testmode, # Deprecate v1.5.0+ + "resource": finding_event.resource, + "resource_region": finding_event.resource_region, + "finding": finding_event.finding_json, + "aws_config_rule": finding_event.aws_config_rule + } \ No newline at end of file diff --git a/source/playbooks/common/test/test_afsbp_parse.py b/source/playbooks/common/test/test_afsbp_parse.py new file mode 100644 index 00000000..9f17bd78 --- /dev/null +++ b/source/playbooks/common/test/test_afsbp_parse.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +## SPDX-License-Identifier: Apache-2.0 + +import boto3 +import json +import botocore.session +from botocore.stub import Stubber +from botocore.config import Config +import pytest +from pytest_mock import mocker + +from parse_input import parse_event + +def event(): + return { + 'expected_control_id': 'AutoScaling.1', + '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/(.*)$', + '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-2: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" + } + } + +def expected(): + return { + "account_id": '111111111111', + "resource_id": 'sharr-test-autoscaling-1', + 'control_id': 'AutoScaling.1', + 'testmode': False, + "finding_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', + "product_arn": 'arn:aws:securityhub:us-east-1::product/aws/securityhub', + "object": { + "Type": 'AwsAccount', + "Id": 'sharr-test-autoscaling-1', + "OutputKey": 'Remediation.Output' + }, + "matches": [ "sharr-test-autoscaling-1" ], + 'details': {}, + 'resource': event().get('Finding').get('Resources')[0], + 'resource_region': 'us-east-2', + 'aws_config_rule': { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + } + +def config_rule(): + return { + "ConfigRules": [ + { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + ] + } + +def ssm_parm(): + return { + 'Parameter': { + 'Name': 'Solutions/SO0111/member_version', + 'Type': 'String', + 'Value': 'v1.5.0' + } + } +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + } +) + +@pytest.fixture(autouse=True) +def run_before_and_after_tests(mocker): + cfg_client = botocore.session.get_session().create_client('config', config=BOTO_CONFIG) + cfg_stubber = Stubber(cfg_client) + cfg_stubber.add_response( + 'describe_config_rules', + config_rule() + ) + cfg_stubber.activate() + mocker.patch('parse_input.connect_to_config', return_value=cfg_client) + + ssm_client = botocore.session.get_session().create_client('ssm', config=BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + ssm_stubber.add_response( + 'get_parameter', + ssm_parm() + ) + ssm_stubber.activate() + mocker.patch('parse_input.connect_to_ssm', return_value=ssm_client) + yield + + cfg_stubber.deactivate() + ssm_stubber.deactivate() + +def test_parse_event(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + parsed_event = parse_event(event(), {}) + assert parsed_event == expected_result + +def test_parse_event_multimatch(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + expected_result['matches'] = [ + "us-east-2", + "sharr-test-autoscaling-1" + ] + test_event = event() + test_event['resource_index'] = 2 + test_event['parse_id_pattern'] = r'^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/(.*)$' + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_bad_finding_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "badvalue" + 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 Id is invalid: badvalue' + +def test_bad_control_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-2:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0//finding/635ceb5d-3dfd-4458-804e-48a42cd723e4" + 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 Id is invalid: arn:aws:securityhub:us-east-2:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0//finding/635ceb5d-3dfd-4458-804e-48a42cd723e4 - missing Control Id' + +def test_control_id_nomatch(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-2:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.1/finding/635ceb5d-3dfd-4458-804e-48a42cd723e4" + 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: Control Id from input (EC2.1) does not match AutoScaling.1' + +def test_bad_account_id(mocker): + test_event = event() + test_event['Finding']['AwsAccountId'] = "1234123412345" + 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: AwsAccountId is invalid: 1234123412345' + +def test_bad_productarn(mocker): + test_event = event() + test_event['Finding']['ProductArn'] = "badvalue" + 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: ProductArn is invalid: badvalue' + +def test_bad_resource_match(mocker): + test_event = event() + test_event['parse_id_pattern'] = '^arn:(?:aws|aws-cn|aws-us-gov):logs:::([A-Za-z0-9.-]{3,63})$' + 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: Invalid resource Id arn:aws:autoscaling:us-east-2:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1' + +def test_no_resource_pattern(mocker): + test_event = event() + expected_result = expected() + expected_result['finding'] = event().get('Finding') + test_event['parse_id_pattern'] = '' + expected_result['resource_id'] = 'arn:aws:autoscaling:us-east-2:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1' + expected_result['matches'] = [] + expected_result['object']['Id'] = expected_result['resource_id'] + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_no_resource_pattern_no_resource_id(mocker): + test_event = event() + test_event['parse_id_pattern'] = '' + test_event['Finding']['Resources'][0]['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: Resource Id is missing from the finding json Resources (Id)' diff --git a/source/playbooks/common/test/test_cis120_parse.py b/source/playbooks/common/test/test_cis120_parse.py new file mode 100644 index 00000000..a25bc77e --- /dev/null +++ b/source/playbooks/common/test/test_cis120_parse.py @@ -0,0 +1,424 @@ +#!/usr/bin/python +## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +## SPDX-License-Identifier: Apache-2.0 + +import boto3 +import json +import botocore.session +from botocore.stub import Stubber +from botocore.config import Config +import pytest +from pytest_mock import mocker + +from parse_input import parse_event + +def event(): + return { + 'expected_control_id': '2.3', + 'parse_id_pattern': '^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$', + 'Finding': { + "ProductArn": "arn:aws:securityhub:us-east-2::product/aws/securityhub", + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" + ], + "Description": "Details: 2.3 Ensure the S3 bucket used to store CloudTrail logs is not publicly accessible", + "SchemaVersion": "2018-10-08", + "Compliance": { + "Status": "WARNING", + "StatusReasons": [ + { + "Description": "The finding is in a WARNING state, because the S3 Bucket associated with this rule is in a different region/account. This rule does not support cross-region/cross-account checks, so it is recommended to disable this control in this region/account and only run it in the region/account where the resource is located.", + "ReasonCode": "S3_BUCKET_CROSS_ACCOUNT_CROSS_REGION" + } + ] + }, + "GeneratorId": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/2.3", + "FirstObservedAt": "2020-05-20T05:02:44.203Z", + "CreatedAt": "2020-05-20T05:02:44.203Z", + "RecordState": "ACTIVE", + "Title": "2.3 Ensure the S3 bucket used to store CloudTrail logs is not publicly accessible", + "Workflow": { + "Status": "NEW" + }, + "LastObservedAt": "2020-06-17T13:01:35.884Z", + "Severity": { + "Normalized": 90, + "Label": "CRITICAL", + "Product": 90, + "Original": "CRITICAL" + }, + "UpdatedAt": "2020-06-17T13:01:25.561Z", + "WorkflowState": "NEW", + "ProductFields": { + "StandardsGuideArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0", + "StandardsGuideSubscriptionArn": "arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0", + "RuleId": "2.3", + "RecommendationUrl": "https://docs.aws.amazon.com/console/securityhub/standards-cis-2.3/remediation", + "RelatedAWSResources:0/name": "securityhub-s3-bucket-public-read-prohibited-4414615a", + "RelatedAWSResources:0/type": "AWS::Config::ConfigRule", + "RelatedAWSResources:1/name": "securityhub-s3-bucket-public-write-prohibited-f104fcda", + "RelatedAWSResources:1/type": "AWS::Config::ConfigRule", + "StandardsControlArn": "arn:aws:securityhub:us-east-2:111111111111:control/cis-aws-foundations-benchmark/v/1.2.0/2.3", + "aws/securityhub/SeverityLabel": "CRITICAL", + "aws/securityhub/ProductName": "Security Hub", + "aws/securityhub/CompanyName": "AWS", + "aws/securityhub/annotation": "The finding is in a WARNING state, because the S3 Bucket associated with this rule is in a different region/account. This rule does not support cross-region/cross-account checks, so it is recommended to disable this control in this region/account and only run it in the region/account where the resource is located.", + "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-2::product/aws/securityhub/arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/2.3/finding/f51c716c-b33c-4949-b748-2ffd22bdceec" + }, + "AwsAccountId": "111111111111", + "Id": "arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/2.3/finding/f51c716c-b33c-4949-b748-2ffd22bdceec", + "Remediation": { + "Recommendation": { + "Text": "For directions on how to fix this issue, please consult the AWS Security Hub CIS documentation.", + "Url": "https://docs.aws.amazon.com/console/securityhub/standards-cis-2.3/remediation" + } + }, + "Resources": [ + { + "Partition": "aws", + "Type": "AwsS3Bucket", + "Region": "us-east-2", + "Id": "arn:aws:s3:::cloudtrail-awslogs-111111111111-kjfskljdfl" + } + ] + } + } + +def expected(): + return { + "account_id": '111111111111', + "resource_id": 'cloudtrail-awslogs-111111111111-kjfskljdfl', + "finding_id": 'arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/2.3/finding/f51c716c-b33c-4949-b748-2ffd22bdceec', + "product_arn": 'arn:aws:securityhub:us-east-2::product/aws/securityhub', + "control_id": '2.3', + "object": { + "Type": 'AwsS3Bucket', + "Id": 'cloudtrail-awslogs-111111111111-kjfskljdfl', + "OutputKey": 'Remediation.Output' + }, + "matches": [ "cloudtrail-awslogs-111111111111-kjfskljdfl" ], + 'details': {}, + 'testmode': False, + 'resource': event().get('Finding').get('Resources')[0], + 'resource_region': None, + 'aws_config_rule': { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + } + +def cis41_event(): + return { + 'expected_control_id': '4.1', + 'parse_id_pattern': '^arn:(?:aws|aws-cn|aws-us-gov):ec2:(?:[a-z]{2}(?:-gov)?-[a-z]+-[0-9]):[0-9]{12}:security-group/(sg-[a-f0-9]{8,17})$', + 'Finding': { + "SchemaVersion": "2018-10-08", + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/4.1/finding/f371b170-1881-4af0-9a33-840c81d91a04", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "ProductName": "Security Hub", + "CompanyName": "AWS", + "Region": "us-east-1", + "GeneratorId": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/4.1", + "AwsAccountId": "111111111111", + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" + ], + "FirstObservedAt": "2020-05-08T08:56:08.195Z", + "LastObservedAt": "2021-07-20T16:43:29.362Z", + "CreatedAt": "2020-05-08T08:56:08.195Z", + "UpdatedAt": "2021-07-20T16:43:26.312Z", + "Severity": { + "Product": 70, + "Label": "HIGH", + "Normalized": 70, + "Original": "HIGH" + }, + "Title": "4.1 Ensure no security groups allow ingress from 0.0.0.0/0 to port 22", + "Description": "Security groups provide stateful filtering of ingress/egress network traffic to AWS resources. It is recommended that no security group allows unrestricted ingress access to port 22.", + "Remediation": { + "Recommendation": { + "Text": "For directions on how to fix this issue, please consult the AWS Security Hub CIS documentation.", + "Url": "https://docs.aws.amazon.com/console/securityhub/standards-cis-4.1/remediation" + } + }, + "ProductFields": { + "StandardsGuideArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0", + "StandardsGuideSubscriptionArn": "arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0", + "RuleId": "4.1", + "RecommendationUrl": "https://docs.aws.amazon.com/console/securityhub/standards-cis-4.1/remediation", + "RelatedAWSResources:0/name": "securityhub-restricted-ssh-33f8347e", + "RelatedAWSResources:0/type": "AWS::Config::ConfigRule", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/cis-aws-foundations-benchmark/v/1.2.0/4.1", + "aws/securityhub/ProductName": "Security Hub", + "aws/securityhub/CompanyName": "AWS", + "Resources:0/Id": "arn:aws:ec2:us-east-1:111111111111:security-group/sg-087af114e4ae4c6ea", + "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-1::product/aws/securityhub/arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/4.1/finding/f371b170-1881-4af0-9a33-840c81d91a04" + }, + "Resources": [ + { + "Type": "AwsEc2SecurityGroup", + "Id": "arn:aws:ec2:us-east-1:111111111111:security-group/sg-087af114e4ae4c6ea", + "Partition": "aws", + "Region": "us-east-1", + "Details": { + "AwsEc2SecurityGroup": { + "GroupName": "launch-wizard-17", + "GroupId": "sg-087af114e4ae4c6ea", + "OwnerId": "111111111111", + "VpcId": "vpc-e5b8f483", + "IpPermissions": [ + { + "IpProtocol": "tcp", + "FromPort": 22, + "ToPort": 22, + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ] + } + ], + "IpPermissionsEgress": [ + { + "IpProtocol": "-1", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ] + } + ] + } + } + } + ], + "Compliance": { + "Status": "FAILED" + }, + "WorkflowState": "NEW", + "Workflow": { + "Status": "NOTIFIED" + }, + "RecordState": "ACTIVE", + "Note": { + "Text": "Remediation failed for CIS control 4.1 in account 111111111111: No output available yet because the step is not successfully executed", + "UpdatedBy": "update_text", + "UpdatedAt": "2021-07-20T18:53:07.918Z" + }, + "FindingProviderFields": { + "Severity": { + "Label": "HIGH", + "Original": "HIGH" + }, + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" + ] + } + } + } + +def cis41_expected(): + return { + "account_id": '111111111111', + "resource_id": 'sg-087af114e4ae4c6ea', + 'testmode': False, + "finding_id": 'arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/4.1/finding/f371b170-1881-4af0-9a33-840c81d91a04', + "product_arn": 'arn:aws:securityhub:us-east-1::product/aws/securityhub', + "control_id": '4.1', + "object": { + "Type": 'AwsEc2SecurityGroup', + "Id": 'sg-087af114e4ae4c6ea', + "OutputKey": 'Remediation.Output' + }, + "matches": [ "sg-087af114e4ae4c6ea" ], + 'details': cis41_event().get('Finding').get('Resources')[0].get('Details'), + 'resource': cis41_event().get('Finding').get('Resources')[0], + 'resource_region': 'us-east-1', + 'aws_config_rule': { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + } + +def config_rule(): + return { + "ConfigRules": [ + { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + ] + } + +def ssm_parm(): + return { + 'Parameter': { + 'Name': 'Solutions/SO0111/member_version', + 'Type': 'String', + 'Value': 'v1.5.0' + } + } + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + } +) + +@pytest.fixture(autouse=True) +def run_before_and_after_tests(mocker): + cfg_client = botocore.session.get_session().create_client('config', config=BOTO_CONFIG) + cfg_stubber = Stubber(cfg_client) + cfg_stubber.add_response( + 'describe_config_rules', + config_rule() + ) + cfg_stubber.activate() + mocker.patch('parse_input.connect_to_config', return_value=cfg_client) + + ssm_client = botocore.session.get_session().create_client('ssm', config=BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + ssm_stubber.add_response( + 'get_parameter', + ssm_parm() + ) + ssm_stubber.activate() + mocker.patch('parse_input.connect_to_ssm', return_value=ssm_client) + yield + + cfg_stubber.deactivate() + ssm_stubber.deactivate() + +def test_parse_event(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + parsed_event = parse_event(event(), {}) + assert parsed_event == expected_result + +def test_parse_cis41(mocker): + expected_result = cis41_expected() + expected_result['finding'] = cis41_event().get('Finding') + parsed_event = parse_event(cis41_event(), {}) + assert parsed_event == expected_result + +def test_parse_event_multimatch(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + expected_result['matches'] = [ + "aws", + "cloudtrail-awslogs-111111111111-kjfskljdfl" + ] + test_event = event() + test_event['resource_index'] = 2 + test_event['parse_id_pattern'] = '^arn:((?:aws|aws-cn|aws-us-gov)):s3:::([A-Za-z0-9.-]{3,63})$' + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_bad_finding_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "badvalue" + 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 Id is invalid: badvalue' + +def test_bad_control_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0//finding/f51c716c-b33c-4949-b748-2ffd22bdceec" + 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 Id is invalid: arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0//finding/f51c716c-b33c-4949-b748-2ffd22bdceec - missing Control Id' + +def test_control_id_nomatch(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-2:111111111111:subscription/cis-aws-foundations-benchmark/v/1.2.0/2.4/finding/f51c716c-b33c-4949-b748-2ffd22bdceec" + 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: Control Id from input (2.4) does not match 2.3' + +def test_bad_account_id(mocker): + test_event = event() + test_event['Finding']['AwsAccountId'] = "1234123412345" + 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: AwsAccountId is invalid: 1234123412345' + +def test_bad_productarn(mocker): + test_event = event() + test_event['Finding']['ProductArn'] = "badvalue" + 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: ProductArn is invalid: badvalue' + +def test_bad_resource_match(mocker): + test_event = event() + test_event['parse_id_pattern'] = '^arn:(?:aws|aws-cn|aws-us-gov):logs:::([A-Za-z0-9.-]{3,63})$' + 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: Invalid resource Id arn:aws:s3:::cloudtrail-awslogs-111111111111-kjfskljdfl' + +def test_no_resource_pattern(mocker): + test_event = event() + expected_result = expected() + expected_result['finding'] = event().get('Finding') + test_event['parse_id_pattern'] = '' + expected_result['resource_id'] = 'arn:aws:s3:::cloudtrail-awslogs-111111111111-kjfskljdfl' + expected_result['matches'] = [] + expected_result['object']['Id'] = expected_result['resource_id'] + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_no_resource_pattern_no_resource_id(mocker): + test_event = event() + + test_event['parse_id_pattern'] = '' + test_event['Finding']['Resources'][0]['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: Resource Id is missing from the finding json Resources (Id)' diff --git a/source/playbooks/common/test/test_deserialize_json.py b/source/playbooks/common/test/test_deserialize_json.py new file mode 100644 index 00000000..56ce11f6 --- /dev/null +++ b/source/playbooks/common/test/test_deserialize_json.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json +from deserialize_json import event_handler + +def event(object): + return { + 'SerializedJson': json.dumps(object) + } + +def test_deserialize(): + object = {'MinRetentionPeriod': '7'} + assert event_handler(event(object), {}) == object diff --git a/source/playbooks/common/test/test_pci321_parse.py b/source/playbooks/common/test/test_pci321_parse.py new file mode 100644 index 00000000..cfe7cc19 --- /dev/null +++ b/source/playbooks/common/test/test_pci321_parse.py @@ -0,0 +1,307 @@ +#!/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 +from botocore.config import Config +import pytest +from pytest_mock import mocker + +from parse_input import parse_event + +def event(): + return { + 'expected_control_id': 'PCI.IAM.6', + 'parse_id_pattern': '^arn:aws:iam::[0-9]{12}:user/([A-Za-z0-9+=,.@-]{1,64})$', + 'Finding': { + "SchemaVersion": "2018-10-08", + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1/PCI.IAM.6/finding/fec91aaf-5016-4c40-9d24-9966e4be80c4", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "GeneratorId": "pci-dss/v/3.2.1/PCI.IAM.6", + "AwsAccountId": "111111111111", + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS" + ], + "FirstObservedAt": "2021-06-01T18:39:09.192Z", + "LastObservedAt": "2021-06-01T18:39:11.050Z", + "CreatedAt": "2021-06-01T18:39:09.192Z", + "UpdatedAt": "2021-06-01T18:39:09.192Z", + "Severity": { + "Product": 40, + "Label": "MEDIUM", + "Normalized": 40, + "Original": "MEDIUM" + }, + "Title": "PCI.IAM.6 MFA should be enabled for all IAM users", + "Description": "This AWS control checks whether the AWS Identity and Access Management users have multi-factor authentication (MFA) enabled.", + "Remediation": { + "Recommendation": { + "Text": "For directions on how to fix this issue, please consult the AWS Security Hub PCI DSS documentation.", + "Url": "https://docs.aws.amazon.com/console/securityhub/PCI.IAM.6/remediation" + } + }, + "ProductFields": { + "StandardsArn": "arn:aws:securityhub:::standards/pci-dss/v/3.2.1", + "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1", + "ControlId": "PCI.IAM.6", + "RecommendationUrl": "https://docs.aws.amazon.com/console/securityhub/PCI.IAM.6/remediation", + "RelatedAWSResources:0/name": "securityhub-iam-user-mfa-enabled-8f8ddc5e", + "RelatedAWSResources:0/type": "AWS::Config::ConfigRule", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/pci-dss/v/3.2.1/PCI.IAM.6", + "aws/securityhub/ProductName": "Security Hub", + "aws/securityhub/CompanyName": "AWS", + "Resources:0/Id": "arn:aws:iam::111111111111:user/foo-bar@baz", + "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-1::product/aws/securityhub/arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1/PCI.IAM.6/finding/fec91aaf-5016-4c40-9d24-9966e4be80c4" + }, + "Resources": [ + { + "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" + } + } + } + ], + "Compliance": { + "Status": "FAILED", + "RelatedRequirements": [ + "PCI DSS 8.3.1" + ] + }, + "WorkflowState": "NEW", + "Workflow": { + "Status": "NEW" + }, + "RecordState": "ACTIVE", + "FindingProviderFields": { + "Severity": { + "Label": "MEDIUM", + "Original": "MEDIUM" + }, + "Types": [ + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS" + ] + } + } + } + +def expected(): + return { + "account_id": '111111111111', + "resource_id": 'foo-bar@baz', + 'control_id': 'PCI.IAM.6', + 'testmode': False, + "finding_id": 'arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1/PCI.IAM.6/finding/fec91aaf-5016-4c40-9d24-9966e4be80c4', + "product_arn": 'arn:aws:securityhub:us-east-1::product/aws/securityhub', + "object": { + "Type": 'AwsIamUser', + "Id": 'foo-bar@baz', + "OutputKey": 'Remediation.Output' + }, + "matches": [ "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" + } + } + }, + "resource_region": None, + 'aws_config_rule': { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + } + +def config_rule(): + return { + "ConfigRules": [ + { + "ConfigRuleName": "s3-bucket-server-side-encryption-enabled", + "ConfigRuleArn": "arn:aws:config:us-east-1:111111111111:config-rule/config-rule-vye3dl", + "ConfigRuleId": "config-rule-vye3dl", + "Description": "Checks whether the S3 bucket policy denies the put-object requests that are not encrypted using AES-256 or AWS KMS.", + "Scope": { + "ComplianceResourceTypes": [ + "AWS::S3::Bucket" + ] + }, + "Source": { + "Owner": "AWS", + "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + }, + "InputParameters": "{}", + "ConfigRuleState": "ACTIVE" + } + ] + } + +def ssm_parm(): + return { + 'Parameter': { + 'Name': 'Solutions/SO0111/member_version', + 'Type': 'String', + 'Value': 'v1.5.0' + } + } +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + } +) + +@pytest.fixture(autouse=True) +def run_before_and_after_tests(mocker): + cfg_client = botocore.session.get_session().create_client('config', config=BOTO_CONFIG) + cfg_stubber = Stubber(cfg_client) + cfg_stubber.add_response( + 'describe_config_rules', + config_rule() + ) + cfg_stubber.activate() + mocker.patch('parse_input.connect_to_config', return_value=cfg_client) + + ssm_client = botocore.session.get_session().create_client('ssm', config=BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + ssm_stubber.add_response( + 'get_parameter', + ssm_parm() + ) + ssm_stubber.activate() + mocker.patch('parse_input.connect_to_ssm', return_value=ssm_client) + yield + + cfg_stubber.deactivate() + ssm_stubber.deactivate() + +def test_parse_event(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + parsed_event = parse_event(event(), {}) + assert parsed_event == expected_result + +def test_parse_event_multimatch(mocker): + expected_result = expected() + expected_result['finding'] = event().get('Finding') + expected_result['matches'] = [ + "iam", + "foo-bar@baz" + ] + test_event = event() + test_event['resource_index'] = 2 + test_event['parse_id_pattern'] = '^arn:aws:(.*?)::[0-9]{12}:user/([A-Za-z0-9+=,.@-]{1,64})$' + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_bad_finding_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "badvalue" + 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 Id is invalid: badvalue' + +def test_bad_control_id(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-1:111111111111:subscription/pci-dss/v/3.2.1//finding/fec91aaf-5016-4c40-9d24-9966e4be80c4" + 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 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(mocker): + test_event = event() + test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-2:111111111111:subscription/pci-dss/v/3.2.1/2.4/finding/fec91aaf-5016-4c40-9d24-9966e4be80c4" + 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: Control Id from input (2.4) does not match PCI.IAM.6' + +def test_bad_account_id(mocker): + test_event = event() + test_event['Finding']['AwsAccountId'] = "1234123412345" + 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: AwsAccountId is invalid: 1234123412345' + +def test_bad_productarn(mocker): + test_event = event() + test_event['Finding']['ProductArn'] = "badvalue" + 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: ProductArn is invalid: badvalue' + +def test_bad_resource_match(mocker): + test_event = event() + test_event['parse_id_pattern'] = '^arn:(?:aws|aws-cn|aws-us-gov):logs:::([A-Za-z0-9.-]{3,63})$' + 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: Invalid resource Id arn:aws:iam::111111111111:user/foo-bar@baz' + +def test_no_resource_pattern(mocker): + test_event = event() + expected_result = expected() + expected_result['finding'] = event().get('Finding') + test_event['parse_id_pattern'] = '' + expected_result['resource_id'] = 'arn:aws:iam::111111111111:user/foo-bar@baz' + expected_result['matches'] = [] + expected_result['object']['Id'] = expected_result['resource_id'] + parsed_event = parse_event(test_event, {}) + assert parsed_event == expected_result + +def test_no_resource_pattern_no_resource_id(mocker): + test_event = event() + + test_event['parse_id_pattern'] = '' + test_event['Finding']['Resources'][0]['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: Resource Id is missing from the finding json Resources (Id)' diff --git a/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml b/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml index 98d160d2..13b530a5 100644 --- a/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml +++ b/source/remediation_runbooks/ConfigureS3BucketPublicAccessBlock.yaml @@ -18,7 +18,7 @@ description: | ## 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. schemaVersion: "0.3" assumeRole: "{{ AutomationAssumeRole }}" @@ -60,7 +60,7 @@ 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+=,.@-]+ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' mainSteps: - name: PutBucketPublicAccessBlock action: "aws:executeAwsApi" @@ -91,7 +91,7 @@ mainSteps: isCritical: true isEnd: true inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: validate_s3_bucket_publicaccessblock InputPayload: Bucket: "{{BucketName}}" diff --git a/source/remediation_runbooks/ConfigureS3PublicAccessBlock.yaml b/source/remediation_runbooks/ConfigureS3PublicAccessBlock.yaml index 831b00df..1576b5ea 100644 --- a/source/remediation_runbooks/ConfigureS3PublicAccessBlock.yaml +++ b/source/remediation_runbooks/ConfigureS3PublicAccessBlock.yaml @@ -29,7 +29,7 @@ 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+=,.@-]+ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' RestrictPublicBuckets: type: Boolean 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. @@ -49,7 +49,7 @@ parameters: outputs: - GetPublicAccessBlock.Output mainSteps: - - + - name: PutAccountPublicAccessBlock action: "aws:executeAwsApi" description: | @@ -70,7 +70,7 @@ mainSteps: - Name: PutAccountPublicAccessBlockResponse Selector: $ Type: StringMap - - + - name: GetPublicAccessBlock action: "aws:executeScript" description: | @@ -81,7 +81,7 @@ mainSteps: timeoutSeconds: 600 isEnd: true inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: handler InputPayload: AccountId: "{{ AccountId }}" diff --git a/source/remediation_runbooks/CreateAccessLoggingBucket.yaml b/source/remediation_runbooks/CreateAccessLoggingBucket.yaml index a497ffae..99219df2 100644 --- a/source/remediation_runbooks/CreateAccessLoggingBucket.yaml +++ b/source/remediation_runbooks/CreateAccessLoggingBucket.yaml @@ -14,7 +14,7 @@ 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+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' BucketName: type: String description: (Required) The bucket name (not the ARN). @@ -30,7 +30,7 @@ mainSteps: InputPayload: BucketName: '{{BucketName}}' AWS_REGION: '{{global:REGION}}' - Runtime: python3.7 + Runtime: python3.8 Handler: create_logging_bucket Script: |- %%SCRIPT=CreateAccessLoggingBucket_createloggingbucket.py%% diff --git a/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml b/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml index 101880c9..ba10c01a 100644 --- a/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml +++ b/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml @@ -19,7 +19,7 @@ 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+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' KMSKeyArn: type: String default: >- @@ -51,7 +51,7 @@ mainSteps: account: '{{global:ACCOUNT_ID}}' region: '{{global:REGION}}' kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.7 + Runtime: python3.8 Handler: create_logging_bucket Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_createloggingbucket.py%% @@ -71,7 +71,7 @@ mainSteps: region: '{{global:REGION}}' kms_key_arn: '{{KMSKeyArn}}' logging_bucket: '{{CreateLoggingBucket.LoggingBucketName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: create_encrypted_bucket Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_createcloudtrailbucket.py%% @@ -86,7 +86,7 @@ mainSteps: cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' partition: '{{AWSPartition}}' account: '{{global:ACCOUNT_ID}}' - Runtime: python3.7 + Runtime: python3.8 Handler: create_bucket_policy Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_createcloudtrailbucketpolicy.py%% @@ -103,7 +103,7 @@ mainSteps: InputPayload: cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.7 + Runtime: python3.8 Handler: enable_cloudtrail Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_enablecloudtrail.py%% @@ -121,7 +121,7 @@ mainSteps: InputPayload: cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' logging_bucket: '{{CreateLoggingBucket.LoggingBucketName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: process_results Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_process_results.py%% diff --git a/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml b/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml index 5868be9c..2c931932 100644 --- a/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml +++ b/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml @@ -15,32 +15,39 @@ assumeRole: "{{ AutomationAssumeRole }}" parameters: 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+=,.@/-]+$ + 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+=,.@-]+$' LogGroupName: type: String description: Name of the log group to be used to create metric filter + allowedPattern: '.*' FilterName: type: String description: Name for the metric filter + allowedPattern: '.*' FilterPattern: type: String description: Filter pattern to create metric filter + allowedPattern: '.*' MetricName: type: String description: Name of the metric for metric filter + allowedPattern: '.*' MetricValue: type: Integer description: Value of the metric for metric filter MetricNamespace: type: String description: Namespace where the metrics will be sent + allowedPattern: '.*' AlarmName: type: String description: Name of the Alarm to be created for the metric filter + allowedPattern: '.*' AlarmDesc: type: String description: Description of the Alarm to be created for the metric filter + allowedPattern: '.*' AlarmThreshold: type: Integer description: Threshold value for the alarm @@ -53,7 +60,7 @@ parameters: allowedPattern: ^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$ mainSteps: - - + - name: CreateTopic action: 'aws:executeScript' outputs: @@ -61,15 +68,15 @@ mainSteps: Selector: $.Payload.topic_arn Type: String inputs: - InputPayload: + InputPayload: kms_key_arn: '{{KMSKeyArn}}' topic_name: '{{SNSTopicName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: create_encrypted_topic Script: |- %%SCRIPT=CreateLogMetricFilterAndAlarm_createtopic.py%% - - - + + - name: CreateMetricFilerAndAlarm action: 'aws:executeScript' outputs: @@ -82,13 +89,13 @@ mainSteps: FilterName: '{{FilterName}}' FilterPattern: '{{FilterPattern}}' MetricName: '{{MetricName}}' - MetricNamespace: '{{MetricNamespace}}' + MetricNamespace: '{{MetricNamespace}}' MetricValue: '{{MetricValue}}' AlarmName: '{{AlarmName}}' AlarmDesc: '{{AlarmDesc}}' AlarmThreshold: '{{AlarmThreshold}}' - TopicArn: '{{CreateTopic.TopicArn}}' - Runtime: python3.7 + TopicArn: '{{CreateTopic.TopicArn}}' + Runtime: python3.8 Handler: verify Script: |- %%SCRIPT=CreateLogMetricFilterAndAlarm.py%% diff --git a/source/remediation_runbooks/DisablePublicAccessToRDSInstance.yaml b/source/remediation_runbooks/DisablePublicAccessToRDSInstance.yaml new file mode 100644 index 00000000..c1f8c722 --- /dev/null +++ b/source/remediation_runbooks/DisablePublicAccessToRDSInstance.yaml @@ -0,0 +1,128 @@ +schemaVersion: "0.3" +description: | + ### Document name - AWSConfigRemediation-DisablePublicAccessToRDSInstance + + ## What does this document do? + The runbook disables public accessibility for the Amazon RDS database instance you specify using + the [ModifyDBInstance](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBInstance.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. + * DbiResourceId: (Required) The resource identifier for the DB instance you want to disable public accessibility. + + ## Output Parameters + * DisablePubliclyAccessibleOnRDS.Response: The standard HTTP response from the ModifyDBInstance API. + +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+=,.@-]+$' + DbiResourceId: + type: String + description: (Required) The resource identifier for the DB instance you want to disable public accessibility. + allowedPattern: "db-[A-Z0-9]{26}" +outputs: + - DisablePubliclyAccessibleOnRDS.Response +mainSteps: + - + name: GetRDSInstanceIdentifier + action: "aws:executeAwsApi" + description: | + ## GetRDSInstanceIdentifier + Gathers the DB instance identifier from the DB instance resource identifier. + ## Outputs + * DbInstanceIdentifier: The Amazon RDS DB instance identifier. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: DescribeDBInstances + Filters: + - Name: "dbi-resource-id" + Values: + - "{{ DbiResourceId }}" + outputs: + - Name: DbInstanceIdentifier + Selector: $.DBInstances[0].DBInstanceIdentifier + Type: String + - + name: VerifyDBInstanceStatus + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: false + description: | + ## VerifyDBInstanceStatus + Verifies the DB instances is in an AVAILABLE state. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DBInstanceStatus" + DesiredValues: + - "available" + - + name: DisablePubliclyAccessibleOnRDS + action: "aws:executeAwsApi" + description: | + ## DisablePubliclyAccessibleOnRDS + Disables public accessibility on your DB instance. + ## Outputs + * Response: The standard HTTP response from the ModifyDBInstance API. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: ModifyDBInstance + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PubliclyAccessible: false + outputs: + - Name: Response + Selector: $ + Type: StringMap + - + name: WaitForDBInstanceStatusToModify + action: "aws:waitForAwsResourceProperty" + timeoutSeconds: 600 + isEnd: false + description: | + ## WaitForDBInstanceStatusToModify + Waits for the DB instance to change to a MODIFYING state. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DBInstanceStatus" + DesiredValues: + - "modifying" + - + name: WaitForDBInstanceStatusToAvailableAfterModify + action: "aws:waitForAwsResourceProperty" + timeoutSeconds: 600 + isEnd: false + description: | + ## WaitForDBInstanceStatusToAvailableAfterModify + Waits for the DB instance to change to an AVAILABLE state + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DBInstanceStatus" + DesiredValues: + - "available" + - + name: VerifyDBInstancePubliclyAccess + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: true + description: | + ## VerifyDBInstancePubliclyAccess + Confirms public accessibility is disabled on the DB instance. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].PubliclyAccessible" + DesiredValues: + - "False" diff --git a/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml b/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml new file mode 100644 index 00000000..e56c98a2 --- /dev/null +++ b/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml @@ -0,0 +1,77 @@ +schemaVersion: "0.3" +description: | + ### Document name - SHARR-DisablePublicAccessToRedshiftCluster + + ## What does this document do? + The runbook disables public accessibility for the Amazon Redshift cluster you specify using the [ModifyCluster] + (https://docs.aws.amazon.com/redshift/latest/APIReference/API_ModifyCluster.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. + * ClusterIdentifier: (Required) The unique identifier of the cluster you want to disable the public accessibility. + + ## Output Parameters + * DisableRedshiftPubliclyAccessible.Response: The standard HTTP response from the ModifyCluster API call. + +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+=,.@-]+$' + ClusterIdentifier: + type: String + description: (Required) The unique identifier of the cluster you want to disable the public accessibility. + allowedPattern: "^(?!.*--)[a-z][a-z0-9-]{0,62}(? + def event_handler(event, context): + return str(event['RetentionPeriod']) + outputs: + - Name: 'CurrentRetentionPeriodString' + Selector: '$.Payload' + Type: 'String' +- name: 'VerifyCurrentRetentionPeriod' + action: 'aws:assertAwsResourceProperty' + inputs: + Service: 'redshift' + Api: 'DescribeClusters' + ClusterIdentifier: '{{ClusterIdentifier}}' + PropertySelector: '$.Clusters[0].AutomatedSnapshotRetentionPeriod' + DesiredValues: + - '{{CastCurrentRetentionPeriodToString.CurrentRetentionPeriodString}}' + isEnd: true + +- name: 'ModifyRetentionPeriod' + action: 'aws:executeAwsApi' + inputs: + Service: 'redshift' + Api: 'ModifyCluster' + ClusterIdentifier: '{{ClusterIdentifier}}' + AutomatedSnapshotRetentionPeriod: '{{MinRetentionPeriod}}' + outputs: + - Name: 'Response' + Selector: '$' + Type: 'StringMap' +- name: 'WaitForClusterAvailability' + action: 'aws:waitForAwsResourceProperty' + timeoutSeconds: 600 + inputs: + Service: 'redshift' + Api: 'DescribeClusters' + ClusterIdentifier: '{{ClusterIdentifier}}' + PropertySelector: '$.Clusters[0].ClusterStatus' + DesiredValues: + - 'available' +- name: 'CastRetentionPeriodToString' + action: 'aws:executeScript' + inputs: + Runtime: 'python3.8' + Handler: 'event_handler' + InputPayload: + RetentionPeriod: '{{MinRetentionPeriod}}' + Script: > + def event_handler(event, context): + return str(event['RetentionPeriod']) + outputs: + - Name: 'MinRetentionPeriodString' + Selector: '$.Payload' + Type: 'String' +- name: 'VerifyModifiedRetentionPeriod' + action: 'aws:assertAwsResourceProperty' + inputs: + Service: 'redshift' + Api: 'DescribeClusters' + ClusterIdentifier: '{{ClusterIdentifier}}' + PropertySelector: '$.Clusters[0].AutomatedSnapshotRetentionPeriod' + DesiredValues: + - '{{CastRetentionPeriodToString.MinRetentionPeriodString}}' + isEnd: true diff --git a/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml b/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml new file mode 100644 index 00000000..8b6edf6c --- /dev/null +++ b/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml @@ -0,0 +1,79 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document name - SHARR-EnableAutomaticVersionUpgradeOnRedshiftCluster + + ## What does this document do? + The runbook enables automatic version upgrade on a Redshift cluster. + + ## Input Parameters + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * ClusterIdentifier: (Required) The unique identifier of the cluster. + * AllowVersionUpgrade: (Optional) Whether to allow version upgrade on the cluster. + + ## Output Parameters + * EnableAutomaticVersionUpgrade.Response: The response of the API call to enable automatic version upgrade on the cluster. +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+=,.@-]+$' + ClusterIdentifier: + type: 'String' + description: '(Required) The unique identifier of the cluster.' + allowedPattern: '^(?!.*--)[a-z][a-z0-9-]{0,62}(? + def event_handler(event, context): + return str(event['AllowVersionUpgrade']) + outputs: + - Name: 'AllowVersionUpgradeString' + Selector: '$.Payload' + Type: 'String' +- name: 'VerifyAutomaticVersionUpgrade' + action: 'aws:assertAwsResourceProperty' + inputs: + Service: 'redshift' + Api: 'DescribeClusters' + ClusterIdentifier: '{{ClusterIdentifier}}' + PropertySelector: '$.Clusters[0].AllowVersionUpgrade' + DesiredValues: + - '{{CastAllowVersionUpgradeToString.AllowVersionUpgradeString}}' + isEnd: true diff --git a/source/remediation_runbooks/EnableCloudTrailEncryption.yaml b/source/remediation_runbooks/EnableCloudTrailEncryption.yaml index dd10b0ce..c410c4f2 100644 --- a/source/remediation_runbooks/EnableCloudTrailEncryption.yaml +++ b/source/remediation_runbooks/EnableCloudTrailEncryption.yaml @@ -20,7 +20,7 @@ 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+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' KMSKeyArn: type: String default: >- @@ -53,7 +53,7 @@ mainSteps: trail: '{{TrailArn}}' region: '{{global:REGION}}' kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.7 + Runtime: python3.8 Handler: enable_trail_encryption Script: |- %%SCRIPT=EnableCloudTrailEncryption.py%% diff --git a/source/remediation_runbooks/EnableCloudTrailLogFileValidation.yaml b/source/remediation_runbooks/EnableCloudTrailLogFileValidation.yaml index 4ecae9a1..30b17f4c 100644 --- a/source/remediation_runbooks/EnableCloudTrailLogFileValidation.yaml +++ b/source/remediation_runbooks/EnableCloudTrailLogFileValidation.yaml @@ -18,8 +18,8 @@ assumeRole: "{{ AutomationAssumeRole }}" parameters: 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+=,.@-]+ + 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+=,.@-]+$' TrailName: type: String description: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. diff --git a/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml b/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml index d2c7c9d9..6aaa9089 100644 --- a/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml +++ b/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml @@ -18,7 +18,7 @@ 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+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' TrailName: type: String description: (Required) The name of the CloudTrail. @@ -26,7 +26,7 @@ parameters: CloudWatchLogsRole: type: String description: (Required) The ARN of the role that allows CloudTrail to log to CloudWatch. - allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+' + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' LogGroupName: type: String description: (Required) The name of the Log Group for CloudTrail logs. @@ -53,7 +53,7 @@ mainSteps: inputs: InputPayload: LogGroup: '{{LogGroupName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: wait_for_loggroup Script: |- %%SCRIPT=EnableCloudTrailToCloudWatchLogging_waitforloggroup.py%% diff --git a/source/remediation_runbooks/EnableCopyTagsToSnapshotOnRDSCluster.yaml b/source/remediation_runbooks/EnableCopyTagsToSnapshotOnRDSCluster.yaml new file mode 100644 index 00000000..e93a76c8 --- /dev/null +++ b/source/remediation_runbooks/EnableCopyTagsToSnapshotOnRDSCluster.yaml @@ -0,0 +1,101 @@ +schemaVersion: "0.3" +description: | + ### Document name - AWSConfigRemediation-EnableCopyTagsToSnapshotOnRDSCluster + + ## What does this document do? + The document enables CopyTagsToSnapshot on an Amazon RDS cluster using the [ModifyDBCluster API](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBCluster.html). 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. + * DbClusterResourceId: (Required) Resource ID of the Amazon RDS Cluster for which CopyTagsToSnapshot needs to be enabled. + * ApplyImmediately: (Optional) A value that indicates whether the modifications in this request and any pending modifications are asynchronously applied as soon as possible, regardless of the PreferredMaintenanceWindow setting for the DB instance. By default, this parameter is disabled. + * Default: false + + ## Output Parameters + * ModifyDBClusterResponse.Output: The response of the ModifyDBCluster API call. + +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+=,.@-]+$' + DbClusterResourceId: + type: String + description: (Required) Resource ID of the Amazon RDS Cluster for which CopyTagsToSnapshot needs to be enabled. + allowedPattern: '^cluster-[A-Z0-9]+$' + ApplyImmediately: + type: Boolean + description: (Optional) A value that indicates whether the modifications in this request and any pending modifications are asynchronously applied as soon as possible, regardless of the PreferredMaintenanceWindow setting for the DB instance. By default, this parameter is disabled. + default: false + +outputs: + - EnableCopyTagsToSnapshot.Output +mainSteps: +- name: GetDBClusterIdentifier + action: aws:executeAwsApi + description: | + ## GetDBClusterIdentifier + Accepts the Resource ID as input and returns the DB cluster identifier. + ## Outputs + * DBClusterIdentifier: The ID of the DB cluster. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: config + Api: GetResourceConfigHistory + resourceId: "{{ DbClusterResourceId }}" + resourceType: AWS::RDS::DBCluster + outputs: + - Name: DBClusterIdentifier + Selector: $.configurationItems[0].resourceName + Type: String +- name: VerifyStatus + action: aws:assertAwsResourceProperty + description: | + ## VerifyStatus + Verifies if `Status` is available before proeeding to the next step. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: DescribeDBClusters + DBClusterIdentifier: "{{ GetDBClusterIdentifier.DBClusterIdentifier }}" + PropertySelector: $.DBClusters[0].Status + DesiredValues: + - "available" +- name: EnableCopyTagsToSnapshot + action: aws:executeAwsApi + description: | + ## EnableCopyTagsToSnapshot + Accepts the cluster name as input and modifies it to set true for `CopyTagsToSnapshot`. + ## Outputs + * Output: Response from the ModifyDBCluster API call. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: ModifyDBCluster + DBClusterIdentifier: "{{ GetDBClusterIdentifier.DBClusterIdentifier }}" + ApplyImmediately: "{{ ApplyImmediately }}" + CopyTagsToSnapshot: True + outputs: + - Name: Output + Selector: $ + Type: StringMap +- name: VerifyDBClusterCopyTagsToSnapshotEnabled + action: aws:assertAwsResourceProperty + description: | + ## VerifyDBClusterCopyTagsToSnapshotEnabled + Verifies that `CopyTagsToSnapshot` has been enabled on the target resource. + ## Outputs + * Output: A success message or failure exception. + timeoutSeconds: 600 + isEnd: true + inputs: + Service: rds + Api: DescribeDBClusters + DBClusterIdentifier: "{{ GetDBClusterIdentifier.DBClusterIdentifier }}" + PropertySelector: $.DBClusters[0].CopyTagsToSnapshot + DesiredValues: + - "True" \ No newline at end of file diff --git a/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml b/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml new file mode 100644 index 00000000..b2a7f674 --- /dev/null +++ b/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml @@ -0,0 +1,80 @@ +description: | + ### Document name - SHARR-EnableDefaultEncryptionS3 + + ## What does this document do? + This document configures default encryption for an Amazon S3 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. + * 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.4 + * CIS v1.2.0: n/a + * PCI: S3.4 + +schemaVersion: "0.3" +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 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+=,.@-]+$' + 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])$) + KmsKeyAlias: + type: String + description: (Required) KMS Customer-Managed Key (CMK) alias or the default value which is created in the SSM parameter at solution deployment (default-s3-encryption) is used to identify that the s3 bucket encryption value should be set to AES-256. + default: 'default-s3-encryption' + allowedPattern: '^$|^[a-zA-Z0-9/_-]{1,256}$' + +mainSteps: + - name: ChooseEncryptionMethod + action: aws:branch + inputs: + Choices: + - NextStep: EncryptWithAES + Variable: '{{KmsKeyAlias}}' + StringEquals: 'default-s3-encryption' + Default: + EncryptWithCMK + + - name: EncryptWithAES + action: aws:executeAwsApi + inputs: + Service: s3 + Api: PutBucketEncryption + Bucket: '{{BucketName}}' + ExpectedBucketOwner: '{{AccountId}}' + ServerSideEncryptionConfiguration: + Rules: + - ApplyServerSideEncryptionByDefault: + SSEAlgorithm: 'AES256' + BucketKeyEnabled: true + isEnd: true + + - name: EncryptWithCMK + action: aws:executeAwsApi + inputs: + Service: s3 + Api: PutBucketEncryption + Bucket: '{{BucketName}}' + ExpectedBucketOwner: '{{AccountId}}' + ServerSideEncryptionConfiguration: + Rules: + - ApplyServerSideEncryptionByDefault: + SSEAlgorithm: 'aws:kms' + KMSMasterKeyID: '{{KmsKeyAlias}}' + BucketKeyEnabled: true + isEnd: true diff --git a/source/remediation_runbooks/EnableEbsEncryptionByDefault.yaml b/source/remediation_runbooks/EnableEbsEncryptionByDefault.yaml index 33f05127..d336417d 100644 --- a/source/remediation_runbooks/EnableEbsEncryptionByDefault.yaml +++ b/source/remediation_runbooks/EnableEbsEncryptionByDefault.yaml @@ -11,14 +11,12 @@ description: | ## Output Parameters * ModifyAccount.EnableEbsEncryptionByDefaultResponse: JSON formatted response from the EnableEbsEncryptionByDefault API. - ## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. - 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+=,.@-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' outputs: - ModifyAccount.EnableEbsEncryptionByDefaultResponse mainSteps: diff --git a/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml b/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml index ac32ff96..5eefc3dc 100644 --- a/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml +++ b/source/remediation_runbooks/EnableEnhancedMonitoringOnRDSInstance.yaml @@ -23,7 +23,7 @@ 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/[a-zA-Z0-9+=,.@_/-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' ResourceId: type: String description: (Required) Resource ID of the Amazon RDS instance for which Enhanced Monitoring needs to be enabled. @@ -115,7 +115,7 @@ mainSteps: isEnd: true timeoutSeconds: 600 inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: handler InputPayload: MonitoringInterval: "{{ MonitoringInterval }}" diff --git a/source/remediation_runbooks/EnableKeyRotation.yaml b/source/remediation_runbooks/EnableKeyRotation.yaml index 3fe29878..db3080d2 100644 --- a/source/remediation_runbooks/EnableKeyRotation.yaml +++ b/source/remediation_runbooks/EnableKeyRotation.yaml @@ -17,7 +17,7 @@ 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+=,.@/-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' KeyId: type: String description: (Required) The Key ID of the AWS KMS symmetric CMK. diff --git a/source/remediation_runbooks/EnableMinorVersionUpgradeOnRDSDBInstance.yaml b/source/remediation_runbooks/EnableMinorVersionUpgradeOnRDSDBInstance.yaml new file mode 100644 index 00000000..6ef7a81e --- /dev/null +++ b/source/remediation_runbooks/EnableMinorVersionUpgradeOnRDSDBInstance.yaml @@ -0,0 +1,93 @@ + +schemaVersion: "0.3" +description: | + ### Document name - AWSConfigRemediation-EnableMinorVersionUpgradeOnRDSDBInstance + + ## What does this document do? + This document enables AutoMinorVersionUpgrade on the Amazon Relational Database Service (Amazon RDS) instance using the [ModifyDBInstance](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBInstance.html) API. + + ## Input parameters + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * DbiResourceId: (Required) Resource ID of the Amazon RDS instance to be modified. + + ## Output parameters + * ModifyDBInstance.Output: The standard HTTP response from the ModifyDBInstance API. +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+=,.@-]+$' + DbiResourceId: + type: String + description: (Required) Resource ID of the Amazon RDS instance for which AutoMinorVersionUpgrade needs to be enabled. + allowedPattern: "^db-[A-Z0-9]{26}$" +outputs: + - ModifyDBInstance.Output +mainSteps: + - name: GetRDSInstanceIdentifier + action: "aws:executeAwsApi" + description: | + ## GetRDSInstanceIdentifier + Makes DescribeDBInstances API call using the database instance resource identifier to get DBInstanceIdentifier. + ## Outputs + * DBInstanceIdentifier: DBInstance identifier of the Amazon RDS instance. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: DescribeDBInstances + Filters: + - Name: dbi-resource-id + Values: + - "{{ DbiResourceId }}" + outputs: + - Name: DBInstanceIdentifier + Selector: "$.DBInstances[0].DBInstanceIdentifier" + Type: String + - name: VerifyDBInstanceStatus + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: false + description: | + ## VerifyDBInstanceStatus + Verifies whether AWS RDS DBInstance status is available before enabling AutoMiniorVersionUpgrade. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DBInstanceStatus" + DesiredValues: + - "available" + - name: ModifyDBInstance + action: "aws:executeAwsApi" + description: | + ## ModifyDBInstance + Makes ModifyDBInstance API call to enable AutoMinorVersionUpgrade on the Amazon RDS instance using the DBInstanceIdentifier. + ## Outputs + * Output: The standard HTTP response from the ModifyDBInstance API. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: ModifyDBInstance + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}" + AutoMinorVersionUpgrade: true + outputs: + - Name: Output + Selector: $ + Type: StringMap + - name: VerifyDBInstanceState + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: true + description: | + ## VerifyDBInstanceState + Verifies the Amazon RDS Instance's "AutoMinorVersionUpgrade" property is set to "True". + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].AutoMinorVersionUpgrade" + DesiredValues: + - "True" \ No newline at end of file diff --git a/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml b/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml new file mode 100644 index 00000000..4ced37ab --- /dev/null +++ b/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml @@ -0,0 +1,127 @@ +schemaVersion: "0.3" +description: | + ### Document name - SHARR-EnableMultiAZOnRDSInstance + + ## What does this document do? + This document enables MultiAZ on an RDS instance. + + ## Input Parameters + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * DbiResourceId: (Required) Resource ID of the RDS instance to be modified. + * ApplyImmediately: (Optional) The MultiAZ on an RDS instance change is applied during the next maintenance window unless the ApplyImmediately parameter is enabled (true) for this request. By default, this parameter is disabled (false). + + ## Output Parameters + * EnableMultiAZ.DBInstance: The standard HTTP response from the ModifyDBInstance API. + +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+=,.@-]+$' + DbiResourceId: + type: String + description: (Required) Resource ID of the RDS instance for which MultiAZ needs to be enabled. + allowedPattern: ^db-[A-Z0-9]{26}$ + ApplyImmediately: + type: Boolean + description: (Optional) MultiAZ on an RDS instance change is applied during the next maintenance window unless the ApplyImmediately parameter is enabled (true) for this request. By default, this parameter is disabled (false). + default: False + allowedValues: + - True + - False + +outputs: + - EnableMultiAZ.DBInstance +mainSteps: + - + name: DescribeDBInstances + action: "aws:executeAwsApi" + description: | + ## DescribeDBInstances + Makes DescribeDBInstances API call using RDS DB instance resource identifiers to get DBInstanceIdentifier. + ## Outputs + * DBInstanceIdentifier: DBInstance identifier of the RDS instance. + * MultiAZ: MultiAZ state of the RDS instance. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: DescribeDBInstances + Filters: + - Name: "dbi-resource-id" + Values: + - "{{ DbiResourceId }}" + outputs: + - Name: DBInstanceIdentifier + Selector: $.DBInstances[0].DBInstanceIdentifier + Type: String + - Name: MultiAZ + Selector: $.DBInstances[0].MultiAZ + Type: Boolean + + - + name: VerifyDBInstanceStatus + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: false + description: | + ## VerifyDBInstanceStatus + Verifies if DB instance status is available before enabling MultiAZ. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ DescribeDBInstances.DBInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DBInstanceStatus" + DesiredValues: + - "available" + + - + name: EndIfMultiAZAlreadyEnabled + action: aws:branch + description: | + ## EndIfMultiAZAlreadyEnabled + Checks if MultiAZ is not enabled on the DB instance. If not enabled, proceed with EnableMultiAZ step. Otherwise, end the flow. + inputs: + Choices: + - NextStep: EnableMultiAZ + Variable: "{{ DescribeDBInstances.MultiAZ }}" + BooleanEquals: false + isEnd: true + + - + name: EnableMultiAZ + action: "aws:executeAwsApi" + description: | + ## EnableMultiAZ + Makes ModifyDBInstance API call to enable MultiAZ on the RDS instance using the DBInstanceIdentifier from the previous step and MultiAZ as true. + ## Outputs + * DBInstance: The standard HTTP response from the ModifyDBInstance API. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: ModifyDBInstance + DBInstanceIdentifier: "{{ DescribeDBInstances.DBInstanceIdentifier }}" + MultiAZ: True + ApplyImmediately: "{{ ApplyImmediately }}" + outputs: + - Name: DBInstance + Selector: $ + Type: StringMap + + - + name: VerifyMultiAZEnabled + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: true + description: | + ## VerifyMultiAZEnabled + Verifies that the RDS Instance's `PendingModifiedValues.MultiAZ` value is `True`. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ DescribeDBInstances.DBInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].PendingModifiedValues.MultiAZ" + DesiredValues: + - "True" diff --git a/source/remediation_runbooks/EnableRDSClusterDeletionProtection.yaml b/source/remediation_runbooks/EnableRDSClusterDeletionProtection.yaml index b58eaad6..02c3277f 100644 --- a/source/remediation_runbooks/EnableRDSClusterDeletionProtection.yaml +++ b/source/remediation_runbooks/EnableRDSClusterDeletionProtection.yaml @@ -18,7 +18,7 @@ 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+=,.@-]+ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' ClusterId: type: String description: (Required) Amazon RDS cluster resourceId for which deletion protection needs to be enabled. diff --git a/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml b/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml new file mode 100644 index 00000000..3fee51ea --- /dev/null +++ b/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml @@ -0,0 +1,90 @@ +schemaVersion: "0.3" +description: | + ### Document Name - SHARR-EnableRDSInstanceDeletionProtection + + ## What does this document do? + This document enables `Deletion Protection` on a given Amazon RDS instance using the [ModifyDBInstance](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBInstance.html) API. + + ## Input Parameters + * ApplyImmediately: (Optional) A value that indicates whether the modifications in this request and any pending modifications + are asynchronously applied as soon as possible, regardless of the PreferredMaintenanceWindow setting for the DB instance. + * Default: "false" + * DbInstanceResourceId: (Required) Amazon RDS Instance resourceId for which deletion protection needs to be enabled. + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * EnableRDSInstanceDeletionProtection.ModifyDBInstanceResponse - The standard HTTP response from the ModifyDBInstance API. + +assumeRole: "{{ AutomationAssumeRole }}" +parameters: + ApplyImmediately: + type: Boolean + description: (Optional) A value that indicates whether the modifications in this request and any pending modifications are asynchronously applied as soon as possible, regardless of the PreferredMaintenanceWindow setting for the DB instance. + default: false + 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+=,.@-]+$' + DbInstanceResourceId: + type: String + description: (Required) Resource ID of the Amazon RDS instance for which deletion protection needs to be enabled. + allowedPattern: "^db-[A-Z0-9]{26}$" +outputs: + - EnableRDSInstanceDeletionProtection.ModifyDBInstanceResponse +mainSteps: + - + name: GetRDSInstanceIdentifier + action: "aws:executeAwsApi" + description: | + ## GetRDSInstanceIdentifier + Makes DescribeDBInstances API call using Amazon RDS Instance DbiResourceId to get DBInstance Identifier. + ## Outputs + * DbInstanceIdentifier: DBInstance Identifier of the Amazon RDS Instance. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: DescribeDBInstances + Filters: + - Name: "dbi-resource-id" + Values: + - "{{ DbInstanceResourceId }}" + outputs: + - Name: DbInstanceIdentifier + Selector: $.DBInstances[0].DBInstanceIdentifier + Type: String + - + name: EnableRDSInstanceDeletionProtection + action: "aws:executeAwsApi" + description: | + ## EnableRDSInstanceDeletionProtection + Makes ModifyDBInstance API call to enable deletion protection on the Amazon RDS Instance using the DBInstanceId from the previous action. + ## Outputs + * DbInstance: The standard HTTP response from the ModifyDBInstance API. + timeoutSeconds: 600 + isEnd: false + inputs: + Service: rds + Api: ModifyDBInstance + ApplyImmediately: "{{ ApplyImmediately }}" + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + DeletionProtection: True + outputs: + - Name: ModifyDBInstanceResponse + Selector: $ + Type: StringMap + - + name: VerifyDBInstanceModification + action: "aws:assertAwsResourceProperty" + timeoutSeconds: 600 + isEnd: true + description: | + ## VerifyDBInstanceModification + Checks whether deletion protection is enabled on Amazon RDS Instance. + inputs: + Service: rds + Api: DescribeDBInstances + DBInstanceIdentifier: "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}" + PropertySelector: "$.DBInstances[0].DeletionProtection" + DesiredValues: + - "True" diff --git a/source/remediation_runbooks/EnableRedshiftClusterAuditLogging.yaml b/source/remediation_runbooks/EnableRedshiftClusterAuditLogging.yaml new file mode 100644 index 00000000..8feb7df2 --- /dev/null +++ b/source/remediation_runbooks/EnableRedshiftClusterAuditLogging.yaml @@ -0,0 +1,122 @@ +description: | + ### Document name - AWSConfigRemediation-EnableRedshiftClusterAuditLogging + + ## What does this document do? + This automation document enables audit logging on the Amazon Redshift cluster using [EnableLogging](https://docs.aws.amazon.com/redshift/latest/APIReference/API_EnableLogging.html) API call with given bucket name and s3 key prefix. + + ## Input Parameters + * ClusterIdentifier: (Required) The unique identifier of the Amazon Redshift cluster on which logging to be started. + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * BucketName: (Required) The name of an existing Amazon S3 bucket where the log files are to be stored. + * S3KeyPrefix: (Optional) The prefix applied to the log file names. + + ## Output Parameters + * EnableLoggingWithPrefix.Response: Standard HTTP response of the EnableLogging API. + * EnableLoggingWithoutPrefix.Response: Standard HTTP response of the EnableLogging API. +schemaVersion: "0.3" +assumeRole: "{{ AutomationAssumeRole }}" +outputs: + - EnableLoggingWithoutPrefix.Response + - EnableLoggingWithPrefix.Response +parameters: + ClusterIdentifier: + type: String + description: The unique identifier of the Amazon Redshift cluster on which the logging logging to be started. + allowedPattern: "^(?!.*--)[a-z][a-z0-9-]{0,62}(?- @@ -52,7 +52,7 @@ mainSteps: vpc: '{{VPC}}' remediation_role: '{{RemediationRole}}' kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.7 + Runtime: python3.8 Handler: enable_flow_logs Script: |- %%SCRIPT=EnableVPCFlowLogs.py%% diff --git a/source/remediation_runbooks/EncryptRDSSnapshot.yaml b/source/remediation_runbooks/EncryptRDSSnapshot.yaml new file mode 100644 index 00000000..d563b38c --- /dev/null +++ b/source/remediation_runbooks/EncryptRDSSnapshot.yaml @@ -0,0 +1,143 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +schemaVersion: '0.3' +description: | + ### Document Name - SHARR-EncryptRDSSnapshot + + ## What does this document do? + This document encrypts an RDS snapshot or cluster snapshot. + + ## Input Parameters + * SourceDBSnapshotIdentifier: (Required) The name of the unencrypted RDS snapshot. Note that this snapshot will be deleted as part of this document's execution. + * TargetDBSnapshotIdentifier: (Required) The name of the encrypted RDS snapshot to create. + * DBSnapshotType: (Required) The type of snapshot (DB or cluster). + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + * KmsKeyId: (Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use. If no key is specified, the default encryption key for snapshots (`alias/aws/rds`) will be used. + + ## Output Parameters + * CopyRdsSnapshotToEncryptedRdsSnapshot.EncryptedSnapshotId: The ID of the encrypted RDS snapshot. + * CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot.EncryptedClusterSnapshotId: The ID of the encrypted RDS cluster snapshot. + + ## Minimum Permissions Required + * `rds:CopyDBSnapshot` + * `rds:CopyDBClusterSnapshot` + * `rds:DescribeDBSnapshots` + * `rds:DescribeDBClusterSnapshots` + * `rds:DeleteDBSnapshot` + * `rds:DeleteDBClusterSnapshot` + + ### Key Permissions + If KmsKeyId is a Customer-Managed Key (CMK), then AutomationAssumeRole must have the following permissions on that key: + * `kms:DescribeKey` + * `kms:CreateGrant` +assumeRole: '{{AutomationAssumeRole}}' +parameters: + SourceDBSnapshotIdentifier: + type: 'String' + description: '(Required) The name of the unencrypted RDS snapshot or cluster snapshot to copy.' + allowedPattern: '^(?:rds:)?(?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}$' + TargetDBSnapshotIdentifier: + type: 'String' + description: '(Required) The name of the encrypted RDS snapshot or cluster snapshot to create.' + allowedPattern: '^(?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}$' + DBSnapshotType: + type: 'String' + allowedValues: + - 'snapshot' + - 'cluster-snapshot' + - 'dbclustersnapshot' + 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+=,.@-]+$' + KmsKeyId: + type: 'String' + description: '(Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use to encrypt the snapshot.' + default: 'alias/aws/rds' + 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: +- 'CopyRdsSnapshotToEncryptedRdsSnapshot.EncryptedSnapshotId' +- 'CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot.EncryptedClusterSnapshotId' +mainSteps: +- name: 'ChooseSnapshotOrClusterSnapshot' + action: 'aws:branch' + inputs: + Choices: + - NextStep: 'CopyRdsSnapshotToEncryptedRdsSnapshot' + Variable: '{{DBSnapshotType}}' + StringEquals: 'snapshot' + - Or: + - Variable: '{{DBSnapshotType}}' + StringEquals: 'cluster-snapshot' + - Variable: '{{DBSnapshotType}}' + StringEquals: 'dbclustersnapshot' + NextStep: 'CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot' + +- name: 'CopyRdsSnapshotToEncryptedRdsSnapshot' + action: 'aws:executeAwsApi' + inputs: + Service: 'rds' + Api: 'CopyDBSnapshot' + SourceDBSnapshotIdentifier: '{{SourceDBSnapshotIdentifier}}' + TargetDBSnapshotIdentifier: '{{TargetDBSnapshotIdentifier}}' + CopyTags: true + KmsKeyId: '{{KmsKeyId}}' + outputs: + - Name: 'EncryptedSnapshotId' + Selector: '$.DBSnapshot.DBSnapshotIdentifier' + Type: 'String' +- name: 'VerifyRdsEncryptedSnapshot' + action: 'aws:waitForAwsResourceProperty' + timeoutSeconds: 14400 + inputs: + Service: 'rds' + Api: 'DescribeDBSnapshots' + Filters: + - Name: 'db-snapshot-id' + Values: + - '{{CopyRdsSnapshotToEncryptedRdsSnapshot.EncryptedSnapshotId}}' + PropertySelector: '$.DBSnapshots[0].Status' + DesiredValues: + - 'available' +- name: 'DeleteUnencryptedRdsSnapshot' + action: 'aws:executeAwsApi' + inputs: + Service: 'rds' + Api: 'DeleteDBSnapshot' + DBSnapshotIdentifier: '{{SourceDBSnapshotIdentifier}}' + isEnd: true + +- name: 'CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot' + action: 'aws:executeAwsApi' + inputs: + Service: 'rds' + Api: 'CopyDBClusterSnapshot' + SourceDBClusterSnapshotIdentifier: '{{SourceDBSnapshotIdentifier}}' + TargetDBClusterSnapshotIdentifier: '{{TargetDBSnapshotIdentifier}}' + CopyTags: true + KmsKeyId: '{{KmsKeyId}}' + outputs: + - Name: 'EncryptedClusterSnapshotId' + Selector: '$.DBClusterSnapshot.DBClusterSnapshotIdentifier' + Type: 'String' +- name: 'VerifyRdsEncryptedClusterSnapshot' + action: 'aws:waitForAwsResourceProperty' + timeoutSeconds: 14400 + inputs: + Service: 'rds' + Api: 'DescribeDBClusterSnapshots' + Filters: + - Name: 'db-cluster-snapshot-id' + Values: + - '{{CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot.EncryptedClusterSnapshotId}}' + PropertySelector: '$.DBClusterSnapshots[0].Status' + DesiredValues: + - 'available' +- name: 'DeleteUnencryptedRdsClusterSnapshot' + action: 'aws:executeAwsApi' + inputs: + Service: 'rds' + Api: 'DeleteDBClusterSnapshot' + DBSnapshotIdentifier: '{{SourceDBSnapshotIdentifier}}' + isEnd: true diff --git a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml index be00f7ea..007e90ca 100644 --- a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml +++ b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml @@ -7,7 +7,7 @@ description: | ## 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 @@ -25,8 +25,8 @@ parameters: 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+=,.@/-]+$ + 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+=,.@-]+$' TestMode: type: Boolean description: Enables test mode, which generates a list of fake volume Ids @@ -46,7 +46,7 @@ mainSteps: region: '{{global:REGION}}' account_id: '{{AccountId}}' testmode: '{{TestMode}}' - Runtime: python3.7 + Runtime: python3.8 Handler: get_public_snapshots Script: |- %%SCRIPT=GetPublicEBSSnapshots.py%% @@ -61,7 +61,7 @@ mainSteps: InputPayload: region: '{{global:REGION}}' snapshots: '{{GetPublicSnapshotIds.Snapshots}}' - Runtime: python3.7 + Runtime: python3.8 Handler: make_snapshots_private Script: |- %%SCRIPT=MakeEBSSnapshotsPrivate.py%% diff --git a/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml b/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml index b9d7adf1..2282aea2 100644 --- a/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml +++ b/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml @@ -21,18 +21,18 @@ description: | assumeRole: "{{ AutomationAssumeRole }}" parameters: - DBSnapshotId: + DBSnapshotId: type: String allowedPattern: ^[a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,}$ - DBSnapshotType: + DBSnapshotType: type: String allowedValues: - cluster-snapshot - snapshot 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+=,.@/-]+$ + 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+=,.@-]+$' outputs: - MakeRDSSnapshotPrivate.Output @@ -47,7 +47,7 @@ mainSteps: InputPayload: DBSnapshotType: '{{DBSnapshotType}}' DBSnapshotId: '{{DBSnapshotId}}' - Runtime: python3.7 + Runtime: python3.8 Handler: make_snapshot_private Script: |- %%SCRIPT=MakeRDSSnapshotPrivate.py%% diff --git a/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml b/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml index 7432dde6..55004c57 100644 --- a/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml +++ b/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml @@ -4,13 +4,13 @@ description: | ## What does this document do? This document removes the public resource policy. A public resource policy - contains a principal "*" or AWS: "*", which allows public access to the + contains a principal "*" or AWS: "*", which allows public access to the function. The remediation is to remove the SID of the public policy. ## Input Parameters * FunctionName: name of the AWS Lambda function that has open access policies * 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 * RemoveLambdaPublicAccess.Output - stdout messages from the remediation @@ -22,13 +22,13 @@ description: | assumeRole: "{{ AutomationAssumeRole }}" parameters: - FunctionName: + FunctionName: type: String allowedPattern: ^[a-zA-Z0-9\-_]{1,64}$ 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+=,.@/-]+$ + 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+=,.@-]+$' outputs: - RemoveLambdaPublicAccess.Output @@ -42,7 +42,7 @@ mainSteps: inputs: InputPayload: FunctionName: '{{FunctionName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: remove_lambda_public_access Script: |- %%SCRIPT=RemoveLambdaPublicAccess.py%% diff --git a/source/remediation_runbooks/RemoveVPCDefaultSecurityGroupRules.yaml b/source/remediation_runbooks/RemoveVPCDefaultSecurityGroupRules.yaml index f69926ec..867236af 100644 --- a/source/remediation_runbooks/RemoveVPCDefaultSecurityGroupRules.yaml +++ b/source/remediation_runbooks/RemoveVPCDefaultSecurityGroupRules.yaml @@ -21,7 +21,7 @@ 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+=,.@-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' outputs: - RemoveRulesAndVerify.Output @@ -59,7 +59,7 @@ mainSteps: ## Outputs * Output: Success message or failure exception. inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: handler InputPayload: GroupId: "{{ GroupId }}" diff --git a/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml b/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml new file mode 100644 index 00000000..a0370efd --- /dev/null +++ b/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml @@ -0,0 +1,95 @@ +description: | + ### Document Name - SHARR-ReplaceCodeBuildClearTextCredentials + + ## What does this document do? + This document is used to replace environment variables containing clear text credentials in a CodeBuild project with Amazon EC2 Systems Manager Parameters. + + ## Input Parameters + * ProjectName: (Required) Name of the CodeBuild project (not the ARN). + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * CreateParameters.Parameters - results of the API calls to create SSM parameters + * CreateParameters.Policy - result of the API call to create an IAM policy for the project to access the new parameters + * CreateParameters.AttachResponse - result of the API call to attach the new IAM policy to the project service role + * UpdateProject.Output - result of the API call to update the project environment with the new parameters +schemaVersion: "0.3" +assumeRole: "{{ AutomationAssumeRole }}" +outputs: + - CreateParameters.Parameters + - CreateParameters.Policy + - CreateParameters.AttachResponse + - UpdateProject.Output +parameters: + ProjectName: + type: String + description: (Required) The project name (not the ARN). + allowedPattern: ^[A-Za-z0-9][A-Za-z0-9\-_]{1,254}$ + 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+=,.@-]+$' +mainSteps: + - name: BatchGetProjects + action: "aws:executeAwsApi" + description: | + ## BatchGetProjects + Gets information about one or more build projects. + inputs: + Service: codebuild + Api: BatchGetProjects + names: [ "{{ ProjectName }}" ] + isCritical: true + maxAttempts: 2 + timeoutSeconds: 600 + outputs: + - Name: ProjectInfo + Selector: $.projects[0] + Type: StringMap + - name: CreateParameters + action: "aws:executeScript" + description: | + ## CreateParameters + Parses project environment variables for credentials. + Creates SSM parameters. + Returns new project environment variables and SSM parameter information (without values). + timeoutSeconds: 600 + isCritical: true + inputs: + Runtime: python3.8 + Handler: replace_credentials + InputPayload: + ProjectInfo: "{{ BatchGetProjects.ProjectInfo }}" + Script: |- + %%SCRIPT=ReplaceCodeBuildClearTextCredentials.py%% + outputs: + - Name: UpdatedProjectEnv + Selector: $.Payload.UpdatedProjectEnv + Type: StringMap + - Name: Parameters + Selector: $.Payload.Parameters + Type: MapList + - Name: Policy + Selector: $.Payload.Policy + Type: StringMap + - Name: AttachResponse + Selector: $.Payload.AttachResponse + Type: StringMap + - name: UpdateProject + action: "aws:executeAwsApi" + description: | + ## UpdateProject + Changes the settings of a build project. + isEnd: true + inputs: + Service: codebuild + Api: UpdateProject + name: "{{ ProjectName }}" + environment: "{{ CreateParameters.UpdatedProjectEnv }}" + isCritical: true + maxAttempts: 2 + timeoutSeconds: 600 + outputs: + - Name: Output + Selector: $.Payload.output + Type: StringMap diff --git a/source/remediation_runbooks/RevokeUnrotatedKeys.yaml b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml index e6bba5af..1d05a2c5 100644 --- a/source/remediation_runbooks/RevokeUnrotatedKeys.yaml +++ b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml @@ -18,7 +18,7 @@ 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+=,.@-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' IAMResourceId: type: String description: (Required) IAM resource unique identifier. @@ -26,7 +26,7 @@ parameters: 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)$ + allowedPattern: ^[1-9][0-9]{0,3}|10000$ default: "90" outputs: - RevokeUnrotatedKeys.Output @@ -42,7 +42,7 @@ mainSteps: ## Outputs * Output: Success message or failure Exception. inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: unrotated_key_handler InputPayload: IAMResourceId: "{{ IAMResourceId }}" diff --git a/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml b/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml index 36b00961..80bfaef0 100644 --- a/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml +++ b/source/remediation_runbooks/RevokeUnusedIAMUserCredentials.yaml @@ -13,14 +13,12 @@ description: | ## Output Parameters * RevokeUnusedIAMUserCredentialsAndVerify.Output - Success message or failure Exception. - ## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. - 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+=,.@-]+$ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' IAMResourceId: type: String description: (Required) IAM resource unique identifier. @@ -43,7 +41,7 @@ mainSteps: ## Outputs * Output: Success message or failure Exception. inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: unused_iam_credentials_handler InputPayload: IAMResourceId: "{{ IAMResourceId }}" @@ -79,20 +77,24 @@ mainSteps: if days_since_creation >= max_credential_usage_age: deactivate_key(user_name, key.get("AccessKeyId")) - def user_has_login_profile(user_name): + def get_login_profile(user_name): try: - iam_client.get_login_profile(UserName=user_name) + return iam_client.get_login_profile(UserName=user_name)["LoginProfile"] except iam_client.exceptions.NoSuchEntityException: return False - return True def delete_unused_password(user_name, max_credential_usage_age): user = iam_client.get_user(UserName=user_name).get("User") - if user_has_login_profile(user_name) and user.get("PasswordLastUsed"): + password_last_used_days = 0 + login_profile = get_login_profile(user_name) + if login_profile and user.get("PasswordLastUsed"): password_last_used = user.get("PasswordLastUsed").replace(tzinfo=None) password_last_used_days = (datetime.now() - password_last_used).days - if password_last_used_days >= max_credential_usage_age: - responses["DeleteUnusedPasswordResponse"] = iam_client.delete_login_profile(UserName=user_name) + elif login_profile and not user.get("PasswordLastUsed"): + password_creation_date = login_profile.get("CreateDate").replace(tzinfo=None) + password_last_used_days = (datetime.now() - password_creation_date).days + if password_last_used_days >= max_credential_usage_age: + responses["DeleteUnusedPasswordResponse"] = iam_client.delete_login_profile(UserName=user_name) def verify_expired_credentials_revoked(responses, user_name): if responses.get("DeactivateUnusedKeysResponse"): @@ -124,9 +126,9 @@ mainSteps: def unused_iam_credentials_handler(event, context): iam_resource_id = event.get("IAMResourceId") user_name = get_user_name(iam_resource_id) - + max_credential_usage_age = int(event.get("MaxCredentialUsageAge")) - + access_keys = list_access_keys(user_name) unused_keys = deactivate_unused_keys(access_keys, max_credential_usage_age, user_name) diff --git a/source/remediation_runbooks/S3BlockDenylist.yaml b/source/remediation_runbooks/S3BlockDenylist.yaml new file mode 100644 index 00000000..6cd00307 --- /dev/null +++ b/source/remediation_runbooks/S3BlockDenylist.yaml @@ -0,0 +1,52 @@ +description: | + ### Document Name - SHARR-S3BlockDenyList + + ## What does this document do? + This document adds an explicit DENY to the bucket policy to prevent cross-account access to specific sensitive API calls. By default these are s3:DeleteBucketPolicy, s3:PutBucketAcl, s3:PutBucketPolicy, s3:PutEncryptionConfiguration, and s3:PutObjectAcl. + + ## Input Parameters + * BucketName: (Required) Bucket whose bucket policy is to be restricted. + * DenyList: (Required) List of permissions to be explicitly denied when the Principal contains a role or user in another account. + * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + + ## Output Parameters + * PutS3BucketPolicyDeny.Output + +schemaVersion: "0.3" +assumeRole: "{{ AutomationAssumeRole }}" +parameters: + BucketName: + type: String + description: (Required) The bucket name (not the ARN). + 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])$) + 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+=,.@-]+$' + DenyList: + type: String + description: (Required) Comma-delimited list (string) of permissions to be explicitly denied when the Principal contains a role or user in another account. + allowedPattern: '.*' +outputs: + - PutS3BucketPolicyDeny.Output +mainSteps: + - + name: PutS3BucketPolicyDeny + action: 'aws:executeScript' + description: | + ## PutS3BucketPolicyDeny + Adds an explicit deny to the bucket policy for specific restricted permissions. + timeoutSeconds: 600 + inputs: + InputPayload: + accountid: '{{global:ACCOUNT_ID}}' + bucket: '{{BucketName}}' + denylist: '{{DenyList}}' + Runtime: python3.8 + Handler: update_bucket_policy + Script: |- + %%SCRIPT=PutS3BucketPolicyDeny.py%% + outputs: + - Name: Output + Selector: $.Payload.output + Type: StringMap diff --git a/source/remediation_runbooks/SetIAMPasswordPolicy.yaml b/source/remediation_runbooks/SetIAMPasswordPolicy.yaml index 5742492d..696fd89f 100644 --- a/source/remediation_runbooks/SetIAMPasswordPolicy.yaml +++ b/source/remediation_runbooks/SetIAMPasswordPolicy.yaml @@ -23,14 +23,14 @@ 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+=,.@-]+ + allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\d{12}:role/[\w+=,.@-]+$' AllowUsersToChangePassword: type: Boolean description: (Optional) Allows all IAM users in your AWS account to use the AWS Management Console to change their own passwords. default: false HardExpiry: type: Boolean - description: (Optional) Prevents IAM users from setting a new password after their password has expired. + description: (Optional) Prevents IAM users from setting a new password after their password has expired. default: false MaxPasswordAge: type: Integer @@ -77,7 +77,7 @@ mainSteps: ## Outputs * Output: Success message with HTTP Response from GetAccountPasswordPolicy API call or failure exception. inputs: - Runtime: python3.7 + Runtime: python3.8 Handler: update_and_verify_iam_user_password_policy InputPayload: AllowUsersToChangePassword: "{{ AllowUsersToChangePassword }}" diff --git a/source/remediation_runbooks/SetSSLBucketPolicy.yaml b/source/remediation_runbooks/SetSSLBucketPolicy.yaml index 517074ed..dc55f68a 100644 --- a/source/remediation_runbooks/SetSSLBucketPolicy.yaml +++ b/source/remediation_runbooks/SetSSLBucketPolicy.yaml @@ -9,7 +9,7 @@ description: | * 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 @@ -27,8 +27,8 @@ parameters: 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+=,.@/-]+$ + 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+=,.@-]+$' BucketName: type: String description: Name of the bucket to have a policy added @@ -47,7 +47,7 @@ mainSteps: InputPayload: accountid: '{{AccountId}}' bucket: '{{BucketName}}' - Runtime: python3.7 + Runtime: python3.8 Handler: add_ssl_bucket_policy Script: |- %%SCRIPT=SetSSLBucketPolicy.py%% diff --git a/source/remediation_runbooks/scripts/PutS3BucketPolicyDeny.py b/source/remediation_runbooks/scripts/PutS3BucketPolicyDeny.py new file mode 100644 index 00000000..5f03546c --- /dev/null +++ b/source/remediation_runbooks/scripts/PutS3BucketPolicyDeny.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +''' +Given a bucket name and list of "sensitive" IAM permissions that shall not be +allowed cross-account, create an explicit deny policy for all cross-account +principals, denying access to all IAM permissions in the deny list for all +resources. + +Note: +- The deny list is a comma-separated list configured on the Config rule in parameter blacklistedActionPattern +''' +import json +import boto3 +import copy +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 get_partition(): + return boto3.client('sts', config=BOTO_CONFIG).get_caller_identity().get('Arn').split(':')[1] + +class BucketToRemediate: + def __init__(self, bucketName): + self.bucket_name = bucketName + self.get_partition_where_running() + self.initialize_bucket_policy_to_none() + + def __str__(self): + return json.dumps(self.__dict__) + + def initialize_bucket_policy_to_none(self): + self.bucket_policy = None + + def get_partition_where_running(self): + self.partition = get_partition() + + def set_account_id_from_event(self, event): + self.account_id = event.get('accountid') or exit('AWS Account not specified') + + def set_denylist_from_event(self, event): + self.denylist = event.get('denylist').split(',') or exit('DenyList is empty or not a comma-delimited string') # Expect a comma seperated list in a string + + def get_current_bucket_policy(self): + try: + self.bucket_policy = connect_to_s3().get_bucket_policy( + Bucket=self.bucket_name, + ExpectedBucketOwner=self.account_id + ).get('Policy') + + except Exception as e: + print(e) + exit(f'Failed to retrieve the bucket policy: {self.account_id} {self.bucket_name}') + + def update_bucket_policy(self): + try: + connect_to_s3().put_bucket_policy( + Bucket=self.bucket_name, + ExpectedBucketOwner=self.account_id, + Policy=self.bucket_policy + ) + except Exception as e: + print(e) + exit(f'Failed to store the new bucket policy: {self.account_id} {self.bucket_name}') + + def __principal_is_asterisk(self, principals): + return (True if isinstance(principals, str) and principals == '*' else False) + + def get_account_principals_from_bucket_policy_statement(self, statement_principals): + aws_account_principals = [] + for principal_type, principal in statement_principals.items(): + if principal_type != 'AWS': + continue # not an AWS account + aws_account_principals = principal if isinstance(principal, list) else [ principal ] + return aws_account_principals + + def create_explicit_deny_in_bucket_policy(self): + new_bucket_policy = json.loads(self.bucket_policy) + deny_statement = DenyStatement(self) + for statement in new_bucket_policy['Statement']: + principals = statement.get('Principal', None) + if principals and not self.__principal_is_asterisk(principals): + account_principals = self.get_account_principals_from_bucket_policy_statement(copy.deepcopy(principals)) + deny_statement.add_next_principal_to_deny(account_principals, self.account_id) + + if deny_statement.deny_statement_json: + new_bucket_policy['Statement'].append(deny_statement.deny_statement_json) + self.bucket_policy = json.dumps(new_bucket_policy) + return True + +class DenyStatement: + def __init__(self, bucket_object): + self.bucket_object = bucket_object + self.initialize_deny_statement() + + def initialize_deny_statement(self): + self.deny_statement_json = {} + self.deny_statement_json["Effect"] = "Deny" + self.deny_statement_json["Principal"] = { + "AWS": [] + } + self.deny_statement_json["Action"] = self.bucket_object.denylist + self.deny_statement_json["Resource"] = [ + f'arn:{self.bucket_object.partition}:s3:::{self.bucket_object.bucket_name}', + f'arn:{self.bucket_object.partition}:s3:::{self.bucket_object.bucket_name}/*', + ] + + def __str__(self): + return json.dumps(self.deny_statement_json) + + def add_next_principal_to_deny(self, principals_to_deny, bucket_account): + if len(principals_to_deny) == 0: + return + this_principal = principals_to_deny.pop() + principal_account = this_principal.split(':')[4] + if principal_account and principal_account != bucket_account: + self.add_deny_principal(this_principal) + + self.add_next_principal_to_deny(principals_to_deny, bucket_account) + + def add_deny_principal(self, principal_arn): + if not principal_arn in self.deny_statement_json["Principal"]["AWS"]: + self.deny_statement_json["Principal"]["AWS"].append(principal_arn) + + def add_deny_resource(self, resource_arn): + if self.deny_statement_json["Resource"] and not resource_arn in self.deny_statement_json.Resource: + self.deny_statement_json["Resource"].append(resource_arn) + +def update_bucket_policy(event, context): + def __get_bucket_from_event(event): + bucket = event.get('bucket') or exit('Bucket not specified') + return bucket + + bucket_to_update = BucketToRemediate(__get_bucket_from_event(event)) + bucket_to_update.set_denylist_from_event(event) + bucket_to_update.set_account_id_from_event(event) + bucket_to_update.get_current_bucket_policy() + if bucket_to_update.create_explicit_deny_in_bucket_policy(): + bucket_to_update.update_bucket_policy() + else: + exit(f'Unable to create an explicit deny statement for {bucket_to_update.bucket_name}') diff --git a/source/remediation_runbooks/scripts/ReplaceCodeBuildClearTextCredentials.py b/source/remediation_runbooks/scripts/ReplaceCodeBuildClearTextCredentials.py new file mode 100644 index 00000000..21957b68 --- /dev/null +++ b/source/remediation_runbooks/scripts/ReplaceCodeBuildClearTextCredentials.py @@ -0,0 +1,175 @@ +#!/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 json import dumps +from boto3 import client +from botocore.config import Config +from botocore.exceptions import ClientError +import re + +boto_config = Config(retries = {'mode': 'standard'}) + +CREDENTIAL_NAMES_UPPER = [ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY' +] + +def connect_to_ssm(boto_config): + return client('ssm', config = boto_config) + +def connect_to_iam(boto_config): + return client('iam', config = boto_config) + +def is_clear_text_credential(env_var): + if not env_var.get('type') == 'PLAINTEXT': + return False + return any(env_var.get('name').upper() == credential_name for credential_name in CREDENTIAL_NAMES_UPPER) + +def get_project_ssm_namespace(project_name): + return f'/CodeBuild/{ project_name }' + +def create_parameter(project_name, env_var): + env_var_name = env_var.get('name') + parameter_name = f'{ get_project_ssm_namespace(project_name) }/env/{ env_var_name }' + + ssm_client = connect_to_ssm(boto_config) + try: + response = ssm_client.put_parameter( + Name = parameter_name, + Description = 'Automatically created by SHARR', + Value = env_var.get("value"), + Type = 'SecureString', + Overwrite = False, + DataType = 'text' + ) + except ClientError as client_exception: + exception_type = client_exception.response['Error']['Code'] + if exception_type == 'ParameterAlreadyExists': + print(f'Parameter { parameter_name } already exists. This remediation may have been run before.') + print('Ignoring exception - remediation continues.') + response = None + else: + exit(f'ERROR: Unhandled client exception: { client_exception }') + except Exception as e: + exit(f'ERROR: could not create SSM parameter { parameter_name }: { str(e) }') + + return response, parameter_name + +def create_policy(region, account, partition, project_name): + iam_client = connect_to_iam(boto_config) + policy_resource_filter = f'arn:{ partition }:ssm:{ region }:{ account }:parameter{ get_project_ssm_namespace(project_name) }/*' + policy_document = { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Effect': 'Allow', + 'Action': [ + 'ssm:GetParameter', + 'ssm:GetParameters' + ], + 'Resource': policy_resource_filter + } + ] + } + policy_name = f'CodeBuildSSMParameterPolicy-{ project_name }-{ region }' + try: + response = iam_client.create_policy( + Description = "Automatically created by SHARR", + PolicyDocument = dumps(policy_document), + PolicyName = policy_name + ) + except ClientError as client_exception: + exception_type = client_exception.response['Error']['Code'] + if exception_type == 'EntityAlreadyExists': + print(f'Policy { "" } already exists. This remediation may have been run before.') + print('Ignoring exception - remediation continues.') + # Attach needs to know the ARN of the created policy + response = { + 'Policy': { + 'Arn': f'arn:{ partition }:iam::{ account }:policy/{ policy_name }' + } + } + else: + exit(f'ERROR: Unhandled client exception: { client_exception }') + except Exception as e: + exit(f'ERROR: could not create access policy { policy_name }: { str(e) }') + return response + +def attach_policy(policy_arn, service_role_name): + iam_client = connect_to_iam(boto_config) + try: + response = iam_client.attach_role_policy( + PolicyArn = policy_arn, + RoleName = service_role_name + ) + except ClientError as client_exception: + exit(f'ERROR: Unhandled client exception: { client_exception }') + except Exception as e: + exit(f'ERROR: could not attach policy { policy_arn } to role { service_role_name }: { str(e) }') + return response + +def parse_project_arn(arn): + pattern = re.compile(r'arn:(aws[a-zA-Z-]*):codebuild:([a-z]{2}(?:-gov)?-[a-z]+-\d):(\d{12}):project/[A-Za-z0-9][A-Za-z0-9\-_]{1,254}$') + match = pattern.match(arn) + if match: + partition = match.group(1) + region = match.group(2) + account = match.group(3) + return partition, region, account + else: + raise ValueError + +def replace_credentials(event, context): + project_info = event.get('ProjectInfo') + project_name = project_info.get('name') + project_env = project_info.get('environment') + project_env_vars = project_env.get('environmentVariables') + updated_project_env_vars = [] + parameters = [] + + for env_var in project_env_vars: + if (is_clear_text_credential(env_var)): + parameter_response, parameter_name = create_parameter(project_name, env_var) + updated_env_var = { + 'name': env_var.get('name'), + 'type': 'PARAMETER_STORE', + 'value': parameter_name + } + updated_project_env_vars.append(updated_env_var) + parameters.append(parameter_response) + else: + updated_project_env_vars.append(env_var) + + updated_project_env = project_env + updated_project_env['environmentVariables'] = updated_project_env_vars + + partition, region, account = parse_project_arn(project_info.get('arn')) + policy = create_policy(region, account, partition, project_name) + service_role_arn = project_info.get('serviceRole') + service_role_name = service_role_arn[service_role_arn.rfind('/') + 1:] + attach_response = attach_policy(policy['Policy']['Arn'], service_role_name) + + # datetimes are not serializable, so convert them to ISO 8601 strings + policy_datetime_keys = ['CreateDate', 'UpdateDate'] + for key in policy_datetime_keys: + if key in policy['Policy']: + policy['Policy'][key] = policy['Policy'][key].isoformat() + + return { + 'UpdatedProjectEnv': updated_project_env, + 'Parameters': parameters, + 'Policy': policy, + 'AttachResponse': attach_response + } diff --git a/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py b/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py index 31bf0437..d415ee63 100644 --- a/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py +++ b/source/remediation_runbooks/scripts/RevokeUnrotatedKeys.py @@ -13,8 +13,7 @@ # 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 +from datetime import datetime, timezone, timedelta import boto3 from botocore.config import Config @@ -27,10 +26,6 @@ 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) @@ -61,15 +56,16 @@ def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): 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 + + now = datetime.now(timezone.utc) + days_since_creation = (now - key.get("CreateDate")).days + last_used_days = (now - last_used.get("LastUsedDate", now)).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 @@ -87,7 +83,7 @@ def verify_expired_credentials_revoked(responses, user_name): 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 diff --git a/source/remediation_runbooks/scripts/test/test_puts3bucketpolicydeny.py b/source/remediation_runbooks/scripts/test/test_puts3bucketpolicydeny.py new file mode 100644 index 00000000..92518a9b --- /dev/null +++ b/source/remediation_runbooks/scripts/test/test_puts3bucketpolicydeny.py @@ -0,0 +1,433 @@ +#!/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 PutS3BucketPolicyDeny as remediation + +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries ={ + 'mode': 'standard' + }, + region_name=my_region +) + +def policy_basic_existing(): + return { + "Version": "2008-10-17", + "Id": "MyBucketPolicy", + "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/*" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root" + }, + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + } + ] + } +def policy_basic_expected(): + return { + "Version": "2008-10-17", + "Id": "MyBucketPolicy", + "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/*" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root" + }, + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + }, + { + "Effect": "Deny", + "Principal": { + "AWS": [ "arn:aws:iam::111122223333:root" ], + + }, + "Action": [ + "s3:DeleteBucketPolicy", + "s3:PutBucketAcl", + "s3:PutBucketPolicy", + "s3:PutObjectAcl", + "s3:PutEncryptionConfiguration" + ], + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + } + ] + } + +def policy_multi_principal_existing(): + return { + "Version": "2008-10-17", + "Id": "MyBucketPolicy", + "Statement": [ + { + "Sid": "S3ReplicationPolicyStmt1", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::111122223333:root", + "arn:aws:iam::111122223333:user/Dave", + "arn:aws:iam::222233334444:user/Lalit" + ] + }, + "Action": [ + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:ReplicateObject", + "s3:ReplicateDelete" + ], + "Resource": [ + "arn:aws:s3:::abucket", + "arn:aws:s3:::abucket/*" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root", + "Service": "ssm.amazonaws.com" + }, + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + } + ] + } + +def policy_multi_principal_expected(): + return { + "Version": "2008-10-17", + "Id": "MyBucketPolicy", + "Statement": [ + { + "Sid": "S3ReplicationPolicyStmt1", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::111122223333:root", + "arn:aws:iam::111122223333:user/Dave", + "arn:aws:iam::222233334444:user/Lalit" + ] + }, + "Action": [ + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:ReplicateObject", + "s3:ReplicateDelete" + ], + "Resource": [ + "arn:aws:s3:::abucket", + "arn:aws:s3:::abucket/*" + ] + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root", + "Service": "ssm.amazonaws.com" + }, + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + }, + { + "Effect": "Deny", + "Principal": { + "AWS": + [ + "arn:aws:iam::111122223333:user/Dave", + "arn:aws:iam::111122223333:root" + ] + }, + "Action": [ + "s3:DeleteBucketPolicy", + "s3:PutBucketAcl", + "s3:PutBucketPolicy", + "s3:PutObjectAcl", + "s3:PutEncryptionConfiguration" + ], + "Resource": [ + "arn:aws:s3:::example", + "arn:aws:s3:::example/*" + ] + } + ] + } + +def policy_statement_no_aws_principals(): + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSCloudTrailAclCheck20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a" + }, + { + "Sid": "AWSCloudTrailWrite20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a/AWSLogs/222233334444/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + } + }, + { + "Sid": "ExternalAccount", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:user/test" + }, + "Action": "s3:PutObjectAcl", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a/*" + } + ] + } + +def policy_statement_no_aws_principals_expected(): + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSCloudTrailAclCheck20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a" + }, + { + "Sid": "AWSCloudTrailWrite20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a/AWSLogs/222233334444/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + } + }, + { + "Sid": "ExternalAccount", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:user/test" + }, + "Action": "s3:PutObjectAcl", + "Resource": "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a/*" + }, + { + "Effect": "Deny", + "Principal": { + "AWS": + [ + "arn:aws:iam::111122223333:user/test" + ] + }, + "Action": [ + "s3:DeleteBucketPolicy", + "s3:PutBucketAcl", + "s3:PutBucketPolicy", + "s3:PutObjectAcl", + "s3:PutEncryptionConfiguration" + ], + "Resource": [ + "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a", + "arn:aws:s3:::aws-cloudtrail-logs-222233334444-d425bf6a/*" + ] + } + ] + } + +def event(): + return { + 'bucket': 'example', + 'accountid': '222233334444', + 'denylist': 's3:DeleteBucketPolicy,s3:PutBucketAcl,s3:PutBucketPolicy,s3:PutObjectAcl,s3:PutEncryptionConfiguration' + } + +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_response( + 'get_bucket_policy', + { + "Policy": json.dumps(policy_basic_existing()) + }, + expected_params={ + 'Bucket': 'example', + 'ExpectedBucketOwner': '222233334444' + } + ) + s3_stubber.add_response( + 'put_bucket_policy', + {}, + expected_params={ + 'Bucket': 'example', + 'ExpectedBucketOwner': '222233334444', + 'Policy': json.dumps(policy_basic_expected()) + } + ) + s3_stubber.activate() + mocker.patch('PutS3BucketPolicyDeny.connect_to_s3', return_value=s3_client) + assert remediation.update_bucket_policy(event(), {}) == None + s3_stubber.deactivate() + +def test_new_policy_multiple(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(policy_multi_principal_existing()) + }, + expected_params={ + 'Bucket': 'example', + 'ExpectedBucketOwner': '222233334444' + } + ) + s3_stubber.add_response( + 'put_bucket_policy', + {}, + expected_params={ + 'Bucket': 'example', + 'ExpectedBucketOwner': '222233334444', + 'Policy': json.dumps(policy_multi_principal_expected()) + } + ) + s3_stubber.activate() + mocker.patch('PutS3BucketPolicyDeny.connect_to_s3', return_value=s3_client) + assert remediation.update_bucket_policy(event(), {}) == None + s3_stubber.deactivate() + +def test_policy_statement_no_aws_principals(mocker): + s3_client = botocore.session.get_session().create_client('s3', config=BOTO_CONFIG) + s3_stubber = Stubber(s3_client) + bucket_name = 'aws-cloudtrail-logs-222233334444-d425bf6a' + s3_stubber.add_response( + 'get_bucket_policy', + { + "Policy": json.dumps(policy_statement_no_aws_principals()) + }, + expected_params={ + 'Bucket': bucket_name, + 'ExpectedBucketOwner': '222233334444' + } + ) + s3_stubber.add_response( + 'put_bucket_policy', + {}, + expected_params={ + 'Bucket': bucket_name, + 'ExpectedBucketOwner': '222233334444', + 'Policy': json.dumps(policy_statement_no_aws_principals_expected()) + } + ) + s3_stubber.activate() + mocker.patch('PutS3BucketPolicyDeny.connect_to_s3', return_value=s3_client) + this_event = event() + this_event['bucket'] = bucket_name + assert remediation.update_bucket_policy(this_event, {}) == None + s3_stubber.deactivate() diff --git a/source/remediation_runbooks/scripts/test/test_replacecodebuildcleartextcredentials.py b/source/remediation_runbooks/scripts/test/test_replacecodebuildcleartextcredentials.py new file mode 100644 index 00000000..f9e74a63 --- /dev/null +++ b/source/remediation_runbooks/scripts/test/test_replacecodebuildcleartextcredentials.py @@ -0,0 +1,679 @@ +#!/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 +import boto3.session +import botocore.session +from botocore.stub import Stubber, ANY +from botocore.config import Config +import pytest + +import ReplaceCodeBuildClearTextCredentials as remediation + +my_session = boto3.session.Session() +my_region = my_session.region_name + +BOTO_CONFIG = Config( + retries = { + 'mode': 'standard' + }, + region_name = my_region +) + +class Case: + def __init__(self, env_vars): + self._env_vars = env_vars + self._project_name = 'invoke-codebuild-2' + self._service_role = f'codebuild-{ self._project_name }-service-role' + self._policy_name = f'CodeBuildSSMParameterPolicy-{ self._project_name }-{ my_region }' + self._policy_arn = f'arn:aws:iam::111111111111:policy/{ self._policy_name }' + self._policy_modtime = datetime.now() + + def event(self): + return { + 'ProjectInfo': { + 'name': self._project_name, + 'arn': f'arn:aws:codebuild:{my_region}:111111111111:project/{ self._project_name }', + 'source': { + 'type': 'NO_SOURCE', + 'gitCloneDepth': 1, + 'buildspec': 'version: 0.2\n\nphases:\n build:\n commands:\n - echo \"Hello world!\"\n', + 'insecureSsl': False + }, + 'secondarySources': [], + 'secondarySourceVersions':[], + 'artifacts': { + 'type': 'NO_ARTIFACTS' + }, + 'secondaryArtifacts': [], + 'cache': { + 'type': 'NO_CACHE' + }, + 'environment': { + 'type': 'ARM_CONTAINER', + 'image': 'aws/codebuild/amazonlinux2-aarch64-standard:2.0', + 'computeType': 'BUILD_GENERAL1_SMALL', + 'environmentVariables': self._env_vars, + 'privilegedMode': False, + 'imagePullCredentialsType': 'CODEBUILD' + }, + 'serviceRole': f'arn:aws:iam::111111111111:role/service-role/{ self._service_role }', + 'timeoutInMinutes': 60, + 'queuedTimeoutInMinutes': 480, + 'encryptionKey': f'arn:aws:kms:{my_region}:111111111111:alias/aws/s3', + 'tags': [], + 'created': '2022-01-28T21:59:12.932000+00:00', + 'lastModified': '2022-02-02T19:16:05.722000+00:00', + 'badge': { + 'badgeEnabled': False + }, + 'logsConfig': { + 'cloudWatchLogs': { + 'status': 'DISABLED' + }, + 's3Logs': { + 'status': 'DISABLED', + 'encryptionDisabled': False + } + }, + 'fileSystemLocations': [], + 'projectVisibility': 'PRIVATE' + } + } + + def parameter_name(self, env_var_name): + return f'{ remediation.get_project_ssm_namespace(self._project_name) }/env/{ env_var_name }' + + def policy(self): + return { + 'Policy': { + 'PolicyName': self._policy_name, + 'PolicyId': '1234567812345678', + 'Arn': self._policy_arn, + 'Path': '/', + 'DefaultVersionId': '', + 'AttachmentCount': 0, + 'PermissionsBoundaryUsageCount': 0, + 'IsAttachable': True, + 'Description': '', + 'CreateDate': self._policy_modtime, + 'UpdateDate': self._policy_modtime, + 'Tags': [] + } + } + + def policy_serialized(self): + policy = self.policy() + policy['Policy']['CreateDate'] = policy['Policy']['CreateDate'].isoformat() + policy['Policy']['UpdateDate'] = policy['Policy']['UpdateDate'].isoformat() + return policy + + def attach_params(self): + return { + 'PolicyArn': self._policy_arn, + 'RoleName': self._service_role + } + +def successful_parameter_response(): + return { + 'Tier': 'Standard', + 'Version': 1 + } + +def test_success(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'create_policy', + test_case.policy() + ) + + iam_stubber.add_response( + 'attach_role_policy', + {}, + test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + project_env = test_case.event()['ProjectInfo']['environment'] + project_env['environmentVariables'] = expected_env_vars + successful_response = { + 'AttachResponse': {}, + 'Parameters': [successful_parameter_response()], + 'Policy': test_case.policy_serialized(), + 'UpdatedProjectEnv': project_env + } + + assert remediation.replace_credentials(test_case.event(), {}) == successful_response + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_multiple_params(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + }, + { + 'name': 'AWS_SECRET_ACCESS_KEY', + 'value': 'test_value_2', + 'type': 'PLAINTEXT' + }, + { + 'name': 'AN_ACCEPTABLE_PARAMETER', + 'value': 'test_value_3', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + }, + { + 'name': 'AWS_SECRET_ACCESS_KEY', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[1]['name']) + }, + { + 'name': 'AN_ACCEPTABLE_PARAMETER', + 'value': 'test_value_3', + 'type': 'PLAINTEXT' + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + for env_var in env_vars[0:2]: + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_var['name']), + 'Description': ANY, + 'Value': env_var['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'create_policy', + test_case.policy() + ) + + iam_stubber.add_response( + 'attach_role_policy', + {}, + test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + project_env = test_case.event()['ProjectInfo']['environment'] + project_env['environmentVariables'] = expected_env_vars + successful_response = { + 'AttachResponse': {}, + 'Parameters': [successful_parameter_response()] * 2, + 'Policy': test_case.policy_serialized(), + 'UpdatedProjectEnv': project_env + } + + assert remediation.replace_credentials(test_case.event(), {}) == successful_response + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_param_exists(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_client_error( + 'put_parameter', + 'ParameterAlreadyExists', + expected_params = { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'create_policy', + test_case.policy() + ) + + iam_stubber.add_response( + 'attach_role_policy', + {}, + test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + project_env = test_case.event()['ProjectInfo']['environment'] + project_env['environmentVariables'] = expected_env_vars + successful_response = { + 'AttachResponse': {}, + 'Parameters': [None], + 'Policy': test_case.policy_serialized(), + 'UpdatedProjectEnv': project_env + } + + assert remediation.replace_credentials(test_case.event(), {}) == successful_response + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_policy_exists(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_client_error( + 'create_policy', + 'EntityAlreadyExists' + ) + + iam_stubber.add_response( + 'attach_role_policy', + {}, + test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + project_env = test_case.event()['ProjectInfo']['environment'] + project_env['environmentVariables'] = expected_env_vars + successful_response = { + 'AttachResponse': {}, + 'Parameters': [successful_parameter_response()], + 'Policy': { + 'Policy': { + 'Arn': test_case.policy_serialized()['Policy']['Arn'] + } + }, + 'UpdatedProjectEnv': project_env + } + + assert remediation.replace_credentials(test_case.event(), {}) == successful_response + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_new_param(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'an_existing_parameter', + 'type': 'PARAMETER_STORE' + }, + { + 'name': 'AWS_SECRET_ACCESS_KEY', + 'value': 'test_value_2', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': 'an_existing_parameter' + }, + { + 'name': 'AWS_SECRET_ACCESS_KEY', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[1]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_vars[1]['name']), + 'Description': ANY, + 'Value': env_vars[1]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'create_policy', + test_case.policy() + ) + + iam_stubber.add_response( + 'attach_role_policy', + {}, + test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + project_env = test_case.event()['ProjectInfo']['environment'] + project_env['environmentVariables'] = expected_env_vars + successful_response = { + 'AttachResponse': {}, + 'Parameters': [successful_parameter_response()], + 'Policy': test_case.policy_serialized(), + 'UpdatedProjectEnv': project_env + } + + assert remediation.replace_credentials(test_case.event(), {}) == successful_response + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_put_parameter_fails(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_client_error( + 'put_parameter', + ' InternalServerError', + http_status_code = 500, + expected_params = { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = None) + + with pytest.raises(SystemExit) as wrapped_exception: + remediation.replace_credentials(test_case.event(), {}) + assert wrapped_exception.type == SystemExit + + ssm_stubber.deactivate() + +def test_create_policy_fails(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_client_error( + 'create_policy', + ' ServiceFailure', + http_status_code = 500 + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + with pytest.raises(SystemExit) as wrapped_exception: + remediation.replace_credentials(test_case.event(), {}) + assert wrapped_exception.type == SystemExit + + ssm_stubber.deactivate() + iam_stubber.deactivate() + +def test_attach_policy_fails(mocker): + env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'test_value', + 'type': 'PLAINTEXT' + } + ] + + test_case = Case(env_vars) + + expected_env_vars = [ + { + 'name': 'AWS_ACCESS_KEY_ID', + 'type': 'PARAMETER_STORE', + 'value': test_case.parameter_name(env_vars[0]['name']) + } + ] + + ssm_client = botocore.session.get_session().create_client('ssm', config = BOTO_CONFIG) + ssm_stubber = Stubber(ssm_client) + + ssm_stubber.add_response( + 'put_parameter', + successful_parameter_response(), + { + 'Name': test_case.parameter_name(env_vars[0]['name']), + 'Description': ANY, + 'Value': env_vars[0]['value'], + 'Type': 'SecureString', + 'Overwrite': False, + 'DataType': 'text' + } + ) + + ssm_stubber.activate() + + iam_client = botocore.session.get_session().create_client('iam', config=BOTO_CONFIG) + iam_stubber = Stubber(iam_client) + + iam_stubber.add_response( + 'create_policy', + test_case.policy() + ) + + iam_stubber.add_client_error( + 'attach_role_policy', + 'ServiceFailure', + http_status_code = 500, + expected_params = test_case.attach_params() + ) + + iam_stubber.activate() + + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_ssm', return_value = ssm_client) + mocker.patch('ReplaceCodeBuildClearTextCredentials.connect_to_iam', return_value = iam_client) + + with pytest.raises(SystemExit) as wrapped_exception: + remediation.replace_credentials(test_case.event(), {}) + assert wrapped_exception.type == SystemExit + + ssm_stubber.deactivate() + iam_stubber.deactivate() diff --git a/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py b/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py index 86b3ddcf..8259c62e 100644 --- a/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py +++ b/source/remediation_runbooks/scripts/test/test_revokeunrotatedkeys.py @@ -19,6 +19,7 @@ from botocore.config import Config import pytest from pytest_mock import mocker +from datetime import datetime, timezone import RevokeUnrotatedKeys as remediation @@ -32,66 +33,70 @@ region_name=my_region ) +def str_time_to_datetime(dt_str): + dt_obj = datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc) + return dt_obj + def iam_resource(): return { "resourceIdentifiers": [ { - "resourceType": "AWS::IAM::User", - "resourceId": "AIDACKCEVSQ6C2EXAMPLE", + "resourceType": "AWS::IAM::User", + "resourceId": "AIDACKCEVSQ6C2EXAMPLE", "resourceName": "someuser" } ] } -def event(): +def event(): return { "IAMResourceId": "AIDACKCEVSQ6C2EXAMPLE", "MaxCredentialUsageAge": "90" } -def access_keys(): +def access_keys(): return { "AccessKeyMetadata": [ { - "UserName": "someuser", - "Status": "Active", - "CreateDate": "2015-05-22T14:43:16Z", + "UserName": "someuser", + "Status": "Active", + "CreateDate": str_time_to_datetime("2015-05-22T14:43:16Z"), "AccessKeyId": "AKIAIOSFODNN7EXAMPLE" - }, + }, { - "UserName": "someuser", - "Status": "Active", - "CreateDate": "2032-09-15T15:20:04Z", + "UserName": "someuser", + "Status": "Active", + "CreateDate": datetime.now(timezone.utc), "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" }, { - "UserName": "someuser", - "Status": "Inactive", - "CreateDate": "2017-10-15T15:20:04Z", + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": str_time_to_datetime("2017-10-15T15:20:04Z"), "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" } ] } -def updated_keys(): +def updated_keys(): return { "AccessKeyMetadata": [ { - "UserName": "someuser", - "Status": "Inactive", - "CreateDate": "2015-05-22T14:43:16Z", + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": str_time_to_datetime("2015-05-22T14:43:16Z"), "AccessKeyId": "AKIAIOSFODNN7EXAMPLE" - }, + }, { - "UserName": "someuser", - "Status": "Active", - "CreateDate": "2032-09-15T15:20:04Z", + "UserName": "someuser", + "Status": "Active", + "CreateDate": datetime.now(timezone.utc), "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" }, { - "UserName": "someuser", - "Status": "Inactive", - "CreateDate": "2017-10-15T15:20:04Z", + "UserName": "someuser", + "Status": "Inactive", + "CreateDate": str_time_to_datetime("2017-10-15T15:20:04Z"), "AccessKeyId": "AKIAI44QH8DHBEXAMPLE" } ] @@ -100,19 +105,19 @@ def updated_keys(): def last_accessed_key(id): return { "AKIAIOSFODNN7EXAMPLE": { - "UserName": "someuser", + "UserName": "someuser", "AccessKeyLastUsed": { - "Region": "N/A", - "ServiceName": "s3", - "LastUsedDate": "2016-03-23T19:55:00Z" + "Region": "N/A", + "ServiceName": "s3", + "LastUsedDate": str_time_to_datetime("2016-03-23T19:55:00Z") } }, "AKIAI44QH8DHBEXAMPLE": { - "UserName": "someuser", + "UserName": "someuser", "AccessKeyLastUsed": { - "Region": "N/A", - "ServiceName": "s3", - "LastUsedDate": "2032-10-01T19:55:00Z" + "Region": "N/A", + "ServiceName": "s3", + "LastUsedDate": datetime.now(timezone.utc) } } }[id] @@ -122,7 +127,7 @@ def successful(): 'http_responses': { 'DeactivateUnusedKeysResponse': [ { - 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE', + 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE', 'Response': { 'ResponseMetadata': { 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE' @@ -130,10 +135,10 @@ def successful(): } } ] - }, + }, 'output': 'Verification of unrotated access keys is successful.' } - + #===================================================================================== # SUCCESS #===================================================================================== @@ -182,7 +187,7 @@ def test_success(mocker): }, { 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE', - 'UserName': 'someuser', + 'UserName': 'someuser', 'Status': 'Inactive' } ) @@ -209,6 +214,6 @@ def test_success(mocker): 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/solution_deploy/.DS_Store b/source/solution_deploy/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f0d56391b104dea5cd675b9c03f72ce3b1fa90de GIT binary patch literal 6148 zcmeHKF=_)r43uIQhBPiy?ic)n#W*kU2g1dT;jjyn{;IqyPs@xXg3WP7iZo#a((KJj zyWA9~lbQMcb)ai2c3r(%fCGv3GHdL6N})hu~|VC~8VJKN=SyRRQX z4foW-0ivaV6p#W^Knh5KUn#&w4cj~?DoOz#PBQ!-zu*cPKjA=9yjBhy4h<&@wgp)i*)mzs3--bz@q{+ zq;J{(U*QY$|6`G!q<|FoR|@$0a6IhsO4VCuFUMZn;BRo|e8Fj0M+ri-V_>vnJg^r^F}+ALT&(3{V%D6!>cez5zp>7b^e& literal 0 HcmV?d00001 diff --git a/source/solution_deploy/bin/solution_deploy.ts b/source/solution_deploy/bin/solution_deploy.ts index a076ef5e..8ef8fba7 100644 --- a/source/solution_deploy/bin/solution_deploy.ts +++ b/source/solution_deploy/bin/solution_deploy.ts @@ -1,81 +1,72 @@ #!/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 * as lambda from '@aws-cdk/aws-lambda'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 import { SolutionDeployStack } from '../lib/solution_deploy-stack'; -import { MemberStack } from '../lib/sharr_member-stack'; -import { RemediationRunbookStack, MemberRoleStack } from '../lib/remediation_runbook-stack'; import { OrchLogStack } from '../lib/orchestrator-log-stack'; +import { RemediationRunbookStack, MemberRoleStack } from '../lib/remediation_runbook-stack'; +import { MemberStack } from '../lib/sharr_member-stack'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk_nag from 'cdk-nag'; +import * as cdk from '@aws-cdk/core'; const SOLUTION_ID = process.env['SOLUTION_ID'] || 'unknown'; const SOLUTION_NAME = process.env['SOLUTION_NAME'] || 'unknown'; const SOLUTION_VERSION = process.env['DIST_VERSION'] || '%%VERSION%%'; const SOLUTION_TMN = process.env['SOLUTION_TRADEMARKEDNAME'] || 'unknown'; const SOLUTION_BUCKET = process.env['DIST_OUTPUT_BUCKET'] || 'unknown'; -const LAMBDA_RUNTIME_PYTHON = lambda.Runtime.PYTHON_3_8 +const LAMBDA_RUNTIME_PYTHON = lambda.Runtime.PYTHON_3_8; const app = new cdk.App(); +cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks({verbose: true})); -let LOG_GROUP = `${SOLUTION_ID}-SHARR-Orchestrator` -LOG_GROUP = LOG_GROUP.replace(/^DEV-/,''); // prefix on every resource name +let LOG_GROUP = `${SOLUTION_ID}-SHARR-Orchestrator`; +LOG_GROUP = LOG_GROUP.replace(/^DEV-/, ''); // prefix on every resource name const solStack = new SolutionDeployStack(app, 'SolutionDeployStack', { - description: '(' + SOLUTION_ID + ') ' + SOLUTION_NAME + ' Administrator Stack, ' + SOLUTION_VERSION, - solutionId: SOLUTION_ID, - solutionVersion: SOLUTION_VERSION, - solutionDistBucket: SOLUTION_BUCKET, - solutionTMN: SOLUTION_TMN, - solutionName: SOLUTION_NAME, - runtimePython: LAMBDA_RUNTIME_PYTHON, - orchLogGroup: LOG_GROUP + description: '(' + SOLUTION_ID + ') ' + SOLUTION_NAME + ' Administrator Stack, ' + SOLUTION_VERSION, + solutionId: SOLUTION_ID, + solutionVersion: SOLUTION_VERSION, + solutionDistBucket: SOLUTION_BUCKET, + solutionTMN: SOLUTION_TMN, + solutionName: SOLUTION_NAME, + runtimePython: LAMBDA_RUNTIME_PYTHON, + orchLogGroup: LOG_GROUP }); -solStack.templateOptions.templateFormatVersion = "2010-09-09" +solStack.templateOptions.templateFormatVersion = '2010-09-09'; const memberStack = new MemberStack(app, 'MemberStack', { - description: '(' + SOLUTION_ID + 'M) ' + SOLUTION_NAME + ' Member Account Stack, ' + SOLUTION_VERSION, - solutionId: SOLUTION_ID, - solutionTMN: SOLUTION_TMN, - solutionDistBucket: SOLUTION_BUCKET, - solutionVersion: SOLUTION_VERSION + description: '(' + SOLUTION_ID + 'M) ' + SOLUTION_NAME + ' Member Account Stack, ' + SOLUTION_VERSION, + solutionId: SOLUTION_ID, + solutionTMN: SOLUTION_TMN, + solutionDistBucket: SOLUTION_BUCKET, + solutionVersion: SOLUTION_VERSION, + runtimePython: LAMBDA_RUNTIME_PYTHON }); -memberStack.templateOptions.templateFormatVersion = "2010-09-09" +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, + 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" +roleStack.templateOptions.templateFormatVersion = '2010-09-09'; +cdk_nag.NagSuppressions.addStackSuppressions(roleStack, [ + {id: 'AwsSolutions-IAM5', reason: 'Resource and action wildcards are needed to remediate findings on arbitrary resources'} +]); 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 + 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" +runbookStack.templateOptions.templateFormatVersion = '2010-09-09'; const orchLogStack = new OrchLogStack(app, 'OrchestratorLogStack', { - description: `(${SOLUTION_ID}L) ${SOLUTION_NAME} Orchestrator Log, ${SOLUTION_VERSION}`, - logGroupName: LOG_GROUP, - solutionId: SOLUTION_ID + description: `(${SOLUTION_ID}L) ${SOLUTION_NAME} Orchestrator Log, ${SOLUTION_VERSION}`, + logGroupName: LOG_GROUP, + solutionId: SOLUTION_ID }); -orchLogStack.templateOptions.templateFormatVersion = "2010-09-09" +orchLogStack.templateOptions.templateFormatVersion = '2010-09-09'; diff --git a/source/solution_deploy/lib/remediation_runbook-stack.ts b/source/solution_deploy/lib/remediation_runbook-stack.ts index 78642da7..71e3ea2c 100644 --- a/source/solution_deploy/lib/remediation_runbook-stack.ts +++ b/source/solution_deploy/lib/remediation_runbook-stack.ts @@ -15,24 +15,25 @@ *****************************************************************************/ // -// Remediation Runbook Stack - installs non standard-specific remediation +// Remediation Runbook Stack - installs non standard-specific remediation // runbooks that are used by one or more standards // import * as cdk from '@aws-cdk/core'; -import { - PolicyStatement, - PolicyDocument, - Effect, - Role, - Policy, - ServicePrincipal, - CfnPolicy, - CfnRole +import { + PolicyStatement, + PolicyDocument, + Effect, + Role, + Policy, + ServicePrincipal, + CfnPolicy, + CfnRole } from '@aws-cdk/aws-iam'; -import { OrchestratorMemberRole } from '../../lib/orchestrator_roles-construct' -import { SsmRemediationRunbook, SsmRole } from '../../lib/ssmplaybook'; +import { OrchestratorMemberRole } from '../../lib/orchestrator_roles-construct'; import { AdminAccountParm } from '../../lib/admin_account_parm-construct'; import { Rds6EnhancedMonitoringRole } from '../../remediation_runbooks/rds6-remediation-resources'; +import { RunbookFactory } from './runbook_factory'; +import { SsmRole } from '../../lib/ssmplaybook'; export interface MemberRoleStackProps { readonly description: string; @@ -42,6 +43,8 @@ export interface MemberRoleStackProps { } export class MemberRoleStack extends cdk.Stack { + _orchestratorMemberRole: OrchestratorMemberRole; + constructor(scope: cdk.App, id: string, props: MemberRoleStackProps) { super(scope, id, props); /******************** @@ -53,21 +56,25 @@ export class MemberRoleStack extends cdk.Stack { const adminAccount = new AdminAccountParm(this, 'AdminAccountParameter', { solutionId: props.solutionId }) - new OrchestratorMemberRole(this, 'OrchestratorMemberRole', { + this._orchestratorMemberRole = new OrchestratorMemberRole(this, 'OrchestratorMemberRole', { solutionId: props.solutionId, adminAccountId: adminAccount.adminAccountNumber.valueAsString, adminRoleName: adminRoleName }) } + + getOrchestratorMemberRole(): OrchestratorMemberRole { + return this._orchestratorMemberRole; + } } export interface StackProps { - readonly description: string; - readonly solutionId: string; - readonly solutionVersion: string; - readonly solutionDistBucket: string; - ssmdocs?: string; - roleStack: MemberRoleStack; + readonly description: string; + readonly solutionId: string; + readonly solutionVersion: string; + readonly solutionDistBucket: string; + ssmdocs?: string; + roleStack: MemberRoleStack; } export class RemediationRunbookStack extends cdk.Stack { @@ -123,13 +130,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG // WARN W12: IAM policy should not allow * resource @@ -153,7 +161,7 @@ export class RemediationRunbookStack extends cdk.Stack { { const remediationName = 'CreateLogMetricFilterAndAlarm' const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); - + const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( "logs:PutMetricFilter", @@ -185,13 +193,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) } //----------------------- @@ -216,13 +225,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -317,13 +327,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -336,7 +347,7 @@ export class RemediationRunbookStack extends cdk.Stack { } } } - + //----------------------- // EnableCloudTrailToCloudWatchLogging // @@ -360,8 +371,8 @@ export class RemediationRunbookStack extends cdk.Stack { ) const ctcw_remediation_policy_doc = new PolicyDocument() - ctcw_remediation_policy_doc.addStatements(ctcw_remediation_policy_statement_1) - ctcw_remediation_policy_doc.addStatements(ctcw_remediation_policy_statement_2) + 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(props.roleStack, 'ctcwremediationrole', { assumedBy: new ServicePrincipal(`cloudtrail.${this.urlSuffix}`), @@ -381,7 +392,7 @@ export class RemediationRunbookStack extends cdk.Stack { }] } } - } + } { const ctperms = new PolicyStatement(); ctperms.addActions("cloudtrail:UpdateTrail") @@ -420,13 +431,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR ' + remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR ' + remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) { let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -463,13 +475,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG // WARN W12: IAM policy should not allow * resource @@ -488,6 +501,56 @@ export class RemediationRunbookStack extends cdk.Stack { } } + //----------------------- + // EnableDefaultEncryptionS3 + // + { + const remediationName = 'EnableDefaultEncryptionS3' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + inlinePolicy.addStatements( + new PolicyStatement({ + actions: [ + "s3:PutEncryptionConfiguration", + "kms:GenerateDataKey" + ], + resources: [ + "*" + ], + effect: Effect.ALLOW + }) + ) + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }) + // CFN-NAG + // WARN W12: IAM policy should not allow * resource + + 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.' + },{ + id: 'W28', + reason: 'Static names chosen intentionally to provide integration in cross-account permissions.' + }] + } + } + } //----------------------- // EnableVPCFlowLogs // @@ -586,13 +649,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -620,7 +684,7 @@ export class RemediationRunbookStack extends cdk.Stack { ) s3Perms.effect = Effect.ALLOW s3Perms.addResources("*"); - + inlinePolicy.addStatements(s3Perms) new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { @@ -630,13 +694,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -672,13 +737,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG @@ -694,7 +760,7 @@ export class RemediationRunbookStack extends cdk.Stack { } } } - + //----------------------- // MakeRDSSnapshotPrivate // @@ -717,13 +783,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG @@ -763,13 +830,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG @@ -820,13 +888,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -839,7 +908,7 @@ export class RemediationRunbookStack extends cdk.Stack { } } } - + //----------------------- // SetSSLBucketPolicy // @@ -875,14 +944,350 @@ export class RemediationRunbookStack extends cdk.Stack { } } - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }) + } + + //----------------------- + // ReplaceCodeBuildClearTextCredentials + // + { + const remediationName = 'ReplaceCodeBuildClearTextCredentials' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + "codeBuild:BatchGetProjects", + "codeBuild:UpdateProject", + "ssm:PutParameter", + "iam:CreatePolicy" + ); + remediationPolicy.effect = Effect.ALLOW + remediationPolicy.addResources("*") + inlinePolicy.addStatements(remediationPolicy) + + // CodeBuild projects are built by service roles + const attachRolePolicy = new PolicyStatement(); + attachRolePolicy.addActions('iam:AttachRolePolicy'); + attachRolePolicy.addResources(`arn:${this.partition}:iam::${this.account}:role/service-role/*`); + inlinePolicy.addStatements(attachRolePolicy) + + // Just in case, explicitly deny permission to modify our own role policy + const attachRolePolicyDeny = new PolicyStatement(); + attachRolePolicyDeny.addActions('iam:AttachRolePolicy'); + attachRolePolicyDeny.effect = Effect.DENY; + attachRolePolicyDeny.addResources(`arn:${this.partition}:iam::${this.account}:role/${remediationRoleNameBase}${remediationName}`); + inlinePolicy.addStatements(attachRolePolicyDeny); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) + + 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.' + }] + } + } + } + //---------------------------- + // S3BlockDenyList + // + { + const remediationName = 'S3BlockDenylist' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + "s3:PutBucketPolicy", + "s3:GetBucketPolicy" + ); + remediationPolicy.effect = Effect.ALLOW + remediationPolicy.addResources("*") + inlinePolicy.addStatements(remediationPolicy) + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }) + + 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.' + }] + } + } + } + + //----------------------------------------- + // AWS-EncryptRdsSnapshot + // + { + const remediationName = 'EncryptRDSSnapshot' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'rds:CopyDBSnapshot', + 'rds:CopyDBClusterSnapshot', + 'rds:DescribeDBSnapshots', + 'rds:DescribeDBClusterSnapshots', + 'rds:DeleteDBSnapshot', + 'rds:DeleteDBClusterSnapshots'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + 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.' + } + ] + } + }; + } + + //----------------------- + // DisablePublicAccessToRedshiftCluster + // + { + const remediationName = 'DisablePublicAccessToRedshiftCluster'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'redshift:ModifyCluster', + 'redshift:DescribeClusters'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + const 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.' + } + ] + } + }; + } + //----------------------- + // EnableRedshiftClusterAuditLogging + // + { + const remediationName = 'EnableRedshiftClusterAuditLogging'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'redshift:DescribeLoggingStatus', + 'redshift:EnableLogging'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + remediationPolicy.addActions( + 's3:PutObject'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + const 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.' + } + ] + } + }; + } + + //----------------------- + // EnableAutomaticVersionUpgradeOnRedshiftCluster + // + { + const remediationName = 'EnableAutomaticVersionUpgradeOnRedshiftCluster'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'redshift:ModifyCluster', + 'redshift:DescribeClusters'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + const 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.' + } + ] + } + }; + } + + //----------------------- + // EnableAutomaticSnapshotsOnRedshiftCluster + // + { + const remediationName = 'EnableAutomaticSnapshotsOnRedshiftCluster'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'redshift:ModifyCluster', + 'redshift:DescribeClusters'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources('*'); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + const 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.' + } + ] + } + }; } //========================================================================= @@ -944,7 +1349,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationPermsEc2.effect = Effect.ALLOW remediationPermsEc2.addResources("*"); inlinePolicy.addStatements(remediationPermsEc2) - + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { solutionId: props.solutionId, ssmDocName: remediationName, @@ -962,10 +1367,11 @@ export class RemediationRunbookStack extends cdk.Stack { } } } + //========================================================================= // The following runbooks are copied from AWS-owned documents to make them - // available to GovCloud and China partition customers. The - // SsmRemediationRunbook should be removed when they become available in + // available to GovCloud and China partition customers. The + // SsmRemediationRunbook should be removed when they become available in // aws-cn and aws-us-gov. The SsmRole must be retained. //========================================================================= //----------------------- @@ -991,13 +1397,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -1033,13 +1440,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -1076,13 +1484,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) } @@ -1108,13 +1517,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG // WARN W12: IAM policy should not allow * resource @@ -1166,13 +1576,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG @@ -1214,13 +1625,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG // WARN W12: IAM policy should not allow * resource @@ -1273,13 +1685,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) // CFN-NAG @@ -1295,13 +1708,161 @@ export class RemediationRunbookStack extends cdk.Stack { } } } + + //----------------------- + // AWSConfigRemediation-EnableCopyTagsToSnapshotOnRDSCluster + // + { + const remediationName = 'EnableCopyTagsToSnapshotOnRDSCluster' + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const iamPerms = new PolicyStatement(); + iamPerms.addActions("iam:GetRole") + iamPerms.effect = Effect.ALLOW + iamPerms.addResources( + 'arn:' + this.partition + ':iam::' + this.account + ':role/RDSEnhancedMonitoringRole' + ); + inlinePolicy.addStatements(iamPerms) + + const configPerms = new PolicyStatement(); + configPerms.addActions("config:GetResourceConfigHistory") + configPerms.effect = Effect.ALLOW + configPerms.addResources("*"); + inlinePolicy.addStatements(configPerms) + + const rdsPerms = new PolicyStatement(); + rdsPerms.addActions( + "rds:DescribeDBClusters", + "rds:ModifyDBCluster" + ) + rdsPerms.effect = Effect.ALLOW + rdsPerms.addResources("*"); + inlinePolicy.addStatements(rdsPerms) + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }) + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }) + + // CFN-NAG + // WARN W12: IAM policy should not allow * resource + + 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* RDS database.' + }] + } + } + } + + //----------------------- + // EnableRDSInstanceDeletionProtection + // + { + const remediationName = 'EnableRDSInstanceDeletionProtection'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const rdsPerms = new PolicyStatement(); + rdsPerms.addActions( + 'rds:DescribeDBInstances', + 'rds:ModifyDBInstance' + ); + rdsPerms.addResources('*'); + inlinePolicy.addStatements(rdsPerms); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + 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* RDS database.' + }] + } + }; + } + + //----------------------- + // EnableMultiAZOnRDSInstance + // + { + const remediationName = 'EnableMultiAZOnRDSInstance'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const rdsPerms = new PolicyStatement(); + rdsPerms.addActions( + 'rds:DescribeDBInstances', + 'rds:ModifyDBInstance' + ); + rdsPerms.addResources('*'); + inlinePolicy.addStatements(rdsPerms); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + + 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* RDS database.' + }] + } + }; + } + //----------------------- // AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules // { const remediationName = 'RemoveVPCDefaultSecurityGroupRules' const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); - + const remediationPolicy1 = new PolicyStatement(); remediationPolicy1.addActions( "ec2:UpdateSecurityGroupRuleDescriptionsEgress", @@ -1329,13 +1890,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -1387,13 +1949,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; @@ -1412,7 +1975,7 @@ export class RemediationRunbookStack extends cdk.Stack { { const remediationName = 'SetIAMPasswordPolicy' const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); - + const remediationPolicy = new PolicyStatement(); remediationPolicy.addActions( "iam:UpdateAccountPasswordPolicy", @@ -1432,13 +1995,14 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - new SsmRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, scriptPath: `${ssmdocs}/scripts`, solutionVersion: props.solutionVersion, - solutionDistBucket: props.solutionDistBucket + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId }) let childToMod = inlinePolicy.node.findChild('Resource') as CfnPolicy; childToMod.cfnOptions.metadata = { @@ -1451,5 +2015,92 @@ export class RemediationRunbookStack extends cdk.Stack { } } + //----------------------- + // AWSConfigRemediation-DisablePublicAccessToRDSInstance + // + { + const remediationName = 'DisablePublicAccessToRDSInstance'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'rds:DescribeDBInstances', + 'rds:ModifyDBInstance'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources("*"); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + 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.' + } + ] + } + }; + } + + //----------------------- + // AWSConfigRemediation-EnableMinorVersionUpgradeOnRDSDBInstance + // + { + const remediationName = 'EnableMinorVersionUpgradeOnRDSDBInstance'; + const inlinePolicy = new Policy(props.roleStack, `SHARR-Remediation-Policy-${remediationName}`); + + const remediationPolicy = new PolicyStatement(); + remediationPolicy.addActions( + 'rds:DescribeDBInstances', + 'rds:ModifyDBInstance'); + remediationPolicy.effect = Effect.ALLOW; + remediationPolicy.addResources("*"); + inlinePolicy.addStatements(remediationPolicy); + + new SsmRole(props.roleStack, 'RemediationRole ' + remediationName, { + solutionId: props.solutionId, + ssmDocName: remediationName, + remediationPolicy: inlinePolicy, + remediationRoleName: `${remediationRoleNameBase}${remediationName}` + }); + + RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + ssmDocName: remediationName, + ssmDocPath: ssmdocs, + ssmDocFileName: `${remediationName}.yaml`, + scriptPath: `${ssmdocs}/scripts`, + solutionVersion: props.solutionVersion, + solutionDistBucket: props.solutionDistBucket, + solutionId: props.solutionId + }); + 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.' + } + ] + } + }; + } } } diff --git a/source/solution_deploy/lib/runbook_factory.ts b/source/solution_deploy/lib/runbook_factory.ts new file mode 100644 index 00000000..1a2ba8b8 --- /dev/null +++ b/source/solution_deploy/lib/runbook_factory.ts @@ -0,0 +1,273 @@ +#!/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 { IssmPlaybookProps, RemediationRunbookProps } from '../../lib/ssmplaybook'; +import * as cdk_nag from 'cdk-nag'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as fs from 'fs'; + +export interface RunbookFactoryProps { + solutionId: string; + runtimePython: lambda.Runtime; + solutionDistBucket: string; + solutionTMN: string; + solutionVersion: string; + region: string; + partition: string; +}; + +export class RunbookFactory extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: RunbookFactoryProps) { + super(scope, id); + + const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/, ''); + + const policy = new iam.Policy(this, 'Policy', { + policyName: RESOURCE_PREFIX + '-SHARR_Runbook_Provider_Policy', + statements: [ + new iam.PolicyStatement({ + actions: [ + 'cloudwatch:PutMetricData' + ], + resources: ['*'] + }), + new iam.PolicyStatement({ + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: ['*'] + }), + new iam.PolicyStatement({ + actions: [ + 'ssm:CreateDocument', + 'ssm:UpdateDocument', + 'ssm:UpdateDocumentDefaultVersion', + 'ssm:ListDocumentVersions', + 'ssm:DeleteDocument' + ], + resources: ['*'] + }) + ] + }); + + const cfnPolicy = policy.node.defaultChild as iam.CfnPolicy; + cfnPolicy.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: 'W12', + reason: 'Resource * is required in order to manage arbitrary SSM documents' + } + ] + } + }; + + cdk_nag.NagSuppressions.addResourceSuppressions(policy, [ + {id: 'AwsSolutions-IAM5', reason: 'Resource * is required in order to manage arbitrary SSM documents'} + ]); + + const role = new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + description: 'Lambda role to allow creation of updatable SSM documents' + }); + + role.attachInlinePolicy(policy); + + const SolutionsBucket = s3.Bucket.fromBucketAttributes(this, 'SolutionsBucket', { + bucketName: props.solutionDistBucket + '-' + props.region + }); + + const memberLambdaLayer = new lambda.LayerVersion(this, 'MemberLambdaLayer', { + compatibleRuntimes: [props.runtimePython], + description: 'SO0111 SHARR Common functions used by the solution member stack', + license: 'https://www.apache.org/licenses/LICENSE-2.0', + code: lambda.Code.fromBucket( + SolutionsBucket, + props.solutionTMN + '/' + props.solutionVersion + '/lambda/memberLayer.zip' + ) + }); + + const lambdaFunction = new lambda.Function(this, 'Function', { + functionName: RunbookFactory.getLambdaFunctionName(props.solutionId), + handler: 'updatableRunbookProvider.lambda_handler', + runtime: props.runtimePython, + description: 'Custom resource to manage versioned SSM documents', + code: lambda.Code.fromBucket( + SolutionsBucket, + props.solutionTMN + '/' + props.solutionVersion + '/lambda/updatableRunbookProvider.py.zip' + ), + environment: { + LOG_LEVEL: 'info', + SOLUTION_ID: `AwsSolution/${props.solutionId}/${props.solutionVersion}` + }, + memorySize: 256, + timeout: cdk.Duration.seconds(600), + role: role, + layers: [memberLambdaLayer], + reservedConcurrentExecutions: 1 + }); + + const cfnLambdaFunction = lambdaFunction.node.defaultChild as lambda.CfnFunction; + cfnLambdaFunction.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' + } + ] + } + }; + } + + static getLambdaFunctionName(solutionId: string): string { + const RESOURCE_PREFIX = solutionId.replace(/^DEV-/, ''); + return `${RESOURCE_PREFIX}-SHARR-updatableRunbookProvider`; + } + + static getServiceToken(scope: cdk.Construct, solutionId: string): string { + const stack = cdk.Stack.of(scope); + return `arn:${stack.partition}:lambda:${stack.region}:${stack.account}:function:${RunbookFactory.getLambdaFunctionName(solutionId)}`; + } + + static getResourceType(): string { + return 'Custom::UpdatableRunbook'; + } + + static createControlRunbook(scope: cdk.Construct, id: string, props: IssmPlaybookProps): cdk.CustomResource { + let scriptPath = ''; + if (props.scriptPath == undefined ) { + scriptPath = `${props.ssmDocPath}/scripts`; + } else { + scriptPath = props.scriptPath; + } + + let commonScripts = ''; + if (props.commonScripts == undefined ) { + commonScripts = '../common'; + } else { + commonScripts = props.commonScripts; + } + + const enableParam = new cdk.CfnParameter(scope, 'Enable ' + props.controlId, { + type: 'String', + description: `Enable/disable availability of remediation for ${props.securityStandard} version ${props.securityStandardVersion} Control ${props.controlId} 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.`, + default: 'Available', + allowedValues: ['Available', 'NOT Available'] + }); + + const installSsmDoc = new cdk.CfnCondition(scope, 'Enable ' + props.controlId + ' Condition', { + expression: cdk.Fn.conditionEquals(enableParam, 'Available') + }); + + const ssmDocName = `SHARR-${props.securityStandard}_${props.securityStandardVersion}_${props.controlId}`; + const ssmDocFQFileName = `${props.ssmDocPath}/${props.ssmDocFileName}`; + const ssmDocType = props.ssmDocFileName.substring(props.ssmDocFileName.length - 4).toLowerCase(); + + const ssmDocIn = fs.readFileSync(ssmDocFQFileName, 'utf8'); + + let ssmDocOut: string = ''; + const re = /^(?\s+)%%SCRIPT=(?