From 47fa71fdfc3a7568ae3d3da9ca232746be527ce1 Mon Sep 17 00:00:00 2001 From: Max Granat Date: Thu, 22 Dec 2022 12:57:28 -0500 Subject: [PATCH] Update to v1.5.1 --- .gitignore | 69 + CHANGELOG.md | 12 + NOTICE.txt | 55 +- README.md | 8 +- deployment/build-s3-dist.sh | 2 +- deployment/requirements.txt | 8 +- deployment/testing_requirements.txt | 39 +- source/Orchestrator/check_ssm_doc_state.py | 18 +- .../test/test_check_ssm_doc_state.py | 72 +- .../test/test_get_approval_requirement.py | 28 +- source/lib/orchestrator_roles-construct.ts | 16 +- source/package.json | 45 +- .../AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml | 8 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml | 8 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml | 8 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_Config.1.yaml | 8 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_Lambda.1.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_Redshift.1.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_Redshift.3.yaml | 6 +- .../AFSBP/ssmdocs/AFSBP_Redshift.4.yaml | 8 +- .../AFSBP/ssmdocs/AFSBP_Redshift.6.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml | 6 +- .../playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml | 6 +- .../ssmdocs/scripts/afsbp_parse_input.py | 94 - .../ssmdocs/scripts/test/test_parse_event.py | 193 - .../__snapshots__/afsbp_stack.test.ts.snap | 2191 +-- source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml | 8 +- source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml | 8 +- source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml | 8 +- source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml | 6 +- source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml | 8 +- source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml | 4 +- source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml | 6 +- .../CIS120/ssmdocs/scripts/cis_parse_input.py | 94 - .../ssmdocs/scripts/test/test_parse_event.py | 324 - .../test/__snapshots__/cis_stack.test.ts.snap | 2173 +-- .../NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml | 14 +- .../scripts/newplaybook_parse_input.py | 81 - .../PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml | 10 +- .../PCI321/ssmdocs/PCI_PCI.CW.1.yaml | 8 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml | 8 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml | 8 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.Config.1.yaml | 8 +- .../PCI321/ssmdocs/PCI_PCI.EC2.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.EC2.2.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.EC2.5.yaml | 4 +- .../PCI321/ssmdocs/PCI_PCI.EC2.6.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.IAM.7.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.IAM.8.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.KMS.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.RDS.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.RDS.2.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.S3.1.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.S3.4.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.S3.5.yaml | 6 +- .../PCI321/ssmdocs/PCI_PCI.S3.6.yaml | 6 +- .../PCI321/ssmdocs/scripts/pci_parse_input.py | 94 - .../scripts/test/test_pci_parse_input.py | 217 - .../__snapshots__/pci321_stack.test.ts.snap | 2188 +-- .../CreateAccessLoggingBucket.yaml | 10 +- .../CreateCloudTrailMultiRegionTrail.yaml | 24 +- .../CreateLogMetricFilterAndAlarm.yaml | 2 +- .../DisablePublicAccessToRedshiftCluster.yaml | 2 +- .../remediation_runbooks/EnableAWSConfig.yaml | 4 +- .../EnableAutoScalingGroupELBHealthCheck.yaml | 4 +- ...leAutomaticSnapshotsOnRedshiftCluster.yaml | 2 +- ...omaticVersionUpgradeOnRedshiftCluster.yaml | 2 +- .../EnableCloudTrailEncryption.yaml | 12 +- .../EnableCloudTrailToCloudWatchLogging.yaml | 15 +- .../EnableDefaultEncryptionS3.yaml | 2 +- .../EnableMultiAZOnRDSInstance.yaml | 2 +- .../EnableRDSInstanceDeletionProtection.yaml | 2 +- .../EnableVPCFlowLogs.yaml | 14 +- .../EncryptRDSSnapshot.yaml | 2 +- .../MakeEBSSnapshotsPrivate.yaml | 2 +- .../MakeRDSSnapshotPrivate.yaml | 2 +- .../RemoveLambdaPublicAccess.yaml | 2 +- .../ReplaceCodeBuildClearTextCredentials.yaml | 2 +- .../RevokeUnrotatedKeys.yaml | 2 +- .../remediation_runbooks/S3BlockDenylist.yaml | 2 +- .../SetSSLBucketPolicy.yaml | 2 +- source/solution_deploy/bin/solution_deploy.ts | 2 +- .../lib/remediation_runbook-stack.ts | 77 +- source/solution_deploy/lib/runbook_factory.ts | 194 +- .../solution_deploy/lib/sharr_member-stack.ts | 10 +- .../lib/solution_deploy-stack.ts | 6 +- source/solution_deploy/source/bin/normalizer | 8 - .../admin_account_parm.test.ts.snap | 8 +- .../__snapshots__/member_stack.test.ts.snap | 401 +- .../__snapshots__/orchestrator.test.ts.snap | 326 +- .../orchestrator_logs.test.ts.snap | 72 +- .../__snapshots__/runbook_stack.test.ts.snap | 13046 ++++++++-------- .../solution_deploy.test.ts.snap | 960 +- source/test/member_stack.test.ts | 2 +- source/test/regex_registry.ts | 21 +- source/test/runbook_validator.test.ts | 8 +- source/test/solution_deploy.test.ts | 6 +- source/test/ssmplaybook.test.ts | 4 +- source/test/test_data/tstest-cis29.yaml | 3 +- source/test/test_data/tstest-rds1.yaml | 3 +- source/test/test_data/tstest-runbook.yaml | 17 +- 136 files changed, 11254 insertions(+), 12520 deletions(-) create mode 100644 .gitignore delete mode 100644 source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py delete mode 100644 source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py delete mode 100644 source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py delete mode 100644 source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py delete mode 100644 source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/newplaybook_parse_input.py delete mode 100644 source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py delete mode 100644 source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py delete mode 100755 source/solution_deploy/source/bin/normalizer diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6f03d781 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ + +.DS_Store +inputs.md/* +source/example* +**/__pycache__/* +source/playbooks/**/_description.txt +deployment/temp/* +deployment/test/* + +**/build +**/package +**/global-s3-assets +**/regional-s3-assets +**/open-source +**/.zip +**/tmp +**/out-tsc + +# dependencies +**/node_modules + +# coverage +**/coverage +**/package +**/.coverage + +# misc +**/npm-debug.log +**/testem.log +**/.vscode/settings.json +**/*.zip +**/*local-runner* +**/*create-stack.sh + + +# System Files +**/.DS_Store +**/.vscode + +# CDK files +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Python modules +*.dist-info +source/solution_deploy/source/certifi +source/solution_deploy/source/chardet +source/solution_deploy/source/idna +source/solution_deploy/source/requests +source/solution_deploy/source/urllib3 + +# Parcel build directories +.cache +.build + +*.idea + +# Build files +source/playbooks/*/template +deployment/setenv.sh +source/solution_deploy/source/bin +source/playbooks/*/source/lib/* +deployment/temp diff --git a/CHANGELOG.md b/CHANGELOG.md index acea2c5d..307ac79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ 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.1] - 2022-12-22 + +### Changed + +- Changed SSM document name prefixes from SHARR to ASR to support stack update +- Upgraded Lambda Python runtimes to 3.9 + +### Fixed + +- Reverted SSM document custom resource provider to resolve intermittent deployment errors +- Fixed bug in AFSBP AutoScaling.1 and PCI.AutoScaling.1 remediation regexes + ## [1.5.0] - 2022-05-31 ### Added diff --git a/NOTICE.txt b/NOTICE.txt index bb97d754..270337cf 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,7 +1,7 @@ -AWS Security Hub Automated Response and Remediation Solution +Automated Security Response on AWS 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/ +in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ 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. @@ -11,15 +11,44 @@ THIRD PARTY COMPONENTS ********************** This software includes third party software subject to the following copyrights: -aws-cdk under the Apache License Version 2.0 -aws-sdk under the Apache License Version 2.0 -bandit under the Apache License Version 2.0 -pytest under the MIT License (MIT) -request under the Apache License Version 2.0 -@types/jest under the Massachusetts Institute of Technology (MIT) license -@types/node under the Massachusetts Institute of Technology (MIT) license -@typescript-eslint/eslint-plugin under the Massachusetts Institute of Technology (MIT) license -@typescript-eslint/parser under the BSD-2-Clause license -jest under the Massachusetts Institute of Technology (MIT) license -typescript under the Apache License Version 2.0 +aws-cdk under the Apache License 2.0 +cdk under the Apache License 2.0 +jest under the MIT License +js-yaml under the MIT License +source-map-support under the MIT License +ts-jest under the MIT License +ts-node under the MIT License +typescript under the Apache License 2.0 +attrs under the MIT License +bandit under the Apache License 2.0 +boto3 under the Apache License 2.0 +botocore under the Apache License 2.0 +certifi under the Mozilla Public License 2.0 +charset-normalizer under the MIT License +coverage under the Apache License 2.0 +exceptiongroup under the MIT License +gitdb under the BSD 3-Clause "New" or "Revised" License +GitPython under the BSD 3-Clause "New" or "Revised" License +idna under the BSD 3-Clause "New" or "Revised" License +iniconfig under the MIT License +jmespath under the MIT License +packaging under the Apache License 2.0 +pbr under the Apache License 2.0 +pip under the MIT License +pluggy under the MIT License +pytest under the MIT License +pytest-cov under the MIT License +pytest-env under the MIT License +pytest-mock under the MIT License +python-dateutil under the Apache License 2.0 and the BSD 3-Clause "New" or "Revised" License +PyYAML under the MIT License +requests under the Apache License 2.0 +s3transfer under the Apache License 2.0 +setuptools under the MIT License +six under the MIT License +smmap under the BSD 3-Clause "New" or "Revised" License +stevedore under the Apache License 2.0 +tomli under the MIT License +urllib3 under the MIT License +virtualenv under the MIT License diff --git a/README.md b/README.md index dfd7bc43..eaafbe76 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Unless noted, all of the following changes are within the folder you just create #### Configure the Playbook -Edit **bin/\.ts**. The following 3 lines are critical to definition of the Playbook. These values enable SHARR to map from the StandardsControlArn in a finding to your remediations. +Edit **bin/\.ts**. The following 3 lines are critical to definition of the Playbook. These values enable ASR to map from the StandardsControlArn in a finding to your remediations. ```typescript const standardShortName = 'NPB' @@ -154,13 +154,13 @@ const remediations: IControl[] = [ #### Create the Remediations -Remediations are executed using SSM Automation Runbooks. Each control has a specific runbook. SHARR Runbooks must follow the naming convention in the **/ssmdocs** folder: +Remediations are executed using SSM Automation Runbooks. Each control has a specific runbook. ASR Runbooks must follow the naming convention in the **/ssmdocs** folder: -.yaml -Follow examples from other Playbooks. Your SHARR runbook must parse the finding data, extract the fields needed for remediation, and execute a remediation runbook, passing the role name. +Follow examples from other Playbooks. Your ASR runbook must parse the finding data, extract the fields needed for remediation, and execute a remediation runbook, passing the role name. -Remediation runbooks are defined in the /source/remediation_runbooks and /source/solution_deploy/remediation_runbooks-stack.ts. The remediation examples provided with the solution are fairly robust and self-documenting. Each definition creates an IAM role and an SSM runbook that is called by the SHARR runbook. +Remediation runbooks are defined in the /source/remediation_runbooks and /source/solution_deploy/remediation_runbooks-stack.ts. The remediation examples provided with the solution are fairly robust and self-documenting. Each definition creates an IAM role and an SSM runbook that is called by the ASR runbook. ### Build and Deploy diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index d04f2dfd..442ff58d 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -20,7 +20,7 @@ # This controls the CDK and AWS Solutions Constructs version. Solutions # Constructs versions map 1:1 to CDK versions. When setting this value, # choose the latest AWS Solutions Constructs version. -required_cdk_version=1.155.0 +required_cdk_version=1.183.0 # Get reference for all important folders template_dir="$PWD" diff --git a/deployment/requirements.txt b/deployment/requirements.txt index 4a5625c7..acf0103f 100644 --- a/deployment/requirements.txt +++ b/deployment/requirements.txt @@ -1 +1,7 @@ -requests>=2.25.0 +requests==2.28.1 +## urllib3 should match Lambda runtime +urllib3==1.26.6 +## The following requirements were added by pip freeze: +certifi==2022.12.7 +charset-normalizer==2.1.1 +idna==3.4 diff --git a/deployment/testing_requirements.txt b/deployment/testing_requirements.txt index 221eeb30..7caf0ecb 100644 --- a/deployment/testing_requirements.txt +++ b/deployment/testing_requirements.txt @@ -1,7 +1,32 @@ -pytest-mock>=3.1.0 -pytest>=4.2.1 -pytest-cov -pytest-env -bandit -boto3==1.23.9 -requests==2.27.1 +bandit==1.7.4 +## boto3 and botocore should match Lambda runtime: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html +boto3==1.20.32 +botocore==1.23.32 +pytest==7.2.0 +pytest-cov==4.0.0 +pytest-env==0.8.1 +pytest-mock==3.10.0 +requests==2.28.1 +## urllib3 and six should match Lambda runtime +urllib3==1.26.13 +six==1.16.0 +## The following requirements were added by pip freeze: +attrs==22.1.0 +certifi==2022.12.7 +charset-normalizer==2.1.1 +coverage==6.5.0 +exceptiongroup==1.0.4 +gitdb==4.0.10 +GitPython==3.1.29 +idna==3.4 +iniconfig==1.1.1 +jmespath==0.10.0 +packaging==22.0 +pbr==5.11.0 +pluggy==1.0.0 +python-dateutil==2.8.2 +PyYAML==6.0 +s3transfer==0.5.2 +smmap==5.0.0 +stevedore==4.1.1 +tomli==2.0.1 diff --git a/source/Orchestrator/check_ssm_doc_state.py b/source/Orchestrator/check_ssm_doc_state.py index 197ca800..b8d5d106 100644 --- a/source/Orchestrator/check_ssm_doc_state.py +++ b/source/Orchestrator/check_ssm_doc_state.py @@ -47,8 +47,8 @@ def _get_ssm_client(account, role, region=''): def _add_doc_state_to_answer(doc, account, region, answer): # Connect to APIs ssm = _get_ssm_client( - account, - ORCH_ROLE_NAME, + account, + ORCH_ROLE_NAME, region ) # Validate input @@ -122,7 +122,7 @@ def lambda_handler(event, context): 'standardsupported': finding.standard_version_supported, 'accountid': finding.account_id, 'resourceregion': finding.resource_region - }) + }) if finding.standard_version_supported != 'True': answer.update({ @@ -133,10 +133,10 @@ def lambda_handler(event, context): # Is there alt workflow configuration? alt_workflow_doc = event.get('Workflow',{}).get('WorkflowDocument', None) - - automation_docid = f'SHARR-{finding.standard_shortname}_{finding.standard_version}_{finding.remediation_control}' + + automation_docid = f'ASR-{finding.standard_shortname}_{finding.standard_version}_{finding.remediation_control}' remediation_role = f'SO0111-Remediate-{finding.standard_shortname}-{finding.standard_version}-{finding.remediation_control}' - + answer.update({ 'automationdocid': automation_docid, 'remediationrole': remediation_role @@ -150,9 +150,9 @@ def lambda_handler(event, context): }) else: _add_doc_state_to_answer( - automation_docid, - finding.account_id, - finding.resource_region, + automation_docid, + finding.account_id, + finding.resource_region, answer ) diff --git a/source/Orchestrator/test/test_check_ssm_doc_state.py b/source/Orchestrator/test/test_check_ssm_doc_state.py index 352900f0..083debe6 100644 --- a/source/Orchestrator/test/test_check_ssm_doc_state.py +++ b/source/Orchestrator/test/test_check_ssm_doc_state.py @@ -39,49 +39,49 @@ region_name=my_region ) -def workflow_doc(): +def workflow_doc(): return { "Document": { - "Status": "Active", - "Hash": "15b9f136e2cb0b47490dc5b38b439905e3f36fe1a8a411c1d278f2f2eb6fe633", - "Name": "test-workflow", + "Status": "Active", + "Hash": "15b9f136e2cb0b47490dc5b38b439905e3f36fe1a8a411c1d278f2f2eb6fe633", + "Name": "test-workflow", "Parameters": [ { - "Type": "String", - "Name": "AutomationAssumeRole", + "Type": "String", + "Name": "AutomationAssumeRole", "Description": "The ARN of the role that allows Automation to perform the actions on your behalf." - }, + }, { - "Type": "StringMap", - "Name": "Finding", + "Type": "StringMap", + "Name": "Finding", "Description": "The Finding data from the Orchestrator Step Function" - }, + }, { - "Type": "StringMap", - "Name": "SSMExec", + "Type": "StringMap", + "Name": "SSMExec", "Description": "Data for decision support in this runbook" - }, + }, { - "Type": "String", - "Name": "RemediationDoc", + "Type": "String", + "Name": "RemediationDoc", "Description": "the SHARR Remediation (ingestion) runbook to execute" } - ], - "Tags": [], - "DocumentType": "Automation", + ], + "Tags": [], + "DocumentType": "Automation", "PlatformTypes": [ - "Windows", - "Linux", + "Windows", + "Linux", "MacOS" - ], - "DocumentVersion": "1", - "HashType": "Sha256", - "CreatedDate": 1633985125.065, - "Owner": "111111111111", - "SchemaVersion": "0.3", - "DefaultVersion": "1", - "DocumentFormat": "YAML", - "LatestVersion": "1", + ], + "DocumentVersion": "1", + "HashType": "Sha256", + "CreatedDate": 1633985125.065, + "Owner": "111111111111", + "SchemaVersion": "0.3", + "DefaultVersion": "1", + "DocumentFormat": "YAML", + "LatestVersion": "1", "Description": "### Document Name - SHARR-Run_Remediation\n\n## What does this document do?\nThis document is executed by the AWS Security Hub Automated Response and Remediation Orchestrator Step Function. It implements controls such as manual approvals based on criteria passed by the Orchestrator.\n\n## Input Parameters\n* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.\n* Finding: (Required) json-formatted finding data\n* RemediationDoc: (Required) remediation runbook to execute after approval\n* SSMExec: (Required) json-formatted data for decision support in determining approval requirement\n" } } @@ -118,7 +118,7 @@ def test_sunny_day(mocker): expected_good_response = { 'accountid': '111111111111', - 'automationdocid': 'SHARR-AFSBP_1.0.0_AutoScaling.1', + 'automationdocid': 'ASR-AFSBP_1.0.0_AutoScaling.1', 'controlid': 'AutoScaling.1', 'logdata': [], 'message': '', @@ -226,7 +226,7 @@ def test_sunny_day(mocker): "Tags": [] } },{ - "Name": "SHARR-AFSBP_1.0.0_AutoScaling.1" + "Name": "ASR-AFSBP_1.0.0_AutoScaling.1" } ) @@ -277,10 +277,10 @@ def test_doc_not_active(mocker): expected_good_response = { 'accountid': '111111111111', - 'automationdocid': 'SHARR-AFSBP_1.0.0_AutoScaling.17', + 'automationdocid': 'ASR-AFSBP_1.0.0_AutoScaling.17', 'controlid': 'AutoScaling.17', 'logdata': [], - 'message': 'Document SHARR-AFSBP_1.0.0_AutoScaling.17 does not exist.', + 'message': 'Document ASR-AFSBP_1.0.0_AutoScaling.17 does not exist.', 'remediation_status': '', 'resourceregion': 'us-east-1', 'remediationrole': 'SO0111-Remediate-AFSBP-1.0.0-AutoScaling.17', @@ -372,7 +372,7 @@ def test_client_error(mocker): expected_good_response = { 'accountid': '111111111111', - 'automationdocid': 'SHARR-AFSBP_1.0.0_AutoScaling.1', + 'automationdocid': 'ASR-AFSBP_1.0.0_AutoScaling.1', 'controlid': 'AutoScaling.1', 'logdata': [], 'message': 'An unhandled client error occurred: ADoorIsAjar', @@ -466,7 +466,7 @@ def test_control_remap(mocker): expected_good_response = { 'accountid': '111111111111', - 'automationdocid': 'SHARR-CIS_1.2.0_1.5', + 'automationdocid': 'ASR-CIS_1.2.0_1.5', 'controlid': '1.6', 'logdata': [], 'message': '', @@ -630,7 +630,7 @@ def test_alt_workflow_with_role(mocker): expected_good_response = { 'accountid': '111111111111', - 'automationdocid': 'SHARR-CIS_1.2.0_1.6', + 'automationdocid': 'ASR-CIS_1.2.0_1.6', 'controlid': '1.6', 'logdata': [], 'message': '', diff --git a/source/Orchestrator/test/test_get_approval_requirement.py b/source/Orchestrator/test/test_get_approval_requirement.py index 1d738ac7..8176bdc0 100644 --- a/source/Orchestrator/test/test_get_approval_requirement.py +++ b/source/Orchestrator/test/test_get_approval_requirement.py @@ -101,7 +101,7 @@ def step_input(): "SecurityStandardVersion": "1.0.0", "AccountId": "111111111111", "Message": "Document Status is not \"Active\": unknown", - "AutomationDocId": "SHARR-AFSBP_1.0.0_AutoScaling.1", + "AutomationDocId": "ASR-AFSBP_1.0.0_AutoScaling.1", "RemediationRole": "SO0111-Remediate-AFSBP-1.0.0-AutoScaling.1", "ControlId": "AutoScaling.1", "SecurityStandard": "AFSBP", @@ -113,10 +113,10 @@ def test_get_approval_req(mocker): """ Verifies that it returns the fanout runbook name """ - os.environ['WORKFLOW_RUNBOOK'] = 'SHARR-RunWorkflow' + os.environ['WORKFLOW_RUNBOOK'] = 'ASR-RunWorkflow' os.environ['WORKFLOW_RUNBOOK_ACCOUNT'] = 'member' expected_result = { - 'workflowdoc': "SHARR-RunWorkflow", + 'workflowdoc': "ASR-RunWorkflow", 'workflowaccount': '111111111111', 'workflowrole': '', 'workflow_data': { @@ -171,12 +171,12 @@ def test_get_approval_req(mocker): "Document": { "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", "HashType": "Sha256", - "Name": "SHARR-RunWorkflow", + "Name": "ASR-RunWorkflow", "Owner": "111111111111", "CreatedDate": "2021-05-13T09:01:20.399000-04:00", "Status": "Active", "DocumentVersion": "1", - "Description": "### Document Name - SHARR-RunWorkflow", + "Description": "### Document Name - ASR-RunWorkflow", "Parameters": [ { "Name": "AutomationAssumeRole", @@ -203,7 +203,7 @@ def test_get_approval_req(mocker): "Tags": [] } },{ - "Name": "SHARR-RunWorkflow" + "Name": "ASR-RunWorkflow" } ) @@ -280,12 +280,12 @@ def test_get_approval_req_no_fanout(mocker): "Document": { "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", "HashType": "Sha256", - "Name": "SHARR-RunWorkflow", + "Name": "ASR-RunWorkflow", "Owner": "111111111111", "CreatedDate": "2021-05-13T09:01:20.399000-04:00", "Status": "Active", "DocumentVersion": "1", - "Description": "### Document Name - SHARR-RunWorkflow", + "Description": "### Document Name - ASR-RunWorkflow", "Parameters": [ { "Name": "AutomationAssumeRole", @@ -312,7 +312,7 @@ def test_get_approval_req_no_fanout(mocker): "Tags": [] } },{ - "Name": "SHARR-RunWorkflow" + "Name": "ASR-RunWorkflow" } ) @@ -334,11 +334,11 @@ def test_workflow_in_admin(mocker): """ Verifies that it returns the fanout runbook name """ - os.environ['WORKFLOW_RUNBOOK'] = 'SHARR-RunWorkflow' + os.environ['WORKFLOW_RUNBOOK'] = 'ASR-RunWorkflow' os.environ['WORKFLOW_RUNBOOK_ACCOUNT'] = 'admin' os.environ['WORKFLOW_RUNBOOK_ROLE'] = 'someotheriamrole' expected_result = { - 'workflowdoc': "SHARR-RunWorkflow", + 'workflowdoc': "ASR-RunWorkflow", 'workflowaccount': LOCAL_ACCOUNT, 'workflowrole': 'someotheriamrole', 'workflow_data': { @@ -393,12 +393,12 @@ def test_workflow_in_admin(mocker): "Document": { "Hash": "be480c5a8771035918c439a0c76e1471306a699b7f275fe7e0bea70903dc569a", "HashType": "Sha256", - "Name": "SHARR-RunWorkflow", + "Name": "ASR-RunWorkflow", "Owner": "111111111111", "CreatedDate": "2021-05-13T09:01:20.399000-04:00", "Status": "Active", "DocumentVersion": "1", - "Description": "### Document Name - SHARR-RunWorkflow", + "Description": "### Document Name - ASR-RunWorkflow", "Parameters": [ { "Name": "AutomationAssumeRole", @@ -425,7 +425,7 @@ def test_workflow_in_admin(mocker): "Tags": [] } },{ - "Name": "SHARR-RunWorkflow" + "Name": "ASR-RunWorkflow" } ) diff --git a/source/lib/orchestrator_roles-construct.ts b/source/lib/orchestrator_roles-construct.ts index ac2f12f7..c8a4149c 100644 --- a/source/lib/orchestrator_roles-construct.ts +++ b/source/lib/orchestrator_roles-construct.ts @@ -18,15 +18,15 @@ import { Stack, Construct, ArnFormat } from '@aws-cdk/core'; -import { - PolicyStatement, - Effect, - Role, - PolicyDocument, +import { + PolicyStatement, + Effect, + Role, + PolicyDocument, ArnPrincipal, ServicePrincipal, - CompositePrincipal, - CfnRole + CompositePrincipal, + CfnRole } from '@aws-cdk/aws-iam'; export interface OrchRoleProps { @@ -66,7 +66,7 @@ export class OrchestratorMemberRole extends Construct { service: 'ssm', region: '*', resource: 'document', - resourceName: 'SHARR-*', + resourceName: 'ASR-*', arnFormat: ArnFormat.SLASH_RESOURCE_NAME }), stack.formatArn({ diff --git a/source/package.json b/source/package.json index 58466181..dbd07641 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "1.5.0", + "version": "1.5.1", "description": "Automated remediation for AWS Security Hub (SO0111)", "bin": { "solution_deploy": "bin/solution_deploy.js" @@ -18,30 +18,29 @@ "cdk": "cdk" }, "devDependencies": { - "@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", + "@aws-cdk/assert": "^1.183.0", + "@aws-cdk/aws-events": "^1.183.0", + "@aws-cdk/aws-iam": "^1.183.0", + "@aws-cdk/aws-kms": "^1.183.0", + "@aws-cdk/aws-lambda": "^1.183.0", + "@aws-cdk/aws-logs": "^1.183.0", + "@aws-cdk/aws-s3": "^1.183.0", + "@aws-cdk/aws-sns": "^1.183.0", + "@aws-cdk/aws-ssm": "^1.183.0", + "@aws-cdk/aws-stepfunctions": "^1.183.0", + "@aws-cdk/aws-stepfunctions-tasks": "^1.183.0", + "@aws-cdk/core": "^1.183.0", + "@types/jest": "^29.2.4", "@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": "^28.1.0", + "@types/node": "^18.11.17", + "aws-cdk": "^1.183.0", + "cdk": "^1.183.0", + "cdk-nag": "^1.14.19", + "jest": "^29.3.1", "js-yaml": "^4.1.0", "source-map-support": "^0.5.21", - "ts-jest": "^28.0.2", - "ts-node": "^10.7.0", - "typescript": "^4.6.4" + "ts-jest": "^29.0.3", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" } } diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml index 160c1f31..6234630b 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_AutoScaling.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_AutoScaling.1 + ### Document Name - ASR-AFSBP_1.0.0_AutoScaling.1 ## What does this document do? This document enables ELB healthcheck on a given AutoScaling Group using the [UpdateAutoScalingGroup] API. @@ -59,7 +59,7 @@ mainSteps: 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/(.*)$' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:autoScalingGroup:(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}):autoScalingGroupName/(.{1,255})$' expected_control_id: - 'AutoScaling.1' Runtime: python3.8 @@ -72,7 +72,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck + DocumentName: ASR-EnableAutoScalingGroupELBHealthCheck TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'ASG health check type updated to ELB' - UpdatedBy: 'SHARR-AFSBP_1.0.0_AutoScaling.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_AutoScaling.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml index b67f6543..4fd7ea16 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.1 + ### Document Name - ASR-AFSBP_1.0.0_CloudTrail.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. @@ -25,7 +25,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for this remediation + description: The ARN of the KMS key created by ASR 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 @@ -61,7 +61,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateCloudTrailMultiRegionTrail + DocumentName: ASR-CreateCloudTrailMultiRegionTrail RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' @@ -76,7 +76,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Multi-region, encrypted AWS CloudTrail successfully created' - UpdatedBy: 'SHARR-AFSBP_1.0.0_CloudTrail.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_CloudTrail.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml index af107a6a..ec421b65 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.2.yaml @@ -1,7 +1,7 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.2 + ### Document Name - ASR-AFSBP_1.0.0_CloudTrail.2 ## What does this document do? - This document enables SSE KMS encryption for log files using the SHARR remediation KMS CMK + This document enables SSE KMS encryption for log files using the ASR remediation KMS CMK ## 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. @@ -65,7 +65,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableCloudTrailEncryption + DocumentName: ASR-EnableCloudTrailEncryption RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' @@ -82,7 +82,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: Encryption enabled on CloudTrail - UpdatedBy: SHARR-AFSBP_1.0.0_CloudTrail.2 + UpdatedBy: ASR-AFSBP_1.0.0_CloudTrail.2 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml index d0eee1a3..3c6c9e91 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.4 + ### Document Name - ASR-AFSBP_1.0.0_CloudTrail.4 ## What does this document do? This document enables CloudTrail log file validation. @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailLogFileValidation + DocumentName: ASR-EnableCloudTrailLogFileValidation TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled CloudTrail log file validation.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_CloudTrail.2.4' + UpdatedBy: 'ASR-AFSBP_1.0.0_CloudTrail.2.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml index 5201deff..e065d11a 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CloudTrail.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_CloudTrail.5 + ### Document Name - ASR-AFSBP_1.0.0_CloudTrail.5 ## What does this document do? This document configures CloudTrail to log to CloudWatch Logs. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + DocumentName: ASR-EnableCloudTrailToCloudWatchLogging TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured CloudTrail logging to CloudWatch Logs Group CloudTrail/{{ParseInput.TrailName}}' - UpdatedBy: 'SHARR-AFSBP_1.0.0_CloudTrail.5' + UpdatedBy: 'ASR-AFSBP_1.0.0_CloudTrail.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml index eaa58e02..25c1971d 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_CodeBuild.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_CodeBuild.2 + ### Document Name - ASR-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. @@ -54,7 +54,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-ReplaceCodeBuildClearTextCredentials + DocumentName: ASR-ReplaceCodeBuildClearTextCredentials RuntimeParameters: ProjectName: '{{ ParseInput.ProjectName }}' AutomationAssumeRole: 'arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-ReplaceCodeBuildClearTextCredentials' @@ -68,7 +68,7 @@ mainSteps: ProductArn: '{{ ParseInput.ProductArn }}' Note: Text: 'Replaced clear text credentials with SSM parameters.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_CodeBuild.2' + UpdatedBy: 'ASR-AFSBP_1.0.0_CodeBuild.2' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml index 5748fdd4..cb18ea27 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Config.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_Config.1 + ### Document Name - ASR-AFSBP_1.0.0_Config.1 ## What does this document do? Enables AWS Config: * Turns on recording for all resources. @@ -29,7 +29,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations + description: The ARN of the KMS key created by ASR for remediations allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' outputs: @@ -66,7 +66,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableAWSConfig + DocumentName: ASR-EnableAWSConfig RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' @@ -83,7 +83,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'AWS Config enabled' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Config.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_Config.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml index f4b58c7d..2605f74f 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_EC2.1 + ### Document Name - ASR-AFSBP_1.0.0_EC2.1 ## What does this document do? This document changes all public EC2 snapshots to private @@ -63,7 +63,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-MakeEBSSnapshotsPrivate + DocumentName: ASR-MakeEBSSnapshotsPrivate RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate' @@ -81,7 +81,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'EBS Snapshot modified to private' - UpdatedBy: 'SHARR-AFSBP_1.0.0_EC2.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_EC2.1' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml index 3eb28f85..2d6b0d4a 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_EC2.2 + ### Document Name - ASR-AFSBP_1.0.0_EC2.2 ## What does this document do? This document deletes ingress and egress rules from default security @@ -69,7 +69,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + DocumentName: ASR-RemoveVPCDefaultSecurityGroupRules TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -88,7 +88,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: Removed rules on default security group - UpdatedBy: SHARR-AFSBP_1.0.0_EC2.2 + UpdatedBy: ASR-AFSBP_1.0.0_EC2.2 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml index bc317a8e..cf364c7e 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_EC2.6 + ### Document Name - ASR-AFSBP_1.0.0_EC2.6 ## What does this document do? Enables VPC Flow Logs for a VPC @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableVPCFlowLogs + DocumentName: ASR-EnableVPCFlowLogs TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled VPC Flow Logs for {{ParseInput.VPC}}' - UpdatedBy: 'SHARR-AFSBP_1.0.0_EC2.6' + UpdatedBy: 'ASR-AFSBP_1.0.0_EC2.6' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml index 04828c43..2b78b6b4 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_EC2.7.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_EC2.7 + ### Document Name - ASR-AFSBP_1.0.0_EC2.7 ## What does this document do? This document enables `EBS Encryption by default` for an AWS account in the current region by calling another SSM document ## Input Parameters @@ -59,7 +59,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableEbsEncryptionByDefault + DocumentName: ASR-EnableEbsEncryptionByDefault RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' @@ -74,7 +74,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled EBS encryption by default' - UpdatedBy: 'SHARR-AFSBP_1.0.0_EC2.7' + UpdatedBy: 'ASR-AFSBP_1.0.0_EC2.7' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml index a06f9b70..07b15872 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.3.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_IAM.3 + ### Document Name - ASR-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**. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RevokeUnrotatedKeys + DocumentName: ASR-RevokeUnrotatedKeys RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' @@ -86,7 +86,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deactivated unrotated keys for {{ ParseInput.IAMUser }}.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_IAM.3' + UpdatedBy: 'ASR-AFSBP_1.0.0_IAM.3' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml index ec5e2c89..0cfe8890 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.7.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_IAM.7 + ### Document Name - ASR-AFSBP_1.0.0_IAM.7 ## What does this document do? This document establishes a default password policy. @@ -57,7 +57,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-SetIAMPasswordPolicy + DocumentName: ASR-SetIAMPasswordPolicy RuntimeParameters: AllowUsersToChangePassword: True HardExpiry: True @@ -80,7 +80,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_IAM.7' + UpdatedBy: 'ASR-AFSBP_1.0.0_IAM.7' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml index 5e507947..2140b6d7 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_IAM.8.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_IAM.8 + ### Document Name - ASR-AFSBP_1.0.0_IAM.8 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. @@ -62,7 +62,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RevokeUnusedIAMUserCredentials + DocumentName: ASR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' @@ -77,7 +77,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deactivated unused keys and expired logins using the AWSConfigRemediation-RevokeUnusedIAMUserCredentials runbook.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_IAM.8' + UpdatedBy: 'ASR-AFSBP_1.0.0_IAM.8' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml index 023916cd..0a25e09f 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Lambda.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_Lambda.1 + ### Document Name - ASR-AFSBP_1.0.0_Lambda.1 ## What does this document do? This document removes the public resource policy. A public resource policy @@ -68,7 +68,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-RemoveLambdaPublicAccess + DocumentName: ASR-RemoveLambdaPublicAccess TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -88,7 +88,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Lamdba {{ParseInput.FunctionName}} policy updated to remove public access' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Lambda.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_Lambda.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml index a90d88a0..6cc3b47a 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.1 + ### Document Name - ASR-AFSBP_1.0.0_RDS.1 ## What does this document do? This document changes public RDS snapshot to private @@ -71,7 +71,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-MakeRDSSnapshotPrivate + DocumentName: ASR-MakeRDSSnapshotPrivate TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -92,7 +92,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: RDS DB Snapshot modified to private - UpdatedBy: SHARR-AFSBP_1.0.0_RDS.1 + UpdatedBy: ASR-AFSBP_1.0.0_RDS.1 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml index 06521f20..96741ee5 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.13.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.13 + ### Document Name - ASR-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. @@ -67,7 +67,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableMinorVersionUpgradeOnRDSDBInstance + DocumentName: ASR-EnableMinorVersionUpgradeOnRDSDBInstance TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -85,7 +85,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Minor Version enabled on the RDS Instance.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.13' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.13' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml index aa5c1255..1c187cc6 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.16.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.16 + ### Document Name - ASR-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. @@ -70,7 +70,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableCopyTagsToSnapshotOnRDSCluster + DocumentName: ASR-EnableCopyTagsToSnapshotOnRDSCluster TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Copy Tags to Snapshots enabled on RDS DB cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.16' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.16' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml index d4f57a94..5ec06bf4 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.2.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 --- description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.2 + ### Document Name - ASR-AFSBP_1.0.0_RDS.2 ## What does this document do? This document disables public access to RDS instances by calling another SSM document @@ -64,7 +64,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-DisablePublicAccessToRDSInstance' + DocumentName: 'ASR-DisablePublicAccessToRDSInstance' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -84,7 +84,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to RDS instance' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.2' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.2' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml index 5177e2e4..24913139 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.4.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.4 + ### Document Name - ASR-AFSBP_1.0.0_RDS.4 ## What does this document do? This document encrypts an unencrypted RDS snapshot by calling another SSM document @@ -79,7 +79,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EncryptRDSSnapshot' + DocumentName: 'ASR-EncryptRDSSnapshot' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -102,7 +102,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Encrypted RDS snapshot' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.4' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.4' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml index 936bc418..ac7726c0 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.5.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.RDS.5 + ### Document Name - ASR-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. @@ -66,7 +66,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EnableMultiAZOnRDSInstance' + DocumentName: 'ASR-EnableMultiAZOnRDSInstance' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -87,7 +87,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured RDS cluster for multiple Availability Zones' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.5' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.5' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml index bd8b4bfc..077b28bb 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.6 + ### Document Name - ASR-AFSBP_1.0.0_RDS.6 ## What does this document do? This document enables `Enhanced Monitoring` on a given Amazon RDS instance by calling another SSM document. @@ -87,7 +87,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableEnhancedMonitoringOnRDSInstance + DocumentName: ASR-EnableEnhancedMonitoringOnRDSInstance TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -108,7 +108,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enhanced Monitoring enabled on RDS DB cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.6' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.6' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml index f39bd8d7..50ebfde8 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.7.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_RDS.7 + ### Document Name - ASR-AFSBP_1.0.0_RDS.7 ## What does this document do? This document enables `Deletion Protection` on a given Amazon RDS cluster by calling another SSM document. @@ -70,7 +70,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableRDSClusterDeletionProtection + DocumentName: ASR-EnableRDSClusterDeletionProtection TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deletion protection enabled on RDS DB cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.7' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.7' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml index e1c88ad4..44ffc686 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_RDS.8.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.RDS.8 + ### Document Name - ASR-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. @@ -66,7 +66,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EnableRDSInstanceDeletionProtection' + DocumentName: 'ASR-EnableRDSInstanceDeletionProtection' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -87,7 +87,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled deletion protection on RDS instance' - UpdatedBy: 'SHARR-AFSBP_1.0.0_RDS.8' + UpdatedBy: 'ASR-AFSBP_1.0.0_RDS.8' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml index 4c1aa690..cb5e2248 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.1.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.0_Redshift.1 + ### Document Name - ASR-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 @@ -66,7 +66,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-DisablePublicAccessToRedshiftCluster' + DocumentName: 'ASR-DisablePublicAccessToRedshiftCluster' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -86,7 +86,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to Redshift cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Redshift.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_Redshift.1' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.3.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.3.yaml index 5ea503d4..81380264 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.3.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.3.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.0_Redshift.3 + ### Document Name - ASR-AFSBP_1.0.0_Redshift.3 ## What does this document do? This document enables automatic snapshots on a Redshift cluster by calling another SSM document @@ -82,7 +82,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EnableAutomaticSnapshotsOnRedshiftCluster' + DocumentName: 'ASR-EnableAutomaticSnapshotsOnRedshiftCluster' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -103,7 +103,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled automatic snapshots on Redshift cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Redshift.3' + UpdatedBy: 'ASR-AFSBP_1.0.0_Redshift.3' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.4.yaml index e2ce9120..733a8a1f 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.4.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.4.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.0_Redshift.4 + ### Document Name - ASR-AFSBP_1.0.0_Redshift.4 ## What does this document do? This document disables public access to a Redshift cluster by calling another SSM document @@ -143,7 +143,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EnableRedshiftClusterAuditLogging' + DocumentName: 'ASR-EnableRedshiftClusterAuditLogging' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -164,7 +164,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled Audit logging for the Redshift cluster.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Redshift.4' + UpdatedBy: 'ASR-AFSBP_1.0.0_Redshift.4' Workflow: Status: 'RESOLVED' description: 'Update finding' @@ -179,7 +179,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Remediation failed the s3 bucket name is not available, review the cloudformation template and select the option Yes for create redshift.4 s3 bucket cloudformation parameter.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Redshift.4' + UpdatedBy: 'ASR-AFSBP_1.0.0_Redshift.4' Workflow: Status: 'NOTIFIED' description: 'Abort remediation as s3 bucket name is unavailable.' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.6.yaml index 9fda16c2..0bc2bea3 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_Redshift.6.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-AFSBP_1.0.0_Redshift.6 + ### Document Name - ASR-AFSBP_1.0.0_Redshift.6 ## What does this document do? This document enables automatic version upgrade on a Redshift cluster by calling another SSM document @@ -82,7 +82,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-EnableAutomaticVersionUpgradeOnRedshiftCluster' + DocumentName: 'ASR-EnableAutomaticVersionUpgradeOnRedshiftCluster' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -103,7 +103,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled automatic version upgrade on Redshift cluster' - UpdatedBy: 'SHARR-AFSBP_1.0.0_Redshift.6' + UpdatedBy: 'ASR-AFSBP_1.0.0_Redshift.6' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml index 191cc7a5..b31467be 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_S3.1 + ### Document Name - ASR-AFSBP_1.0.0_S3.1 ## What does this document do? This document blocks public access to all buckets by default at the account level. @@ -59,7 +59,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-ConfigureS3PublicAccessBlock + DocumentName: ASR-ConfigureS3PublicAccessBlock RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock' @@ -78,7 +78,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured the account to block public S3 access.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.1' + UpdatedBy: 'ASR-AFSBP_1.0.0_S3.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml index 8b630047..c5d0224b 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_S3.2 + ### Document Name - ASR-AFSBP_1.0.0_S3.2 ## What does this document do? This document blocks all public access to an S3 bucket. @@ -62,7 +62,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock + DocumentName: ASR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' @@ -81,7 +81,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to S3 bucket.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.2' + UpdatedBy: 'ASR-AFSBP_1.0.0_S3.2' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml index 01cddebd..de359731 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_S3.4 + ### Document Name - ASR-AFSBP_1.0.0_S3.4 ## What does this document do? This document enables AES-256 as the default encryption for an S3 bucket. @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableDefaultEncryptionS3 + DocumentName: ASR-EnableDefaultEncryptionS3 RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' BucketName: '{{ParseInput.BucketName}}' @@ -88,7 +88,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled default encryption for {{ParseInput.BucketName}}' - UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.4' + UpdatedBy: 'ASR-AFSBP_1.0.0_S3.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml index d8e7914e..87171fe7 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_S3.5 + ### Document Name - ASR-AFSBP_1.0.0_S3.5 ## What does this document do? This document adds a bucket policy to restrict internet access to https only. @@ -63,7 +63,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-SetSSLBucketPolicy + DocumentName: ASR-SetSSLBucketPolicy RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AccountId: '{{ParseInput.AccountId}}' @@ -79,7 +79,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Added SSL-only access policy to S3 bucket.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.5' + UpdatedBy: 'ASR-AFSBP_1.0.0_S3.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml index a3e74080..e5afaf34 100644 --- a/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml +++ b/source/playbooks/AFSBP/ssmdocs/AFSBP_S3.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-AFSBP_1.0.0_S3.6 + ### Document Name - ASR-AFSBP_1.0.0_S3.6 ## What does this document do? This document restricts cross-account access to a bucket in the local account. @@ -84,7 +84,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-S3BlockDenylist + DocumentName: ASR-S3BlockDenylist RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' @@ -101,7 +101,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Added explicit deny for sensitive bucket access from another account.' - UpdatedBy: 'SHARR-AFSBP_1.0.0_S3.6' + UpdatedBy: 'ASR-AFSBP_1.0.0_S3.6' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py b/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py deleted file mode 100644 index bdd8202c..00000000 --- a/source/playbooks/AFSBP/ssmdocs/scripts/afsbp_parse_input.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -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/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'] - - 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}') - - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - 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 - } \ No newline at end of file diff --git a/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py b/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py deleted file mode 100644 index 999484f6..00000000 --- a/source/playbooks/AFSBP/ssmdocs/scripts/test/test_parse_event.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -import pytest - -from afsbp_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-1:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1", - "Partition": "aws", - "Region": "us-east-1" - } - ], - "Compliance": { - "Status": "FAILED", - "StatusReasons": [ - { - "ReasonCode": "CONFIG_EVALUATIONS_EMPTY", - "Description": "AWS Config evaluated your resources against the rule. The rule did not apply to the AWS resources in its scope, the specified resources were deleted, or the evaluation results were deleted." - } - ] - }, - "WorkflowState": "NEW", - "Workflow": { - "Status": "NEW" - }, - "RecordState": "ACTIVE" - } - } - -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] - } - -def test_parse_event(): - parsed_event = parse_event(event(), {}) - assert parsed_event == expected() - -def test_parse_event_multimatch(): - expected_result = expected() - expected_result['matches'] = [ - "us-east-1", - "sharr-test-autoscaling-1" - ] - test_event = event() - test_event['resource_index'] = 2 - test_event['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/(.*)$' - parsed_event = parse_event(test_event, {}) - assert parsed_event == expected_result - -def test_bad_finding_id(): - 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(): - test_event = event() - test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-1: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-1: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(): - test_event = event() - test_event['Finding']['Id'] = "arn:aws:securityhub:us-east-1: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(): - 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(): - 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(): - 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-1:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1' - -def test_no_resource_pattern(): - - test_event = event() - expected_result = expected() - - test_event['parse_id_pattern'] = '' - expected_result['resource_id'] = 'arn:aws:autoscaling:us-east-1: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(): - 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/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap index 7e08a132..56051f27 100644 --- a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap +++ b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap @@ -1,27 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Member Stack - AFSBP 1`] = ` -Object { - "Conditions": Object { - "EnableEC21Condition": Object { - "Fn::Equals": Array [ - Object { +{ + "Conditions": { + "EnableEC21Condition": { + "Fn::Equals": [ + { "Ref": "EnableEC21", }, "Available", ], }, - "EnableLambda1Condition": Object { - "Fn::Equals": Array [ - Object { + "EnableLambda1Condition": { + "Fn::Equals": [ + { "Ref": "EnableLambda1", }, "Available", ], }, - "EnableRDS1Condition": Object { - "Fn::Equals": Array [ - Object { + "EnableRDS1Condition": { + "Fn::Equals": [ + { "Ref": "EnableRDS1", }, "Available", @@ -29,9 +29,9 @@ Object { }, }, "Description": "test;", - "Parameters": Object { - "EnableEC21": Object { - "AllowedValues": Array [ + "Parameters": { + "EnableEC21": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -39,8 +39,8 @@ 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", }, - "EnableLambda1": Object { - "AllowedValues": Array [ + "EnableLambda1": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -48,8 +48,8 @@ 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", }, - "EnableRDS1": Object { - "AllowedValues": Array [ + "EnableRDS1": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -57,1000 +57,1039 @@ Object { "Description": "Enable/disable availability of remediation for AFSBP version 1.0.0 Control RDS.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, }, - "Resources": Object { - "AFSBPEC21": Object { + "Resources": { + "ControlAFSBPEC21": { "Condition": "EnableEC21Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "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. - - ## Documentation Links - * [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1) - -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+=,.@-]+$' - -outputs: - - Remediation.Output - - ParseInput.AffectedObject - -mainSteps: - - - name: ParseInput - action: 'aws:executeScript' - outputs: - - Name: FindingId - Selector: $.Payload.finding_id - Type: String - - Name: ProductArn - Selector: $.Payload.product_arn - Type: String - - Name: AffectedObject - Selector: $.Payload.object - Type: StringMap - - Name: AccountId - Selector: $.Payload.account_id - Type: String - - Name: TestMode - Selector: $.Payload.testmode - Type: Boolean - inputs: - InputPayload: - Finding: '{{Finding}}' - parse_id_pattern: '' - resource_index: 2 - expected_control_id: - - 'EC2.1' - Runtime: python3.8 - Handler: parse_event - Script: |- - #!/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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. + +## Documentation Links +* [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1) +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "Finding": "{{Finding}}", + "expected_control_id": [ + "EC2.1", + ], + "parse_id_pattern": "", + "resource_index": 2, }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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' - inputs: - DocumentName: SHARR-MakeEBSSnapshotsPrivate - RuntimeParameters: - AccountId: '{{ParseInput.AccountId}}' - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate' - TestMode: '{{ParseInput.TestMode}}' - isEnd: false - - - - name: UpdateFinding - action: 'aws:executeAwsApi' - inputs: - Service: securityhub - Api: BatchUpdateFindings - FindingIdentifiers: - - Id: '{{ParseInput.FindingId}}' - ProductArn: '{{ParseInput.ProductArn}}' - Note: - Text: 'EBS Snapshot modified to private' - UpdatedBy: 'SHARR-AFSBP_1.0.0_EC2.1' - Workflow: - Status: 'RESOLVED' - description: Update finding - isEnd: true -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-AFSBP_1.0.0_EC2.1", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-MakeEBSSnapshotsPrivate", + "RuntimeParameters": { + "AccountId": "{{ParseInput.AccountId}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate", + "TestMode": "{{ParseInput.TestMode}}", + }, }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "EBS Snapshot modified to private", + "UpdatedBy": "ASR-AFSBP_1.0.0_EC2.1", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the EC2.1 finding", + "type": "StringMap", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-AFSBP_1.0.0_EC2.1", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "AFSBPLambda1": Object { + "ControlAFSBPLambda1": { "Condition": "EnableLambda1Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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. - - ## 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 Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-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 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+=,.@-]+' - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. + +## 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 Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-1) +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "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})$", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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 - } - - - - 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}}' - - - - 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", + + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-RemoveLambdaPublicAccess", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}", + "FunctionName": "{{ ParseInput.FunctionName }}", + }, + "TargetLocations": [ + { + "Accounts": [ + "{{ParseInput.RemediationAccount}}", + ], + "ExecutionRoleName": "{{RemediationRoleName}}", + "Regions": [ + "{{ParseInput.RemediationRegion}}", + ], + }, + ], }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Lamdba {{ParseInput.FunctionName}} policy updated to remove public access", + "UpdatedBy": "ASR-AFSBP_1.0.0_Lambda.1", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the Lambda.1 finding", + "type": "StringMap", + }, + "RemediationRoleName": { + "allowedPattern": "^[\\w+=,.@-]+", + "default": "SO0111-RemoveLambdaPublicAccess", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-AFSBP_1.0.0_Lambda.1", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "AFSBPRDS1": Object { + "ControlAFSBPRDS1": { "Condition": "EnableRDS1Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "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. - - ## 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+=,.@-]+' - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. + +## Documentation Links +* [AFSBP RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-1) +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "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, }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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 - - - 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 - - - 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", + + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "ParseInput", + "nextStep": "Remediation", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-MakeRDSSnapshotPrivate", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}", + "DBSnapshotId": "{{ParseInput.DBSnapshotId}}", + "DBSnapshotType": "{{ParseInput.DBSnapshotType}}", + }, + "TargetLocations": [ + { + "Accounts": [ + "{{ParseInput.RemediationAccount}}", + ], + "ExecutionRoleName": "{{RemediationRoleName}}", + "Regions": [ + "{{ParseInput.RemediationRegion}}", + ], + }, + ], }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "Remediation", + "nextStep": "UpdateFinding", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "RDS DB Snapshot modified to private", + "UpdatedBy": "ASR-AFSBP_1.0.0_RDS.1", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the RDS.1 finding", + "type": "StringMap", + }, + "RemediationRoleName": { + "allowedPattern": "^[\\w+=,.@-]+", + "default": "SO0111-MakeRDSSnapshotPrivate", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-AFSBP_1.0.0_RDS.1", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, }, } `; exports[`Primary Stack - AFSBP 1`] = ` -Object { +{ "Description": "test;", - "Mappings": Object { - "SourceCode": Object { - "General": Object { + "Mappings": { + "SourceCode": { + "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.1.1", "S3Bucket": "sharrbukkit", }, }, }, - "Parameters": Object { - "AFSBPExample1AutoTrigger": Object { - "AllowedValues": Array [ + "Parameters": { + "AFSBPExample1AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -1058,8 +1097,8 @@ Object { "Description": "This will fully enable automated remediation for AFSBP Example.1", "Type": "String", }, - "AFSBPExample3AutoTrigger": Object { - "AllowedValues": Array [ + "AFSBPExample3AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -1067,8 +1106,8 @@ Object { "Description": "This will fully enable automated remediation for AFSBP Example.3", "Type": "String", }, - "AFSBPExample5AutoTrigger": Object { - "AllowedValues": Array [ + "AFSBPExample5AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -1076,56 +1115,56 @@ Object { "Description": "This will fully enable automated remediation for AFSBP Example.5", "Type": "String", }, - "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": Object { + "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/Solutions/SO0111/OrchestratorArn", "Type": "AWS::SSM::Parameter::Value", }, }, - "Resources": Object { - "AFSBPExample1AutoEventRuleC2A31DE2": Object { - "Properties": Object { + "Resources": { + "AFSBPExample1AutoEventRuleC2A31DE2": { + "Properties": { "Description": "Remediate AFSBP Example.1 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "aws-foundational-security-best-practices/v/1.0.0/Example.1", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "AFSBP_Example.1_AutoTrigger", - "State": Object { + "State": { "Ref": "AFSBPExample1AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "AFSBPExample1EventsRuleRole2EAEBD38", "Arn", ], @@ -1135,14 +1174,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "AFSBPExample1EventsRuleRole2EAEBD38": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample1EventsRuleRole2EAEBD38": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -1152,14 +1191,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "AFSBPExample1EventsRuleRoleDefaultPolicy7C237931": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample1EventsRuleRoleDefaultPolicy7C237931": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -1167,58 +1206,58 @@ Object { "Version": "2012-10-17", }, "PolicyName": "AFSBPExample1EventsRuleRoleDefaultPolicy7C237931", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "AFSBPExample1EventsRuleRole2EAEBD38", }, ], }, "Type": "AWS::IAM::Policy", }, - "AFSBPExample3AutoEventRule804387B8": Object { - "Properties": Object { + "AFSBPExample3AutoEventRule804387B8": { + "Properties": { "Description": "Remediate AFSBP Example.3 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "aws-foundational-security-best-practices/v/1.0.0/Example.3", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "AFSBP_Example.3_AutoTrigger", - "State": Object { + "State": { "Ref": "AFSBPExample3AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "AFSBPExample3EventsRuleRole5956A03B", "Arn", ], @@ -1228,14 +1267,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "AFSBPExample3EventsRuleRole5956A03B": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample3EventsRuleRole5956A03B": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -1245,14 +1284,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "AFSBPExample3EventsRuleRoleDefaultPolicy6964C066": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample3EventsRuleRoleDefaultPolicy6964C066": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -1260,58 +1299,58 @@ Object { "Version": "2012-10-17", }, "PolicyName": "AFSBPExample3EventsRuleRoleDefaultPolicy6964C066", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "AFSBPExample3EventsRuleRole5956A03B", }, ], }, "Type": "AWS::IAM::Policy", }, - "AFSBPExample5AutoEventRuleD0EDB507": Object { - "Properties": Object { + "AFSBPExample5AutoEventRuleD0EDB507": { + "Properties": { "Description": "Remediate AFSBP Example.5 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "aws-foundational-security-best-practices/v/1.0.0/Example.5", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "AFSBP_Example.5_AutoTrigger", - "State": Object { + "State": { "Ref": "AFSBPExample5AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "AFSBPExample5EventsRuleRole80D6194D", "Arn", ], @@ -1321,14 +1360,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "AFSBPExample5EventsRuleRole80D6194D": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample5EventsRuleRole80D6194D": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -1338,14 +1377,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "AFSBPExample5EventsRuleRoleDefaultPolicy34C14018": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "AFSBPExample5EventsRuleRoleDefaultPolicy34C14018": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -1353,16 +1392,16 @@ Object { "Version": "2012-10-17", }, "PolicyName": "AFSBPExample5EventsRuleRoleDefaultPolicy34C14018", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "AFSBPExample5EventsRuleRole80D6194D", }, ], }, "Type": "AWS::IAM::Policy", }, - "StandardShortName7DDF6BE6": Object { - "Properties": Object { + "StandardShortName7DDF6BE6": { + "Properties": { "Description": "Provides a short (1-12) character abbreviation for the standard.", "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/shortname", "Type": "String", @@ -1370,8 +1409,8 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "StandardVersionCB2C6951": Object { - "Properties": Object { + "StandardVersionCB2C6951": { + "Properties": { "Description": "This parameter controls whether the SHARR step function will process findings for this version of the standard.", "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/status", "Type": "String", diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml index 5f0dad27..09896fe9 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.3.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_1.3 + ### Document Name - ASR-CIS_1.2.0_1.3 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. @@ -62,7 +62,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RevokeUnusedIAMUserCredentials + DocumentName: ASR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' @@ -77,7 +77,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deactivated unused keys and expired logins for {{ ParseInput.IAMUser }}.' - UpdatedBy: 'SHARR-CIS_1.2.0_1.3' + UpdatedBy: 'ASR-CIS_1.2.0_1.3' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml index 365adf01..b5e9f7d7 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_1.4 + ### Document Name - ASR-CIS_1.2.0_1.4 ## What does this document do? This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RevokeUnrotatedKeys + DocumentName: ASR-RevokeUnrotatedKeys RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}' @@ -86,7 +86,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deactivated unrotated keys for {{ ParseInput.IAMUser }}.' - UpdatedBy: 'SHARR-CIS_1.2.0_1.4' + UpdatedBy: 'ASR-CIS_1.2.0_1.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml index 90dc1827..c86bda57 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_1.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_1.5 + ### Document Name - ASR-CIS_1.2.0_1.5 ## What does this document do? This document establishes a default password policy. @@ -64,7 +64,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-SetIAMPasswordPolicy + DocumentName: ASR-SetIAMPasswordPolicy RuntimeParameters: AllowUsersToChangePassword: True HardExpiry: True @@ -87,7 +87,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.' - UpdatedBy: 'SHARR-CIS_1.2.0_1.5' + UpdatedBy: 'ASR-CIS_1.2.0_1.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml index 17aac947..326bf482 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.1 + ### Document Name - ASR-CIS_1.2.0_2.1 ## What does this document do? Creates a multi-region trail with KMS encryption and enables CloudTrail @@ -26,7 +26,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for this remediation + description: The ARN of the KMS key created by ASR 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 @@ -68,7 +68,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateCloudTrailMultiRegionTrail + DocumentName: ASR-CreateCloudTrailMultiRegionTrail RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' @@ -83,7 +83,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Multi-region, encrypted AWS CloudTrail successfully created' - UpdatedBy: 'SHARR-CIS_1.2.0_2.11' + UpdatedBy: 'ASR-CIS_1.2.0_2.11' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml index 0b07f975..752cdf42 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.2 + ### Document Name - ASR-CIS_1.2.0_2.2 ## What does this document do? This document enables CloudTrail log file validation. @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailLogFileValidation + DocumentName: ASR-EnableCloudTrailLogFileValidation TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled CloudTrail log file validation.' - UpdatedBy: 'SHARR-CIS_1.2.0_2.2' + UpdatedBy: 'ASR-CIS_1.2.0_2.2' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml index afbdba4d..5148dc9b 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.3.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.3 + ### Document Name - ASR-CIS_1.2.0_2.3 ## What does this document do? This document blocks public access to the CloudTrail S3 bucket. @@ -61,7 +61,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock + DocumentName: ASR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' @@ -80,7 +80,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to CloudTrail logs bucket.' - UpdatedBy: 'SHARR-CIS_1.2.0_2.3' + UpdatedBy: 'ASR-CIS_1.2.0_2.3' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml index 9e895e5e..3092732a 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.4 + ### Document Name - ASR-CIS_1.2.0_2.4 ## What does this document do? This document configures CloudTrail to log to CloudWatch Logs. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + DocumentName: ASR-EnableCloudTrailToCloudWatchLogging TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured CloudTrail logging to CloudWatch Logs Group CloudTrail/{{ParseInput.TrailName}}' - UpdatedBy: 'SHARR-CIS_1.2.0_2.4' + UpdatedBy: 'ASR-CIS_1.2.0_2.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml index 10ce9e83..6e2ac605 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.5 + ### Document Name - ASR-CIS_1.2.0_2.5 ## What does this document do? Enables AWS Config: * Turns on recording for all resources. @@ -29,7 +29,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations + description: The ARN of the KMS key created by ASR for remediations allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' outputs: @@ -66,7 +66,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableAWSConfig + DocumentName: ASR-EnableAWSConfig RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' @@ -83,7 +83,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'AWS Config enabled' - UpdatedBy: 'SHARR-CIS_1.2.0_2.5' + UpdatedBy: 'ASR-CIS_1.2.0_2.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml index 415a3f31..edb29a8e 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.6 + ### Document Name - ASR-CIS_1.2.0_2.6 ## What does this document do? Configures access logging for a CloudTrail S3 bucket. @@ -60,7 +60,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateAccessLoggingBucket + DocumentName: ASR-CreateAccessLoggingBucket RuntimeParameters: BucketName: 'so0111-cloudtrailaccesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket' @@ -90,7 +90,7 @@ mainSteps: Note: Text: 'Created S3 bucket so0111-cloudtrailaccesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}} for logging access to {{ParseInput.CloudTrailBucket}}' - UpdatedBy: 'SHARR-CIS_1.2.0_2.6' + UpdatedBy: 'ASR-CIS_1.2.0_2.6' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml index 34140bbd..528dcbe8 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.7.yaml @@ -1,7 +1,7 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.7 + ### Document Name - ASR-CIS_1.2.0_2.7 ## What does this document do? - This document enables SSE KMS encryption for log files using the SHARR remediation KMS CMK + This document enables SSE KMS encryption for log files using the ASR remediation KMS CMK ## 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. @@ -63,7 +63,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableCloudTrailEncryption + DocumentName: ASR-EnableCloudTrailEncryption RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' @@ -80,7 +80,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Encryption enabled on CloudTrail {{ParseInput.TrailArn}}' - UpdatedBy: 'SHARR-CIS_1.2.0_2.7' + UpdatedBy: 'ASR-CIS_1.2.0_2.7' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml index b3a2314b..5385c606 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.8.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.8 + ### Document Name - ASR-CIS_1.2.0_2.8 ## What does this document do? Enables rotation for customer-managed KMS keys. @@ -67,7 +67,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableKeyRotation + DocumentName: ASR-EnableKeyRotation TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -86,7 +86,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled KMS Customer Managed Key rotation for {{ParseInput.KMSKeyId}}' - UpdatedBy: 'SHARR-CIS_1.2.0_2.8' + UpdatedBy: 'ASR-CIS_1.2.0_2.8' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml b/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml index afd2941c..06a33f5a 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_2.9.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.9 + ### Document Name - ASR-CIS_1.2.0_2.9 ## What does this document do? Enables VPC Flow Logs for a VPC @@ -60,7 +60,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableVPCFlowLogs + DocumentName: ASR-EnableVPCFlowLogs RuntimeParameters: VPC: '{{ParseInput.VPC}}' RemediationRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole' @@ -76,7 +76,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled VPC Flow Logs for {{ParseInput.VPC}}' - UpdatedBy: 'SHARR-CIS_1.2.0_2.9' + UpdatedBy: 'ASR-CIS_1.2.0_2.9' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml index 72534d8f..c5eefae7 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_3.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_3.x + ### Document Name - ASR-CIS_1.2.0_3.x ## What does this document do? Remediates the following CIS findings: @@ -71,7 +71,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations + description: The ARN of the KMS key created by ASR for remediations allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' mainSteps: @@ -140,7 +140,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateLogMetricFilterAndAlarm + DocumentName: ASR-CreateLogMetricFilterAndAlarm RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm' FilterName: '{{ GetMetricFilterAndAlarmInputValue.FilterName }}' @@ -165,7 +165,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Added metric filter to the log group and notifications to SNS topic SO0111-SHARR-LocalAlarmNotification.' - UpdatedBy: 'SHARR-CIS_1.2.0_3.1' + UpdatedBy: 'ASR-CIS_1.2.0_3.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml index b4360b00..176013ad 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_4.1 + ### Document Name - ASR-CIS_1.2.0_4.1 ## What does this document do? Removes public access from an EC2 Security Group for controls CIS 4.1 and CIS 4.2 @@ -87,7 +87,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to the security group {{ ParseInput.GroupId }}.' - UpdatedBy: 'SHARR-CIS_1.2.0_4.1' + UpdatedBy: 'ASR-CIS_1.2.0_4.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml b/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml index 6565a083..d0cd5675 100644 --- a/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml +++ b/source/playbooks/CIS120/ssmdocs/CIS_4.3.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_4.3 + ### Document Name - ASR-CIS_1.2.0_4.3 ## What does this document do? Removes all access from an EC2 Default Security Group to restrict all traffic. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + DocumentName: ASR-RemoveVPCDefaultSecurityGroupRules TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -89,7 +89,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Removed all access to the default security group {{ ParseInput.GroupId }}.' - UpdatedBy: 'SHARR-CIS_1.2.0_4.3' + UpdatedBy: 'ASR-CIS_1.2.0_4.3' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py b/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py deleted file mode 100644 index 8d9de456..00000000 --- a/source/playbooks/CIS120/ssmdocs/scripts/cis_parse_input.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -import re - -def get_control_id_from_arn(finding_id_arn): - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\.2\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id_arn - ) - if check_finding_id: - control_id = check_finding_id.group(1) - return control_id - else: - exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') - -def parse_event(event, context): - expected_control_id = event['expected_control_id'] - parse_id_pattern = event['parse_id_pattern'] - resource_id_matches = [] - finding = event['Finding'] - testmode = bool('testmode' in finding) - - finding_id = finding['Id'] - - 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}') - - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - 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 - } \ No newline at end of file diff --git a/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py b/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py deleted file mode 100644 index 7f1271e3..00000000 --- a/source/playbooks/CIS120/ssmdocs/scripts/test/test_parse_event.py +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -import pytest - -from cis_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] - } - -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] - } -def test_parse_event(): - parsed_event = parse_event(event(), {}) - assert parsed_event == expected() - -def test_parse_cis41(): - parsed_event = parse_event(cis41_event(), {}) - assert parsed_event == cis41_expected() - -def test_parse_event_multimatch(): - expected_result = expected() - 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(): - 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(): - 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(): - 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(): - 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(): - 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(): - 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(): - test_event = event() - expected_result = expected() - - 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(): - 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/CIS120/test/__snapshots__/cis_stack.test.ts.snap b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap index 48871d20..bc7f27e2 100644 --- a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap +++ b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`default stack 1`] = ` -Object { +{ "Description": "test;", - "Mappings": Object { - "SourceCode": Object { - "General": Object { + "Mappings": { + "SourceCode": { + "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.1.1", "S3Bucket": "sharrbukkit", }, }, }, - "Parameters": Object { - "CIS11AutoTrigger": Object { - "AllowedValues": Array [ + "Parameters": { + "CIS11AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -21,8 +21,8 @@ Object { "Description": "This will fully enable automated remediation for CIS 1.1", "Type": "String", }, - "CIS12AutoTrigger": Object { - "AllowedValues": Array [ + "CIS12AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -30,8 +30,8 @@ Object { "Description": "This will fully enable automated remediation for CIS 1.2", "Type": "String", }, - "CIS13AutoTrigger": Object { - "AllowedValues": Array [ + "CIS13AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -39,31 +39,31 @@ Object { "Description": "This will fully enable automated remediation for CIS 1.3", "Type": "String", }, - "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": Object { + "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/Solutions/SO0111/OrchestratorArn", "Type": "AWS::SSM::Parameter::Value", }, }, - "Resources": Object { - "CIS11AutoEventRule5178D649": Object { - "Properties": Object { + "Resources": { + "CIS11AutoEventRule5178D649": { + "Properties": { "Description": "Remediate CIS 1.1 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ - Object { - "Fn::Join": Array [ + "GeneratorId": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.1", @@ -71,35 +71,35 @@ Object { ], }, ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "CIS_1.1_AutoTrigger", - "State": Object { + "State": { "Ref": "CIS11AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "CIS11EventsRuleRoleB8D228E0", "Arn", ], @@ -109,14 +109,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "CIS11EventsRuleRoleB8D228E0": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "CIS11EventsRuleRoleB8D228E0": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -126,14 +126,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "CIS11EventsRuleRoleDefaultPolicy66E09676": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "CIS11EventsRuleRoleDefaultPolicy66E09676": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -141,33 +141,33 @@ Object { "Version": "2012-10-17", }, "PolicyName": "CIS11EventsRuleRoleDefaultPolicy66E09676", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "CIS11EventsRuleRoleB8D228E0", }, ], }, "Type": "AWS::IAM::Policy", }, - "CIS12AutoEventRuleD40B64BC": Object { - "Properties": Object { + "CIS12AutoEventRuleD40B64BC": { + "Properties": { "Description": "Remediate CIS 1.2 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ - Object { - "Fn::Join": Array [ + "GeneratorId": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.2", @@ -175,35 +175,35 @@ Object { ], }, ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "CIS_1.2_AutoTrigger", - "State": Object { + "State": { "Ref": "CIS12AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "CIS12EventsRuleRoleA8389A61", "Arn", ], @@ -213,14 +213,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "CIS12EventsRuleRoleA8389A61": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "CIS12EventsRuleRoleA8389A61": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -230,14 +230,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "CIS12EventsRuleRoleDefaultPolicyA48EF9DD": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "CIS12EventsRuleRoleDefaultPolicyA48EF9DD": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -245,33 +245,33 @@ Object { "Version": "2012-10-17", }, "PolicyName": "CIS12EventsRuleRoleDefaultPolicyA48EF9DD", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "CIS12EventsRuleRoleA8389A61", }, ], }, "Type": "AWS::IAM::Policy", }, - "CIS13AutoEventRuleC6C84C81": Object { - "Properties": Object { + "CIS13AutoEventRuleC6C84C81": { + "Properties": { "Description": "Remediate CIS 1.3 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ - Object { - "Fn::Join": Array [ + "GeneratorId": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3", @@ -279,35 +279,35 @@ Object { ], }, ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "CIS_1.3_AutoTrigger", - "State": Object { + "State": { "Ref": "CIS13AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "CIS13EventsRuleRoleFEBE574F", "Arn", ], @@ -317,14 +317,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "CIS13EventsRuleRoleDefaultPolicy2E3E119B": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "CIS13EventsRuleRoleDefaultPolicy2E3E119B": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -332,22 +332,22 @@ Object { "Version": "2012-10-17", }, "PolicyName": "CIS13EventsRuleRoleDefaultPolicy2E3E119B", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "CIS13EventsRuleRoleFEBE574F", }, ], }, "Type": "AWS::IAM::Policy", }, - "CIS13EventsRuleRoleFEBE574F": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "CIS13EventsRuleRoleFEBE574F": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -357,8 +357,8 @@ Object { }, "Type": "AWS::IAM::Role", }, - "StandardShortName7DDF6BE6": Object { - "Properties": Object { + "StandardShortName7DDF6BE6": { + "Properties": { "Description": "Provides a short (1-12) character abbreviation for the standard.", "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/shortname", "Type": "String", @@ -366,8 +366,8 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "StandardVersionCB2C6951": Object { - "Properties": Object { + "StandardVersionCB2C6951": { + "Properties": { "Description": "This parameter controls whether the SHARR step function will process findings for this version of the standard.", "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/1.2.0/status", "Type": "String", @@ -380,27 +380,27 @@ Object { `; exports[`default stack 2`] = ` -Object { - "Conditions": Object { - "Enable13Condition": Object { - "Fn::Equals": Array [ - Object { +{ + "Conditions": { + "Enable13Condition": { + "Fn::Equals": [ + { "Ref": "Enable13", }, "Available", ], }, - "Enable15Condition": Object { - "Fn::Equals": Array [ - Object { + "Enable15Condition": { + "Fn::Equals": [ + { "Ref": "Enable15", }, "Available", ], }, - "Enable21Condition": Object { - "Fn::Equals": Array [ - Object { + "Enable21Condition": { + "Fn::Equals": [ + { "Ref": "Enable21", }, "Available", @@ -408,9 +408,9 @@ Object { }, }, "Description": "test;", - "Parameters": Object { - "Enable13": Object { - "AllowedValues": Array [ + "Parameters": { + "Enable13": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -418,8 +418,8 @@ 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", }, - "Enable15": Object { - "AllowedValues": Array [ + "Enable15": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -427,8 +427,8 @@ 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", }, - "Enable21": Object { - "AllowedValues": Array [ + "Enable21": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -436,973 +436,994 @@ Object { "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 2.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", }, - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, }, - "Resources": Object { - "CIS13": Object { + "Resources": { + "ControlCIS13": { "Condition": "Enable13Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "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. - - ## 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 -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-CIS_1.2.0_1.3 + +## 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. + +## 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": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "Finding": "{{Finding}}", + "expected_control_id": [ + "1.3", + ], + "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):iam::\\d{12}:user(?:(?:\\u002F)|(?:\\u002F[\\u0021-\\u007F]{1,510}\\u002F))([\\w+=,.@-]{1,64})$", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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' - - - 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 -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CIS_1.2.0_1.3", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-RevokeUnusedIAMUserCredentials", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials", + "IAMResourceId": "{{ ParseInput.IAMResourceId }}", + }, }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Deactivated unused keys and expired logins for {{ ParseInput.IAMUser }}.", + "UpdatedBy": "ASR-CIS_1.2.0_1.3", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the 1.3 finding", + "type": "StringMap", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CIS_1.2.0_1.3", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "CIS15": Object { + "ControlCIS15": { "Condition": "Enable15Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-CIS_1.2.0_1.5 - - ## What does this document do? - This document establishes a default password policy. - - ## 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 - - ## 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) - - -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+=,.@-]+$' - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-CIS_1.2.0_1.5 + +## What does this document do? +This document establishes a default password policy. + +## 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 + +## 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": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "Finding": "{{Finding}}", + "expected_control_id": [ + "1.5", + "1.6", + "1.7", + "1.8", + "1.9", + "1.10", + "1.11", + ], + "parse_id_pattern": "", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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' - - - 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 -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CIS_1.2.0_1.5", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String", + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String", + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-SetIAMPasswordPolicy", + "RuntimeParameters": { + "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, + }, }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", + "UpdatedBy": "ASR-CIS_1.2.0_1.5", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the 1.5 finding", + "type": "StringMap", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CIS_1.2.0_1.5", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "CIS21": Object { + "ControlCIS21": { "Condition": "Enable21Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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. - - ## 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) - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. + +## 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": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "Finding": "{{Finding}}", + "expected_control_id": [ + "2.1", + ], + "parse_id_pattern": "", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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-CreateCloudTrailMultiRegionTrail - RuntimeParameters: - AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' - AWSPartition: '{{global:AWS_PARTITION}}' - - - name: UpdateFinding - action: 'aws:executeAwsApi' - inputs: - Service: securityhub - Api: BatchUpdateFindings - FindingIdentifiers: - - Id: '{{ParseInput.FindingId}}' - ProductArn: '{{ParseInput.ProductArn}}' - Note: - Text: 'Multi-region, encrypted AWS CloudTrail successfully created' - UpdatedBy: 'SHARR-CIS_1.2.0_2.11' - Workflow: - Status: RESOLVED - description: Update finding - isEnd: true -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CIS_1.2.0_2.1", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-CreateCloudTrailMultiRegionTrail", + "RuntimeParameters": { + "AWSPartition": "{{global:AWS_PARTITION}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail", + }, }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Multi-region, encrypted AWS CloudTrail successfully created", + "UpdatedBy": "ASR-CIS_1.2.0_2.11", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the 2.1 finding", + "type": "StringMap", + }, + "KMSKeyArn": { + "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 ASR for this remediation", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CIS_1.2.0_2.1", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "RemapCIS4245EB49A0": Object { - "Properties": Object { + "RemapCIS4245EB49A0": { + "Properties": { "Description": "Remap the CIS 4.2 finding to CIS 4.1 remediation", "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/1.2.0-4.2", "Type": "String", diff --git a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml index 5b41bf23..8bb4c969 100644 --- a/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml +++ b/source/playbooks/NEWPLAYBOOK/ssmdocs/AFSBP_RDS.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARRRemediation-AFSBP_RDS.6 + ### Document Name - ASRRemediation-AFSBP_RDS.6 ## What does this document do? This document enables `Enhanced Monitoring` on a given Amazon RDS instance by calling another SSM document. @@ -43,14 +43,14 @@ mainSteps: Type: StringMap inputs: InputPayload: - Finding: '{{Finding}}' + Finding: '{{Finding}}' Runtime: python3.8 Handler: parse_event Script: |- %%SCRIPT=common/parse_input.py%% isEnd: false - - + - name: GetMonitoringRoleArn action: aws:executeAwsApi description: | @@ -60,7 +60,7 @@ mainSteps: inputs: Service: iam Api: GetRole - RoleName: 'SO0111-SHARR-RDSEnhancedMonitoring' + RoleName: 'SO0111-ASR-RDSEnhancedMonitoring' outputs: - Name: Arn Selector: $.Role.Arn @@ -77,7 +77,7 @@ mainSteps: MonitoringRoleArn: '{{GetMonitoringRoleArn.Arn}}' AutomationAssumeRole: '{{ AutomationAssumeRole }}' - - + - name: VerifyRemediation action: 'aws:executeScript' outputs: @@ -103,7 +103,7 @@ mainSteps: } } - - + - name: UpdateFinding action: 'aws:executeAwsApi' inputs: @@ -114,7 +114,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deletion protection enabled on RDS DB cluster' - UpdatedBy: 'SHARRRemediation-AFSBP_RDS.7' + UpdatedBy: 'ASRRemediation-AFSBP_RDS.7' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/newplaybook_parse_input.py b/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/newplaybook_parse_input.py deleted file mode 100644 index 6afc8a2d..00000000 --- a/source/playbooks/NEWPLAYBOOK/ssmdocs/scripts/newplaybook_parse_input.py +++ /dev/null @@ -1,81 +0,0 @@ -import re - -def get_value_by_path(finding, path): - path_levels = path.split('.') - previous_level = finding - for level in path_levels: - this_level = previous_level.get(level) - previous_level = this_level - return this_level - -def parse_event(event, context): - expected_control_id = event['expected_control_id'] - parse_id_pattern = event['parse_id_pattern'] - resource_id_matches = [] - - finding = event['Finding'] - - finding_id = finding['Id'] - control_id = '' - # Finding Id present and valid - check_finding_id = re.match('^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/cis-aws-foundations-benchmark/v/1\\.2\\.0/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$',finding_id) - - if not check_finding_id: - exit(f'ERROR: Finding Id is invalid: {finding_id}') - else: - control_id = check_finding_id.group(1) - - account_id = finding['AwsAccountId'] - if not re.match('^\\d{12}$', account_id): - exit(f'ERROR: AwsAccountId is invalid: {account_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}') - - # ResourceType - resource_type = finding['Resources'][0]['Type'] - - # Regex match Id to get remediation-specific identifier - identifier_raw = finding['Resources'][0]['Id'] - - if parse_id_pattern: - identifier_match = re.match( - parse_id_pattern, - identifier_raw - ) - - if not identifier_match: - exit(f'ERROR: Invalid resource Id {identifier_raw}') - else: - for group in range(1, len(identifier_match.groups())+1): - resource_id_matches.append(identifier_match.group(group)) - if 'resource_index' in event: - resource_id = identifier_match.group(event['resource_index']) - else: - resource_id = identifier_match.group(1) - else: - resource_id = identifier_raw - - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - - affected_object = {'Type': resource_type, 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - 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 - } \ No newline at end of file diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml index bfabfa43..b98e7483 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.AutoScaling.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.AutoScaling.1 + ### Document Name - ASR-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. @@ -60,7 +60,7 @@ mainSteps: 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/(.*)$' + parse_id_pattern: '^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:autoScalingGroup:(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}):autoScalingGroupName/(.{1,255})$' expected_control_id: - 'PCI.AutoScaling.1' Runtime: python3.8 @@ -73,11 +73,11 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableAutoScalingGroupELBHealthCheck + DocumentName: ASR-EnableAutoScalingGroupELBHealthCheck TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] - ExecutionRoleName: 'SO0111-SHARR-Orchestrator-Member' + ExecutionRoleName: 'SO0111-EnableAutoScalingGroupELBHealthCheck' RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck' AutoScalingGroupName: '{{ParseInput.AutoScalingGroupName}}' @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'ASG health check type updated to ELB' - UpdatedBy: 'SHARR-PCI_3.2.1_AutoScaling.1' + UpdatedBy: 'ASR-PCI_3.2.1_AutoScaling.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml index d0d89a05..b3181bf5 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CW.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CW.1 + ### Document Name - ASR-PCI_3.2.1_CW.1 ## What does this document do? Creates a log metric filter and alarm for usage of "root" account @@ -41,7 +41,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations + description: The ARN of the KMS key created by ASR for remediations allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' mainSteps: @@ -110,7 +110,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateLogMetricFilterAndAlarm + DocumentName: ASR-CreateLogMetricFilterAndAlarm RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateLogMetricFilterAndAlarm' FilterName: '{{ GetMetricFilterAndAlarmInputValue.FilterName }}' @@ -135,7 +135,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Added metric filter and alarm to the log group.' - UpdatedBy: 'SHARR-PCI_3.2.1_CW.1' + UpdatedBy: 'ASR-PCI_3.2.1_CW.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml index 905097de..2f1e1290 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.1.yaml @@ -1,7 +1,7 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CloudTrail.1 + ### Document Name - ASR-PCI_3.2.1_CloudTrail.1 ## What does this document do? - This document enables SSE KMS encryption for log files using the SHARR remediation KMS CMK + This document enables SSE KMS encryption for log files using the ASR remediation KMS CMK ## 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. @@ -66,7 +66,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-EnableCloudTrailEncryption + DocumentName: ASR-EnableCloudTrailEncryption RuntimeParameters: TrailRegion: '{{ParseInput.TrailRegion}}' TrailArn: '{{ParseInput.TrailArn}}' @@ -84,7 +84,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: Encryption enabled on CloudTrail - UpdatedBy: SHARR-PCI_3.2.1_CloudTrail.1 + UpdatedBy: ASR-PCI_3.2.1_CloudTrail.1 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml index 18e48602..fbc0eb94 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CloudTrail.2 + ### Document Name - ASR-PCI_3.2.1_CloudTrail.2 ## What does this document do? Creates a multi-region trail with KMS encryption and enables CloudTrail Note: this remediation will create a NEW trail. @@ -26,7 +26,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for this remediation + description: The ARN of the KMS key created by ASR 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: @@ -66,7 +66,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateCloudTrailMultiRegionTrail + DocumentName: ASR-CreateCloudTrailMultiRegionTrail RuntimeParameters: AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail' AWSPartition: '{{global:AWS_PARTITION}}' @@ -82,7 +82,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Multi-region, encrypted AWS CloudTrail successfully created' - UpdatedBy: 'SHARR-PCI_3.2.1_CloudTrail.2' + UpdatedBy: 'ASR-PCI_3.2.1_CloudTrail.2' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml index 6c2e06b4..2005c516 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.3.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CloudTrail.3 + ### Document Name - ASR-PCI_3.2.1_CloudTrail.3 ## What does this document do? This document enables CloudTrail log file validation. @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailLogFileValidation + DocumentName: ASR-EnableCloudTrailLogFileValidation TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled CloudTrail log file validation.' - UpdatedBy: 'SHARR-PCI_3.2.1_CloudTrail.3' + UpdatedBy: 'ASR-PCI_3.2.1_CloudTrail.3' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml index aabe1e8a..d5d9d8cd 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CloudTrail.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CloudTrail.4 + ### Document Name - ASR-PCI_3.2.1_CloudTrail.4 ## What does this document do? This document configures CloudTrail to log to CloudWatch Logs. @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableCloudTrailToCloudWatchLogging + DocumentName: ASR-EnableCloudTrailToCloudWatchLogging TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -91,7 +91,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured CloudTrail logging to CloudWatch Logs Group CloudTrail/{{ParseInput.TrailName}}' - UpdatedBy: 'SHARR-PCI_3.2.1_CloudTrail.4' + UpdatedBy: 'ASR-PCI_3.2.1_CloudTrail.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml index b3367a3e..e36616fa 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.CodeBuild.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_CodeBuild.2 + ### Document Name - ASR-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. @@ -54,7 +54,7 @@ mainSteps: - name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-ReplaceCodeBuildClearTextCredentials + DocumentName: ASR-ReplaceCodeBuildClearTextCredentials RuntimeParameters: ProjectName: '{{ ParseInput.ProjectName }}' AutomationAssumeRole: 'arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-ReplaceCodeBuildClearTextCredentials' @@ -68,7 +68,7 @@ mainSteps: ProductArn: '{{ ParseInput.ProductArn }}' Note: Text: 'Replaced clear text credentials with SSM parameters.' - UpdatedBy: 'SHARR-PCI_3.2.1_CodeBuild.2' + UpdatedBy: 'ASR-PCI_3.2.1_CodeBuild.2' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml index cf3a33d0..2182484b 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Config.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_Config.1 + ### Document Name - ASR-PCI_3.2.1_Config.1 ## What does this document do? Enables AWS Config: * Turns on recording for all resources. @@ -29,7 +29,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations + description: The ARN of the KMS key created by ASR for remediations allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$' outputs: @@ -69,7 +69,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableAWSConfig + DocumentName: ASR-EnableAWSConfig RuntimeParameters: SNSTopicName: 'SO0111-SHARR-AWSConfigNotification' KMSKeyArn: '{{KMSKeyArn}}' @@ -86,7 +86,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'AWS Config enabled' - UpdatedBy: 'SHARR-PCI_3.2.1_Config.1' + UpdatedBy: 'ASR-PCI_3.2.1_Config.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml index 46a555c5..90f51043 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_EC2.1 + ### Document Name - ASR-PCI_3.2.1_EC2.1 ## What does this document do? This document changes all public EC2 snapshots to private @@ -63,7 +63,7 @@ mainSteps: name: Remediation action: 'aws:executeAutomation' inputs: - DocumentName: SHARR-MakeEBSSnapshotsPrivate + DocumentName: ASR-MakeEBSSnapshotsPrivate RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-MakeEBSSnapshotsPrivate' @@ -81,7 +81,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'EBS Snapshot modified to private' - UpdatedBy: 'SHARR-PCI_3.2.1_EC2.1' + UpdatedBy: 'ASR-PCI_3.2.1_EC2.1' Workflow: Status: 'RESOLVED' description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml index 836f526e..00066a36 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.2.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_EC2.2 + ### Document Name - ASR-PCI_3.2.1_EC2.2 ## What does this document do? This document deletes ingress and egress rules from default security @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RemoveVPCDefaultSecurityGroupRules + DocumentName: ASR-RemoveVPCDefaultSecurityGroupRules TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: Removed rules on default security group - UpdatedBy: SHARR-PCI_3.2.1_EC2.2 + UpdatedBy: ASR-PCI_3.2.1_EC2.2 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml index 50d51806..6470d40b 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_EC2.5 + ### Document Name - ASR-PCI_3.2.1_EC2.5 ## What does this document do? Removes public access to remove server administrative ports from an EC2 Security Group @@ -73,7 +73,7 @@ mainSteps: 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' + UpdatedBy: 'ASR-PCI_3.2.1_EC2.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml index accd3fc5..cc8e73c7 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.EC2.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.EC2.6 + ### Document Name - ASR-PCI_3.2.1_PCI.EC2.6 ## What does this document do? Enables VPC Flow Logs for a VPC @@ -69,7 +69,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableVPCFlowLogs + DocumentName: ASR-EnableVPCFlowLogs TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -88,7 +88,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled VPC Flow Logs for {{ParseInput.VPC}}' - UpdatedBy: 'SHARR-PCI_3.2.1_PCI.EC2.6' + UpdatedBy: 'ASR-PCI_3.2.1_PCI.EC2.6' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml index 96c271e7..87693840 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.7.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_IAM.7 + ### Document Name - ASR-PCI_3.2.1_IAM.7 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. @@ -65,7 +65,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RevokeUnusedIAMUserCredentials + DocumentName: ASR-RevokeUnusedIAMUserCredentials RuntimeParameters: IAMResourceId: '{{ ParseInput.IAMResourceId }}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials' @@ -80,7 +80,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Deactivated unused keys and expired logins using the AWSConfigRemediation-RevokeUnusedIAMUserCredentials runbook.' - UpdatedBy: 'SHARR-PCI_3.2.1_IAM.7' + UpdatedBy: 'ASR-PCI_3.2.1_IAM.7' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml index 30a2b2a5..ac3e495f 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.IAM.8.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.IAM.8 + ### Document Name - ASR-PCI_3.2.1_PCI.IAM.8 ## What does this document do? This document establishes a default password policy. @@ -58,7 +58,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-SetIAMPasswordPolicy + DocumentName: ASR-SetIAMPasswordPolicy RuntimeParameters: AllowUsersToChangePassword: True HardExpiry: True @@ -81,7 +81,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.' - UpdatedBy: 'SHARR-PCI_3.2.1_IAM.8' + UpdatedBy: 'ASR-PCI_3.2.1_IAM.8' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml index 99374356..52c3e2fc 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.KMS.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.KMS.1 + ### Document Name - ASR-PCI_3.2.1_PCI.KMS.1 ## What does this document do? Enables rotation for customer-managed KMS keys. @@ -73,7 +73,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableKeyRotation + DocumentName: ASR-EnableKeyRotation TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -92,7 +92,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled KMS Customer Managed Key rotation for {{ParseInput.KMSKeyId}}' - UpdatedBy: 'SHARR-PCI_3.2.1_PCI.KMS.1' + UpdatedBy: 'ASR-PCI_3.2.1_PCI.KMS.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml index 4c3cb489..4276c7c3 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Lambda.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_Lambda.1 + ### Document Name - ASR-PCI_3.2.1_Lambda.1 ## What does this document do? This document removes the public resource policy. A public resource policy @@ -70,7 +70,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-RemoveLambdaPublicAccess + DocumentName: ASR-RemoveLambdaPublicAccess TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -90,7 +90,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Lamdba {{ParseInput.FunctionName}} policy updated to remove public access' - UpdatedBy: 'SHARR-PCI_3.2.1_Lambda.1 ' + UpdatedBy: 'ASR-PCI_3.2.1_Lambda.1 ' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml index 979b7e21..e277d8e6 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_RDS.1 + ### Document Name - ASR-PCI_3.2.1_RDS.1 ## What does this document do? This document changes public RDS snapshot to private @@ -73,7 +73,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-MakeRDSSnapshotPrivate + DocumentName: ASR-MakeRDSSnapshotPrivate TargetLocations: - Accounts: [ '{{ParseInput.RemediationAccount}}' ] Regions: [ '{{ParseInput.RemediationRegion}}' ] @@ -94,7 +94,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: RDS DB Snapshot modified to private - UpdatedBy: SHARR-PCI_3.2.1_RDS.1 + UpdatedBy: ASR-PCI_3.2.1_RDS.1 Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml index 2dfbde36..f52ea8b6 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.RDS.2.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 --- description: | - ### Document Name - SHARR-PCI_3.2.1_RDS.2 + ### Document Name - ASR-PCI_3.2.1_RDS.2 ## What does this document do? This document disables public access to RDS instances by calling another SSM document @@ -64,7 +64,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-DisablePublicAccessToRDSInstance' + DocumentName: 'ASR-DisablePublicAccessToRDSInstance' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -84,7 +84,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to RDS instance' - UpdatedBy: 'SHARR-PCI_3.2.1_RDS.2' + UpdatedBy: 'ASR-PCI_3.2.1_RDS.2' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml index fd3579f6..37d69f31 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.Redshift.1.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 --- description: | - ### Document Name - SHARR-PCI_3.2.1_Redshift.1 + ### Document Name - ASR-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 @@ -65,7 +65,7 @@ mainSteps: - name: 'Remediation' action: 'aws:executeAutomation' inputs: - DocumentName: 'SHARR-DisablePublicAccessToRedshiftCluster' + DocumentName: 'ASR-DisablePublicAccessToRedshiftCluster' TargetLocations: - Accounts: - '{{ParseInput.RemediationAccount}}' @@ -85,7 +85,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to Redshift cluster' - UpdatedBy: 'SHARR-PCI_3.2.1_Redshift.1' + UpdatedBy: 'ASR-PCI_3.2.1_Redshift.1' Workflow: Status: 'RESOLVED' description: 'Update finding' diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml index 4421af3c..c929af03 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_S3.1 + ### Document Name - ASR-PCI_3.2.1_S3.1 ## What does this document do? This document blocks public access to an S3 bucket. @@ -61,7 +61,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-ConfigureS3BucketPublicAccessBlock + DocumentName: ASR-ConfigureS3BucketPublicAccessBlock RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3BucketPublicAccessBlock' @@ -80,7 +80,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Disabled public access to S3 bucket.' - UpdatedBy: 'SHARR-PCI_3.2.1_S3.1' + UpdatedBy: 'ASR-PCI_3.2.1_S3.1' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.4.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.4.yaml index 70e037da..de39232f 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.4.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.4.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.S3.4 + ### Document Name - ASR-PCI_3.2.1_PCI.S3.4 ## What does this document do? This document enables AES-256 as the default encryption for an S3 bucket. @@ -71,7 +71,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-EnableDefaultEncryptionS3 + DocumentName: ASR-EnableDefaultEncryptionS3 RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' BucketName: '{{ParseInput.BucketName}}' @@ -88,7 +88,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Enabled default encryption for {{ParseInput.BucketName}}' - UpdatedBy: 'SHARR-PCI_3.2.1_PCI.S3.4' + UpdatedBy: 'ASR-PCI_3.2.1_PCI.S3.4' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml index 950f518e..6f5b210b 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.5.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.S3.5 + ### Document Name - ASR-PCI_3.2.1_PCI.S3.5 ## What does this document do? This document adds a bucket policy to restrict internet access to https only. @@ -63,7 +63,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-SetSSLBucketPolicy + DocumentName: ASR-SetSSLBucketPolicy RuntimeParameters: BucketName: '{{ParseInput.BucketName}}' AccountId: '{{ParseInput.AccountId}}' @@ -79,7 +79,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Added SSL-only access policy to S3 bucket.' - UpdatedBy: 'SHARR-PCI_3.2.1_PCI.S3.5' + UpdatedBy: 'ASR-PCI_3.2.1_PCI.S3.5' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml index 2d8080ad..76536173 100644 --- a/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml +++ b/source/playbooks/PCI321/ssmdocs/PCI_PCI.S3.6.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-PCI_3.2.1_S3.6 + ### Document Name - ASR-PCI_3.2.1_S3.6 ## What does this document do? This document blocks public access to all buckets by default at the account level. @@ -60,7 +60,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-ConfigureS3PublicAccessBlock + DocumentName: ASR-ConfigureS3PublicAccessBlock RuntimeParameters: AccountId: '{{ParseInput.AccountId}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-ConfigureS3PublicAccessBlock' @@ -79,7 +79,7 @@ mainSteps: ProductArn: '{{ParseInput.ProductArn}}' Note: Text: 'Configured the account to block public S3 access.' - UpdatedBy: 'SHARR-PCI_3.2.1_S3.6' + UpdatedBy: 'ASR-PCI_3.2.1_S3.6' Workflow: Status: RESOLVED description: Update finding diff --git a/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py b/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py deleted file mode 100644 index f9b4e6ca..00000000 --- a/source/playbooks/PCI321/ssmdocs/scripts/pci_parse_input.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -import re - -def get_control_id_from_arn(finding_id_arn): - check_finding_id = re.match( - '^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:subscription/pci-dss/v/3\\.2\\.1/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', - finding_id_arn - ) - if check_finding_id: - control_id = check_finding_id.group(1) - return control_id - else: - exit(f'ERROR: Finding Id is invalid: {finding_id_arn}') - -def parse_event(event, context): - 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'] - - 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}') - - if not resource_id: - exit('ERROR: Resource Id is missing from the finding json Resources (Id)') - - affected_object = {'Type': resource['Type'], 'Id': resource_id, 'OutputKey': 'Remediation.Output'} - 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 - } diff --git a/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py b/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py deleted file mode 100644 index 06495904..00000000 --- a/source/playbooks/PCI321/ssmdocs/scripts/test/test_pci_parse_input.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/python -############################################################################### -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # -# # -# Licensed under the Apache License Version 2.0 (the "License"). You may not # -# use this file except in compliance with the License. A copy of the License # -# is located at # -# # -# http://www.apache.org/licenses/LICENSE-2.0/ # -# # -# or in the "license" file accompanying this file. This file is distributed # -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # -# or implied. See the License for the specific language governing permis- # -# sions and limitations under the License. # -############################################################################### -import pytest - -from pci_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" - } - } - } - } - -def test_parse_event(): - parsed_event = parse_event(event(), {}) - assert parsed_event == expected() - -def test_parse_event_multimatch(): - expected_result = expected() - 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(): - 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(): - 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(): - 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(): - 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(): - 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(): - 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(): - test_event = event() - expected_result = expected() - - 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(): - 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/PCI321/test/__snapshots__/pci321_stack.test.ts.snap b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap index b555c447..c4a1d049 100644 --- a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap +++ b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`default stack 1`] = ` -Object { +{ "Description": "test;", - "Mappings": Object { - "SourceCode": Object { - "General": Object { + "Mappings": { + "SourceCode": { + "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.1.1", "S3Bucket": "sharrbukkit", }, }, }, - "Parameters": Object { - "PCIPCIAutoScaling1AutoTrigger": Object { - "AllowedValues": Array [ + "Parameters": { + "PCIPCIAutoScaling1AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -21,8 +21,8 @@ Object { "Description": "This will fully enable automated remediation for PCI PCI.AutoScaling.1", "Type": "String", }, - "PCIPCIEC26AutoTrigger": Object { - "AllowedValues": Array [ + "PCIPCIEC26AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -30,8 +30,8 @@ Object { "Description": "This will fully enable automated remediation for PCI PCI.EC2.6", "Type": "String", }, - "PCIPCIIAM8AutoTrigger": Object { - "AllowedValues": Array [ + "PCIPCIIAM8AutoTrigger": { + "AllowedValues": [ "ENABLED", "DISABLED", ], @@ -39,56 +39,56 @@ Object { "Description": "This will fully enable automated remediation for PCI PCI.IAM.8", "Type": "String", }, - "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": Object { + "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/Solutions/SO0111/OrchestratorArn", "Type": "AWS::SSM::Parameter::Value", }, }, - "Resources": Object { - "PCIPCIAutoScaling1AutoEventRuleCDFEB9FF": Object { - "Properties": Object { + "Resources": { + "PCIPCIAutoScaling1AutoEventRuleCDFEB9FF": { + "Properties": { "Description": "Remediate PCI PCI.AutoScaling.1 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "pci-dss/v/3.2.1/PCI.AutoScaling.1", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "PCI_PCI.AutoScaling.1_AutoTrigger", - "State": Object { + "State": { "Ref": "PCIPCIAutoScaling1AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "PCIPCIAutoScaling1EventsRuleRole3283761D", "Arn", ], @@ -98,14 +98,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "PCIPCIAutoScaling1EventsRuleRole3283761D": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIAutoScaling1EventsRuleRole3283761D": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -115,14 +115,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "PCIPCIAutoScaling1EventsRuleRoleDefaultPolicy7F317AE9": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIAutoScaling1EventsRuleRoleDefaultPolicy7F317AE9": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -130,58 +130,58 @@ Object { "Version": "2012-10-17", }, "PolicyName": "PCIPCIAutoScaling1EventsRuleRoleDefaultPolicy7F317AE9", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "PCIPCIAutoScaling1EventsRuleRole3283761D", }, ], }, "Type": "AWS::IAM::Policy", }, - "PCIPCIEC26AutoEventRule084B7A4B": Object { - "Properties": Object { + "PCIPCIEC26AutoEventRule084B7A4B": { + "Properties": { "Description": "Remediate PCI PCI.EC2.6 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "pci-dss/v/3.2.1/PCI.EC2.6", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "PCI_PCI.EC2.6_AutoTrigger", - "State": Object { + "State": { "Ref": "PCIPCIEC26AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "PCIPCIEC26EventsRuleRole8A61F75E", "Arn", ], @@ -191,14 +191,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "PCIPCIEC26EventsRuleRole8A61F75E": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIEC26EventsRuleRole8A61F75E": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -208,14 +208,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "PCIPCIEC26EventsRuleRoleDefaultPolicy22C238AF": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIEC26EventsRuleRoleDefaultPolicy22C238AF": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -223,58 +223,58 @@ Object { "Version": "2012-10-17", }, "PolicyName": "PCIPCIEC26EventsRuleRoleDefaultPolicy22C238AF", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "PCIPCIEC26EventsRuleRole8A61F75E", }, ], }, "Type": "AWS::IAM::Policy", }, - "PCIPCIIAM8AutoEventRuleD053739F": Object { - "Properties": Object { + "PCIPCIIAM8AutoEventRuleD053739F": { + "Properties": { "Description": "Remediate PCI PCI.IAM.8 automatic remediation trigger event rule.", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, - "GeneratorId": Array [ + "GeneratorId": [ "pci-dss/v/3.2.1/PCI.IAM.8", ], - "RecordState": Array [ + "RecordState": [ "ACTIVE", ], - "Workflow": Object { - "Status": Array [ + "Workflow": { + "Status": [ "NEW", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Imported", ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "PCI_PCI.IAM.8_AutoTrigger", - "State": Object { + "State": { "Ref": "PCIPCIIAM8AutoTrigger", }, - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "PCIPCIIAM8EventsRuleRoleE8D97921", "Arn", ], @@ -284,14 +284,14 @@ Object { }, "Type": "AWS::Events::Rule", }, - "PCIPCIIAM8EventsRuleRoleDefaultPolicy8C6970ED": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIIAM8EventsRuleRoleDefaultPolicy8C6970ED": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, @@ -299,22 +299,22 @@ Object { "Version": "2012-10-17", }, "PolicyName": "PCIPCIIAM8EventsRuleRoleDefaultPolicy8C6970ED", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "PCIPCIIAM8EventsRuleRoleE8D97921", }, ], }, "Type": "AWS::IAM::Policy", }, - "PCIPCIIAM8EventsRuleRoleE8D97921": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "PCIPCIIAM8EventsRuleRoleE8D97921": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -324,8 +324,8 @@ Object { }, "Type": "AWS::IAM::Role", }, - "StandardShortName7DDF6BE6": Object { - "Properties": Object { + "StandardShortName7DDF6BE6": { + "Properties": { "Description": "Provides a short (1-12) character abbreviation for the standard.", "Name": "/Solutions/SO0111/pci-dss/shortname", "Type": "String", @@ -333,8 +333,8 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "StandardVersionCB2C6951": Object { - "Properties": Object { + "StandardVersionCB2C6951": { + "Properties": { "Description": "This parameter controls whether the SHARR step function will process findings for this version of the standard.", "Name": "/Solutions/SO0111/pci-dss/3.2.1/status", "Type": "String", @@ -347,27 +347,27 @@ Object { `; exports[`default stack 2`] = ` -Object { - "Conditions": Object { - "EnablePCIAutoScaling1Condition": Object { - "Fn::Equals": Array [ - Object { +{ + "Conditions": { + "EnablePCIAutoScaling1Condition": { + "Fn::Equals": [ + { "Ref": "EnablePCIAutoScaling1", }, "Available", ], }, - "EnablePCIEC26Condition": Object { - "Fn::Equals": Array [ - Object { + "EnablePCIEC26Condition": { + "Fn::Equals": [ + { "Ref": "EnablePCIEC26", }, "Available", ], }, - "EnablePCIIAM8Condition": Object { - "Fn::Equals": Array [ - Object { + "EnablePCIIAM8Condition": { + "Fn::Equals": [ + { "Ref": "EnablePCIIAM8", }, "Available", @@ -375,9 +375,9 @@ Object { }, }, "Description": "test;", - "Parameters": Object { - "EnablePCIAutoScaling1": Object { - "AllowedValues": Array [ + "Parameters": { + "EnablePCIAutoScaling1": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -385,8 +385,8 @@ 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", }, - "EnablePCIEC26": Object { - "AllowedValues": Array [ + "EnablePCIEC26": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -394,8 +394,8 @@ 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", }, - "EnablePCIIAM8": Object { - "AllowedValues": Array [ + "EnablePCIIAM8": { + "AllowedValues": [ "Available", "NOT Available", ], @@ -403,983 +403,1021 @@ Object { "Description": "Enable/disable availability of remediation for PCI version 3.2.1 Control PCI.IAM.8 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, }, - "Resources": Object { - "PCIPCIAutoScaling1": Object { + "Resources": { + "ControlPCIPCIAutoScaling1": { "Condition": "EnablePCIAutoScaling1Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "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. - - ## 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 - - ## 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: - - 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+=,.@-]+$' - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. + +## 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 + +## Documentation Links +* [PCI AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-autoscaling-1) +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "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:(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}):autoScalingGroupName/(.{1,255})$", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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-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 -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.AutoScaling.1", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-EnableAutoScalingGroupELBHealthCheck", + "RuntimeParameters": { + "AutoScalingGroupName": "{{ParseInput.AutoScalingGroupName}}", + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableAutoScalingGroupELBHealthCheck", + }, + "TargetLocations": [ + { + "Accounts": [ + "{{ParseInput.RemediationAccount}}", + ], + "ExecutionRoleName": "SO0111-EnableAutoScalingGroupELBHealthCheck", + "Regions": [ + "{{ParseInput.RemediationRegion}}", + ], + }, + ], }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "ASG health check type updated to ELB", + "UpdatedBy": "ASR-PCI_3.2.1_AutoScaling.1", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "Remediation.Output", + "ParseInput.AffectedObject", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the PCI.AutoScaling.1 finding", + "type": "StringMap", + }, + "HealthCheckGracePeriod": { + "default": 30, + "description": "ELB Health Check Grace Period", + "type": "Integer", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-PCI_3.2.1_PCI.AutoScaling.1", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "PCIPCIEC26": Object { + "ControlPCIPCIEC26": { "Condition": "EnablePCIEC26Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.EC2.6 - - ## 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. - - ## 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 -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+=,.@-]+' - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-PCI_3.2.1_PCI.EC2.6 + +## 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. + +## 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": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "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}$)", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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 - } - - - 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 -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.EC2.6", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "ParseInput", + "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", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-EnableVPCFlowLogs", + "RuntimeParameters": { + "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/{{RemediationRoleName}}", + "RemediationRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-EnableVPCFlowLogs-remediationRole", + "VPC": "{{ParseInput.VPC}}", + }, + "TargetLocations": [ + { + "Accounts": [ + "{{ParseInput.RemediationAccount}}", + ], + "ExecutionRoleName": "{{RemediationRoleName}}", + "Regions": [ + "{{ParseInput.RemediationRegion}}", + ], + }, + ], }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Enabled VPC Flow Logs for {{ParseInput.VPC}}", + "UpdatedBy": "ASR-PCI_3.2.1_PCI.EC2.6", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, + ], + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the PCI.EC2.6 finding", + "type": "StringMap", + }, + "RemediationRoleName": { + "allowedPattern": "^[\\w+=,.@-]+", + "default": "SO0111-EnableVPCFlowLogs", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-PCI_3.2.1_PCI.EC2.6", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "PCIPCIIAM8": Object { + "ControlPCIPCIIAM8": { "Condition": "EnablePCIIAM8Condition", - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-PCI_3.2.1_PCI.IAM.8 - - ## What does this document do? - This document establishes a default password policy. - - ## Security Standards and Controls - * CIS 1.5 - 1.11 - * AFSBP IAM.7 - * PCI IAM.8 - - ## 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) - -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 - - 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' + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-PCI_3.2.1_PCI.IAM.8 + +## What does this document do? +This document establishes a default password policy. + +## Security Standards and Controls +* CIS 1.5 - 1.11 +* AFSBP IAM.7 +* PCI IAM.8 + +## 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": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "parse_event", + "InputPayload": { + "Finding": "{{Finding}}", + "expected_control_id": [ + "PCI.IAM.8", + ], + "parse_id_pattern": "", }, - user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' + "Runtime": "python3.8", + "Script": "#!/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 ) - 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' - - - 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 -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-PCI_3.2.1_PCI.IAM.8", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + 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 + }", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "ParseInput", + "outputs": [ + { + "Name": "FindingId", + "Selector": "$.Payload.finding_id", + "Type": "String", + }, + { + "Name": "ProductArn", + "Selector": "$.Payload.product_arn", + "Type": "String", + }, + { + "Name": "AffectedObject", + "Selector": "$.Payload.object", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:executeAutomation", + "inputs": { + "DocumentName": "ASR-SetIAMPasswordPolicy", + "RuntimeParameters": { + "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, + }, }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "Remediation", + }, + { + "action": "aws:executeAwsApi", + "description": "Update finding", + "inputs": { + "Api": "BatchUpdateFindings", + "FindingIdentifiers": [ + { + "Id": "{{ParseInput.FindingId}}", + "ProductArn": "{{ParseInput.ProductArn}}", + }, + ], + "Note": { + "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", + "UpdatedBy": "ASR-PCI_3.2.1_IAM.8", + }, + "Service": "securityhub", + "Workflow": { + "Status": "RESOLVED", + }, }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "UpdateFinding", + }, ], + "outputs": [ + "ParseInput.AffectedObject", + "Remediation.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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": { + "description": "The input from the Orchestrator Step function for the PCI.IAM.8 finding", + "type": "StringMap", + }, + }, + "schemaVersion": "0.3", }, - "VersionName": "v1.1.1", + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-PCI_3.2.1_PCI.IAM.8", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, }, } diff --git a/source/remediation_runbooks/CreateAccessLoggingBucket.yaml b/source/remediation_runbooks/CreateAccessLoggingBucket.yaml index 99219df2..a53921b8 100644 --- a/source/remediation_runbooks/CreateAccessLoggingBucket.yaml +++ b/source/remediation_runbooks/CreateAccessLoggingBucket.yaml @@ -1,9 +1,9 @@ description: | - ### Document Name - SHARR-CreateAccessLoggingBucket - + ### Document Name - ASR-CreateAccessLoggingBucket + ## What does this document do? Creates an S3 bucket for access logging. - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * BucketName: (Required) Name of the bucket to create @@ -23,11 +23,11 @@ outputs: - CreateAccessLoggingBucket.Output mainSteps: - - + - name: CreateAccessLoggingBucket action: 'aws:executeScript' inputs: - InputPayload: + InputPayload: BucketName: '{{BucketName}}' AWS_REGION: '{{global:REGION}}' Runtime: python3.8 diff --git a/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml b/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml index ba10c01a..82e81806 100644 --- a/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml +++ b/source/remediation_runbooks/CreateCloudTrailMultiRegionTrail.yaml @@ -1,9 +1,9 @@ description: | - ### Document Name - SHARR-CreateCloudTrailMultiRegionTrail + ### Document Name - ASR-CreateCloudTrailMultiRegionTrail ## What does this document do? Creates a multi-region trail with KMS encryption and enables CloudTrail Note: this remediation will create a NEW trail. - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data @@ -24,7 +24,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for this remediation + description: The ARN of the KMS key created by ASR 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})))$' AWSPartition: type: String @@ -39,7 +39,7 @@ outputs: - Remediation.Output mainSteps: - - + - name: CreateLoggingBucket action: 'aws:executeScript' outputs: @@ -47,10 +47,10 @@ mainSteps: Selector: $.Payload.logging_bucket Type: String inputs: - InputPayload: + InputPayload: account: '{{global:ACCOUNT_ID}}' region: '{{global:REGION}}' - kms_key_arn: '{{KMSKeyArn}}' + kms_key_arn: '{{KMSKeyArn}}' Runtime: python3.8 Handler: create_logging_bucket Script: |- @@ -58,7 +58,7 @@ mainSteps: isEnd: false - - + - name: CreateCloudTrailBucket action: 'aws:executeScript' outputs: @@ -66,7 +66,7 @@ mainSteps: Selector: $.Payload.cloudtrail_bucket Type: String inputs: - InputPayload: + InputPayload: account: '{{global:ACCOUNT_ID}}' region: '{{global:REGION}}' kms_key_arn: '{{KMSKeyArn}}' @@ -75,14 +75,14 @@ mainSteps: Handler: create_encrypted_bucket Script: |- %%SCRIPT=CreateCloudTrailMultiRegionTrail_createcloudtrailbucket.py%% - + isEnd: false - - + - name: CreateCloudTrailBucketPolicy action: 'aws:executeScript' inputs: - InputPayload: + InputPayload: cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' partition: '{{AWSPartition}}' account: '{{global:ACCOUNT_ID}}' @@ -100,7 +100,7 @@ mainSteps: Selector: $.Payload.cloudtrail_bucket Type: String inputs: - InputPayload: + InputPayload: cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' kms_key_arn: '{{KMSKeyArn}}' Runtime: python3.8 diff --git a/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml b/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml index 2c931932..0e645812 100644 --- a/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml +++ b/source/remediation_runbooks/CreateLogMetricFilterAndAlarm.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CreateLogMetricFilterAndAlarm + ### Document Name - ASR-CreateLogMetricFilterAndAlarm ## What does this document do? Creates a metric filter for a given log group and also creates and alarm for the metric. diff --git a/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml b/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml index e56c98a2..929bd1b6 100644 --- a/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml +++ b/source/remediation_runbooks/DisablePublicAccessToRedshiftCluster.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-DisablePublicAccessToRedshiftCluster + ### Document name - ASR-DisablePublicAccessToRedshiftCluster ## What does this document do? The runbook disables public accessibility for the Amazon Redshift cluster you specify using the [ModifyCluster] diff --git a/source/remediation_runbooks/EnableAWSConfig.yaml b/source/remediation_runbooks/EnableAWSConfig.yaml index a97ba8f0..3fdd7a8b 100644 --- a/source/remediation_runbooks/EnableAWSConfig.yaml +++ b/source/remediation_runbooks/EnableAWSConfig.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-EnableAWSConfig + ### Document name - ASR-EnableAWSConfig ## What does this document do? Enables AWS Config: @@ -61,7 +61,7 @@ mainSteps: action: 'aws:executeAutomation' isEnd: false inputs: - DocumentName: SHARR-CreateAccessLoggingBucket + DocumentName: ASR-CreateAccessLoggingBucket RuntimeParameters: BucketName: 'so0111-accesslogs-{{global:ACCOUNT_ID}}-{{global:REGION}}' AutomationAssumeRole: 'arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateAccessLoggingBucket' diff --git a/source/remediation_runbooks/EnableAutoScalingGroupELBHealthCheck.yaml b/source/remediation_runbooks/EnableAutoScalingGroupELBHealthCheck.yaml index dd1eae95..22133446 100644 --- a/source/remediation_runbooks/EnableAutoScalingGroupELBHealthCheck.yaml +++ b/source/remediation_runbooks/EnableAutoScalingGroupELBHealthCheck.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-EnableAutoScalingGroupELBHealthCheck + ### Document name - ASR-EnableAutoScalingGroupELBHealthCheck ## What does this document do? This runbook enables health checks for the Amazon EC2 Auto Scaling (Auto Scaling) group you specify using the [UpdateAutoScalingGroup](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_UpdateAutoScalingGroup.html) API. @@ -28,7 +28,7 @@ parameters: AutoScalingGroupName: type: String description: (Required) The Amazon Resource Name (ARN) of the auto scaling group that you want to enable health checks on. - allowedPattern: ^[\u0020-\uD7FF\uE000-\uFFFD\uD800\uDC00-\uDBFF\uDFFF]{1,255}$ + allowedPattern: '^.{1,255}$' HealthCheckGracePeriod: type: Integer description: (Optional) The amount of time, in seconds, that Auto Scaling waits before checking the health status of an Amazon Elastic Compute Cloud (Amazon EC2) instance that has come into service. diff --git a/source/remediation_runbooks/EnableAutomaticSnapshotsOnRedshiftCluster.yaml b/source/remediation_runbooks/EnableAutomaticSnapshotsOnRedshiftCluster.yaml index 408dc1e7..794106ca 100644 --- a/source/remediation_runbooks/EnableAutomaticSnapshotsOnRedshiftCluster.yaml +++ b/source/remediation_runbooks/EnableAutomaticSnapshotsOnRedshiftCluster.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document name - SHARR-EnableAutomaticSnapshotsOnRedshiftCluster + ### Document name - ASR-EnableAutomaticSnapshotsOnRedshiftCluster ## What does this document do? The runbook enables automatic snapshots on a Redshift cluster. diff --git a/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml b/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml index 8b6edf6c..41d833cf 100644 --- a/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml +++ b/source/remediation_runbooks/EnableAutomaticVersionUpgradeOnRedshiftCluster.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document name - SHARR-EnableAutomaticVersionUpgradeOnRedshiftCluster + ### Document name - ASR-EnableAutomaticVersionUpgradeOnRedshiftCluster ## What does this document do? The runbook enables automatic version upgrade on a Redshift cluster. diff --git a/source/remediation_runbooks/EnableCloudTrailEncryption.yaml b/source/remediation_runbooks/EnableCloudTrailEncryption.yaml index c410c4f2..f554614c 100644 --- a/source/remediation_runbooks/EnableCloudTrailEncryption.yaml +++ b/source/remediation_runbooks/EnableCloudTrailEncryption.yaml @@ -1,8 +1,8 @@ description: | - ### Document Name - SHARR-EnableCloudTrailEncryption + ### Document Name - ASR-EnableCloudTrailEncryption ## What does this document do? Enables encryption on a CloudTrail using the provided KMS CMK - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data @@ -25,7 +25,7 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for this remediation + description: The ARN of the KMS key created by ASR 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})))$' TrailRegion: type: String @@ -39,7 +39,7 @@ outputs: - Remediation.Output mainSteps: - - + - name: Remediation action: 'aws:executeScript' outputs: @@ -47,12 +47,12 @@ mainSteps: Selector: $.Payload.response Type: StringMap inputs: - InputPayload: + InputPayload: exec_region: '{{global:REGION}}' trail_region: '{{TrailRegion}}' trail: '{{TrailArn}}' region: '{{global:REGION}}' - kms_key_arn: '{{KMSKeyArn}}' + kms_key_arn: '{{KMSKeyArn}}' Runtime: python3.8 Handler: enable_trail_encryption Script: |- diff --git a/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml b/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml index 6aaa9089..b3f96a5f 100644 --- a/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml +++ b/source/remediation_runbooks/EnableCloudTrailToCloudWatchLogging.yaml @@ -1,8 +1,8 @@ description: | - ### Document Name - SHARR-EnableCloudTrailToCloudWatchLogging + ### Document Name - ASR-EnableCloudTrailToCloudWatchLogging ## What does this document do? Creates a CloudWatch logs group for CloudTrail data. - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data @@ -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. @@ -35,7 +35,7 @@ outputs: - UpdateTrailToCWLogs.Output mainSteps: - - + - name: CreateLogGroup action: 'aws:executeAwsApi' inputs: @@ -47,11 +47,11 @@ mainSteps: - Name: Output Selector: $ Type: StringMap - - + - name: WaitForCreation action: 'aws:executeScript' inputs: - InputPayload: + InputPayload: LogGroup: '{{LogGroupName}}' Runtime: python3.8 Handler: wait_for_loggroup @@ -64,7 +64,7 @@ mainSteps: isEnd: false - - + - name: UpdateTrailToCWLogs action: 'aws:executeAwsApi' inputs: @@ -78,4 +78,3 @@ mainSteps: - Name: Output Selector: $ Type: StringMap - \ No newline at end of file diff --git a/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml b/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml index b2a7f674..66ef2330 100644 --- a/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml +++ b/source/remediation_runbooks/EnableDefaultEncryptionS3.yaml @@ -1,5 +1,5 @@ description: | - ### Document name - SHARR-EnableDefaultEncryptionS3 + ### Document name - ASR-EnableDefaultEncryptionS3 ## What does this document do? This document configures default encryption for an Amazon S3 Bucket. diff --git a/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml b/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml index 4ced37ab..9d704f35 100644 --- a/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml +++ b/source/remediation_runbooks/EnableMultiAZOnRDSInstance.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-EnableMultiAZOnRDSInstance + ### Document name - ASR-EnableMultiAZOnRDSInstance ## What does this document do? This document enables MultiAZ on an RDS instance. diff --git a/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml b/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml index 3fee51ea..4f8bbaa6 100644 --- a/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml +++ b/source/remediation_runbooks/EnableRDSInstanceDeletionProtection.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document Name - SHARR-EnableRDSInstanceDeletionProtection + ### Document Name - ASR-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. diff --git a/source/remediation_runbooks/EnableVPCFlowLogs.yaml b/source/remediation_runbooks/EnableVPCFlowLogs.yaml index 0b76b718..bd0a6b82 100644 --- a/source/remediation_runbooks/EnableVPCFlowLogs.yaml +++ b/source/remediation_runbooks/EnableVPCFlowLogs.yaml @@ -1,8 +1,8 @@ description: | - ### Document Name - SHARR-EnableVPCFlowLogs + ### Document Name - ASR-EnableVPCFlowLogs ## What does this document do? Enables VPC Flow Logs for a given VPC - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * VPC: VPC Id of the VPC for which logs are to be enabled @@ -33,14 +33,14 @@ parameters: type: String default: >- {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations requiring encryption + description: The ARN of the KMS key created by ASR for remediations requiring encryption 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 mainSteps: - - + - name: Remediation action: 'aws:executeScript' outputs: @@ -48,10 +48,10 @@ mainSteps: Selector: $.Payload.response Type: StringMap inputs: - InputPayload: + InputPayload: vpc: '{{VPC}}' - remediation_role: '{{RemediationRole}}' - kms_key_arn: '{{KMSKeyArn}}' + remediation_role: '{{RemediationRole}}' + kms_key_arn: '{{KMSKeyArn}}' Runtime: python3.8 Handler: enable_flow_logs Script: |- diff --git a/source/remediation_runbooks/EncryptRDSSnapshot.yaml b/source/remediation_runbooks/EncryptRDSSnapshot.yaml index d563b38c..8a72252e 100644 --- a/source/remediation_runbooks/EncryptRDSSnapshot.yaml +++ b/source/remediation_runbooks/EncryptRDSSnapshot.yaml @@ -3,7 +3,7 @@ --- schemaVersion: '0.3' description: | - ### Document Name - SHARR-EncryptRDSSnapshot + ### Document Name - ASR-EncryptRDSSnapshot ## What does this document do? This document encrypts an RDS snapshot or cluster snapshot. diff --git a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml index 007e90ca..33698c0f 100644 --- a/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml +++ b/source/remediation_runbooks/MakeEBSSnapshotsPrivate.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-MakeEBSSnapshotPrivate + ### Document name - ASR-MakeEBSSnapshotPrivate ## What does this document do? This runbook works an the account level to remove public share on all EBS snapshots diff --git a/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml b/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml index 2282aea2..6516eb5c 100644 --- a/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml +++ b/source/remediation_runbooks/MakeRDSSnapshotPrivate.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-MakeRDSSnapshotPrivate + ### Document name - ASR-MakeRDSSnapshotPrivate ## What does this document do? This runbook removes public access to an RDS Snapshot diff --git a/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml b/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml index 55004c57..33a5e8b9 100644 --- a/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml +++ b/source/remediation_runbooks/RemoveLambdaPublicAccess.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-RemoveLambdaPublicAccess + ### Document name - ASR-RemoveLambdaPublicAccess ## What does this document do? This document removes the public resource policy. A public resource policy diff --git a/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml b/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml index a0370efd..ed58d0dd 100644 --- a/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml +++ b/source/remediation_runbooks/ReplaceCodeBuildClearTextCredentials.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-ReplaceCodeBuildClearTextCredentials + ### Document Name - ASR-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. diff --git a/source/remediation_runbooks/RevokeUnrotatedKeys.yaml b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml index 1d05a2c5..e8233630 100644 --- a/source/remediation_runbooks/RevokeUnrotatedKeys.yaml +++ b/source/remediation_runbooks/RevokeUnrotatedKeys.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document Name - SHARR-RevokeUnrotatedKeys + ### Document Name - ASR-RevokeUnrotatedKeys ## What does this document do? This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. It will disabled keys that have been used within the previous 90 days by have not been rotated by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html). Please note, this automation document requires AWS Config to be enabled. diff --git a/source/remediation_runbooks/S3BlockDenylist.yaml b/source/remediation_runbooks/S3BlockDenylist.yaml index 6cd00307..eae2f691 100644 --- a/source/remediation_runbooks/S3BlockDenylist.yaml +++ b/source/remediation_runbooks/S3BlockDenylist.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-S3BlockDenyList + ### Document Name - ASR-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. diff --git a/source/remediation_runbooks/SetSSLBucketPolicy.yaml b/source/remediation_runbooks/SetSSLBucketPolicy.yaml index dc55f68a..c7e4f3f1 100644 --- a/source/remediation_runbooks/SetSSLBucketPolicy.yaml +++ b/source/remediation_runbooks/SetSSLBucketPolicy.yaml @@ -1,6 +1,6 @@ schemaVersion: "0.3" description: | - ### Document name - SHARR-SetSSLBucketPolicy + ### Document name - ASR-SetSSLBucketPolicy ## What does this document do? This document adds a bucket policy to require transmission over HTTPS for the given S3 bucket by adding a policy statement to the bucket policy. diff --git a/source/solution_deploy/bin/solution_deploy.ts b/source/solution_deploy/bin/solution_deploy.ts index 8ef8fba7..7974b2cf 100644 --- a/source/solution_deploy/bin/solution_deploy.ts +++ b/source/solution_deploy/bin/solution_deploy.ts @@ -14,7 +14,7 @@ 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_9; const app = new cdk.App(); cdk.Aspects.of(app).add(new cdk_nag.AwsSolutionsChecks({verbose: true})); diff --git a/source/solution_deploy/lib/remediation_runbook-stack.ts b/source/solution_deploy/lib/remediation_runbook-stack.ts index 71e3ea2c..7b764e76 100644 --- a/source/solution_deploy/lib/remediation_runbook-stack.ts +++ b/source/solution_deploy/lib/remediation_runbook-stack.ts @@ -130,7 +130,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -193,7 +193,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -225,7 +225,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -285,7 +285,7 @@ export class RemediationRunbookStack extends cdk.Stack { ) ssmPerms.effect = Effect.ALLOW ssmPerms.addResources( - `arn:${this.partition}:ssm:*:${this.account}:automation-definition/SHARR-CreateAccessLoggingBucket:*` + `arn:${this.partition}:ssm:*:${this.account}:automation-definition/ASR-CreateAccessLoggingBucket:*` ); inlinePolicy.addStatements(ssmPerms) } @@ -327,7 +327,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -431,7 +431,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR ' + remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR ' + remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -475,7 +475,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -526,7 +526,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -649,7 +649,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -694,7 +694,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -737,7 +737,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -783,7 +783,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -830,7 +830,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -888,7 +888,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -944,7 +944,7 @@ export class RemediationRunbookStack extends cdk.Stack { } } - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -993,7 +993,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1036,7 +1036,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1083,7 +1083,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1128,7 +1128,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1177,7 +1177,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1222,7 +1222,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1267,7 +1267,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1397,7 +1397,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1440,7 +1440,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1484,7 +1484,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1517,7 +1517,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1576,7 +1576,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1625,7 +1625,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1672,7 +1672,8 @@ export class RemediationRunbookStack extends cdk.Stack { const rdsPerms = new PolicyStatement(); rdsPerms.addActions( "rds:DescribeDBClusters", - "rds:ModifyDBCluster" + "rds:ModifyDBCluster", + "rds:ModifyDBInstance" ) rdsPerms.effect = Effect.ALLOW rdsPerms.addResources("*"); @@ -1685,7 +1686,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1746,7 +1747,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1792,7 +1793,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1835,7 +1836,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1890,7 +1891,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1949,7 +1950,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -1995,7 +1996,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }) - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -2037,7 +2038,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, @@ -2081,7 +2082,7 @@ export class RemediationRunbookStack extends cdk.Stack { remediationRoleName: `${remediationRoleNameBase}${remediationName}` }); - RunbookFactory.createRemediationRunbook(this, 'SHARR '+ remediationName, { + RunbookFactory.createRemediationRunbook(this, 'ASR '+ remediationName, { ssmDocName: remediationName, ssmDocPath: ssmdocs, ssmDocFileName: `${remediationName}.yaml`, diff --git a/source/solution_deploy/lib/runbook_factory.ts b/source/solution_deploy/lib/runbook_factory.ts index 1a2ba8b8..590ea8b9 100644 --- a/source/solution_deploy/lib/runbook_factory.ts +++ b/source/solution_deploy/lib/runbook_factory.ts @@ -1,162 +1,18 @@ #!/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. * - *****************************************************************************/ - +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 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 { CfnDocument } from '@aws-cdk/aws-ssm'; 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; -}; +import * as yaml from 'js-yaml'; export class RunbookFactory extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: RunbookFactoryProps) { + constructor(scope: cdk.Construct, id: string) { 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 { + static createControlRunbook(scope: cdk.Construct, id: string, props: IssmPlaybookProps): CfnDocument { let scriptPath = ''; if (props.scriptPath == undefined ) { scriptPath = `${props.ssmDocPath}/scripts`; @@ -182,7 +38,7 @@ export class RunbookFactory extends cdk.Construct { expression: cdk.Fn.conditionEquals(enableParam, 'Available') }); - const ssmDocName = `SHARR-${props.securityStandard}_${props.securityStandardVersion}_${props.controlId}`; + const ssmDocName = `ASR-${props.securityStandard}_${props.securityStandardVersion}_${props.controlId}`; const ssmDocFQFileName = `${props.ssmDocPath}/${props.ssmDocFileName}`; const ssmDocType = props.ssmDocFileName.substring(props.ssmDocFileName.length - 4).toLowerCase(); @@ -210,26 +66,21 @@ export class RunbookFactory extends cdk.Construct { } } - const ssmDoc = new cdk.CustomResource(scope, id, { - serviceToken: RunbookFactory.getServiceToken(scope, props.solutionId), - resourceType: RunbookFactory.getResourceType(), - properties: { - Name: ssmDocName, - Content: ssmDocOut, - DocumentFormat: ssmDocType.toUpperCase(), - VersionName: props.solutionVersion, - DocumentType: 'Automation' - } + const ssmDoc = new CfnDocument(scope, `Control ${id}`, { + name: ssmDocName, + content: yaml.load(ssmDocOut), + documentFormat: ssmDocType.toUpperCase(), + documentType: 'Automation', + updateMethod: 'NewVersion', }); - const ssmDocCfnResource = ssmDoc.node.defaultChild as cdk.CfnCustomResource; - ssmDocCfnResource.cfnOptions.condition = installSsmDoc; + ssmDoc.cfnOptions.condition = installSsmDoc; return ssmDoc; } static createRemediationRunbook(scope: cdk.Construct, id: string, props: RemediationRunbookProps) { - const ssmDocName = `SHARR-${props.ssmDocName}`; + const ssmDocName = `ASR-${props.ssmDocName}`; let scriptPath = ''; if (props.scriptPath == undefined) { scriptPath = 'ssmdocs/scripts'; @@ -257,15 +108,12 @@ export class RunbookFactory extends cdk.Construct { } } - const runbook = new cdk.CustomResource(scope, id, { - serviceToken: RunbookFactory.getServiceToken(scope, props.solutionId), - resourceType: RunbookFactory.getResourceType(), - properties: { - Name: ssmDocName, - Content: ssmDocOut, - DocumentFormat: ssmDocType.toUpperCase(), - DocumentType: 'Automation' - } + const runbook = new CfnDocument(scope, id, { + content: yaml.load(ssmDocOut), + documentFormat: ssmDocType.toUpperCase(), + documentType: 'Automation', + name: ssmDocName, + updateMethod: 'NewVersion', }); return runbook; diff --git a/source/solution_deploy/lib/sharr_member-stack.ts b/source/solution_deploy/lib/sharr_member-stack.ts index 8a9cbece..abce96e9 100644 --- a/source/solution_deploy/lib/sharr_member-stack.ts +++ b/source/solution_deploy/lib/sharr_member-stack.ts @@ -218,15 +218,7 @@ export class MemberStack extends cdk.Stack { }} }); - const runbookFactory = new RunbookFactory(this, 'RunbookProvider', { - solutionId: props.solutionId, - runtimePython: props.runtimePython, - solutionDistBucket: props.solutionDistBucket, - solutionTMN: props.solutionTMN, - solutionVersion: props.solutionVersion, - region: this.region, - partition: this.partition - }); + const runbookFactory = new RunbookFactory(this, 'RunbookProvider'); //------------------------------------------------------------------------- // Runbooks - shared automations diff --git a/source/solution_deploy/lib/solution_deploy-stack.ts b/source/solution_deploy/lib/solution_deploy-stack.ts index 001a333a..83f4e524 100644 --- a/source/solution_deploy/lib/solution_deploy-stack.ts +++ b/source/solution_deploy/lib/solution_deploy-stack.ts @@ -164,11 +164,7 @@ export class SolutionDeployStack extends cdk.Stack { * @type {lambda.LayerVersion} */ const sharrLambdaLayer = new lambda.LayerVersion(this, 'SharrLambdaLayer', { - compatibleRuntimes: [ - lambda.Runtime.PYTHON_3_6, - lambda.Runtime.PYTHON_3_7, - lambda.Runtime.PYTHON_3_8 - ], + compatibleRuntimes: [lambda.Runtime.PYTHON_3_9], description: 'SO0111 SHARR Common functions used by the solution', license: "https://www.apache.org/licenses/LICENSE-2.0", code: lambda.Code.fromBucket( diff --git a/source/solution_deploy/source/bin/normalizer b/source/solution_deploy/source/bin/normalizer deleted file mode 100755 index ed58a841..00000000 --- a/source/solution_deploy/source/bin/normalizer +++ /dev/null @@ -1,8 +0,0 @@ -#!/root/.pyenv/versions/3.8.10/bin/python3.8 -# -*- coding: utf-8 -*- -import re -import sys -from charset_normalizer.cli.normalizer import cli_detect -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_detect()) diff --git a/source/test/__snapshots__/admin_account_parm.test.ts.snap b/source/test/__snapshots__/admin_account_parm.test.ts.snap index 4791a5d7..8803e29c 100644 --- a/source/test/__snapshots__/admin_account_parm.test.ts.snap +++ b/source/test/__snapshots__/admin_account_parm.test.ts.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AdminParm Test Stack 1`] = ` -Object { - "Parameters": Object { - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", +{ + "Parameters": { + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, diff --git a/source/test/__snapshots__/member_stack.test.ts.snap b/source/test/__snapshots__/member_stack.test.ts.snap index 10b224c5..eda59a14 100644 --- a/source/test/__snapshots__/member_stack.test.ts.snap +++ b/source/test/__snapshots__/member_stack.test.ts.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`default stack 1`] = ` -Object { - "Conditions": Object { - "EnableS3BucketForRedShift4": Object { - "Fn::Equals": Array [ - Object { +{ + "Conditions": { + "EnableS3BucketForRedShift4": { + "Fn::Equals": [ + { "Ref": "CreateS3BucketForRedshiftAuditLogging", }, "yes", @@ -13,42 +13,42 @@ Object { }, }, "Description": "test;", - "Mappings": Object { - "SourceCode": Object { - "General": Object { + "Mappings": { + "SourceCode": { + "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.1.1", "S3Bucket": "sharrbukkit", }, }, }, - "Metadata": Object { - "AWS::CloudFormation::Interface": Object { - "ParameterGroups": Array [ - Object { - "Label": Object { + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { "default": "LogGroup Configuration", }, - "Parameters": Array [ + "Parameters": [ "LogGroupName", ], }, - Object { - "Label": Object { + { + "Label": { "default": "Playbooks", }, - "Parameters": Array [], + "Parameters": [], }, ], - "ParameterLabels": Object { - "LogGroupName": Object { + "ParameterLabels": { + "LogGroupName": { "default": "Provide the name of the LogGroup to be used to create Metric Filters and Alarms", }, }, }, }, - "Parameters": Object { - "CreateS3BucketForRedshiftAuditLogging": Object { - "AllowedValues": Array [ + "Parameters": { + "CreateS3BucketForRedshiftAuditLogging": { + "AllowedValues": [ "yes", "no", ], @@ -56,201 +56,34 @@ Object { "Description": "Create S3 Bucket For Redshift Cluster Audit Logging.", "Type": "String", }, - "LogGroupName": Object { + "LogGroupName": { "Description": "Name of the log group to be used to create metric filters and cloudwatch alarms. You must use a Log Group that is the the logging destination of a multi-region CloudTrail", "Type": "String", }, - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, }, - "Resources": Object { - "RunbookProviderFunction82CD9D9B": Object { - "DependsOn": Array [ - "RunbookProviderRoleBC4E91CA", - ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W58", - "reason": "False positive. Access is provided via a policy", - }, - Object { - "id": "W89", - "reason": "There is no need to run this lambda in a VPC", - }, - ], - }, - }, - "Properties": Object { - "Code": Object { - "S3Bucket": Object { - "Fn::Join": Array [ - "", - Array [ - "sharrbukkit-", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "S3Key": "aws-security-hub-automated-response-and-remediation/v1.1.1/lambda/updatableRunbookProvider.py.zip", - }, - "Description": "Custom resource to manage versioned SSM documents", - "Environment": Object { - "Variables": Object { - "LOG_LEVEL": "info", - "SOLUTION_ID": "AwsSolution/SO0111/v1.1.1", - }, - }, - "FunctionName": "SO0111-SHARR-updatableRunbookProvider", - "Handler": "updatableRunbookProvider.lambda_handler", - "Layers": Array [ - Object { - "Ref": "RunbookProviderMemberLambdaLayerF3BD824A", - }, - ], - "MemorySize": 256, - "ReservedConcurrentExecutions": 1, - "Role": Object { - "Fn::GetAtt": Array [ - "RunbookProviderRoleBC4E91CA", - "Arn", - ], - }, - "Runtime": "python3.8", - "Timeout": 600, - }, - "Type": "AWS::Lambda::Function", - }, - "RunbookProviderMemberLambdaLayerF3BD824A": Object { - "Properties": Object { - "CompatibleRuntimes": Array [ - "python3.8", - ], - "Content": Object { - "S3Bucket": Object { - "Fn::Join": Array [ - "", - Array [ - "sharrbukkit-", - Object { - "Ref": "AWS::Region", - }, - ], - ], - }, - "S3Key": "aws-security-hub-automated-response-and-remediation/v1.1.1/lambda/memberLayer.zip", - }, - "Description": "SO0111 SHARR Common functions used by the solution member stack", - "LicenseInfo": "https://www.apache.org/licenses/LICENSE-2.0", - }, - "Type": "AWS::Lambda::LayerVersion", - }, - "RunbookProviderPolicy12B46DCD": Object { - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "AwsSolutions-IAM5", - "reason": "Resource * is required in order to manage arbitrary SSM documents", - }, - ], - }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W12", - "reason": "Resource * is required in order to manage arbitrary SSM documents", - }, - ], - }, - }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "cloudwatch:PutMetricData", - "Effect": "Allow", - "Resource": "*", - }, - Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - ], - "Effect": "Allow", - "Resource": "*", - }, - Object { - "Action": Array [ - "ssm:CreateDocument", - "ssm:UpdateDocument", - "ssm:UpdateDocumentDefaultVersion", - "ssm:ListDocumentVersions", - "ssm:DeleteDocument", - ], - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "SO0111-SHARR_Runbook_Provider_Policy", - "Roles": Array [ - Object { - "Ref": "RunbookProviderRoleBC4E91CA", - }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "RunbookProviderRoleBC4E91CA": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "lambda.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "Description": "Lambda role to allow creation of updatable SSM documents", - }, - "Type": "AWS::IAM::Role", - }, - "RunbookStackNoRoles": Object { - "DependsOn": Array [ - "RunbookProviderFunction82CD9D9B", - "RunbookProviderMemberLambdaLayerF3BD824A", - "RunbookProviderPolicy12B46DCD", - "RunbookProviderRoleBC4E91CA", - ], - "Properties": Object { - "TemplateURL": Object { - "Fn::Join": Array [ + "Resources": { + "RunbookStackNoRoles": { + "Properties": { + "TemplateURL": { + "Fn::Join": [ "", - Array [ + [ "https://", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket", ], }, "-reference.s3.amazonaws.com/", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix", @@ -263,42 +96,42 @@ Object { }, "Type": "AWS::CloudFormation::Stack", }, - "S3BucketForRedShiftAuditLogging652E7355": Object { + "S3BucketForRedShiftAuditLogging652E7355": { "Condition": "EnableS3BucketForRedShift4", "DeletionPolicy": "Retain", - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-S1", "reason": "Logs bucket does not require logging configuration", }, - Object { + { "id": "AwsSolutions-S10", "reason": "Secure transport requirement is redundant for this use case", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W35", "reason": "Logs bucket does not require logging configuration", }, ], }, }, - "Properties": Object { - "BucketEncryption": Object { - "ServerSideEncryptionConfiguration": Array [ - Object { - "ServerSideEncryptionByDefault": Object { + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256", }, }, ], }, - "PublicAccessBlockConfiguration": Object { + "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, "IgnorePublicAcls": true, @@ -308,49 +141,49 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, - "S3BucketForRedShiftAuditLoggingBucketPolicyAB8BAA40": Object { + "S3BucketForRedShiftAuditLoggingBucketPolicyAB8BAA40": { "Condition": "EnableS3BucketForRedShift4", "DeletionPolicy": "Retain", - "DependsOn": Array [ + "DependsOn": [ "S3BucketForRedShiftAuditLogging652E7355", ], - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-S10", "reason": "Secure transport requirement is redundant for this use case", }, ], }, }, - "Properties": Object { - "Bucket": Object { + "Properties": { + "Bucket": { "Ref": "S3BucketForRedShiftAuditLogging652E7355", }, - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "PolicyDocument": { + "Statement": [ + { + "Action": [ "s3:GetBucketAcl", "s3:PutObject", ], "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "redshift.amazonaws.com", }, - "Resource": Array [ - Object { - "Fn::GetAtt": Array [ + "Resource": [ + { + "Fn::GetAtt": [ "S3BucketForRedShiftAuditLogging652E7355", "Arn", ], }, - Object { - "Fn::Sub": Array [ + { + "Fn::Sub": [ "arn:\${AWS::Partition}:s3:::\${BucketName}/*", - Object { - "BucketName": Object { + { + "BucketName": { "Ref": "S3BucketForRedShiftAuditLogging652E7355", }, }, @@ -366,13 +199,13 @@ Object { "Type": "AWS::S3::BucketPolicy", "UpdateReplacePolicy": "Retain", }, - "SHARRKeyAliasEBF509D8": Object { - "Properties": Object { + "SHARRKeyAliasEBF509D8": { + "Properties": { "Description": "KMS Customer Managed Key that will encrypt data for remediations", "Name": "/Solutions/SO0111/CMK_REMEDIATION_ARN", "Type": "String", - "Value": Object { - "Fn::GetAtt": Array [ + "Value": { + "Fn::GetAtt": [ "SHARRRemediationKeyE744743D", "Arn", ], @@ -380,8 +213,8 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SHARRMemberVersionEDAB5C42": Object { - "Properties": Object { + "SHARRMemberVersionEDAB5C42": { + "Properties": { "Description": "Version of the AWS Security Hub Automated Response and Remediation solution", "Name": "/Solutions/SO0111/member-version", "Type": "String", @@ -389,11 +222,11 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SHARRRemediationKeyAlias5531874D": Object { - "Properties": Object { + "SHARRRemediationKeyAlias5531874D": { + "Properties": { "AliasName": "alias/SO0111-SHARR-Remediation-Key", - "TargetKeyId": Object { - "Fn::GetAtt": Array [ + "TargetKeyId": { + "Fn::GetAtt": [ "SHARRRemediationKeyE744743D", "Arn", ], @@ -401,14 +234,14 @@ Object { }, "Type": "AWS::KMS::Alias", }, - "SHARRRemediationKeyE744743D": Object { + "SHARRRemediationKeyE744743D": { "DeletionPolicy": "Retain", - "Properties": Object { + "Properties": { "EnableKeyRotation": true, - "KeyPolicy": Object { - "Statement": Array [ - Object { - "Action": Array [ + "KeyPolicy": { + "Statement": [ + { + "Action": [ "kms:GenerateDataKey", "kms:GenerateDataKeyPair", "kms:GenerateDataKeyPairWithoutPlaintext", @@ -421,42 +254,42 @@ Object { "kms:DescribeCustomKeyStores", ], "Effect": "Allow", - "Principal": Object { - "Service": Array [ + "Principal": { + "Service": [ "sns.amazonaws.com", "s3.amazonaws.com", - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "logs.", - Object { + { "Ref": "AWS::URLSuffix", }, ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "logs.", - Object { + { "Ref": "AWS::Region", }, ".", - Object { + { "Ref": "AWS::URLSuffix", }, ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "cloudtrail.", - Object { + { "Ref": "AWS::URLSuffix", }, ], @@ -467,20 +300,20 @@ Object { }, "Resource": "*", }, - Object { + { "Action": "kms:*", "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::", - Object { + { "Ref": "AWS::AccountId", }, ":root", @@ -497,8 +330,8 @@ Object { "Type": "AWS::KMS::Key", "UpdateReplacePolicy": "Retain", }, - "SSMParameterForS34EncryptionKeyAlias73DD8A98": Object { - "Properties": Object { + "SSMParameterForS34EncryptionKeyAlias73DD8A98": { + "Properties": { "Description": "Parameter to store encryption key alias for the PCI.S3.4/AFSBP.S3.4, replace the default value with the KMS Key Alias, other wise the remediation will enable the default AES256 encryption for the bucket.", "Name": "/Solutions/SO0111/afsbp/1.0.0/S3.4/KmsKeyAlias", "Type": "String", @@ -506,27 +339,27 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SSMParameterForS3BucketNameForREDSHIFT441DD36B1": Object { + "SSMParameterForS3BucketNameForREDSHIFT441DD36B1": { "Condition": "EnableS3BucketForRedShift4", - "DependsOn": Array [ + "DependsOn": [ "S3BucketForRedShiftAuditLogging652E7355", ], - "Properties": Object { + "Properties": { "Description": "Parameter to store the S3 bucket name for the remediation AFSBP.REDSHIFT.4, the default value is bucket-name which has to be updated by the user before using the remediation.", "Name": "/Solutions/SO0111/afsbp/1.0.0/REDSHIFT.4/S3BucketNameForAuditLogging", "Type": "String", - "Value": Object { + "Value": { "Ref": "S3BucketForRedShiftAuditLogging652E7355", }, }, "Type": "AWS::SSM::Parameter", }, - "SSMParameterLogGroupName47918519": Object { - "Properties": Object { + "SSMParameterLogGroupName47918519": { + "Properties": { "Description": "Parameter to store log group name", "Name": "/Solutions/SO0111/Metrics_LogGroupName", "Type": "String", - "Value": Object { + "Value": { "Ref": "LogGroupName", }, }, diff --git a/source/test/__snapshots__/orchestrator.test.ts.snap b/source/test/__snapshots__/orchestrator.test.ts.snap index 4799cf05..a8e7ac41 100644 --- a/source/test/__snapshots__/orchestrator.test.ts.snap +++ b/source/test/__snapshots__/orchestrator.test.ts.snap @@ -1,141 +1,141 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`test App Orchestrator Construct 1`] = ` -Object { - "Mappings": Object { - "ServiceprincipalMap": Object { - "af-south-1": Object { +{ + "Mappings": { + "ServiceprincipalMap": { + "af-south-1": { "states": "states.af-south-1.amazonaws.com", }, - "ap-east-1": Object { + "ap-east-1": { "states": "states.ap-east-1.amazonaws.com", }, - "ap-northeast-1": Object { + "ap-northeast-1": { "states": "states.ap-northeast-1.amazonaws.com", }, - "ap-northeast-2": Object { + "ap-northeast-2": { "states": "states.ap-northeast-2.amazonaws.com", }, - "ap-northeast-3": Object { + "ap-northeast-3": { "states": "states.ap-northeast-3.amazonaws.com", }, - "ap-south-1": Object { + "ap-south-1": { "states": "states.ap-south-1.amazonaws.com", }, - "ap-southeast-1": Object { + "ap-southeast-1": { "states": "states.ap-southeast-1.amazonaws.com", }, - "ap-southeast-2": Object { + "ap-southeast-2": { "states": "states.ap-southeast-2.amazonaws.com", }, - "ap-southeast-3": Object { + "ap-southeast-3": { "states": "states.ap-southeast-3.amazonaws.com", }, - "ca-central-1": Object { + "ca-central-1": { "states": "states.ca-central-1.amazonaws.com", }, - "cn-north-1": Object { + "cn-north-1": { "states": "states.cn-north-1.amazonaws.com", }, - "cn-northwest-1": Object { + "cn-northwest-1": { "states": "states.cn-northwest-1.amazonaws.com", }, - "eu-central-1": Object { + "eu-central-1": { "states": "states.eu-central-1.amazonaws.com", }, - "eu-north-1": Object { + "eu-north-1": { "states": "states.eu-north-1.amazonaws.com", }, - "eu-south-1": Object { + "eu-south-1": { "states": "states.eu-south-1.amazonaws.com", }, - "eu-south-2": Object { + "eu-south-2": { "states": "states.eu-south-2.amazonaws.com", }, - "eu-west-1": Object { + "eu-west-1": { "states": "states.eu-west-1.amazonaws.com", }, - "eu-west-2": Object { + "eu-west-2": { "states": "states.eu-west-2.amazonaws.com", }, - "eu-west-3": Object { + "eu-west-3": { "states": "states.eu-west-3.amazonaws.com", }, - "me-south-1": Object { + "me-south-1": { "states": "states.me-south-1.amazonaws.com", }, - "sa-east-1": Object { + "sa-east-1": { "states": "states.sa-east-1.amazonaws.com", }, - "us-east-1": Object { + "us-east-1": { "states": "states.us-east-1.amazonaws.com", }, - "us-east-2": Object { + "us-east-2": { "states": "states.us-east-2.amazonaws.com", }, - "us-gov-east-1": Object { + "us-gov-east-1": { "states": "states.us-gov-east-1.amazonaws.com", }, - "us-gov-west-1": Object { + "us-gov-west-1": { "states": "states.us-gov-west-1.amazonaws.com", }, - "us-iso-east-1": Object { + "us-iso-east-1": { "states": "states.amazonaws.com", }, - "us-iso-west-1": Object { + "us-iso-west-1": { "states": "states.amazonaws.com", }, - "us-isob-east-1": Object { + "us-isob-east-1": { "states": "states.amazonaws.com", }, - "us-west-1": Object { + "us-west-1": { "states": "states.us-west-1.amazonaws.com", }, - "us-west-2": Object { + "us-west-2": { "states": "states.us-west-2.amazonaws.com", }, }, }, - "Parameters": Object { - "ReuseOrchestratorLogGroup": Object { - "AllowedValues": Array [ + "Parameters": { + "ReuseOrchestratorLogGroup": { + "AllowedValues": [ "yes", "no", ], "Default": "no", - "Description": "Reuse existing Orchestrator Log Group? Choose \\"yes\\" if the log group already exists, else \\"no\\"", + "Description": "Reuse existing Orchestrator Log Group? Choose "yes" if the log group already exists, else "no"", "Type": "String", }, }, - "Resources": Object { - "OrchestratorNestedLogStack5F778DA0": Object { - "Properties": Object { - "Parameters": Object { - "KmsKeyArn": Object { - "Fn::GetAtt": Array [ + "Resources": { + "OrchestratorNestedLogStack5F778DA0": { + "Properties": { + "Parameters": { + "KmsKeyArn": { + "Fn::GetAtt": [ "SHARRKeyC551FE02", "Value", ], }, - "ReuseOrchestratorLogGroup": Object { + "ReuseOrchestratorLogGroup": { "Ref": "ReuseOrchestratorLogGroup", }, }, - "TemplateURL": Object { - "Fn::Join": Array [ + "TemplateURL": { + "Fn::Join": [ "", - Array [ + [ "https://", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket", ], }, "-reference.s3.amazonaws.com/", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix", @@ -148,37 +148,37 @@ Object { }, "Type": "AWS::CloudFormation::Stack", }, - "OrchestratorRole9CF251DB": Object { + "OrchestratorRole9CF251DB": { "DeletionPolicy": "Retain", - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-IAM5", "reason": "CloudWatch Logs permissions require resource * except for DescribeLogGroups, except for GovCloud, which only works with resource *", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W11", "reason": "CloudWatch Logs permissions require resource * except for DescribeLogGroups, except for GovCloud, which only works with resource *", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { - "Service": Object { - "Fn::FindInMap": Array [ + "Principal": { + "Service": { + "Fn::FindInMap": [ "ServiceprincipalMap", - Object { + { "Ref": "AWS::Region", }, "states", @@ -189,12 +189,12 @@ Object { ], "Version": "2012-10-17", }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "logs:CreateLogDelivery", "logs:GetLogDelivery", "logs:UpdateLogDelivery", @@ -207,24 +207,24 @@ Object { "Effect": "Allow", "Resource": "*", }, - Object { + { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ + "Resource": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:", - Object { + { "Ref": "AWS::Region", }, ":", - Object { + { "Ref": "AWS::AccountId", }, ":function:undefined", @@ -233,27 +233,27 @@ Object { }, ], }, - Object { - "Action": Array [ + { + "Action": [ "kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":kms:", - Object { + { "Ref": "AWS::Region", }, ":", - Object { + { "Ref": "AWS::AccountId", }, ":alias/bbb-SHARR-Key", @@ -271,87 +271,87 @@ Object { "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Retain", }, - "OrchestratorSHARROrchestratorArnC8FB076A": Object { - "Properties": Object { + "OrchestratorSHARROrchestratorArnC8FB076A": { + "Properties": { "Description": "Arn of the SHARR Orchestrator Step Function. This step function routes findings to remediation runbooks.", "Name": "/Solutions/bbb/OrchestratorArn", "Type": "String", - "Value": Object { + "Value": { "Ref": "OrchestratorStateMachine1E795392", }, }, "Type": "AWS::SSM::Parameter", }, - "OrchestratorStateMachine1E795392": Object { - "DependsOn": Array [ + "OrchestratorStateMachine1E795392": { + "DependsOn": [ "OrchestratorNestedLogStack5F778DA0", "OrchestratorRole9CF251DB", ], - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-SF1", "reason": "False alarm. Logging configuration is overridden to log ALL.", }, - Object { + { "id": "AwsSolutions-SF2", "reason": "X-Ray is not needed for this use case.", }, ], }, }, - "Properties": Object { - "DefinitionString": Object { - "Fn::Join": Array [ + "Properties": { + "DefinitionString": { + "Fn::Join": [ "", - Array [ - "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Remediation Approval Requirement\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", - Object { + [ + "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","Parameters":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType"},"Iterator":{"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOTNEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\",\\"ResourceRegion.$\\":\\"$.Payload.resourceregion\\"},\\"Resource\\":\\"arn:", - Object { + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"Automation Document is not Active":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)","State.$":"States.Format('REMEDIATIONNOTACTIVE')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"},"Automation Doc Active?":{"Type":"Choice","Choices":[{"Variable":"$.AutomationDocument.DocState","StringEquals":"ACTIVE","Next":"Execute Remediation"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTACTIVE","Next":"Automation Document is not Active"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTENABLED","Next":"Security Standard is not enabled"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTFOUND","Next":"No Remediation for Control"}],"Default":"check_ssm_doc_state Error"},"Get Automation Document State":{"Next":"Automation Doc Active?","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Get the status of the remediation automation document in the target account","TimeoutSeconds":60,"ResultPath":"$.AutomationDocument","ResultSelector":{"DocState.$":"$.Payload.status","Message.$":"$.Payload.message","SecurityStandard.$":"$.Payload.securitystandard","SecurityStandardVersion.$":"$.Payload.securitystandardversion","SecurityStandardSupported.$":"$.Payload.standardsupported","ControlId.$":"$.Payload.controlid","AccountId.$":"$.Payload.accountid","RemediationRole.$":"$.Payload.remediationrole","AutomationDocId.$":"$.Payload.automationdocid","ResourceRegion.$":"$.Payload.resourceregion"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"Get Remediation Approval Requirement\\":{\\"Next\\":\\"Get Automation Document State\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Determine whether the selected remediation requires manual approval\\",\\"TimeoutSeconds\\":300,\\"ResultPath\\":\\"$.Workflow\\",\\"ResultSelector\\":{\\"WorkflowDocument.$\\":\\"$.Payload.workflowdoc\\",\\"WorkflowAccount.$\\":\\"$.Payload.workflowaccount\\",\\"WorkflowRole.$\\":\\"$.Payload.workflowrole\\",\\"WorkflowConfig.$\\":\\"$.Payload.workflow_data\\"},\\"Resource\\":\\"arn:", - Object { + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"Get Remediation Approval Requirement":{"Next":"Get Automation Document State","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Determine whether the selected remediation requires manual approval","TimeoutSeconds":300,"ResultPath":"$.Workflow","ResultSelector":{"WorkflowDocument.$":"$.Payload.workflowdoc","WorkflowAccount.$":"$.Payload.workflowaccount","WorkflowRole.$":"$.Payload.workflowrole","WorkflowConfig.$":"$.Payload.workflow_data"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"Remediation Queued\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"Account.$\\":\\"$.Payload.executionaccount\\",\\"Region.$\\":\\"$.Payload.executionregion\\"},\\"Resource\\":\\"arn:", - Object { + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"Orchestrator Failed":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Orchestrator failed: {}', $.Error)","State.$":"States.Format('LAMBDAERROR')","Details.$":"States.Format('Cause: {}', $.Cause)"},"Payload.$":"$"},"Next":"notify"},"Execute Remediation":{"Next":"Remediation Queued","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Execute the SSM Automation Document in the target account","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.SSMExecution","ResultSelector":{"ExecState.$":"$.Payload.status","Message.$":"$.Payload.message","ExecId.$":"$.Payload.executionid","Account.$":"$.Payload.executionaccount","Region.$":"$.Payload.executionregion"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"Remediation Queued\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)\\",\\"State.$\\":\\"States.Format('QUEUED')\\",\\"ExecId.$\\":\\"$.SSMExecution.ExecId\\"}},\\"Next\\":\\"Queued Notification\\"},\\"Queued Notification\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notification that a remediation has queued\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.notificationResult\\",\\"Resource\\":\\"arn:", - Object { + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"Remediation Queued":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","AutomationDocument.$":"$.AutomationDocument","SSMExecution.$":"$.SSMExecution","Notification":{"Message.$":"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)","State.$":"States.Format('QUEUED')","ExecId.$":"$.SSMExecution.ExecId"}},"Next":"Queued Notification"},"Queued Notification":{"Next":"execMonitor","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notification that a remediation has queued","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.notificationResult","Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"execMonitor\\":{\\"Next\\":\\"Remediation completed?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Monitor the remediation execution until done\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.Remediation\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"RemediationState.$\\":\\"$.Payload.remediation_status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"LogData.$\\":\\"$.Payload.logdata\\",\\"AffectedObject.$\\":\\"$.Payload.affected_object\\"},\\"Resource\\":\\"arn:", - Object { + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"execMonitor":{"Next":"Remediation completed?","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Monitor the remediation execution until done","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.Remediation","ResultSelector":{"ExecState.$":"$.Payload.status","ExecId.$":"$.Payload.executionid","RemediationState.$":"$.Payload.remediation_status","Message.$":"$.Payload.message","LogData.$":"$.Payload.logdata","AffectedObject.$":"$.Payload.affected_object"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"arn:aws:lambda:us-east-1:111122223333:function/foobar\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", + ":states:::lambda:invoke","Parameters":{"FunctionName":"arn:aws:lambda:us-east-1:111122223333:function/foobar","Payload.$":"$"}},"Wait for Remediation":{"Type":"Wait","Seconds":15,"Next":"execMonitor"},"Remediation completed?":{"Type":"Choice","Choices":[{"Variable":"$.Remediation.RemediationState","StringEquals":"Failed","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Success","Next":"Remediation Succeeded"},{"Variable":"$.Remediation.ExecState","StringEquals":"TimedOut","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Cancelling","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Cancelled","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Failed","Next":"Remediation Failed"}],"Default":"Wait for Remediation"},"Remediation Failed":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","SSMExecution.$":"$.SSMExecution","AutomationDocument.$":"$.AutomationDocument","Notification":{"Message.$":"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)","State.$":"$.Remediation.ExecState","Details.$":"$.Remediation.LogData","ExecId.$":"$.Remediation.ExecId","AffectedObject.$":"$.Remediation.AffectedObject"}},"Next":"notify"},"Remediation Succeeded":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion","Notification":{"Message.$":"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)","State.$":"States.Format('SUCCESS')","Details.$":"$.Remediation.LogData","ExecId.$":"$.Remediation.ExecId","AffectedObject.$":"$.Remediation.AffectedObject"}},"Next":"notify"},"check_ssm_doc_state Error":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)","State.$":"States.Format('LAMBDAERROR')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"Security Standard is not enabled":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)","State.$":"States.Format('STANDARDNOTENABLED')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"},"No Remediation for Control":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)","State.$":"States.Format('NOREMEDIATION')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"}}},"ItemsPath":"$.Findings"},"EOJ":{"Type":"Pass","Comment":"END-OF-JOB","End":true}},"TimeoutSeconds":900}", ], ], }, - "LoggingConfiguration": Object { - "Destinations": Array [ - Object { - "CloudWatchLogsLogGroup": Object { - "LogGroupArn": Object { - "Fn::Join": Array [ + "LoggingConfiguration": { + "Destinations": [ + { + "CloudWatchLogsLogGroup": { + "LogGroupArn": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":logs:", - Object { + { "Ref": "AWS::Region", }, ":", - Object { + { "Ref": "AWS::AccountId", }, ":log-group:ORCH_LOG_GROUP:*", @@ -364,8 +364,8 @@ Object { "IncludeExecutionData": true, "Level": "ALL", }, - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "OrchestratorRole9CF251DB", "Arn", ], @@ -374,13 +374,13 @@ Object { }, "Type": "AWS::StepFunctions::StateMachine", }, - "SHARRKeyC551FE02": Object { - "Properties": Object { + "SHARRKeyC551FE02": { + "Properties": { "Description": "KMS Customer Managed Key that SHARR will use to encrypt data", "Name": "/Solutions/SO0111/CMK_ARN", "Type": "String", - "Value": Object { - "Fn::GetAtt": Array [ + "Value": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], @@ -388,11 +388,11 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SHARRkeyAlias37E34763": Object { - "Properties": Object { + "SHARRkeyAlias37E34763": { + "Properties": { "AliasName": "alias/TO0111-SHARR-Key", - "TargetKeyId": Object { - "Fn::GetAtt": Array [ + "TargetKeyId": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], @@ -400,14 +400,14 @@ Object { }, "Type": "AWS::KMS::Alias", }, - "SHARRkeyE6BD0F56": Object { + "SHARRkeyE6BD0F56": { "DeletionPolicy": "Retain", - "Properties": Object { + "Properties": { "EnableKeyRotation": true, - "KeyPolicy": Object { - "Statement": Array [ - Object { - "Action": Array [ + "KeyPolicy": { + "Statement": [ + { + "Action": [ "kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", @@ -415,15 +415,15 @@ Object { "kms:Describe*", ], "Effect": "Allow", - "Principal": Object { - "Service": Array [ + "Principal": { + "Service": [ "sns.amazonaws.com", - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "logs.", - Object { + { "Ref": "AWS::URLSuffix", }, ], @@ -433,20 +433,20 @@ Object { }, "Resource": "*", }, - Object { + { "Action": "kms:*", "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::", - Object { + { "Ref": "AWS::AccountId", }, ":root", @@ -456,8 +456,8 @@ Object { }, "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "kms:Create*", "kms:Describe*", "kms:Enable*", @@ -475,17 +475,17 @@ Object { "kms:UntagResource", ], "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::", - Object { + { "Ref": "AWS::AccountId", }, ":root", diff --git a/source/test/__snapshots__/orchestrator_logs.test.ts.snap b/source/test/__snapshots__/orchestrator_logs.test.ts.snap index 2d6b4794..b5475168 100644 --- a/source/test/__snapshots__/orchestrator_logs.test.ts.snap +++ b/source/test/__snapshots__/orchestrator_logs.test.ts.snap @@ -1,16 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Global Roles Stack 1`] = ` -Object { - "Conditions": Object { - "EncryptedLogGroup": Object { - "Fn::And": Array [ - Object { +{ + "Conditions": { + "EncryptedLogGroup": { + "Fn::And": [ + { "Condition": "isNotGovCloud", }, - Object { - "Fn::Equals": Array [ - Object { + { + "Fn::Equals": [ + { "Ref": "ReuseOrchestratorLogGroup", }, "no", @@ -18,18 +18,18 @@ Object { }, ], }, - "UnencryptedLogGroup": Object { - "Fn::And": Array [ - Object { - "Fn::Not": Array [ - Object { + "UnencryptedLogGroup": { + "Fn::And": [ + { + "Fn::Not": [ + { "Condition": "isNotGovCloud", }, ], }, - Object { - "Fn::Equals": Array [ - Object { + { + "Fn::Equals": [ + { "Ref": "ReuseOrchestratorLogGroup", }, "no", @@ -37,11 +37,11 @@ Object { }, ], }, - "isNotGovCloud": Object { - "Fn::Not": Array [ - Object { - "Fn::Equals": Array [ - Object { + "isNotGovCloud": { + "Fn::Not": [ + { + "Fn::Equals": [ + { "Ref": "AWS::Partition", }, "aws-us-gov", @@ -51,47 +51,47 @@ Object { }, }, "Description": "test;", - "Parameters": Object { - "KmsKeyArn": Object { + "Parameters": { + "KmsKeyArn": { "Description": "ARN of the KMS key to use to encrypt log data.", "Type": "String", }, - "ReuseOrchestratorLogGroup": Object { - "AllowedValues": Array [ + "ReuseOrchestratorLogGroup": { + "AllowedValues": [ "yes", "no", ], "Default": "no", - "Description": "Reuse existing Orchestrator Log Group? Choose \\"yes\\" if the log group already exists, else \\"no\\"", + "Description": "Reuse existing Orchestrator Log Group? Choose "yes" if the log group already exists, else "no"", "Type": "String", }, }, - "Resources": Object { - "OrchestratorLogsEFDFFA92": Object { + "Resources": { + "OrchestratorLogsEFDFFA92": { "Condition": "UnencryptedLogGroup", "DeletionPolicy": "Retain", - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W84", "reason": "KmsKeyId is not supported in GovCloud.", }, ], }, }, - "Properties": Object { + "Properties": { "LogGroupName": "TestLogGroup", "RetentionInDays": 365, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Retain", }, - "OrchestratorLogsEncrypted072D6E38": Object { + "OrchestratorLogsEncrypted072D6E38": { "Condition": "EncryptedLogGroup", "DeletionPolicy": "Retain", - "Properties": Object { - "KmsKeyId": Object { + "Properties": { + "KmsKeyId": { "Ref": "KmsKeyArn", }, "LogGroupName": "TestLogGroup", diff --git a/source/test/__snapshots__/runbook_stack.test.ts.snap b/source/test/__snapshots__/runbook_stack.test.ts.snap index 5bb9a62b..8bdfecdf 100644 --- a/source/test/__snapshots__/runbook_stack.test.ts.snap +++ b/source/test/__snapshots__/runbook_stack.test.ts.snap @@ -1,142 +1,142 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Global Roles Stack 1`] = ` -Object { +{ "Description": "test;", - "Mappings": Object { - "ServiceprincipalMap": Object { - "af-south-1": Object { + "Mappings": { + "ServiceprincipalMap": { + "af-south-1": { "ssm": "ssm.af-south-1.amazonaws.com", }, - "ap-east-1": Object { + "ap-east-1": { "ssm": "ssm.ap-east-1.amazonaws.com", }, - "ap-northeast-1": Object { + "ap-northeast-1": { "ssm": "ssm.amazonaws.com", }, - "ap-northeast-2": Object { + "ap-northeast-2": { "ssm": "ssm.amazonaws.com", }, - "ap-northeast-3": Object { + "ap-northeast-3": { "ssm": "ssm.amazonaws.com", }, - "ap-south-1": Object { + "ap-south-1": { "ssm": "ssm.amazonaws.com", }, - "ap-southeast-1": Object { + "ap-southeast-1": { "ssm": "ssm.amazonaws.com", }, - "ap-southeast-2": Object { + "ap-southeast-2": { "ssm": "ssm.amazonaws.com", }, - "ap-southeast-3": Object { + "ap-southeast-3": { "ssm": "ssm.ap-southeast-3.amazonaws.com", }, - "ca-central-1": Object { + "ca-central-1": { "ssm": "ssm.amazonaws.com", }, - "cn-north-1": Object { + "cn-north-1": { "ssm": "ssm.amazonaws.com", }, - "cn-northwest-1": Object { + "cn-northwest-1": { "ssm": "ssm.amazonaws.com", }, - "eu-central-1": Object { + "eu-central-1": { "ssm": "ssm.amazonaws.com", }, - "eu-north-1": Object { + "eu-north-1": { "ssm": "ssm.amazonaws.com", }, - "eu-south-1": Object { + "eu-south-1": { "ssm": "ssm.eu-south-1.amazonaws.com", }, - "eu-south-2": Object { + "eu-south-2": { "ssm": "ssm.eu-south-2.amazonaws.com", }, - "eu-west-1": Object { + "eu-west-1": { "ssm": "ssm.amazonaws.com", }, - "eu-west-2": Object { + "eu-west-2": { "ssm": "ssm.amazonaws.com", }, - "eu-west-3": Object { + "eu-west-3": { "ssm": "ssm.amazonaws.com", }, - "me-south-1": Object { + "me-south-1": { "ssm": "ssm.me-south-1.amazonaws.com", }, - "sa-east-1": Object { + "sa-east-1": { "ssm": "ssm.amazonaws.com", }, - "us-east-1": Object { + "us-east-1": { "ssm": "ssm.amazonaws.com", }, - "us-east-2": Object { + "us-east-2": { "ssm": "ssm.amazonaws.com", }, - "us-gov-east-1": Object { + "us-gov-east-1": { "ssm": "ssm.amazonaws.com", }, - "us-gov-west-1": Object { + "us-gov-west-1": { "ssm": "ssm.amazonaws.com", }, - "us-iso-east-1": Object { + "us-iso-east-1": { "ssm": "ssm.amazonaws.com", }, - "us-iso-west-1": Object { + "us-iso-west-1": { "ssm": "ssm.us-iso-west-1.amazonaws.com", }, - "us-isob-east-1": Object { + "us-isob-east-1": { "ssm": "ssm.amazonaws.com", }, - "us-west-1": Object { + "us-west-1": { "ssm": "ssm.amazonaws.com", }, - "us-west-2": Object { + "us-west-2": { "ssm": "ssm.amazonaws.com", }, }, }, - "Parameters": Object { - "SecHubAdminAccount": Object { - "AllowedPattern": "\\\\d{12}", + "Parameters": { + "SecHubAdminAccount": { + "AllowedPattern": "\\d{12}", "Description": "Admin account number", "Type": "String", }, }, - "Resources": Object { - "OrchestratorMemberRoleMemberAccountRoleBE9AD9D5": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Resources": { + "OrchestratorMemberRoleMemberAccountRoleBE9AD9D5": { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W11", "reason": "Resource * is required due to the administrative nature of the solution.", }, - Object { + { "id": "W28", "reason": "Static names chosen intentionally to provide integration in cross-account permissions", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::", - Object { + { "Ref": "SecHubAdminAccount", }, ":role/SO0111-SHARR-Orchestrator-Admin", @@ -145,14 +145,14 @@ Object { }, }, }, - Object { + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { - "Service": Object { - "Fn::FindInMap": Array [ + "Principal": { + "Service": { + "Fn::FindInMap": [ "ServiceprincipalMap", - Object { + { "Ref": "AWS::Region", }, "ssm", @@ -163,26 +163,26 @@ Object { ], "Version": "2012-10-17", }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "iam:PassRole", "iam:GetRole", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::", - Object { + { "Ref": "AWS::AccountId", }, ":role/SO0111-*", @@ -190,64 +190,64 @@ Object { ], }, }, - Object { + { "Action": "ssm:StartAutomationExecution", "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ + "Resource": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:", - Object { + { "Ref": "AWS::AccountId", }, - ":document/SHARR-*", + ":document/ASR-*", ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:", - Object { + { "Ref": "AWS::AccountId", }, ":automation-definition/*", ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*::automation-definition/*", ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:", - Object { + { "Ref": "AWS::AccountId", }, ":automation-execution/*", @@ -256,23 +256,23 @@ Object { }, ], }, - Object { - "Action": Array [ + { + "Action": [ "ssm:DescribeAutomationExecutions", "ssm:GetAutomationExecution", ], "Effect": "Allow", "Resource": "*", }, - Object { + { "Action": "ssm:DescribeDocument", "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:*:document/*", @@ -280,18 +280,18 @@ Object { ], }, }, - Object { - "Action": Array [ + { + "Action": [ "ssm:GetParameters", "ssm:GetParameter", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:*:parameter/Solutions/SO0111/*", @@ -299,13 +299,13 @@ Object { ], }, }, - Object { + { "Action": "config:DescribeConfigRules", "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "cloudwatch:PutMetricData", "securityhub:BatchUpdateFindings", ], @@ -327,6743 +327,6681 @@ Object { `; exports[`Regional Documents 1`] = ` -Object { +{ "Description": "test;", - "Resources": Object { - "SHARRConfigureS3BucketPublicAccessBlock": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - AWSConfigRemediation-ConfigureS3BucketPublicAccessBlock - - ## What does this document do? - This document is used to create or modify the PublicAccessBlock configuration for an Amazon S3 bucket. - - ## Input Parameters - * BucketName: (Required) Name of the S3 bucket (not the ARN). - * RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy. - * Default: \\"true\\" - * BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket. - * Default: \\"true\\" - * IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket. - * Default: \\"true\\" - * BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - * Default: \\"true\\" - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - - ## Output Parameters - * GetBucketPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call - - ## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. -schemaVersion: \\"0.3\\" -assumeRole: \\"{{ AutomationAssumeRole }}\\" -outputs: - - GetBucketPublicAccessBlock.Output -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])$) - RestrictPublicBuckets: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy. - default: true - allowedValues: - - true - - false - BlockPublicAcls: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket. - default: true - allowedValues: - - true - - false - IgnorePublicAcls: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket. - default: true - allowedValues: - - true - - false - BlockPublicPolicy: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - default: true - allowedValues: - - true - - 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+=,.@-]+$' -mainSteps: - - name: PutBucketPublicAccessBlock - action: \\"aws:executeAwsApi\\" - description: | - ## PutBucketPublicAccessBlock - Creates or modifies the PublicAccessBlock configuration for a S3 Bucket. - isEnd: false - inputs: - Service: s3 - Api: PutPublicAccessBlock - Bucket: \\"{{BucketName}}\\" - PublicAccessBlockConfiguration: - RestrictPublicBuckets: \\"{{ RestrictPublicBuckets }}\\" - BlockPublicAcls: \\"{{ BlockPublicAcls }}\\" - IgnorePublicAcls: \\"{{ IgnorePublicAcls }}\\" - BlockPublicPolicy: \\"{{ BlockPublicPolicy }}\\" - isCritical: true - maxAttempts: 2 - timeoutSeconds: 600 - - name: GetBucketPublicAccessBlock - action: \\"aws:executeScript\\" - description: | - ## GetBucketPublicAccessBlock - Retrieves the S3 PublicAccessBlock configuration for a S3 Bucket. - ## Outputs - * Output: JSON formatted response from the GetPublicAccessBlock API call. - timeoutSeconds: 600 - isCritical: true - isEnd: true - inputs: - Runtime: python3.8 - Handler: validate_s3_bucket_publicaccessblock - InputPayload: - Bucket: \\"{{BucketName}}\\" - RestrictPublicBuckets: \\"{{ RestrictPublicBuckets }}\\" - BlockPublicAcls: \\"{{ BlockPublicAcls }}\\" - IgnorePublicAcls: \\"{{ IgnorePublicAcls }}\\" - BlockPublicPolicy: \\"{{ BlockPublicPolicy }}\\" - Script: |- - import boto3 - - def validate_s3_bucket_publicaccessblock(event, context): - s3_client = boto3.client(\\"s3\\") - bucket = event[\\"Bucket\\"] - restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] - block_public_acls = event[\\"BlockPublicAcls\\"] - ignore_public_acls = event[\\"IgnorePublicAcls\\"] - block_public_policy = event[\\"BlockPublicPolicy\\"] - - output = s3_client.get_public_access_block(Bucket=bucket) - updated_block_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicAcls\\"] - updated_ignore_acl = output[\\"PublicAccessBlockConfiguration\\"][\\"IgnorePublicAcls\\"] - updated_block_policy = output[\\"PublicAccessBlockConfiguration\\"][\\"BlockPublicPolicy\\"] - updated_restrict_buckets = output[\\"PublicAccessBlockConfiguration\\"][\\"RestrictPublicBuckets\\"] - - if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ - and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: - return { - \\"output\\": - { - \\"message\\": \\"Bucket public access block configuration successfully set.\\", - \\"configuration\\": output[\\"PublicAccessBlockConfiguration\\"] - } - } - else: - info = \\"CONFIGURATION VALUES DO NOT MATCH WITH PARAMETERS PROVIDED VALUES RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\".format( - restrict_public_buckets, - block_public_acls, - ignore_public_acls, - block_public_policy - ) - raise Exception(info) - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap - + "Resources": { + "ASRConfigureS3BucketPublicAccessBlock": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-ConfigureS3BucketPublicAccessBlock + +## What does this document do? +This document is used to create or modify the PublicAccessBlock configuration for an Amazon S3 bucket. + +## Input Parameters +* BucketName: (Required) Name of the S3 bucket (not the ARN). +* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy. + * Default: "true" +* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket. + * Default: "true" +* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket. + * Default: "true" +* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. + * Default: "true" +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* GetBucketPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call + +## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-ConfigureS3BucketPublicAccessBlock", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## PutBucketPublicAccessBlock +Creates or modifies the PublicAccessBlock configuration for a S3 Bucket. +", + "inputs": { + "Api": "PutPublicAccessBlock", + "Bucket": "{{BucketName}}", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", + }, + "Service": "s3", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isCritical": true, + "isEnd": false, + "maxAttempts": 2, + "name": "PutBucketPublicAccessBlock", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeScript", + "description": "## GetBucketPublicAccessBlock +Retrieves the S3 PublicAccessBlock configuration for a S3 Bucket. +## Outputs +* Output: JSON formatted response from the GetPublicAccessBlock API call. +", + "inputs": { + "Handler": "validate_s3_bucket_publicaccessblock", + "InputPayload": { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "Bucket": "{{BucketName}}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", + }, + "Runtime": "python3.8", + "Script": "import boto3 + +def validate_s3_bucket_publicaccessblock(event, context): + s3_client = boto3.client("s3") + bucket = event["Bucket"] + restrict_public_buckets = event["RestrictPublicBuckets"] + block_public_acls = event["BlockPublicAcls"] + ignore_public_acls = event["IgnorePublicAcls"] + block_public_policy = event["BlockPublicPolicy"] + + output = s3_client.get_public_access_block(Bucket=bucket) + updated_block_acl = output["PublicAccessBlockConfiguration"]["BlockPublicAcls"] + updated_ignore_acl = output["PublicAccessBlockConfiguration"]["IgnorePublicAcls"] + updated_block_policy = output["PublicAccessBlockConfiguration"]["BlockPublicPolicy"] + updated_restrict_buckets = output["PublicAccessBlockConfiguration"]["RestrictPublicBuckets"] + + if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\ + and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: + return { + "output": + { + "message": "Bucket public access block configuration successfully set.", + "configuration": output["PublicAccessBlockConfiguration"] + } + } + else: + info = "CONFIGURATION VALUES DO NOT MATCH WITH PARAMETERS PROVIDED VALUES RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}".format( + restrict_public_buckets, + block_public_acls, + ignore_public_acls, + block_public_policy + ) + raise Exception(info)", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isCritical": true, + "isEnd": true, + "name": "GetBucketPublicAccessBlock", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, ], + "outputs": [ + "GetBucketPublicAccessBlock.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "BlockPublicAcls": { + "allowedValues": [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for this bucket and objects in this bucket.", + "type": "Boolean", + }, + "BlockPublicPolicy": { + "allowedValues": [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for this bucket. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", + "type": "Boolean", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "(Required) The bucket name (not the ARN).", + "type": "String", + }, + "IgnorePublicAcls": { + "allowedValues": [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for this bucket and objects in this bucket. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on this bucket and objects in this bucket.", + "type": "Boolean", + }, + "RestrictPublicBuckets": { + "allowedValues": [ + true, + false, + ], + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for this bucket. Setting this element to TRUE restricts access to this bucket to only AWS services and authorized users within this account if the bucket has a public policy.", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-ConfigureS3BucketPublicAccessBlock", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRConfigureS3PublicAccessBlock": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - AWSConfigRemediation-ConfigureS3PublicAccessBlock - - ## What does this document do? - This document is used to create or modify the S3 [PublicAccessBlock](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options) configuration for an AWS account. - - ## Input Parameters - * AccountId: (Required) Account ID of the account for which the S3 Account Public Access Block is to be configured. - * RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account. - * Default: \\"true\\" - * BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account. - * Default: \\"true\\" - * IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain. - * Default: \\"true\\" - * BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - * Default: \\"true\\" - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - - ## Output Parameters - * GetPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call. - -schemaVersion: \\"0.3\\" -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - AccountId: - type: String - description: (Required) The account ID for the AWS account whose PublicAccessBlock configuration you want to set. - allowedPattern: ^\\\\d{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+=,.@-]+$' - 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. - default: true - BlockPublicAcls: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account. - default: true - IgnorePublicAcls: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain. - default: true - BlockPublicPolicy: - type: Boolean - description: (Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. - default: true -outputs: - - GetPublicAccessBlock.Output -mainSteps: - - - name: PutAccountPublicAccessBlock - action: \\"aws:executeAwsApi\\" - description: | - ## PutAccountPublicAccessBlock - Creates or modifies the S3 PublicAccessBlock configuration for an AWS account. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: s3control - Api: PutPublicAccessBlock - AccountId: \\"{{ AccountId }}\\" - PublicAccessBlockConfiguration: - RestrictPublicBuckets: \\"{{ RestrictPublicBuckets }}\\" - BlockPublicAcls: \\"{{ BlockPublicAcls }}\\" - IgnorePublicAcls: \\"{{ IgnorePublicAcls }}\\" - BlockPublicPolicy: \\"{{ BlockPublicPolicy }}\\" - outputs: - - Name: PutAccountPublicAccessBlockResponse - Selector: $ - Type: StringMap - - - name: GetPublicAccessBlock - action: \\"aws:executeScript\\" - description: | - ## GetPublicAccessBlock - Retrieves the S3 PublicAccessBlock configuration for an AWS account. - ## Outputs - * Output: JSON formatted response from the GetPublicAccessBlock API call. - timeoutSeconds: 600 - isEnd: true - inputs: - Runtime: python3.8 - Handler: handler - InputPayload: - AccountId: \\"{{ AccountId }}\\" - RestrictPublicBuckets: \\"{{ RestrictPublicBuckets }}\\" - BlockPublicAcls: \\"{{ BlockPublicAcls }}\\" - IgnorePublicAcls: \\"{{ IgnorePublicAcls }}\\" - BlockPublicPolicy: \\"{{ BlockPublicPolicy }}\\" - Script: |- - import boto3 - from time import sleep - - def verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy): - s3control_client = boto3.client('s3control') - wait_time = 30 - max_time = 480 - retry_count = 1 - max_retries = max_time/wait_time - while retry_count <= max_retries: - sleep(wait_time) - retry_count = retry_count + 1 - get_public_access_response = s3control_client.get_public_access_block(AccountId=account_id) - updated_block_acl = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicAcls'] - updated_ignore_acl = get_public_access_response['PublicAccessBlockConfiguration']['IgnorePublicAcls'] - updated_block_policy = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicPolicy'] - updated_restrict_buckets = get_public_access_response['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] - if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\\\ - and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: - return { - \\"output\\": { - \\"message\\": \\"Verification successful. S3 Public Access Block Updated.\\", - \\"HTTPResponse\\": get_public_access_response[\\"PublicAccessBlockConfiguration\\"] - }, - } - raise Exception( - \\"VERFICATION FAILED. S3 GetPublicAccessBlock CONFIGURATION VALUES \\" - \\"DO NOT MATCH WITH PARAMETERS PROVIDED VALUES \\" - \\"RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}\\" - .format(updated_restrict_buckets, updated_block_acl, updated_ignore_acl, updated_block_policy) - ) - - def handler(event, context): - account_id = event[\\"AccountId\\"] - restrict_public_buckets = event[\\"RestrictPublicBuckets\\"] - block_public_acls = event[\\"BlockPublicAcls\\"] - ignore_public_acls = event[\\"IgnorePublicAcls\\"] - block_public_policy = event[\\"BlockPublicPolicy\\"] - return verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy) - - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap + "ASRConfigureS3PublicAccessBlock": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-ConfigureS3PublicAccessBlock + +## What does this document do? +This document is used to create or modify the S3 [PublicAccessBlock](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options) configuration for an AWS account. + +## Input Parameters +* AccountId: (Required) Account ID of the account for which the S3 Account Public Access Block is to be configured. +* RestrictPublicBuckets: (Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account. + * Default: "true" +* BlockPublicAcls: (Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account. + * Default: "true" +* IgnorePublicAcls: (Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain. + * Default: "true" +* BlockPublicPolicy: (Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access. + * Default: "true" +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* GetPublicAccessBlock.Output - JSON formatted response from the GetPublicAccessBlock API call. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-ConfigureS3PublicAccessBlock", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## PutAccountPublicAccessBlock +Creates or modifies the S3 PublicAccessBlock configuration for an AWS account. +", + "inputs": { + "AccountId": "{{ AccountId }}", + "Api": "PutPublicAccessBlock", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", + }, + "Service": "s3control", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "PutAccountPublicAccessBlock", + "outputs": [ + { + "Name": "PutAccountPublicAccessBlockResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:executeScript", + "description": "## GetPublicAccessBlock +Retrieves the S3 PublicAccessBlock configuration for an AWS account. +## Outputs +* Output: JSON formatted response from the GetPublicAccessBlock API call. +", + "inputs": { + "Handler": "handler", + "InputPayload": { + "AccountId": "{{ AccountId }}", + "BlockPublicAcls": "{{ BlockPublicAcls }}", + "BlockPublicPolicy": "{{ BlockPublicPolicy }}", + "IgnorePublicAcls": "{{ IgnorePublicAcls }}", + "RestrictPublicBuckets": "{{ RestrictPublicBuckets }}", + }, + "Runtime": "python3.8", + "Script": "import boto3 +from time import sleep + +def verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy): + s3control_client = boto3.client('s3control') + wait_time = 30 + max_time = 480 + retry_count = 1 + max_retries = max_time/wait_time + while retry_count <= max_retries: + sleep(wait_time) + retry_count = retry_count + 1 + get_public_access_response = s3control_client.get_public_access_block(AccountId=account_id) + updated_block_acl = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicAcls'] + updated_ignore_acl = get_public_access_response['PublicAccessBlockConfiguration']['IgnorePublicAcls'] + updated_block_policy = get_public_access_response['PublicAccessBlockConfiguration']['BlockPublicPolicy'] + updated_restrict_buckets = get_public_access_response['PublicAccessBlockConfiguration']['RestrictPublicBuckets'] + if updated_block_acl == block_public_acls and updated_ignore_acl == ignore_public_acls \\ + and updated_block_policy == block_public_policy and updated_restrict_buckets == restrict_public_buckets: + return { + "output": { + "message": "Verification successful. S3 Public Access Block Updated.", + "HTTPResponse": get_public_access_response["PublicAccessBlockConfiguration"] + }, + } + raise Exception( + "VERFICATION FAILED. S3 GetPublicAccessBlock CONFIGURATION VALUES " + "DO NOT MATCH WITH PARAMETERS PROVIDED VALUES " + "RestrictPublicBuckets: {}, BlockPublicAcls: {}, IgnorePublicAcls: {}, BlockPublicPolicy: {}" + .format(updated_restrict_buckets, updated_block_acl, updated_ignore_acl, updated_block_policy) + ) + +def handler(event, context): + account_id = event["AccountId"] + restrict_public_buckets = event["RestrictPublicBuckets"] + block_public_acls = event["BlockPublicAcls"] + ignore_public_acls = event["IgnorePublicAcls"] + block_public_policy = event["BlockPublicPolicy"] + return verify_s3_public_access_block(account_id, restrict_public_buckets, block_public_acls, ignore_public_acls, block_public_policy)", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "GetPublicAccessBlock", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, ], + "outputs": [ + "GetPublicAccessBlock.Output", + ], + "parameters": { + "AccountId": { + "allowedPattern": "^\\d{12}$", + "description": "(Required) The account ID for the AWS account whose PublicAccessBlock configuration you want to set.", + "type": "String", + }, + "AutomationAssumeRole": { + "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", + }, + "BlockPublicAcls": { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public access control lists (ACLs) for buckets in this account.", + "type": "Boolean", + }, + "BlockPublicPolicy": { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should block public bucket policies for buckets in this account. Setting this element to TRUE causes Amazon S3 to reject calls to PUT Bucket policy if the specified bucket policy allows public access.", + "type": "Boolean", + }, + "IgnorePublicAcls": { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should ignore public ACLs for buckets in this account. Setting this element to TRUE causes Amazon S3 to ignore all public ACLs on buckets in this account and any objects that they contain.", + "type": "Boolean", + }, + "RestrictPublicBuckets": { + "default": true, + "description": "(Optional) Specifies whether Amazon S3 should restrict public bucket policies for buckets in this account. Setting this element to TRUE restricts access to buckets with public policies to only AWS services and authorized users within this account.", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-ConfigureS3PublicAccessBlock", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRCreateAccessLoggingBucket": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-CreateAccessLoggingBucket - - ## What does this document do? - Creates an S3 bucket for access logging. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * BucketName: (Required) Name of the bucket to create - -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+=,.@-]+$' - 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])$) -outputs: - - CreateAccessLoggingBucket.Output - -mainSteps: - - - name: CreateAccessLoggingBucket - action: 'aws:executeScript' - inputs: - InputPayload: - BucketName: '{{BucketName}}' - AWS_REGION: '{{global:REGION}}' - Runtime: python3.8 - Handler: create_logging_bucket - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.exceptions import ClientError - from botocore.config import Config - - def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - - def create_logging_bucket(event, context): - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - try: - kwargs = { - 'Bucket': event['BucketName'], - 'GrantWrite': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery', - 'GrantReadACP': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery' - } - if event['AWS_REGION'] != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': event['AWS_REGION'] - } - - s3.create_bucket(**kwargs) - - s3.put_bucket_encryption( - Bucket=event['BucketName'], - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'AES256' - } - } - ] - } - ) - return { - \\"output\\": { - \\"Message\\": f'Bucket {event[\\"BucketName\\"]} created' - } - } - except ClientError as error: - if error.response['Error']['Code'] != 'BucketAlreadyExists' and \\\\ - error.response['Error']['Code'] != 'BucketAlreadyOwnedByYou': - exit(str(error)) - else: - return { - \\"output\\": { - \\"Message\\": f'Bucket {event[\\"BucketName\\"]} already exists' - } - } - except Exception as e: - print(e) - exit(str(e)) - - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap + "ASRCreateAccessLoggingBucket": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-CreateAccessLoggingBucket + +## What does this document do? +Creates an S3 bucket for access logging. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* BucketName: (Required) Name of the bucket to create +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "create_logging_bucket", + "InputPayload": { + "AWS_REGION": "{{global:REGION}}", + "BucketName": "{{BucketName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.exceptions import ClientError +from botocore.config import Config + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_logging_bucket(event, context): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + try: + kwargs = { + 'Bucket': event['BucketName'], + 'GrantWrite': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery', + 'GrantReadACP': 'uri=http://acs.amazonaws.com/groups/s3/LogDelivery' + } + if event['AWS_REGION'] != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': event['AWS_REGION'] + } - isEnd: true + s3.create_bucket(**kwargs) -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CreateAccessLoggingBucket", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + s3.put_bucket_encryption( + Bucket=event['BucketName'], + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'AES256' + } + } + ] + } + ) + return { + "output": { + "Message": f'Bucket {event["BucketName"]} created' + } + } + except ClientError as error: + if error.response['Error']['Code'] != 'BucketAlreadyExists' and \\ + error.response['Error']['Code'] != 'BucketAlreadyOwnedByYou': + exit(str(error)) + else: + return { + "output": { + "Message": f'Bucket {event["BucketName"]} already exists' + } + } + except Exception as e: + print(e) + exit(str(e))", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "CreateAccessLoggingBucket", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + }, ], + "outputs": [ + "CreateAccessLoggingBucket.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "(Required) The bucket name (not the ARN).", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CreateAccessLoggingBucket", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRCreateCloudTrailMultiRegionTrail": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-CreateCloudTrailMultiRegionTrail - ## What does this document do? - Creates a multi-region trail with KMS encryption and enables CloudTrail - Note: this remediation will create a NEW trail. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data - - ## Security Standards / Controls - * AFSBP v1.0.0: CloudTrail.1 - * CIS v1.2.0: 2.1 - * PCI: CloudTrail.2 - -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+=,.@-]+$' - 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})))$' - AWSPartition: - type: String - default: 'aws' - description: 'Partition for creation of ARNs.' - allowedValues: - - aws - - aws-cn - - aws-us-gov - -outputs: - - Remediation.Output - -mainSteps: - - - name: CreateLoggingBucket - action: 'aws:executeScript' - outputs: - - Name: LoggingBucketName - Selector: $.Payload.logging_bucket - Type: String - inputs: - InputPayload: - account: '{{global:ACCOUNT_ID}}' - region: '{{global:REGION}}' - kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.8 - Handler: create_logging_bucket - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - ERROR_CREATING_BUCKET = 'Error creating bucket ' - - def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - - def create_logging_bucket(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - kms_key_arn = event['kms_key_arn'] - aws_account = event['account'] - aws_region = event['region'] - bucket_name = 'so0111-access-logs-' + aws_region + '-' + aws_account - - if create_bucket(s3, bucket_name, aws_region) == 'bucket_exists': - return {\\"logging_bucket\\": bucket_name} - encrypt_bucket(s3, bucket_name, kms_key_arn) - put_access_block(s3, bucket_name) - put_bucket_acl(s3, bucket_name) - - return {\\"logging_bucket\\": bucket_name} - - def create_bucket(s3, bucket_name, aws_region): - try: - kwargs = { - 'Bucket': bucket_name, - 'ACL': 'private' - } - if aws_region != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': aws_region - } - - s3.create_bucket(**kwargs) - - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - # bucket already exists - return - if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: - print('Bucket ' + bucket_name + ' already exists') - return 'bucket_exists' - else: - print(ex) - exit(ERROR_CREATING_BUCKET + bucket_name) - except Exception as e: - print(e) - exit(ERROR_CREATING_BUCKET + bucket_name) - - def encrypt_bucket(s3, bucket_name, kms_key_arn): - try: - s3.put_bucket_encryption( - Bucket=bucket_name, - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'aws:kms', - 'KMSMasterKeyID': kms_key_arn.split('key/')[1] - } - } - ] - } - ) - except Exception as e: - exit('Error encrypting bucket ' + bucket_name + ': ' + str(e)) - - def put_access_block(s3, bucket_name): - try: - s3.put_public_access_block( - Bucket=bucket_name, - PublicAccessBlockConfiguration={ - 'BlockPublicAcls': True, - 'IgnorePublicAcls': True, - 'BlockPublicPolicy': True, - 'RestrictPublicBuckets': True - } - ) - except Exception as e: - exit('Error setting public access block for bucket ' + bucket_name + ': ' + str(e)) - - def put_bucket_acl(s3, bucket_name): - try: - s3.put_bucket_acl( - Bucket=bucket_name, - GrantReadACP='uri=http://acs.amazonaws.com/groups/s3/LogDelivery', - GrantWrite='uri=http://acs.amazonaws.com/groups/s3/LogDelivery' - ) - except Exception as e: - exit('Error setting ACL for bucket ' + bucket_name + ': ' + str(e)) - - - + "ASRCreateCloudTrailMultiRegionTrail": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-CreateCloudTrailMultiRegionTrail +## What does this document do? +Creates a multi-region trail with KMS encryption and enables CloudTrail +Note: this remediation will create a NEW trail. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data + +## Security Standards / Controls +* AFSBP v1.0.0: CloudTrail.1 +* CIS v1.2.0: 2.1 +* PCI: CloudTrail.2 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "create_logging_bucket", + "InputPayload": { + "account": "{{global:ACCOUNT_ID}}", + "kms_key_arn": "{{KMSKeyArn}}", + "region": "{{global:REGION}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +ERROR_CREATING_BUCKET = 'Error creating bucket ' + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_logging_bucket(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + kms_key_arn = event['kms_key_arn'] + aws_account = event['account'] + aws_region = event['region'] + bucket_name = 'so0111-access-logs-' + aws_region + '-' + aws_account + + if create_bucket(s3, bucket_name, aws_region) == 'bucket_exists': + return {"logging_bucket": bucket_name} + encrypt_bucket(s3, bucket_name, kms_key_arn) + put_access_block(s3, bucket_name) + put_bucket_acl(s3, bucket_name) + + return {"logging_bucket": bucket_name} + +def create_bucket(s3, bucket_name, aws_region): + try: + kwargs = { + 'Bucket': bucket_name, + 'ACL': 'private' + } + if aws_region != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': aws_region + } - isEnd: false - - - - name: CreateCloudTrailBucket - action: 'aws:executeScript' - outputs: - - Name: CloudTrailBucketName - Selector: $.Payload.cloudtrail_bucket - Type: String - inputs: - InputPayload: - account: '{{global:ACCOUNT_ID}}' - region: '{{global:REGION}}' - kms_key_arn: '{{KMSKeyArn}}' - logging_bucket: '{{CreateLoggingBucket.LoggingBucketName}}' - Runtime: python3.8 - Handler: create_encrypted_bucket - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0 # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - - def create_encrypted_bucket(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - kms_key_arn = event['kms_key_arn'] - aws_account = event['account'] - aws_region = event['region'] - logging_bucket = event['logging_bucket'] - bucket_name = 'so0111-aws-cloudtrail-' + aws_account - - if create_s3_bucket(s3, bucket_name, aws_region) == 'bucket_exists': - return {\\"cloudtrail_bucket\\": bucket_name} - put_bucket_encryption(s3, bucket_name, kms_key_arn) - put_public_access_block(s3, bucket_name) - put_bucket_logging(s3, bucket_name, logging_bucket) - - return {\\"cloudtrail_bucket\\": bucket_name} - - def create_s3_bucket(s3, bucket_name, aws_region): - try: - kwargs = { - 'Bucket': bucket_name, - 'ACL': 'private' - } - if aws_region != 'us-east-1': - kwargs['CreateBucketConfiguration'] = { - 'LocationConstraint': aws_region - } - - s3.create_bucket(**kwargs) - - except ClientError as client_ex: - exception_type = client_ex.response['Error']['Code'] - if exception_type in [\\"BucketAlreadyExists\\", \\"BucketAlreadyOwnedByYou\\"]: - print('Bucket ' + bucket_name + ' already exists') - return 'bucket_exists' - else: - exit('Error creating bucket ' + bucket_name + ' ' + str(client_ex)) - except Exception as e: - exit('Error creating bucket ' + bucket_name + ' ' + str(e)) - - def put_bucket_encryption(s3, bucket_name, kms_key_arn): - try: - s3.put_bucket_encryption( - Bucket=bucket_name, - ServerSideEncryptionConfiguration={ - 'Rules': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'aws:kms', - 'KMSMasterKeyID': kms_key_arn.split('key/')[1] - } - } - ] - } - ) - except Exception as e: - print(e) - exit('Error applying encryption to bucket ' + bucket_name + ' with key ' + kms_key_arn) - - def put_public_access_block(s3, bucket_name): - try: - s3.put_public_access_block( - Bucket=bucket_name, - PublicAccessBlockConfiguration={ - 'BlockPublicAcls': True, - 'IgnorePublicAcls': True, - 'BlockPublicPolicy': True, - 'RestrictPublicBuckets': True - } - ) - except Exception as e: - exit(f'Error setting public access block for bucket {bucket_name}: {str(e)}') - - def put_bucket_logging(s3, bucket_name, logging_bucket): - try: - s3.put_bucket_logging( - Bucket=bucket_name, - BucketLoggingStatus={ - 'LoggingEnabled': { - 'TargetBucket': logging_bucket, - 'TargetPrefix': 'cloudtrail-access-logs' + s3.create_bucket(**kwargs) + + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # bucket already exists - return + if exception_type in ["BucketAlreadyExists", "BucketAlreadyOwnedByYou"]: + print('Bucket ' + bucket_name + ' already exists') + return 'bucket_exists' + else: + print(ex) + exit(ERROR_CREATING_BUCKET + bucket_name) + except Exception as e: + print(e) + exit(ERROR_CREATING_BUCKET + bucket_name) + +def encrypt_bucket(s3, bucket_name, kms_key_arn): + try: + s3.put_bucket_encryption( + Bucket=bucket_name, + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'aws:kms', + 'KMSMasterKeyID': kms_key_arn.split('key/')[1] } } - ) - except Exception as e: - print(e) - exit('Error setting public access block for bucket ' + bucket_name) - - - isEnd: false - - - - name: CreateCloudTrailBucketPolicy - action: 'aws:executeScript' - inputs: - InputPayload: - cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' - partition: '{{AWSPartition}}' - account: '{{global:ACCOUNT_ID}}' - Runtime: python3.8 - Handler: create_bucket_policy - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_s3(boto_config): - return boto3.client('s3', config=boto_config) - - def create_bucket_policy(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - s3 = connect_to_s3(boto_config) - - cloudtrail_bucket = event['cloudtrail_bucket'] - aws_partition = event['partition'] - aws_account = event['account'] - try: - bucket_policy = { - \\"Version\\": \\"2012-10-17\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSCloudTrailAclCheck20150319\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"cloudtrail.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:GetBucketAcl\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket - }, - { - \\"Sid\\": \\"AWSCloudTrailWrite20150319\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": [ - \\"cloudtrail.amazonaws.com\\" - ] - }, - \\"Action\\": \\"s3:PutObject\\", - \\"Resource\\": \\"arn:\\" + aws_partition + \\":s3:::\\" + cloudtrail_bucket + \\"/AWSLogs/\\" + aws_account + \\"/*\\", - \\"Condition\\": { - \\"StringEquals\\": { - \\"s3:x-amz-acl\\": \\"bucket-owner-full-control\\" - } - } + ] + } + ) + except Exception as e: + exit('Error encrypting bucket ' + bucket_name + ': ' + str(e)) + +def put_access_block(s3, bucket_name): + try: + s3.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + 'BlockPublicAcls': True, + 'IgnorePublicAcls': True, + 'BlockPublicPolicy': True, + 'RestrictPublicBuckets': True + } + ) + except Exception as e: + exit('Error setting public access block for bucket ' + bucket_name + ': ' + str(e)) + +def put_bucket_acl(s3, bucket_name): + try: + s3.put_bucket_acl( + Bucket=bucket_name, + GrantReadACP='uri=http://acs.amazonaws.com/groups/s3/LogDelivery', + GrantWrite='uri=http://acs.amazonaws.com/groups/s3/LogDelivery' + ) + except Exception as e: + exit('Error setting ACL for bucket ' + bucket_name + ': ' + str(e))", + }, + "isEnd": false, + "name": "CreateLoggingBucket", + "outputs": [ + { + "Name": "LoggingBucketName", + "Selector": "$.Payload.logging_bucket", + "Type": "String", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "create_encrypted_bucket", + "InputPayload": { + "account": "{{global:ACCOUNT_ID}}", + "kms_key_arn": "{{KMSKeyArn}}", + "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", + "region": "{{global:REGION}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_encrypted_bucket(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + kms_key_arn = event['kms_key_arn'] + aws_account = event['account'] + aws_region = event['region'] + logging_bucket = event['logging_bucket'] + bucket_name = 'so0111-aws-cloudtrail-' + aws_account + + if create_s3_bucket(s3, bucket_name, aws_region) == 'bucket_exists': + return {"cloudtrail_bucket": bucket_name} + put_bucket_encryption(s3, bucket_name, kms_key_arn) + put_public_access_block(s3, bucket_name) + put_bucket_logging(s3, bucket_name, logging_bucket) + + return {"cloudtrail_bucket": bucket_name} + +def create_s3_bucket(s3, bucket_name, aws_region): + try: + kwargs = { + 'Bucket': bucket_name, + 'ACL': 'private' + } + if aws_region != 'us-east-1': + kwargs['CreateBucketConfiguration'] = { + 'LocationConstraint': aws_region + } + + s3.create_bucket(**kwargs) + + except ClientError as client_ex: + exception_type = client_ex.response['Error']['Code'] + if exception_type in ["BucketAlreadyExists", "BucketAlreadyOwnedByYou"]: + print('Bucket ' + bucket_name + ' already exists') + return 'bucket_exists' + else: + exit('Error creating bucket ' + bucket_name + ' ' + str(client_ex)) + except Exception as e: + exit('Error creating bucket ' + bucket_name + ' ' + str(e)) + +def put_bucket_encryption(s3, bucket_name, kms_key_arn): + try: + s3.put_bucket_encryption( + Bucket=bucket_name, + ServerSideEncryptionConfiguration={ + 'Rules': [ + { + 'ApplyServerSideEncryptionByDefault': { + 'SSEAlgorithm': 'aws:kms', + 'KMSMasterKeyID': kms_key_arn.split('key/')[1] } - ] - } - s3.put_bucket_policy( - Bucket=cloudtrail_bucket, - Policy=json.dumps(bucket_policy) - ) - return { - \\"output\\": { - \\"Message\\": f'Set bucket policy for bucket {cloudtrail_bucket}' } + ] + } + ) + except Exception as e: + print(e) + exit('Error applying encryption to bucket ' + bucket_name + ' with key ' + kms_key_arn) + +def put_public_access_block(s3, bucket_name): + try: + s3.put_public_access_block( + Bucket=bucket_name, + PublicAccessBlockConfiguration={ + 'BlockPublicAcls': True, + 'IgnorePublicAcls': True, + 'BlockPublicPolicy': True, + 'RestrictPublicBuckets': True + } + ) + except Exception as e: + exit(f'Error setting public access block for bucket {bucket_name}: {str(e)}') + +def put_bucket_logging(s3, bucket_name, logging_bucket): + try: + s3.put_bucket_logging( + Bucket=bucket_name, + BucketLoggingStatus={ + 'LoggingEnabled': { + 'TargetBucket': logging_bucket, + 'TargetPrefix': 'cloudtrail-access-logs' } - except Exception as e: - print(e) - exit('PutBucketPolicy failed: ' + str(e)) - - isEnd: false - - - - name: EnableCloudTrail - action: 'aws:executeScript' - outputs: - - Name: CloudTrailBucketName - Selector: $.Payload.cloudtrail_bucket - Type: String - inputs: - InputPayload: - cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' - kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.8 - Handler: enable_cloudtrail - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_cloudtrail(boto_config): - return boto3.client('cloudtrail', config=boto_config) - - def enable_cloudtrail(event, context): - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - ct = connect_to_cloudtrail(boto_config) - - try: - ct.create_trail( - Name='multi-region-cloud-trail', - S3BucketName=event['cloudtrail_bucket'], - IncludeGlobalServiceEvents=True, - EnableLogFileValidation=True, - IsMultiRegionTrail=True, - KmsKeyId=event['kms_key_arn'] - ) - ct.start_logging( - Name='multi-region-cloud-trail' - ) - return { - \\"output\\": { - \\"Message\\": f'CloudTrail Trail multi-region-cloud-trail created' + } + ) + except Exception as e: + print(e) + exit('Error setting public access block for bucket ' + bucket_name)", + }, + "isEnd": false, + "name": "CreateCloudTrailBucket", + "outputs": [ + { + "Name": "CloudTrailBucketName", + "Selector": "$.Payload.cloudtrail_bucket", + "Type": "String", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "create_bucket_policy", + "InputPayload": { + "account": "{{global:ACCOUNT_ID}}", + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "partition": "{{AWSPartition}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_s3(boto_config): + return boto3.client('s3', config=boto_config) + +def create_bucket_policy(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + s3 = connect_to_s3(boto_config) + + cloudtrail_bucket = event['cloudtrail_bucket'] + aws_partition = event['partition'] + aws_account = event['account'] + try: + bucket_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSCloudTrailAclCheck20150319", + "Effect": "Allow", + "Principal": { + "Service": [ + "cloudtrail.amazonaws.com" + ] + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:" + aws_partition + ":s3:::" + cloudtrail_bucket + }, + { + "Sid": "AWSCloudTrailWrite20150319", + "Effect": "Allow", + "Principal": { + "Service": [ + "cloudtrail.amazonaws.com" + ] + }, + "Action": "s3:PutObject", + "Resource": "arn:" + aws_partition + ":s3:::" + cloudtrail_bucket + "/AWSLogs/" + aws_account + "/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } } } - except Exception as e: - exit('Error enabling AWS Config: ' + str(e)) - - - isEnd: false - - - - name: Remediation - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $ - Type: StringMap - inputs: - InputPayload: - cloudtrail_bucket: '{{CreateCloudTrailBucket.CloudTrailBucketName}}' - logging_bucket: '{{CreateLoggingBucket.LoggingBucketName}}' - Runtime: python3.8 - Handler: process_results - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - def process_results(event, context): - print(f'Created encrypted CloudTrail bucket {event[\\"cloudtrail_bucket\\"]}') - print(f'Created access logging for CloudTrail bucket in bucket {event[\\"logging_bucket\\"]}') - print('Enabled multi-region AWS CloudTrail') - return { - \\"response\\": { - \\"message\\": \\"AWS CloudTrail successfully enabled\\", - \\"status\\": \\"Success\\" + ] + } + s3.put_bucket_policy( + Bucket=cloudtrail_bucket, + Policy=json.dumps(bucket_policy) + ) + return { + "output": { + "Message": f'Set bucket policy for bucket {cloudtrail_bucket}' } - } - isEnd: true - -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CreateCloudTrailMultiRegionTrail", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + } + except Exception as e: + print(e) + exit('PutBucketPolicy failed: ' + str(e))", }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "CreateCloudTrailBucketPolicy", + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "enable_cloudtrail", + "InputPayload": { + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "kms_key_arn": "{{KMSKeyArn}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_cloudtrail(boto_config): + return boto3.client('cloudtrail', config=boto_config) + +def enable_cloudtrail(event, context): + + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + ct = connect_to_cloudtrail(boto_config) + + try: + ct.create_trail( + Name='multi-region-cloud-trail', + S3BucketName=event['cloudtrail_bucket'], + IncludeGlobalServiceEvents=True, + EnableLogFileValidation=True, + IsMultiRegionTrail=True, + KmsKeyId=event['kms_key_arn'] + ) + ct.start_logging( + Name='multi-region-cloud-trail' + ) + return { + "output": { + "Message": f'CloudTrail Trail multi-region-cloud-trail created' + } + } + except Exception as e: + exit('Error enabling AWS Config: ' + str(e)) + ", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": false, + "name": "EnableCloudTrail", + "outputs": [ + { + "Name": "CloudTrailBucketName", + "Selector": "$.Payload.cloudtrail_bucket", + "Type": "String", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "process_results", + "InputPayload": { + "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", + "logging_bucket": "{{CreateLoggingBucket.LoggingBucketName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +def process_results(event, context): + print(f'Created encrypted CloudTrail bucket {event["cloudtrail_bucket"]}') + print(f'Created access logging for CloudTrail bucket in bucket {event["logging_bucket"]}') + print('Enabled multi-region AWS CloudTrail') + return { + "response": { + "message": "AWS CloudTrail successfully enabled", + "status": "Success" + } + }", + }, + "isEnd": true, + "name": "Remediation", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "Remediation.Output", ], + "parameters": { + "AWSPartition": { + "allowedValues": [ + "aws", + "aws-cn", + "aws-us-gov", + ], + "default": "aws", + "description": "Partition for creation of ARNs.", + "type": "String", + }, + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "KMSKeyArn": { + "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 ASR for this remediation", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CreateCloudTrailMultiRegionTrail", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRCreateLogMetricFilterAndAlarm": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-CreateLogMetricFilterAndAlarm - ## What does this document do? - Creates a metric filter for a given log group and also creates and alarm for the metric. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * CloudWatch Log Group Name: Name of the CloudWatch log group to use to create metric filter - * Alarm Value: Threshhold value for the creating an alarm for the CloudWatch Alarm - - ## Security Standards / Controls - * CIS v1.2.0: 3.1-3.14 -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+=,.@-]+$' - 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 - KMSKeyArn: - type: String - description: The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket - 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})))$' - SNSTopicName: - type: String - allowedPattern: ^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$ - -mainSteps: - - - name: CreateTopic - action: 'aws:executeScript' - outputs: - - Name: TopicArn - Selector: $.Payload.topic_arn - Type: String - inputs: - InputPayload: - kms_key_arn: '{{KMSKeyArn}}' - topic_name: '{{SNSTopicName}}' - Runtime: python3.8 - Handler: create_encrypted_topic - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0 # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - boto_config = Config( - retries ={ - 'mode': 'standard' + "ASRCreateLogMetricFilterAndAlarm": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-CreateLogMetricFilterAndAlarm +## What does this document do? +Creates a metric filter for a given log group and also creates and alarm for the metric. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* CloudWatch Log Group Name: Name of the CloudWatch log group to use to create metric filter +* Alarm Value: Threshhold value for the creating an alarm for the CloudWatch Alarm + +## Security Standards / Controls +* CIS v1.2.0: 3.1-3.14 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "create_encrypted_topic", + "InputPayload": { + "kms_key_arn": "{{KMSKeyArn}}", + "topic_name": "{{SNSTopicName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) + +def connect_to_sns(): + return boto3.client('sns', config=boto_config) + +def connect_to_ssm(): + return boto3.client('ssm', config=boto_config) + +def create_encrypted_topic(event, context): + + kms_key_arn = event['kms_key_arn'] + new_topic = False + topic_arn = '' + topic_name = event['topic_name'] + + try: + sns = connect_to_sns() + topic_arn = sns.create_topic( + Name=topic_name, + Attributes={ + 'KmsMasterKeyId': kms_key_arn.split('key/')[1] } - ) - - def connect_to_sns(): - return boto3.client('sns', config=boto_config) - - def connect_to_ssm(): - return boto3.client('ssm', config=boto_config) - - def create_encrypted_topic(event, context): - - kms_key_arn = event['kms_key_arn'] - new_topic = False - topic_arn = '' - topic_name = event['topic_name'] - - try: - sns = connect_to_sns() - topic_arn = sns.create_topic( - Name=topic_name, - Attributes={ - 'KmsMasterKeyId': kms_key_arn.split('key/')[1] - } - )['TopicArn'] - new_topic = True - - except ClientError as client_exception: - exception_type = client_exception.response['Error']['Code'] - if exception_type == 'InvalidParameter': - print(f'Topic {topic_name} already exists. This remediation may have been run before.') - print('Ignoring exception - remediation continues.') - topic_arn = sns.create_topic( - Name=topic_name - )['TopicArn'] - else: - exit(f'ERROR: Unhandled client exception: {client_exception}') - - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - if new_topic: - try: - ssm = connect_to_ssm() - ssm.put_parameter( - Name='/Solutions/SO0111/SNS_Topic_CIS3.x', - Description='SNS Topic for AWS Config updates', - Type='String', - Overwrite=True, - Value=topic_arn - ) - except Exception as e: - exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') - - create_topic_policy(topic_arn) + )['TopicArn'] + new_topic = True + + except ClientError as client_exception: + exception_type = client_exception.response['Error']['Code'] + if exception_type == 'InvalidParameter': + print(f'Topic {topic_name} already exists. This remediation may have been run before.') + print('Ignoring exception - remediation continues.') + topic_arn = sns.create_topic( + Name=topic_name + )['TopicArn'] + else: + exit(f'ERROR: Unhandled client exception: {client_exception}') + + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + if new_topic: + try: + ssm = connect_to_ssm() + ssm.put_parameter( + Name='/Solutions/SO0111/SNS_Topic_CIS3.x', + Description='SNS Topic for AWS Config updates', + Type='String', + Overwrite=True, + Value=topic_arn + ) + except Exception as e: + exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') + + create_topic_policy(topic_arn) + + return {"topic_arn": topic_arn} + +def create_topic_policy(topic_arn): + sns = connect_to_sns() + try: + topic_policy = { + "Id": "Policy_ID", + "Statement": [ + { + "Sid": "AWSConfigSNSPolicy", + "Effect": "Allow", + "Principal": { + "Service": "cloudwatch.amazonaws.com" + }, + "Action": "SNS:Publish", + "Resource": topic_arn, + }] + } - return {\\"topic_arn\\": topic_arn} - - def create_topic_policy(topic_arn): - sns = connect_to_sns() - try: - topic_policy = { - \\"Id\\": \\"Policy_ID\\", - \\"Statement\\": [ - { - \\"Sid\\": \\"AWSConfigSNSPolicy\\", - \\"Effect\\": \\"Allow\\", - \\"Principal\\": { - \\"Service\\": \\"cloudwatch.amazonaws.com\\" - }, - \\"Action\\": \\"SNS:Publish\\", - \\"Resource\\": topic_arn, - }] - } - - sns.set_topic_attributes( - TopicArn=topic_arn, - AttributeName='Policy', - AttributeValue=json.dumps(topic_policy) - ) - except Exception as e: - exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}') - - - - - name: CreateMetricFilerAndAlarm - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - LogGroupName: '{{LogGroupName}}' - FilterName: '{{FilterName}}' - FilterPattern: '{{FilterPattern}}' - MetricName: '{{MetricName}}' - MetricNamespace: '{{MetricNamespace}}' - MetricValue: '{{MetricValue}}' - AlarmName: '{{AlarmName}}' - AlarmDesc: '{{AlarmDesc}}' - AlarmThreshold: '{{AlarmThreshold}}' - TopicArn: '{{CreateTopic.TopicArn}}' - Runtime: python3.8 - Handler: verify - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - import logging - import os - from botocore.config import Config - - boto_config = Config( - retries={ - 'max_attempts': 10, - 'mode': 'standard' - } + sns.set_topic_attributes( + TopicArn=topic_arn, + AttributeName='Policy', + AttributeValue=json.dumps(topic_policy) ) - - log = logging.getLogger() - LOG_LEVEL = str(os.getenv('LogLevel', 'INFO')) - log.setLevel(LOG_LEVEL) - - - def get_service_client(service_name): - \\"\\"\\" - Returns the service client for given the service name - :param service_name: name of the service - :return: service client - \\"\\"\\" - log.debug(\\"Getting the service client for service: {}\\".format(service_name)) - return boto3.client(service_name, config=boto_config) - - - def put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value): - \\"\\"\\" - Puts the metric filter on the CloudWatch log group with provided values - :param cw_log_group: Name of the CloudWatch log group - :param filter_name: Name of the filter - :param filter_pattern: Pattern for the filter - :param metric_name: Name of the metric - :param metric_namespace: Namespace where metric is logged - :param metric_value: Value to be logged for the metric - \\"\\"\\" - logs_client = get_service_client('logs') - log.debug(\\"Putting the metric filter with values: {}\\".format([ - cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value])) - try: - logs_client.put_metric_filter( - logGroupName=cw_log_group, - filterName=filter_name, - filterPattern=filter_pattern, - metricTransformations=[ - { - 'metricName': metric_name, - 'metricNamespace': metric_namespace, - 'metricValue': str(metric_value), - 'unit': 'Count' - } - ] - ) - except Exception as e: - exit(\\"Exception occurred while putting metric filter: \\" + str(e)) - log.debug(\\"Successfully added the metric filter.\\") - - - def put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn): - \\"\\"\\" - Puts the metric alarm for the metric name with provided values - :param alarm_name: Name for the alarm - :param alarm_desc: Description for the alarm - :param alarm_threshold: Threshold value for the alarm - :param metric_name: Name of the metric - :param metric_namespace: Namespace where metric is logged - \\"\\"\\" - cw_client = get_service_client('cloudwatch') - log.debug(\\"Putting the metric alarm with values {}\\".format( - [alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace])) - try: - cw_client.put_metric_alarm( - AlarmName=alarm_name, - AlarmDescription=alarm_desc, - ActionsEnabled=True, - OKActions=[ - topic_arn - ], - AlarmActions=[ - topic_arn - ], - MetricName=metric_name, - Namespace=metric_namespace, - Statistic='Sum', - Period=300, - Unit='Count', - EvaluationPeriods=12, - DatapointsToAlarm=1, - Threshold=alarm_threshold, - ComparisonOperator='GreaterThanOrEqualToThreshold', - TreatMissingData='notBreaching' - ) - except Exception as e: - exit(\\"Exception occurred while putting metric alarm: \\" + str(e)) - log.debug(\\"Successfully added metric alarm.\\") - - - def verify(event, context): - log.info(\\"Begin handler\\") - log.debug(\\"====Print Event====\\") - log.debug(event) - - filter_name = event['FilterName'] - filter_pattern = event['FilterPattern'] - metric_name = event['MetricName'] - metric_namespace = event['MetricNamespace'] - metric_value = event['MetricValue'] - alarm_name = event['AlarmName'] - alarm_desc = event['AlarmDesc'] - alarm_threshold = event['AlarmThreshold'] - cw_log_group = event['LogGroupName'] - topic_arn = event['TopicArn'] - - put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value) - put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn) - return { - \\"response\\": { - \\"message\\": f'Created filter {event[\\"FilterName\\"]} for metric {event[\\"MetricName\\"]}, and alarm {event[\\"AlarmName\\"]}', - \\"status\\": \\"Success\\" - } - } - - -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-CreateLogMetricFilterAndAlarm", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + except Exception as e: + exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", }, - ":function:SO0111-SHARR-updatableRunbookProvider", + "name": "CreateTopic", + "outputs": [ + { + "Name": "TopicArn", + "Selector": "$.Payload.topic_arn", + "Type": "String", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "verify", + "InputPayload": { + "AlarmDesc": "{{AlarmDesc}}", + "AlarmName": "{{AlarmName}}", + "AlarmThreshold": "{{AlarmThreshold}}", + "FilterName": "{{FilterName}}", + "FilterPattern": "{{FilterPattern}}", + "LogGroupName": "{{LogGroupName}}", + "MetricName": "{{MetricName}}", + "MetricNamespace": "{{MetricNamespace}}", + "MetricValue": "{{MetricValue}}", + "TopicArn": "{{CreateTopic.TopicArn}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +import logging +import os +from botocore.config import Config + +boto_config = Config( + retries={ + 'max_attempts': 10, + 'mode': 'standard' + } +) + +log = logging.getLogger() +LOG_LEVEL = str(os.getenv('LogLevel', 'INFO')) +log.setLevel(LOG_LEVEL) + + +def get_service_client(service_name): + """ + Returns the service client for given the service name + :param service_name: name of the service + :return: service client + """ + log.debug("Getting the service client for service: {}".format(service_name)) + return boto3.client(service_name, config=boto_config) + + +def put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value): + """ + Puts the metric filter on the CloudWatch log group with provided values + :param cw_log_group: Name of the CloudWatch log group + :param filter_name: Name of the filter + :param filter_pattern: Pattern for the filter + :param metric_name: Name of the metric + :param metric_namespace: Namespace where metric is logged + :param metric_value: Value to be logged for the metric + """ + logs_client = get_service_client('logs') + log.debug("Putting the metric filter with values: {}".format([ + cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value])) + try: + logs_client.put_metric_filter( + logGroupName=cw_log_group, + filterName=filter_name, + filterPattern=filter_pattern, + metricTransformations=[ + { + 'metricName': metric_name, + 'metricNamespace': metric_namespace, + 'metricValue': str(metric_value), + 'unit': 'Count' + } + ] + ) + except Exception as e: + exit("Exception occurred while putting metric filter: " + str(e)) + log.debug("Successfully added the metric filter.") + + +def put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn): + """ + Puts the metric alarm for the metric name with provided values + :param alarm_name: Name for the alarm + :param alarm_desc: Description for the alarm + :param alarm_threshold: Threshold value for the alarm + :param metric_name: Name of the metric + :param metric_namespace: Namespace where metric is logged + """ + cw_client = get_service_client('cloudwatch') + log.debug("Putting the metric alarm with values {}".format( + [alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace])) + try: + cw_client.put_metric_alarm( + AlarmName=alarm_name, + AlarmDescription=alarm_desc, + ActionsEnabled=True, + OKActions=[ + topic_arn + ], + AlarmActions=[ + topic_arn ], + MetricName=metric_name, + Namespace=metric_namespace, + Statistic='Sum', + Period=300, + Unit='Count', + EvaluationPeriods=12, + DatapointsToAlarm=1, + Threshold=alarm_threshold, + ComparisonOperator='GreaterThanOrEqualToThreshold', + TreatMissingData='notBreaching' + ) + except Exception as e: + exit("Exception occurred while putting metric alarm: " + str(e)) + log.debug("Successfully added metric alarm.") + + +def verify(event, context): + log.info("Begin handler") + log.debug("====Print Event====") + log.debug(event) + + filter_name = event['FilterName'] + filter_pattern = event['FilterPattern'] + metric_name = event['MetricName'] + metric_namespace = event['MetricNamespace'] + metric_value = event['MetricValue'] + alarm_name = event['AlarmName'] + alarm_desc = event['AlarmDesc'] + alarm_threshold = event['AlarmThreshold'] + cw_log_group = event['LogGroupName'] + topic_arn = event['TopicArn'] + + put_metric_filter(cw_log_group, filter_name, filter_pattern, metric_name, metric_namespace, metric_value) + put_metric_alarm(alarm_name, alarm_desc, alarm_threshold, metric_name, metric_namespace, topic_arn) + return { + "response": { + "message": f'Created filter {event["FilterName"]} for metric {event["MetricName"]}, and alarm {event["AlarmName"]}', + "status": "Success" + } + }", + }, + "name": "CreateMetricFilerAndAlarm", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, ], + "parameters": { + "AlarmDesc": { + "allowedPattern": ".*", + "description": "Description of the Alarm to be created for the metric filter", + "type": "String", + }, + "AlarmName": { + "allowedPattern": ".*", + "description": "Name of the Alarm to be created for the metric filter", + "type": "String", + }, + "AlarmThreshold": { + "description": "Threshold value for the alarm", + "type": "Integer", + }, + "AutomationAssumeRole": { + "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", + }, + "FilterName": { + "allowedPattern": ".*", + "description": "Name for the metric filter", + "type": "String", + }, + "FilterPattern": { + "allowedPattern": ".*", + "description": "Filter pattern to create metric filter", + "type": "String", + }, + "KMSKeyArn": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", + "description": "The ARN of a KMS key to use for encryption of the SNS Topic and Config bucket", + "type": "String", + }, + "LogGroupName": { + "allowedPattern": ".*", + "description": "Name of the log group to be used to create metric filter", + "type": "String", + }, + "MetricName": { + "allowedPattern": ".*", + "description": "Name of the metric for metric filter", + "type": "String", + }, + "MetricNamespace": { + "allowedPattern": ".*", + "description": "Namespace where the metrics will be sent", + "type": "String", + }, + "MetricValue": { + "description": "Value of the metric for metric filter", + "type": "Integer", + }, + "SNSTopicName": { + "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-CreateLogMetricFilterAndAlarm", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRDisablePublicAccessToRDSInstance": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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\\" - + "ASRDisablePublicAccessToRDSInstance": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-DisablePublicAccessToRDSInstance", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## GetRDSInstanceIdentifier +Gathers the DB instance identifier from the DB instance resource identifier. +## Outputs +* DbInstanceIdentifier: The Amazon RDS DB instance identifier. +", + "inputs": { + "Api": "DescribeDBInstances", + "Filters": [ + { + "Name": "dbi-resource-id", + "Values": [ + "{{ DbiResourceId }}", + ], + }, + ], + "Service": "rds", + }, + "isEnd": false, + "name": "GetRDSInstanceIdentifier", + "outputs": [ + { + "Name": "DbInstanceIdentifier", + "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceStatus +Verifies the DB instances is in an AVAILABLE state. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "VerifyDBInstanceStatus", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeAwsApi", + "description": "## DisablePubliclyAccessibleOnRDS +Disables public accessibility on your DB instance. +## Outputs +* Response: The standard HTTP response from the ModifyDBInstance API. +", + "inputs": { + "Api": "ModifyDBInstance", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "PubliclyAccessible": false, + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "DisablePubliclyAccessibleOnRDS", + "outputs": [ + { + "Name": "Response", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:waitForAwsResourceProperty", + "description": "## WaitForDBInstanceStatusToModify +Waits for the DB instance to change to a MODIFYING state. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DesiredValues": [ + "modifying", + ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": false, + "name": "WaitForDBInstanceStatusToModify", + "timeoutSeconds": 600, + }, + { + "action": "aws:waitForAwsResourceProperty", + "description": "## WaitForDBInstanceStatusToAvailableAfterModify +Waits for the DB instance to change to an AVAILABLE state +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", + }, + "isEnd": false, + "name": "WaitForDBInstanceStatusToAvailableAfterModify", + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstancePubliclyAccess +Confirms public accessibility is disabled on the DB instance. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DesiredValues": [ + "False", + ], + "PropertySelector": "$.DBInstances[0].PubliclyAccessible", + "Service": "rds", + }, + "isEnd": true, + "name": "VerifyDBInstancePubliclyAccess", + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "DisablePubliclyAccessibleOnRDS.Response", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "DbiResourceId": { + "allowedPattern": "db-[A-Z0-9]{26}", + "description": "(Required) The resource identifier for the DB instance you want to disable public accessibility.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-DisablePublicAccessToRDSInstance", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRDisablePublicAccessToRedshiftCluster": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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 - + "ASREnableAutomaticSnapshotsOnRedshiftCluster": { + "Properties": { + "Content": { + "assumeRole": "{{AutomationAssumeRole}}", + "description": "### Document name - ASR-EnableAutomaticSnapshotsOnRedshiftCluster + +## What does this document do? +The runbook enables automatic snapshots 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. +* RetentionPeriod: (Optional) The minimum retention period for the automatic snapshots in days. + +## Output Parameters +* QueryRetentionPeriod.CurrentRetentionPeriod: The retention period of the cluster in days at the start of the automation. +* ModifyRetentionPeriod.Response: The response of the API call to modify the retention period of the cluster. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableAutomaticSnapshotsOnRedshiftCluster", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "Service": "redshift", + }, + "name": "QueryRetentionPeriod", + "outputs": [ + { + "Name": "CurrentRetentionPeriod", + "Selector": "$.Clusters[0].AutomatedSnapshotRetentionPeriod", + "Type": "Integer", + }, + ], + }, + { + "action": "aws:branch", + "inputs": { + "Choices": [ + { + "NextStep": "CastCurrentRetentionPeriodToString", + "NumericGreaterOrEquals": "{{MinRetentionPeriod}}", + "Variable": "{{QueryRetentionPeriod.CurrentRetentionPeriod}}", + }, + ], + "Default": "ModifyRetentionPeriod", + }, + "name": "ChooseModifyRetentionPeriod", + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "event_handler", + "InputPayload": { + "RetentionPeriod": "{{QueryRetentionPeriod.CurrentRetentionPeriod}}", + }, + "Runtime": "python3.8", + "Script": "def event_handler(event, context): + return str(event['RetentionPeriod']) +", + }, + "name": "CastCurrentRetentionPeriodToString", + "outputs": [ + { + "Name": "CurrentRetentionPeriodString", + "Selector": "$.Payload", + "Type": "String", + }, + ], + }, + { + "action": "aws:assertAwsResourceProperty", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "DesiredValues": [ + "{{CastCurrentRetentionPeriodToString.CurrentRetentionPeriodString}}", + ], + "PropertySelector": "$.Clusters[0].AutomatedSnapshotRetentionPeriod", + "Service": "redshift", + }, + "isEnd": true, + "name": "VerifyCurrentRetentionPeriod", + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "ModifyCluster", + "AutomatedSnapshotRetentionPeriod": "{{MinRetentionPeriod}}", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "Service": "redshift", + }, + "name": "ModifyRetentionPeriod", + "outputs": [ + { + "Name": "Response", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:waitForAwsResourceProperty", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.Clusters[0].ClusterStatus", + "Service": "redshift", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "WaitForClusterAvailability", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "event_handler", + "InputPayload": { + "RetentionPeriod": "{{MinRetentionPeriod}}", + }, + "Runtime": "python3.8", + "Script": "def event_handler(event, context): + return str(event['RetentionPeriod']) +", }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "CastRetentionPeriodToString", + "outputs": [ + { + "Name": "MinRetentionPeriodString", + "Selector": "$.Payload", + "Type": "String", + }, + ], + }, + { + "action": "aws:assertAwsResourceProperty", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "DesiredValues": [ + "{{CastRetentionPeriodToString.MinRetentionPeriodString}}", + ], + "PropertySelector": "$.Clusters[0].AutomatedSnapshotRetentionPeriod", + "Service": "redshift", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyModifiedRetentionPeriod", + }, ], + "outputs": [ + "QueryRetentionPeriod.CurrentRetentionPeriod", + "ModifyRetentionPeriod.Response", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "ClusterIdentifier": { + "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 - + "ASREnableAutomaticVersionUpgradeOnRedshiftCluster": { + "Properties": { + "Content": { + "assumeRole": "{{AutomationAssumeRole}}", + "description": "### Document name - ASR-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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableAutomaticVersionUpgradeOnRedshiftCluster", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "inputs": { + "AllowVersionUpgrade": "{{AllowVersionUpgrade}}", + "Api": "ModifyCluster", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "Service": "redshift", + }, + "name": "EnableAutomaticVersionUpgrade", + "outputs": [ + { + "Name": "Response", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:waitForAwsResourceProperty", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.Clusters[0].ClusterStatus", + "Service": "redshift", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "WaitForClusterAvailability", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "event_handler", + "InputPayload": { + "AllowVersionUpgrade": "{{AllowVersionUpgrade}}", + }, + "Runtime": "python3.8", + "Script": "def event_handler(event, context): + return str(event['AllowVersionUpgrade']) +", }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "CastAllowVersionUpgradeToString", + "outputs": [ + { + "Name": "AllowVersionUpgradeString", + "Selector": "$.Payload", + "Type": "String", + }, + ], + }, + { + "action": "aws:assertAwsResourceProperty", + "inputs": { + "Api": "DescribeClusters", + "ClusterIdentifier": "{{ClusterIdentifier}}", + "DesiredValues": [ + "{{CastAllowVersionUpgradeToString.AllowVersionUpgradeString}}", + ], + "PropertySelector": "$.Clusters[0].AllowVersionUpgrade", + "Service": "redshift", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyAutomaticVersionUpgrade", + }, + ], + "outputs": [ + "EnableAutomaticVersionUpgrade.Response", ], + "parameters": { + "AllowVersionUpgrade": { + "default": true, + "description": "(Optional) Whether to allow version upgrade on the cluster.", + "type": "Boolean", + }, + "AutomationAssumeRole": { + "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", + }, + "ClusterIdentifier": { + "allowedPattern": "^(?!.*--)[a-z][a-z0-9-]{0,62}(?- - {{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})))$' - TrailRegion: - type: String - description: 'Region the CloudTrail is in' - allowedPattern: '^[a-z]{2}(?:-gov)?-[a-z]+-\\\\d$' - TrailArn: - type: String - description: 'ARN of the CloudTrail' - allowedPattern: '^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\\\d):\\\\d{12}:trail/[A-Za-z0-9._-]{3,128}$' -outputs: - - Remediation.Output - -mainSteps: - - - name: Remediation - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - exec_region: '{{global:REGION}}' - trail_region: '{{TrailRegion}}' - trail: '{{TrailArn}}' - region: '{{global:REGION}}' - kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.8 - Handler: enable_trail_encryption - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_cloudtrail(region, boto_config): - return boto3.client('cloudtrail', region_name=region, config=boto_config) - - def enable_trail_encryption(event, context): - \\"\\"\\" - remediates CloudTrail.2 by enabling SSE-KMS - On success returns a string map - On failure returns NoneType - \\"\\"\\" - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - - if event['trail_region'] != event['exec_region']: - exit('ERROR: cross-region remediation is not yet supported') - - ctrail_client = connect_to_cloudtrail(event['trail_region'], boto_config) - kms_key_arn = event['kms_key_arn'] - - try: - ctrail_client.update_trail( - Name=event['trail'], - KmsKeyId=kms_key_arn - ) - return { - \\"response\\": { - \\"message\\": f'Enabled KMS CMK encryption on {event[\\"trail\\"]}', - \\"status\\": \\"Success\\" - } - } - except Exception as e: - exit(f'Error enabling SSE-KMS encryption: {str(e)}') - + if event['trail_region'] != event['exec_region']: + exit('ERROR: cross-region remediation is not yet supported') - isEnd: true + ctrail_client = connect_to_cloudtrail(event['trail_region'], boto_config) + kms_key_arn = event['kms_key_arn'] -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailEncryption", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + try: + ctrail_client.update_trail( + Name=event['trail'], + KmsKeyId=kms_key_arn + ) + return { + "response": { + "message": f'Enabled KMS CMK encryption on {event["trail"]}', + "status": "Success" + } + } + except Exception as e: + exit(f'Error enabling SSE-KMS encryption: {str(e)}')", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "Remediation", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "Remediation.Output", ], + "parameters": { + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "KMSKeyArn": { + "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 ASR for this remediation", + "type": "String", + }, + "TrailArn": { + "allowedPattern": "^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:trail/[A-Za-z0-9._-]{3,128}$", + "description": "ARN of the CloudTrail", + "type": "String", + }, + "TrailRegion": { + "allowedPattern": "^[a-z]{2}(?:-gov)?-[a-z]+-\\d$", + "description": "Region the CloudTrail is in", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableCloudTrailEncryption", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableCloudTrailLogFileValidation": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - AWSConfigRemediation-EnableCloudTrailLogFileValidation - - ## What does this document do? - This runbook enables log file validation for your AWS CloudTrail trail using the [UpdateTrail](https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_UpdateTrail.html) API. - - ## Input Parameters - * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. - * TrailName: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. - - ## Output Parameters - * UpdateTrail.Output: The response of the UpdateTrail API call. - - ## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. - -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+=,.@-]+$' - TrailName: - type: String - description: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. - allowedPattern: (^arn:(aws[a-zA-Z-]*)?:cloudtrail:[a-z0-9-]+:\\\\d{12}:trail\\\\/(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$)|(^(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$)[-\\\\w.]{3,128}$) -outputs: - - UpdateTrail.Output -mainSteps: - - name: UpdateTrail - action: aws:executeAwsApi - description: | - ## UpdateTrail - Enables log file validation for the AWS CloudTrail trail you specify in the TrailName parameter. - ## Outputs - * Output: Response from the UpdateTrail API call. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: cloudtrail - Api: UpdateTrail - Name: \\"{{ TrailName }}\\" - EnableLogFileValidation: True - outputs: - - Name: Output - Selector: $ - Type: StringMap - - name: VerifyTrail - action: aws:assertAwsResourceProperty - description: | - ## VerifyTrail - Verifies log file validation is enabled for your trail. - timeoutSeconds: 600 - isEnd: true - inputs: - Service: cloudtrail - Api: GetTrail - Name: \\"{{ TrailName }}\\" - PropertySelector: $.Trail.LogFileValidationEnabled - DesiredValues: - - \\"True\\" + "ASREnableCloudTrailLogFileValidation": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableCloudTrailLogFileValidation + +## What does this document do? +This runbook enables log file validation for your AWS CloudTrail trail using the [UpdateTrail](https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_UpdateTrail.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* TrailName: (Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for. + +## Output Parameters +* UpdateTrail.Output: The response of the UpdateTrail API call. +## Note: this is a local copy of the AWS-owned document to enable support in aws-cn and aws-us-gov partitions. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailLogFileValidation", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## UpdateTrail +Enables log file validation for the AWS CloudTrail trail you specify in the TrailName parameter. +## Outputs +* Output: Response from the UpdateTrail API call. +", + "inputs": { + "Api": "UpdateTrail", + "EnableLogFileValidation": true, + "Name": "{{ TrailName }}", + "Service": "cloudtrail", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "UpdateTrail", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyTrail +Verifies log file validation is enabled for your trail. +", + "inputs": { + "Api": "GetTrail", + "DesiredValues": [ + "True", + ], + "Name": "{{ TrailName }}", + "PropertySelector": "$.Trail.LogFileValidationEnabled", + "Service": "cloudtrail", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyTrail", + "timeoutSeconds": 600, + }, ], + "outputs": [ + "UpdateTrail.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "TrailName": { + "allowedPattern": "(^arn:(aws[a-zA-Z-]*)?:cloudtrail:[a-z0-9-]+:\\d{12}:trail\\/(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$)[-\\w.]{3,128}$)|(^(?![-_.])(?!.*[-_.]{2})(?!.*[-_.]$)(?!^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$)[-\\w.]{3,128}$)", + "description": "(Required) The name or Amazon Resource Name (ARN) of the trail you want to enable log file validation for.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", - }, - "SHARREnableCloudTrailToCloudWatchLogging": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document Name - SHARR-EnableCloudTrailToCloudWatchLogging - ## What does this document do? - Creates a CloudWatch logs group for CloudTrail data. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data - - ## Security Standards / Controls - * AFSBP v1.0.0: N/A - * CIS v1.2.0: 2.4 - * PCI: CloudTrail.4 - -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+=,.@-]+$' - TrailName: - type: String - description: (Required) The name of the CloudTrail. - allowedPattern: '^[A-Za-z0-9._-]{3,128}$' - 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+=,.@-]+$' - LogGroupName: - type: String - description: (Required) The name of the Log Group for CloudTrail logs. - allowedPattern: '^[a-zA-Z0-9-_./]{1,512}$' -outputs: - - UpdateTrailToCWLogs.Output - -mainSteps: - - - name: CreateLogGroup - action: 'aws:executeAwsApi' - inputs: - Service: logs - Api: CreateLogGroup - logGroupName: '{{LogGroupName}}' - description: Create the log group - outputs: - - Name: Output - Selector: $ - Type: StringMap - - - name: WaitForCreation - action: 'aws:executeScript' - inputs: - InputPayload: - LogGroup: '{{LogGroupName}}' - Runtime: python3.8 - Handler: wait_for_loggroup - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - from botocore.config import Config - import time - - def connect_to_logs(boto_config): - return boto3.client('logs', config=boto_config) - - def wait_for_loggroup(event, context): - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - cwl_client = connect_to_logs(boto_config) - - max_retries = 3 - attempts = 0 - while attempts < max_retries: - try: - describe_group = cwl_client.describe_log_groups(logGroupNamePrefix=event['LogGroup']) - print(len(describe_group['logGroups'])) - for group in describe_group['logGroups']: - if group['logGroupName'] == event['LogGroup']: - return str(group['arn']) - # no match - wait and retry - time.sleep(2) - attempts += 1 - - except Exception as e: - exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: {str(e)}') - - exit(f'Failed to create Log Group {event[\\"LogGroup\\"]}: Timed out') - - - outputs: - - Name: CloudWatchLogsGroupArn - Selector: $.Payload - Type: String - - isEnd: false - - - - name: UpdateTrailToCWLogs - action: 'aws:executeAwsApi' - inputs: - Service: cloudtrail - Api: UpdateTrail - Name: '{{TrailName}}' - CloudWatchLogsLogGroupArn: '{{WaitForCreation.CloudWatchLogsGroupArn}}' - CloudWatchLogsRoleArn: '{{CloudWatchLogsRole}}' - description: Enable logging to CloudWatch Logs - outputs: - - Name: Output - Selector: $ - Type: StringMap - -", "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-EnableCloudTrailToCloudWatchLogging", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], - ], - }, + "Name": "ASR-EnableCloudTrailLogFileValidation", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableCopyTagsToSnapshotOnRDSCluster": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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\\" + "ASREnableCloudTrailToCloudWatchLogging": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-EnableCloudTrailToCloudWatchLogging +## What does this document do? +Creates a CloudWatch logs group for CloudTrail data. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data + +## Security Standards / Controls +* AFSBP v1.0.0: N/A +* CIS v1.2.0: 2.4 +* PCI: CloudTrail.4 ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableCopyTagsToSnapshotOnRDSCluster", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "Create the log group", + "inputs": { + "Api": "CreateLogGroup", + "Service": "logs", + "logGroupName": "{{LogGroupName}}", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "CreateLogGroup", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "wait_for_loggroup", + "InputPayload": { + "LogGroup": "{{LogGroupName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import boto3 +from botocore.config import Config +import time + +def connect_to_logs(boto_config): + return boto3.client('logs', config=boto_config) + +def wait_for_loggroup(event, context): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + cwl_client = connect_to_logs(boto_config) + + max_retries = 3 + attempts = 0 + while attempts < max_retries: + try: + describe_group = cwl_client.describe_log_groups(logGroupNamePrefix=event['LogGroup']) + print(len(describe_group['logGroups'])) + for group in describe_group['logGroups']: + if group['logGroupName'] == event['LogGroup']: + return str(group['arn']) + # no match - wait and retry + time.sleep(2) + attempts += 1 + + except Exception as e: + exit(f'Failed to create Log Group {event["LogGroup"]}: {str(e)}') + + exit(f'Failed to create Log Group {event["LogGroup"]}: Timed out')", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "WaitForCreation", + "outputs": [ + { + "Name": "CloudWatchLogsGroupArn", + "Selector": "$.Payload", + "Type": "String", + }, + ], + }, + { + "action": "aws:executeAwsApi", + "description": "Enable logging to CloudWatch Logs", + "inputs": { + "Api": "UpdateTrail", + "CloudWatchLogsLogGroupArn": "{{WaitForCreation.CloudWatchLogsGroupArn}}", + "CloudWatchLogsRoleArn": "{{CloudWatchLogsRole}}", + "Name": "{{TrailName}}", + "Service": "cloudtrail", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "name": "UpdateTrailToCWLogs", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "UpdateTrailToCWLogs.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "CloudWatchLogsRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows CloudTrail to log to CloudWatch.", + "type": "String", + }, + "LogGroupName": { + "allowedPattern": "^[a-zA-Z0-9-_./]{1,512}$", + "description": "(Required) The name of the Log Group for CloudTrail logs.", + "type": "String", + }, + "TrailName": { + "allowedPattern": "^[A-Za-z0-9._-]{3,128}$", + "description": "(Required) The name of the CloudTrail.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableCloudTrailToCloudWatchLogging", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableDefaultEncryptionS3": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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 - + "ASREnableCopyTagsToSnapshotOnRDSCluster": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableDefaultEncryptionS3", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "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. +", + "inputs": { + "Api": "GetResourceConfigHistory", + "Service": "config", + "resourceId": "{{ DbClusterResourceId }}", + "resourceType": "AWS::RDS::DBCluster", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "GetDBClusterIdentifier", + "outputs": [ + { + "Name": "DBClusterIdentifier", + "Selector": "$.configurationItems[0].resourceName", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyStatus +Verifies if \`Status\` is available before proeeding to the next step. +", + "inputs": { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetDBClusterIdentifier.DBClusterIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBClusters[0].Status", + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "VerifyStatus", + "timeoutSeconds": 600, + }, + { + "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. +", + "inputs": { + "Api": "ModifyDBCluster", + "ApplyImmediately": "{{ ApplyImmediately }}", + "CopyTagsToSnapshot": true, + "DBClusterIdentifier": "{{ GetDBClusterIdentifier.DBClusterIdentifier }}", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": false, + "name": "EnableCopyTagsToSnapshot", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBClusterCopyTagsToSnapshotEnabled +Verifies that \`CopyTagsToSnapshot\` has been enabled on the target resource. +## Outputs +* Output: A success message or failure exception. +", + "inputs": { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetDBClusterIdentifier.DBClusterIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.DBClusters[0].CopyTagsToSnapshot", + "Service": "rds", + }, + "isEnd": true, + "name": "VerifyDBClusterCopyTagsToSnapshotEnabled", + "timeoutSeconds": 600, + }, ], + "outputs": [ + "EnableCopyTagsToSnapshot.Output", + ], + "parameters": { + "ApplyImmediately": { + "default": false, + "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.", + "type": "Boolean", + }, + "AutomationAssumeRole": { + "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", + }, + "DbClusterResourceId": { + "allowedPattern": "^cluster-[A-Z0-9]+$", + "description": "(Required) Resource ID of the Amazon RDS Cluster for which CopyTagsToSnapshot needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableCopyTagsToSnapshotOnRDSCluster", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableEbsEncryptionByDefault": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document Name - AWSConfigRemediation-EnableEbsEncryptionByDefault - - ## What does this document do? - This document enables EBS encryption by default for an AWS account in the current region using the [EnableEbsEncryptionByDefault](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableEbsEncryptionByDefault.html) API. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - - ## Output Parameters - * ModifyAccount.EnableEbsEncryptionByDefaultResponse: JSON formatted response from the EnableEbsEncryptionByDefault API. - -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+=,.@-]+$' -outputs: - - ModifyAccount.EnableEbsEncryptionByDefaultResponse -mainSteps: - - - name: ModifyAccount - action: \\"aws:executeAwsApi\\" - description: | - ## ModifyAccount - Enables EBS encryption by default for the account in the current region. - ## Outputs - * EnableEbsEncryptionByDefaultResponse: Response from the EnableEbsEncryptionByDefault API. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: ec2 - Api: EnableEbsEncryptionByDefault - outputs: - - Name: EnableEbsEncryptionByDefaultResponse - Selector: $ - Type: StringMap - - - name: VerifyEbsEncryptionByDefault - action: \\"aws:assertAwsResourceProperty\\" - timeoutSeconds: 600 - isEnd: true - description: | - ## VerifyEbsEncryptionByDefault - Checks if EbsEncryptionByDefault is enabled correctly from the previous step. - inputs: - Service: ec2 - Api: GetEbsEncryptionByDefault - PropertySelector: \\"$.EbsEncryptionByDefault\\" - DesiredValues: - - \\"True\\" + "ASREnableDefaultEncryptionS3": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - ASR-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 ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableEbsEncryptionByDefault", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:branch", + "inputs": { + "Choices": [ + { + "NextStep": "EncryptWithAES", + "StringEquals": "default-s3-encryption", + "Variable": "{{KmsKeyAlias}}", + }, + ], + "Default": "EncryptWithCMK", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "ChooseEncryptionMethod", + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "PutBucketEncryption", + "Bucket": "{{BucketName}}", + "ExpectedBucketOwner": "{{AccountId}}", + "ServerSideEncryptionConfiguration": { + "Rules": [ + { + "ApplyServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256", + }, + "BucketKeyEnabled": true, + }, + ], + }, + "Service": "s3", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": true, + "name": "EncryptWithAES", + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "PutBucketEncryption", + "Bucket": "{{BucketName}}", + "ExpectedBucketOwner": "{{AccountId}}", + "ServerSideEncryptionConfiguration": { + "Rules": [ + { + "ApplyServerSideEncryptionByDefault": { + "KMSMasterKeyID": "{{KmsKeyAlias}}", + "SSEAlgorithm": "aws:kms", + }, + "BucketKeyEnabled": true, + }, + ], + }, + "Service": "s3", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "EncryptWithCMK", + }, ], + "parameters": { + "AccountId": { + "allowedPattern": "^[0-9]{12}$", + "description": "Account ID of the account for the finding", + "type": "String", + }, + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "Name of the bucket to have a policy added", + "type": "String", + }, + "KmsKeyAlias": { + "allowedPattern": "^$|^[a-zA-Z0-9/_-]{1,256}$", + "default": "default-s3-encryption", + "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.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableDefaultEncryptionS3", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableEnhancedMonitoringOnRDSInstance": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document Name - AWSConfigRemediation-EnableEnhancedMonitoringOnRDSInstance - - ## What does this document do? - This document is used to enable enhanced monitoring on an RDS Instance using the input parameter DB Instance resourceId. - - ## Input Parameters - * ResourceId: (Required) Resource ID of the RDS DB Instance. - * MonitoringInterval: (Optional) - * The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. - * If MonitoringRoleArn is specified, then you must also set MonitoringInterval to a value other than 0. - * Valid Values: 1, 5, 10, 15, 30, 60 - * Default: 60 - * MonitoringRoleArn: (Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - - ## Output Parameters - * EnableEnhancedMonitoring.DbInstance - The standard HTTP response from the ModifyDBInstance API. - -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+=,.@-]+$' - ResourceId: - type: String - description: (Required) Resource ID of the Amazon RDS instance for which Enhanced Monitoring needs to be enabled. - allowedPattern: \\"db-[A-Z0-9]{26}\\" - MonitoringInterval: - type: Integer - description: (Optional) The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. - default: 60 - allowedValues: - - 1 - - 5 - - 10 - - 15 - - 30 - - 60 - MonitoringRoleArn: - type: String - description: (Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. - allowedPattern: ^arn:(aws[a-zA-Z-]*)?:iam::\\\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$ -outputs: - - EnableEnhancedMonitoring.DbInstance -mainSteps: - - - name: DescribeDBInstances - action: \\"aws:executeAwsApi\\" - description: | - ## DescribeDBInstances - Makes describeDBInstances API call using RDS Instance DbiResourceId to get DBInstanceId. - ## Outputs - * DbInstanceIdentifier: DBInstance Identifier of the RDS Instance. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: rds - Api: DescribeDBInstances - Filters: - - Name: \\"dbi-resource-id\\" - Values: - - \\"{{ ResourceId }}\\" - outputs: - - Name: DbInstanceIdentifier - Selector: $.DBInstances[0].DBInstanceIdentifier - Type: String - - - name: VerifyDBInstanceStatus - action: \\"aws:assertAwsResourceProperty\\" - timeoutSeconds: 600 - isEnd: false - description: | - ## VerifyDBInstanceStatus - Verifies if DB Instance status is available before enabling enhanced monitoring. - inputs: - Service: rds - Api: DescribeDBInstances - DBInstanceIdentifier: \\"{{ DescribeDBInstances.DbInstanceIdentifier }}\\" - PropertySelector: \\"$.DBInstances[0].DBInstanceStatus\\" - DesiredValues: - - \\"available\\" - - - name: EnableEnhancedMonitoring - action: \\"aws:executeAwsApi\\" - description: | - ## EnableEnhancedMonitoring - Makes ModifyDBInstance API call to enable Enhanced Monitoring on the RDS Instance - using the DBInstanceId from the previous action. - ## Outputs - * DbInstance: The standard HTTP response from the ModifyDBInstance API. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: rds - Api: ModifyDBInstance - ApplyImmediately: False - DBInstanceIdentifier: \\"{{ DescribeDBInstances.DbInstanceIdentifier }}\\" - MonitoringInterval: \\"{{ MonitoringInterval }}\\" - MonitoringRoleArn: \\"{{ MonitoringRoleArn }}\\" - outputs: - - Name: DbInstance - Selector: $ - Type: StringMap - - - name: VerifyEnhancedMonitoringEnabled - action: \\"aws:executeScript\\" - description: | - ## VerifyEnhancedMonitoringEnabled - Checks that the enhanced monitoring is enabled on RDS Instance in the previous step exists. - ## Outputs - * Output: The standard HTTP response from the ModifyDBInstance API. - isEnd: true - timeoutSeconds: 600 - inputs: - Runtime: python3.8 - Handler: handler - InputPayload: - MonitoringInterval: \\"{{ MonitoringInterval }}\\" - DBIdentifier: \\"{{ DescribeDBInstances.DbInstanceIdentifier }}\\" - Script: |- - import boto3 - import time - - def handler(event, context): - rds_client = boto3.client(\\"rds\\") - db_instance_id = event[\\"DBIdentifier\\"] - monitoring_interval = event[\\"MonitoringInterval\\"] - - try: - rds_waiter = rds_client.get_waiter(\\"db_instance_available\\") - rds_waiter.wait(DBInstanceIdentifier=db_instance_id) - - db_instances = rds_client.describe_db_instances( - DBInstanceIdentifier=db_instance_id) - - for db_instance in db_instances.get(\\"DBInstances\\", [{}]): - db_monitoring_interval = db_instance.get(\\"MonitoringInterval\\") - - if db_monitoring_interval == monitoring_interval: - return { - \\"output\\": db_instances[\\"ResponseMetadata\\"] - } - else: - info = \\"VERIFICATION FAILED. RDS INSTANCE MONITORING INTERVAL {} IS NOT ENABLED WITH THE REQUIRED VALUE {}\\".format( - db_monitoring_interval, monitoring_interval) - raise Exception(info) - except Exception as e: - raise e - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap + "ASREnableEbsEncryptionByDefault": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-EnableEbsEncryptionByDefault + +## What does this document do? +This document enables EBS encryption by default for an AWS account in the current region using the [EnableEbsEncryptionByDefault](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableEbsEncryptionByDefault.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +## Output Parameters +* ModifyAccount.EnableEbsEncryptionByDefaultResponse: JSON formatted response from the EnableEbsEncryptionByDefault API. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableEnhancedMonitoringOnRDSInstance", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## ModifyAccount +Enables EBS encryption by default for the account in the current region. +## Outputs +* EnableEbsEncryptionByDefaultResponse: Response from the EnableEbsEncryptionByDefault API. +", + "inputs": { + "Api": "EnableEbsEncryptionByDefault", + "Service": "ec2", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "ModifyAccount", + "outputs": [ + { + "Name": "EnableEbsEncryptionByDefaultResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyEbsEncryptionByDefault +Checks if EbsEncryptionByDefault is enabled correctly from the previous step. +", + "inputs": { + "Api": "GetEbsEncryptionByDefault", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.EbsEncryptionByDefault", + "Service": "ec2", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyEbsEncryptionByDefault", + "timeoutSeconds": 600, + }, ], + "outputs": [ + "ModifyAccount.EnableEbsEncryptionByDefaultResponse", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + }, + "schemaVersion": "0.3", }, - }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", - }, - "SHARREnableKeyRotation": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - AWSConfigRemediation-EnableKeyRotation - - ## What does this document do? - This document enables automatic key rotation for the given AWS Key Management Service (KMS) symmetric customer master key(CMK) using [EnableKeyRotation](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html) API. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * KeyId: (Required) The Key ID of the AWS KMS symmetric CMK. - - ## Output Parameters - * EnableKeyRotation.EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. - -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+=,.@-]+$' - KeyId: - type: String - description: (Required) The Key ID of the AWS KMS symmetric CMK. - allowedPattern: \\"[a-z0-9-]{1,2048}\\" - -outputs: - - EnableKeyRotation.EnableKeyRotationResponse -mainSteps: - - - name: EnableKeyRotation - action: aws:executeAwsApi - description: | - ## EnableKeyRotation - Enables automatic key rotation for the given AWS KMS CMK. - ## Outputs - * EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: kms - Api: EnableKeyRotation - KeyId: \\"{{ KeyId }}\\" - outputs: - - Name: EnableKeyRotationResponse - Selector: $ - Type: StringMap - - - name: VerifyKeyRotation - action: \\"aws:assertAwsResourceProperty\\" - timeoutSeconds: 600 - isEnd: true - description: | - ## VerifyKeyRotation - Verifies that the KeyRotationEnabled is set to true for the given AWS KMS CMK. - inputs: - Service: kms - Api: GetKeyRotationStatus - KeyId: \\"{{ KeyId }}\\" - PropertySelector: $.KeyRotationEnabled - DesiredValues: - - \\"True\\" -", "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-EnableKeyRotation", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], - ], - }, + "Name": "ASR-EnableEbsEncryptionByDefault", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableMinorVersionUpgradeOnRDSDBInstance": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": " -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\\" + "ASREnableEnhancedMonitoringOnRDSInstance": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-EnableEnhancedMonitoringOnRDSInstance + +## What does this document do? +This document is used to enable enhanced monitoring on an RDS Instance using the input parameter DB Instance resourceId. + +## Input Parameters +* ResourceId: (Required) Resource ID of the RDS DB Instance. +* MonitoringInterval: (Optional) + * The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. + * If MonitoringRoleArn is specified, then you must also set MonitoringInterval to a value other than 0. + * Valid Values: 1, 5, 10, 15, 30, 60 + * Default: 60 +* MonitoringRoleArn: (Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* EnableEnhancedMonitoring.DbInstance - The standard HTTP response from the ModifyDBInstance API. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableMinorVersionUpgradeOnRDSDBInstance", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## DescribeDBInstances + Makes describeDBInstances API call using RDS Instance DbiResourceId to get DBInstanceId. +## Outputs +* DbInstanceIdentifier: DBInstance Identifier of the RDS Instance. +", + "inputs": { + "Api": "DescribeDBInstances", + "Filters": [ + { + "Name": "dbi-resource-id", + "Values": [ + "{{ ResourceId }}", + ], + }, + ], + "Service": "rds", + }, + "isEnd": false, + "name": "DescribeDBInstances", + "outputs": [ + { + "Name": "DbInstanceIdentifier", + "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceStatus +Verifies if DB Instance status is available before enabling enhanced monitoring. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "VerifyDBInstanceStatus", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeAwsApi", + "description": "## EnableEnhancedMonitoring + Makes ModifyDBInstance API call to enable Enhanced Monitoring on the RDS Instance + using the DBInstanceId from the previous action. +## Outputs + * DbInstance: The standard HTTP response from the ModifyDBInstance API. +", + "inputs": { + "Api": "ModifyDBInstance", + "ApplyImmediately": false, + "DBInstanceIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "MonitoringInterval": "{{ MonitoringInterval }}", + "MonitoringRoleArn": "{{ MonitoringRoleArn }}", + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "EnableEnhancedMonitoring", + "outputs": [ + { + "Name": "DbInstance", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:executeScript", + "description": "## VerifyEnhancedMonitoringEnabled +Checks that the enhanced monitoring is enabled on RDS Instance in the previous step exists. +## Outputs +* Output: The standard HTTP response from the ModifyDBInstance API. +", + "inputs": { + "Handler": "handler", + "InputPayload": { + "DBIdentifier": "{{ DescribeDBInstances.DbInstanceIdentifier }}", + "MonitoringInterval": "{{ MonitoringInterval }}", + }, + "Runtime": "python3.8", + "Script": "import boto3 +import time + +def handler(event, context): + rds_client = boto3.client("rds") + db_instance_id = event["DBIdentifier"] + monitoring_interval = event["MonitoringInterval"] + + try: + rds_waiter = rds_client.get_waiter("db_instance_available") + rds_waiter.wait(DBInstanceIdentifier=db_instance_id) + + db_instances = rds_client.describe_db_instances( + DBInstanceIdentifier=db_instance_id) + + for db_instance in db_instances.get("DBInstances", [{}]): + db_monitoring_interval = db_instance.get("MonitoringInterval") + + if db_monitoring_interval == monitoring_interval: + return { + "output": db_instances["ResponseMetadata"] + } + else: + info = "VERIFICATION FAILED. RDS INSTANCE MONITORING INTERVAL {} IS NOT ENABLED WITH THE REQUIRED VALUE {}".format( + db_monitoring_interval, monitoring_interval) + raise Exception(info) + except Exception as e: + raise e", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyEnhancedMonitoringEnabled", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "EnableEnhancedMonitoring.DbInstance", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "MonitoringInterval": { + "allowedValues": [ + 1, + 5, + 10, + 15, + 30, + 60, + ], + "default": 60, + "description": "(Optional) The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance.", + "type": "Integer", + }, + "MonitoringRoleArn": { + "allowedPattern": "^arn:(aws[a-zA-Z-]*)?:iam::\\d{12}:role/[a-zA-Z0-9+=,.@_/-]+$", + "description": "(Required) The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs.", + "type": "String", + }, + "ResourceId": { + "allowedPattern": "db-[A-Z0-9]{26}", + "description": "(Required) Resource ID of the Amazon RDS instance for which Enhanced Monitoring needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, - }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", - }, - "SHARREnableMultiAZOnRDSInstance": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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\\" - -", "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-EnableMultiAZOnRDSInstance", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], - ], - }, + "Name": "ASR-EnableEnhancedMonitoringOnRDSInstance", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableRDSClusterDeletionProtection": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - AWSConfigRemediation-EnableRDSClusterDeletionProtection - - ## What does this document do? - This document enables \`Deletion Protection\` on a given Amazon RDS cluster using the [ModifyDBCluster](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBCluster.html) API. - Please note, AWS Config is required to be enabled in this region for this document to work as it requires the resource ID recorded by the AWS Config service. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * ClusterId: (Required) Resource ID of the Amazon RDS cluster. - - ## Output Parameters - * EnableRDSClusterDeletionProtection.ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. - -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+=,.@-]+$' - ClusterId: - type: String - description: (Required) Amazon RDS cluster resourceId for which deletion protection needs to be enabled. - allowedPattern: ^[a-zA-Z0-9-]{1,35}$ - -outputs: - - EnableRDSClusterDeletionProtection.ModifyDBClusterResponse -mainSteps: - - - name: GetRDSClusterIdentifer - action: \\"aws:executeAwsApi\\" - description: | - ## GetRDSClusterIdentifer - Accepts the resource ID of the Amazon RDS Cluster as input and returns the cluster name. - ## Outputs - * DbClusterIdentifier: The ID of the DB cluster for which the input parameter matches DbClusterResourceId element from the output of the DescribeDBClusters API call. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: config - Api: GetResourceConfigHistory - resourceId: \\"{{ ClusterId }}\\" - resourceType: \\"AWS::RDS::DBCluster\\" - limit: 1 - outputs: - - Name: DbClusterIdentifier - Selector: $.configurationItems[0].resourceName - Type: String - - - name: VerifyDBClusterStatus - action: \\"aws:assertAwsResourceProperty\\" - timeoutSeconds: 600 - isEnd: false - description: | - ## VerifyDBClusterStatus - Verifies if the DB Cluster status is available before enabling cluster deletion protection. - inputs: - Service: rds - Api: DescribeDBClusters - DBClusterIdentifier: \\"{{ GetRDSClusterIdentifer.DbClusterIdentifier }}\\" - PropertySelector: \\"$.DBClusters[0].Status\\" - DesiredValues: - - \\"available\\" - - - name: EnableRDSClusterDeletionProtection - action: \\"aws:executeAwsApi\\" - description: | - ## EnableRDSClusterDeletionProtection - Enables deletion protection on the Amazon RDS Cluster. - ## Outputs - * ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. - timeoutSeconds: 600 - isEnd: false - inputs: - Service: rds - Api: ModifyDBCluster - DBClusterIdentifier: \\"{{ GetRDSClusterIdentifer.DbClusterIdentifier }}\\" - DeletionProtection: True - outputs: - - Name: ModifyDBClusterResponse - Selector: $ - Type: StringMap - - - name: VerifyDBClusterModification - action: \\"aws:assertAwsResourceProperty\\" - description: | - ## VerifyDBClusterModification - Verifies that deletion protection has been enabled for the given Amazon RDS database cluster. - timeoutSeconds: 600 - isEnd: true - inputs: - Service: rds - Api: DescribeDBClusters - DBClusterIdentifier: \\"{{ GetRDSClusterIdentifer.DbClusterIdentifier }}\\" - PropertySelector: \\"$.DBClusters[0].DeletionProtection\\" - DesiredValues: - - \\"True\\" + "ASREnableKeyRotation": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableKeyRotation + +## What does this document do? +This document enables automatic key rotation for the given AWS Key Management Service (KMS) symmetric customer master key(CMK) using [EnableKeyRotation](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html) API. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* KeyId: (Required) The Key ID of the AWS KMS symmetric CMK. +## Output Parameters +* EnableKeyRotation.EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableRDSClusterDeletionProtection", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## EnableKeyRotation +Enables automatic key rotation for the given AWS KMS CMK. +## Outputs +* EnableKeyRotationResponse: The standard HTTP response from the EnableKeyRotation API. +", + "inputs": { + "Api": "EnableKeyRotation", + "KeyId": "{{ KeyId }}", + "Service": "kms", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "EnableKeyRotation", + "outputs": [ + { + "Name": "EnableKeyRotationResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyKeyRotation +Verifies that the KeyRotationEnabled is set to true for the given AWS KMS CMK. +", + "inputs": { + "Api": "GetKeyRotationStatus", + "DesiredValues": [ + "True", + ], + "KeyId": "{{ KeyId }}", + "PropertySelector": "$.KeyRotationEnabled", + "Service": "kms", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyKeyRotation", + "timeoutSeconds": 600, + }, ], + "outputs": [ + "EnableKeyRotation.EnableKeyRotationResponse", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "KeyId": { + "allowedPattern": "[a-z0-9-]{1,2048}", + "description": "(Required) The Key ID of the AWS KMS symmetric CMK.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableKeyRotation", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableRDSInstanceDeletionProtection": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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\\" + "ASREnableMinorVersionUpgradeOnRDSDBInstance": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableRDSInstanceDeletionProtection", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "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. +", + "inputs": { + "Api": "DescribeDBInstances", + "Filters": [ + { + "Name": "dbi-resource-id", + "Values": [ + "{{ DbiResourceId }}", + ], + }, + ], + "Service": "rds", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "GetRDSInstanceIdentifier", + "outputs": [ + { + "Name": "DBInstanceIdentifier", + "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceStatus +Verifies whether AWS RDS DBInstance status is available before enabling AutoMiniorVersionUpgrade. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBInstances[0].DBInstanceStatus", + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "VerifyDBInstanceStatus", + "timeoutSeconds": 600, + }, + { + "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. +", + "inputs": { + "Api": "ModifyDBInstance", + "AutoMinorVersionUpgrade": true, + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": false, + "name": "ModifyDBInstance", + "outputs": [ + { + "Name": "Output", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceState +Verifies the Amazon RDS Instance's "AutoMinorVersionUpgrade" property is set to "True". +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DBInstanceIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.DBInstances[0].AutoMinorVersionUpgrade", + "Service": "rds", + }, + "isEnd": true, + "name": "VerifyDBInstanceState", + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "ModifyDBInstance.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "DbiResourceId": { + "allowedPattern": "^db-[A-Z0-9]{26}$", + "description": "(Required) Resource ID of the Amazon RDS instance for which AutoMinorVersionUpgrade needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableMinorVersionUpgradeOnRDSDBInstance", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREnableRedshiftClusterAuditLogging": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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}(?- - {{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}} - description: The ARN of the KMS key created by SHARR for remediations requiring encryption - 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 - -mainSteps: - - - name: Remediation - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - vpc: '{{VPC}}' - remediation_role: '{{RemediationRole}}' - kms_key_arn: '{{KMSKeyArn}}' - Runtime: python3.8 - Handler: enable_flow_logs - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import boto3 - import time - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_logs(boto_config): - return boto3.client('logs', config=boto_config) - - def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - - def log_group_exists(client, group): - try: - log_group_verification = client.describe_log_groups( - logGroupNamePrefix=group - )['logGroups'] - if len(log_group_verification) >= 1: - for existing_loggroup in log_group_verification: - if existing_loggroup['logGroupName'] == group: - return 1 - return 0 - - except Exception as e: - exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') - - def wait_for_loggroup(client, wait_interval, max_retries, loggroup): - attempts = 1 - while not log_group_exists(client, loggroup): - time.sleep(wait_interval) - attempts += 1 - if attempts > max_retries: - exit(f'Timeout waiting for log group {loggroup} to become active') - - def flowlogs_active(client, loggroup): - # searches for flow log status, filtered on unique CW Log Group created earlier - try: - flow_status = client.describe_flow_logs( - DryRun=False, - Filters=[ - { - 'Name': 'log-group-name', - 'Values': [loggroup] - }, - ] - )['FlowLogs'] - if len(flow_status) == 1 and flow_status[0]['FlowLogStatus'] == 'ACTIVE': - return 1 - else: - return 0 - - except Exception as e: - exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') - - def wait_for_flowlogs(client, wait_interval, max_retries, loggroup): - attempts = 1 - while not flowlogs_active(client, loggroup): - time.sleep(wait_interval) - attempts += 1 - if attempts > max_retries: - exit(f'Timeout waiting for flowlogs to log group {loggroup} to become active') - - def enable_flow_logs(event, context): - \\"\\"\\" - remediates CloudTrail.2 by enabling SSE-KMS - On success returns a string map - On failure returns NoneType - \\"\\"\\" - max_retries = event.get('retries', 12) # max number of waits for actions to complete. - wait_interval = event.get('wait', 5) # how many seconds between attempts - - boto_config_args = { - 'retries': { - 'mode': 'standard' - } - } - - boto_config = Config(**boto_config_args) - - if 'vpc' not in event or 'remediation_role' not in event or 'kms_key_arn' not in event: - exit('Error: missing vpc from input') - - logs_client = connect_to_logs(boto_config) - ec2_client = connect_to_ec2(boto_config) - - kms_key_arn = event['kms_key_arn'] # for logs encryption at rest - - # set dynamic variable for CW Log Group for VPC Flow Logs - vpc_flow_loggroup = \\"VPCFlowLogs/\\" + event['vpc'] - # create cloudwatch log group - try: - logs_client.create_log_group( - logGroupName=vpc_flow_loggroup, - kmsKeyId=kms_key_arn - ) - except ClientError as client_error: - exception_type = client_error.response['Error']['Code'] - - if exception_type in [\\"ResourceAlreadyExistsException\\"]: - print(f'CloudWatch Logs group {vpc_flow_loggroup} already exists') - else: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') - - except Exception as e: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(e)}') - - # wait for CWL creation to propagate - wait_for_loggroup(logs_client, wait_interval, max_retries, vpc_flow_loggroup) - - # create VPC Flow Logging - try: - ec2_client.create_flow_logs( - DryRun=False, - DeliverLogsPermissionArn=event['remediation_role'], - LogGroupName=vpc_flow_loggroup, - ResourceIds=[event['vpc']], - ResourceType='VPC', - TrafficType='REJECT', - LogDestinationType='cloud-watch-logs' - ) - except ClientError as client_error: - exception_type = client_error.response['Error']['Code'] - - if exception_type in [\\"FlowLogAlreadyExists\\"]: - return { - \\"response\\": { - \\"message\\": f'VPC Flow Logs for {event[\\"vpc\\"]} already enabled', - \\"status\\": \\"Success\\" - } - } - else: - exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') - except Exception as e: - exit(f'create_flow_logs failed {str(e)}') - - # wait for Flow Log creation to propagate. Exits on timeout (no need to check results) - wait_for_flowlogs(ec2_client, wait_interval, max_retries, vpc_flow_loggroup) - - # wait_for_flowlogs will exit if unsuccessful after max_retries * wait_interval (60 seconds by default) - return { - \\"response\\": { - \\"message\\": f'VPC Flow Logs enabled for {event[\\"vpc\\"]} to {vpc_flow_loggroup}', - \\"status\\": \\"Success\\" - } - } - - - isEnd: true - + "isEnd": false, + "name": "VerifyDBInstanceStatus", + "timeoutSeconds": 600, + }, + { + "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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EnableVPCFlowLogs", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "inputs": { + "Choices": [ + { + "BooleanEquals": false, + "NextStep": "EnableMultiAZ", + "Variable": "{{ DescribeDBInstances.MultiAZ }}", + }, + ], }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": true, + "name": "EndIfMultiAZAlreadyEnabled", + }, + { + "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. +", + "inputs": { + "Api": "ModifyDBInstance", + "ApplyImmediately": "{{ ApplyImmediately }}", + "DBInstanceIdentifier": "{{ DescribeDBInstances.DBInstanceIdentifier }}", + "MultiAZ": true, + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "EnableMultiAZ", + "outputs": [ + { + "Name": "DBInstance", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyMultiAZEnabled +Verifies that the RDS Instance's \`PendingModifiedValues.MultiAZ\` value is \`True\`. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ DescribeDBInstances.DBInstanceIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.DBInstances[0].PendingModifiedValues.MultiAZ", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyMultiAZEnabled", + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "EnableMultiAZ.DBInstance", ], + "parameters": { + "ApplyImmediately": { + "allowedValues": [ + true, + false, + ], + "default": false, + "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).", + "type": "Boolean", + }, + "AutomationAssumeRole": { + "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", + }, + "DbiResourceId": { + "allowedPattern": "^db-[A-Z0-9]{26}$", + "description": "(Required) Resource ID of the RDS instance for which MultiAZ needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableMultiAZOnRDSInstance", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARREncryptRDSSnapshot": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "# 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 - + "ASREnableRDSClusterDeletionProtection": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-EnableRDSClusterDeletionProtection + +## What does this document do? +This document enables \`Deletion Protection\` on a given Amazon RDS cluster using the [ModifyDBCluster](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyDBCluster.html) API. +Please note, AWS Config is required to be enabled in this region for this document to work as it requires the resource ID recorded by the AWS Config service. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* ClusterId: (Required) Resource ID of the Amazon RDS cluster. + +## Output Parameters +* EnableRDSClusterDeletionProtection.ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-EncryptRDSSnapshot", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## GetRDSClusterIdentifer +Accepts the resource ID of the Amazon RDS Cluster as input and returns the cluster name. +## Outputs +* DbClusterIdentifier: The ID of the DB cluster for which the input parameter matches DbClusterResourceId element from the output of the DescribeDBClusters API call. +", + "inputs": { + "Api": "GetResourceConfigHistory", + "Service": "config", + "limit": 1, + "resourceId": "{{ ClusterId }}", + "resourceType": "AWS::RDS::DBCluster", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "GetRDSClusterIdentifer", + "outputs": [ + { + "Name": "DbClusterIdentifier", + "Selector": "$.configurationItems[0].resourceName", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBClusterStatus +Verifies if the DB Cluster status is available before enabling cluster deletion protection. +", + "inputs": { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DesiredValues": [ + "available", + ], + "PropertySelector": "$.DBClusters[0].Status", + "Service": "rds", + }, + "isEnd": false, + "name": "VerifyDBClusterStatus", + "timeoutSeconds": 600, + }, + { + "action": "aws:executeAwsApi", + "description": "## EnableRDSClusterDeletionProtection +Enables deletion protection on the Amazon RDS Cluster. +## Outputs +* ModifyDBClusterResponse: The standard HTTP response from the ModifyDBCluster API. +", + "inputs": { + "Api": "ModifyDBCluster", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DeletionProtection": true, + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "EnableRDSClusterDeletionProtection", + "outputs": [ + { + "Name": "ModifyDBClusterResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBClusterModification +Verifies that deletion protection has been enabled for the given Amazon RDS database cluster. +", + "inputs": { + "Api": "DescribeDBClusters", + "DBClusterIdentifier": "{{ GetRDSClusterIdentifer.DbClusterIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.DBClusters[0].DeletionProtection", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyDBClusterModification", + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "EnableRDSClusterDeletionProtection.ModifyDBClusterResponse", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "ClusterId": { + "allowedPattern": "^[a-zA-Z0-9-]{1,35}$", + "description": "(Required) Amazon RDS cluster resourceId for which deletion protection needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EnableRDSClusterDeletionProtection", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRMakeEBSSnapshotsPrivate": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - SHARR-MakeEBSSnapshotPrivate - - ## What does this document do? - This runbook works an the account level to remove public share on all EBS snapshots - - ## Input Parameters - * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. - - ## Output Parameters - - * Remediation.Output - stdout messages from the remediation - - ## Security Standards / Controls - * AFSBP v1.0.0: EC2.1 - * CIS v1.2.0: n/a - * PCI: EC2.1 - -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - AccountId: - type: String - description: Account ID of the account for which snapshots are to be checked. - 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+=,.@-]+$' - TestMode: - type: Boolean - description: Enables test mode, which generates a list of fake volume Ids - default: false - -outputs: - - Remediation.Output -mainSteps: - - name: GetPublicSnapshotIds - action: 'aws:executeScript' - outputs: - - Name: Snapshots - Selector: $.Payload - Type: StringList - inputs: - InputPayload: - region: '{{global:REGION}}' - account_id: '{{AccountId}}' - testmode: '{{TestMode}}' - Runtime: python3.8 - Handler: get_public_snapshots - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - - def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - - def get_public_snapshots(event, context): - account_id = event['account_id'] - - if 'testmode' in event and event['testmode']: - return [ - \\"snap-12341234123412345\\", - \\"snap-12341234123412345\\", - \\"snap-12341234123412345\\", - \\"snap-12341234123412345\\", - \\"snap-12341234123412345\\" - ] - - return list_public_snapshots(account_id) - - def list_public_snapshots(account_id): - ec2 = connect_to_ec2(boto_config) - control_token = 'start' - try: - - public_snapshot_ids = [] - - while control_token: - - if control_token == 'start': # needed a value to start the loop. Now reset it - control_token = '' - - kwargs = { - 'MaxResults': 100, - 'OwnerIds': [ account_id ], - 'RestorableByUserIds': [ 'all' ] - } - if control_token: - kwargs['NextToken'] = control_token - - response = ec2.describe_snapshots( - **kwargs - ) - - for snapshot in response['Snapshots']: - public_snapshot_ids.append(snapshot['SnapshotId']) - - if 'NextToken' in response: - control_token = response['NextToken'] - else: - control_token = '' - - return public_snapshot_ids - - except Exception as e: - print(e) - exit('Failed to describe_snapshots') - - - - name: Remediation - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - region: '{{global:REGION}}' - snapshots: '{{GetPublicSnapshotIds.Snapshots}}' - Runtime: python3.8 - Handler: make_snapshots_private - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_ec2(boto_config): - return boto3.client('ec2', config=boto_config) - - def make_snapshots_private(event, context): - boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - ec2 = connect_to_ec2(boto_config) - - remediated = [] - snapshots = event['snapshots'] - - success_count = 0 - - for snapshot_id in snapshots: - try: - ec2.modify_snapshot_attribute( - Attribute='CreateVolumePermission', - CreateVolumePermission={ - 'Remove': [{'Group': 'all'}] - }, - SnapshotId=snapshot_id - ) - print(f'Snapshot {snapshot_id} permissions set to private') - - remediated.append(snapshot_id) - success_count += 1 - except Exception as e: - print(e) - print(f'FAILED to remediate Snapshot {snapshot_id}') - - result=json.dumps(ec2.describe_snapshots( - SnapshotIds=remediated - ), indent=2, default=str) - print(result) - - return { - \\"response\\": { - \\"message\\": f'{success_count} of {len(snapshots)} Snapshot permissions set to private', - \\"status\\": \\"Success\\" - } - } - + "ASREnableRDSInstanceDeletionProtection": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-MakeEBSSnapshotsPrivate", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "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. +", + "inputs": { + "Api": "DescribeDBInstances", + "Filters": [ + { + "Name": "dbi-resource-id", + "Values": [ + "{{ DbInstanceResourceId }}", + ], + }, + ], + "Service": "rds", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": false, + "name": "GetRDSInstanceIdentifier", + "outputs": [ + { + "Name": "DbInstanceIdentifier", + "Selector": "$.DBInstances[0].DBInstanceIdentifier", + "Type": "String", + }, + ], + "timeoutSeconds": 600, + }, + { + "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. +", + "inputs": { + "Api": "ModifyDBInstance", + "ApplyImmediately": "{{ ApplyImmediately }}", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DeletionProtection": true, + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": false, + "name": "EnableRDSInstanceDeletionProtection", + "outputs": [ + { + "Name": "ModifyDBInstanceResponse", + "Selector": "$", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## VerifyDBInstanceModification +Checks whether deletion protection is enabled on Amazon RDS Instance. +", + "inputs": { + "Api": "DescribeDBInstances", + "DBInstanceIdentifier": "{{ GetRDSInstanceIdentifier.DbInstanceIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.DBInstances[0].DeletionProtection", + "Service": "rds", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "VerifyDBInstanceModification", + "timeoutSeconds": 600, + }, ], - }, - }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", - }, - "SHARRMakeRDSSnapshotPrivate": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - SHARR-MakeRDSSnapshotPrivate - - ## What does this document do? - This runbook removes public access to an RDS Snapshot - - ## Input Parameters - * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. - * DBSnapshotId: identifier of the public snapshot - * DBSnapshotType: snapshot or cluster-snapshot - - ## Output Parameters - - * Remediation.Output - stdout messages from the remediation - - ## Security Standards / Controls - * AFSBP v1.0.0: RDS.1 - * CIS v1.2.0: n/a - * PCI: RDS.1 - -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - DBSnapshotId: - type: String - allowedPattern: ^[a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,}$ - DBSnapshotType: - type: String - allowedValues: - - cluster-snapshot - - snapshot - 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+=,.@-]+$' - -outputs: - - MakeRDSSnapshotPrivate.Output -mainSteps: - - name: MakeRDSSnapshotPrivate - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - DBSnapshotType: '{{DBSnapshotType}}' - DBSnapshotId: '{{DBSnapshotId}}' - Runtime: python3.8 - Handler: make_snapshot_private - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - def connect_to_rds(): - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - return boto3.client('rds', config=boto_config) - - def make_snapshot_private(event, context): - - rds_client = connect_to_rds() - snapshot_id = event['DBSnapshotId'] - snapshot_type = event['DBSnapshotType'] - try: - if (snapshot_type == 'snapshot'): - rds_client.modify_db_snapshot_attribute( - DBSnapshotIdentifier=snapshot_id, - AttributeName='restore', - ValuesToRemove=['all'] - ) - elif (snapshot_type == 'cluster-snapshot'): - rds_client.modify_db_cluster_snapshot_attribute( - DBClusterSnapshotIdentifier=snapshot_id, - AttributeName='restore', - ValuesToRemove=['all'] - ) - else: - exit(f'Unrecognized snapshot_type {snapshot_type}') - - print(f'Remediation completed: {snapshot_id} public access removed.') - return { - \\"response\\": { - \\"message\\": f'Snapshot {snapshot_id} permissions set to private', - \\"status\\": \\"Success\\" - } - } - except Exception as e: - exit(f'Remediation failed for {snapshot_id}: {str(e)}') - - -", + "outputs": [ + "EnableRDSInstanceDeletionProtection.ModifyDBInstanceResponse", + ], + "parameters": { + "ApplyImmediately": { + "default": false, + "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.", + "type": "Boolean", + }, + "AutomationAssumeRole": { + "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", + }, + "DbInstanceResourceId": { + "allowedPattern": "^db-[A-Z0-9]{26}$", + "description": "(Required) Resource ID of the Amazon RDS instance for which deletion protection needs to be enabled.", + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-MakeRDSSnapshotPrivate", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "Name": "ASR-EnableRDSInstanceDeletionProtection", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASREnableRedshiftClusterAuditLogging": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "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. +", + "mainSteps": [ + { + "action": "aws:branch", + "description": "## CheckS3KeyPrefix +Checks whether S3KeyPrefix provided in the input parameters. +", + "inputs": { + "Choices": [ + { + "NextStep": "EnableLoggingWithoutPrefix", + "StringEquals": "", + "Variable": "{{S3KeyPrefix}}", + }, + ], + "Default": "EnableLoggingWithPrefix", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isEnd": true, + "name": "CheckS3KeyPrefix", + }, + { + "action": "aws:executeAwsApi", + "description": "## EnableLoggingWithoutPrefix +Enables logging on the given Amazon Redshift cluster using the [EnableLogging](https://docs.aws.amazon.com/redshift/latest/APIReference/API_EnableLogging.html) API with given bucket name in input parameters. +## Outputs +* Response: Standard HTTP response of the EnableLogging API. +", + "inputs": { + "Api": "EnableLogging", + "BucketName": "{{BucketName}}", + "ClusterIdentifier": "{{ ClusterIdentifier }}", + "Service": "redshift", }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "EnableLoggingWithoutPrefix", + "nextStep": "AssertClusterLoggingEnabled", + "outputs": [ + { + "Name": "Response", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:executeAwsApi", + "description": "## EnableLoggingWithPrefix +Enables logging on the given Amazon Redshift cluster using the [EnableLogging](https://docs.aws.amazon.com/redshift/latest/APIReference/API_EnableLogging.html) API with given bucket name and s3 key prefix in input parameters. +## Outputs +* Response: Standard HTTP response of the EnableLogging API. +", + "inputs": { + "Api": "EnableLogging", + "BucketName": "{{BucketName}}", + "ClusterIdentifier": "{{ ClusterIdentifier }}", + "S3KeyPrefix": "{{S3KeyPrefix}}", + "Service": "redshift", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], - ], - }, - }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", - }, - "SHARRRemoveLambdaPublicAccess": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - SHARR-RemoveLambdaPublicAccess - - ## 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. - - ## 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 - - ## Security Standards / Controls - * AFSBP v1.0.0: Lambda.1 - * CIS v1.2.0: n/a - * PCI: Lambda.1 - -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - FunctionName: - type: String - allowedPattern: ^[a-zA-Z0-9\\\\-_]{1,64}$ - 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+=,.@-]+$' - -outputs: - - RemoveLambdaPublicAccess.Output -mainSteps: - - name: RemoveLambdaPublicAccess - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - FunctionName: '{{FunctionName}}' - Runtime: python3.8 - Handler: remove_lambda_public_access - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - - def connect_to_lambda(boto_config): - return boto3.client('lambda', config=boto_config) - - def print_policy_before(policy): - print('Resource Policy to be deleted:') - print(json.dumps(policy, indent=2, default=str)) - - def remove_resource_policy(functionname, sid, client): - try: - client.remove_permission( - FunctionName=functionname, - StatementId=sid - ) - print(f'SID {sid} removed from Lambda function {functionname}') - except Exception as e: - exit(f'FAILED: SID {sid} was NOT removed from Lambda function {functionname} - {str(e)}') - - def remove_public_statement(client, functionname, statement, principal_source): - for principal in list(principal_source): - if principal == \\"*\\" or (isinstance(principal, dict) and principal.get(\\"AWS\\",\\"\\") == \\"*\\"): - print_policy_before(statement) - remove_resource_policy(functionname, statement['Sid'], client) - break # there will only be one that matches - - def remove_lambda_public_access(event, context): - - client = connect_to_lambda(boto_config) - - functionname = event['FunctionName'] - try: - response = client.get_policy(FunctionName=functionname) - policy = response['Policy'] - policy_json = json.loads(policy) - statements = policy_json['Statement'] - - print('Scanning for public resource policies in ' + functionname) - - for statement in statements: - remove_public_statement(client, functionname, statement, list(statement['Principal'])) - - client.get_policy(FunctionName=functionname) - - verify(functionname) - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - if exception_type in ['ResourceNotFoundException']: - print(\\"Remediation completed. Resource policy is now empty.\\") - else: - exit(f'ERROR: Remediation failed for RemoveLambdaPublicAccess: {str(ex)}') - except Exception as e: - exit(f'ERROR: Remediation failed for RemoveLambdaPublicAccess: {str(e)}') - - def verify(function_name_to_check): - - client = connect_to_lambda(boto_config) - - try: - response = client.get_policy(FunctionName=function_name_to_check) - - print(\\"Remediation executed successfully. Policy after:\\") - print(json.dumps(response, indent=2, default=str)) - - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - if exception_type in ['ResourceNotFoundException']: - print(\\"Remediation completed. Resource policy is now empty.\\") - else: - exit(f'ERROR: {exception_type} on get_policy') - except Exception as e: - exit(f'Exception while retrieving lambda function policy: {str(e)}') - - + "name": "EnableLoggingWithPrefix", + "outputs": [ + { + "Name": "Response", + "Selector": "$", + "Type": "StringMap", + }, + ], + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## AssertClusterBucketPrefix +Verifies whether the value of the "S3KeyPrefix" parameter is used for logging for the given Amazon Redshift cluster. ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-RemoveLambdaPublicAccess", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "inputs": { + "Api": "DescribeLoggingStatus", + "ClusterIdentifier": "{{ ClusterIdentifier }}", + "DesiredValues": [ + "{{S3KeyPrefix}}/", + ], + "PropertySelector": "$.S3KeyPrefix", + "Service": "redshift", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "AssertClusterBucketPrefix", + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## AssertClusterLoggingEnabled +Verifies whether the "LoggingEnabled" property is set to "True" for the given Amazon Redshift cluster. +", + "inputs": { + "Api": "DescribeLoggingStatus", + "ClusterIdentifier": "{{ ClusterIdentifier }}", + "DesiredValues": [ + "True", + ], + "PropertySelector": "$.LoggingEnabled", + "Service": "redshift", }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "AssertClusterLoggingEnabled", + }, + { + "action": "aws:assertAwsResourceProperty", + "description": "## AssertClusterLoggingBucket +Checks whether the value of the "BucketName" parameter is used for the audit logging configuration of the given Amazon Redshift cluster. +", + "inputs": { + "Api": "DescribeLoggingStatus", + "ClusterIdentifier": "{{ ClusterIdentifier }}", + "DesiredValues": [ + "{{BucketName}}", + ], + "PropertySelector": "$.BucketName", + "Service": "redshift", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "AssertClusterLoggingBucket", + }, ], + "outputs": [ + "EnableLoggingWithoutPrefix.Response", + "EnableLoggingWithPrefix.Response", + ], + "parameters": { + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "The name of an existing Amazon S3 bucket where the log files are to be stored.", + "type": "String", + }, + "ClusterIdentifier": { + "allowedPattern": "^(?!.*--)[a-z][a-z0-9-]{0,62}(?= 1: + for existing_loggroup in log_group_verification: + if existing_loggroup['logGroupName'] == group: + return 1 + return 0 + + except Exception as e: + exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') + +def wait_for_loggroup(client, wait_interval, max_retries, loggroup): + attempts = 1 + while not log_group_exists(client, loggroup): + time.sleep(wait_interval) + attempts += 1 + if attempts > max_retries: + exit(f'Timeout waiting for log group {loggroup} to become active') + +def flowlogs_active(client, loggroup): + # searches for flow log status, filtered on unique CW Log Group created earlier + try: + flow_status = client.describe_flow_logs( + DryRun=False, + Filters=[ + { + 'Name': 'log-group-name', + 'Values': [loggroup] + }, + ] + )['FlowLogs'] + if len(flow_status) == 1 and flow_status[0]['FlowLogStatus'] == 'ACTIVE': + return 1 + else: + return 0 + + except Exception as e: + exit(f'EnableVPCFlowLogs failed - unhandled exception {str(e)}') + +def wait_for_flowlogs(client, wait_interval, max_retries, loggroup): + attempts = 1 + while not flowlogs_active(client, loggroup): + time.sleep(wait_interval) + attempts += 1 + if attempts > max_retries: + exit(f'Timeout waiting for flowlogs to log group {loggroup} to become active') + +def enable_flow_logs(event, context): + """ + remediates CloudTrail.2 by enabling SSE-KMS + On success returns a string map + On failure returns NoneType + """ + max_retries = event.get('retries', 12) # max number of waits for actions to complete. + wait_interval = event.get('wait', 5) # how many seconds between attempts + + boto_config_args = { + 'retries': { + 'mode': 'standard' + } + } + + boto_config = Config(**boto_config_args) + + if 'vpc' not in event or 'remediation_role' not in event or 'kms_key_arn' not in event: + exit('Error: missing vpc from input') + + logs_client = connect_to_logs(boto_config) + ec2_client = connect_to_ec2(boto_config) + + kms_key_arn = event['kms_key_arn'] # for logs encryption at rest + + # set dynamic variable for CW Log Group for VPC Flow Logs + vpc_flow_loggroup = "VPCFlowLogs/" + event['vpc'] + # create cloudwatch log group + try: + logs_client.create_log_group( + logGroupName=vpc_flow_loggroup, + kmsKeyId=kms_key_arn + ) + except ClientError as client_error: + exception_type = client_error.response['Error']['Code'] + if exception_type in ["ResourceAlreadyExistsException"]: + print(f'CloudWatch Logs group {vpc_flow_loggroup} already exists') + else: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') + + except Exception as e: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(e)}') + + # wait for CWL creation to propagate + wait_for_loggroup(logs_client, wait_interval, max_retries, vpc_flow_loggroup) + + # create VPC Flow Logging + try: + ec2_client.create_flow_logs( + DryRun=False, + DeliverLogsPermissionArn=event['remediation_role'], + LogGroupName=vpc_flow_loggroup, + ResourceIds=[event['vpc']], + ResourceType='VPC', + TrafficType='REJECT', + LogDestinationType='cloud-watch-logs' + ) + except ClientError as client_error: + exception_type = client_error.response['Error']['Code'] + + if exception_type in ["FlowLogAlreadyExists"]: return { - \\"output\\": \\"Security group closed successfully.\\" + "response": { + "message": f'VPC Flow Logs for {event["vpc"]} already enabled', + "status": "Success" + } } - outputs: - - Name: Output - Selector: $.Payload.output - Type: String - -", + else: + exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') + except Exception as e: + exit(f'create_flow_logs failed {str(e)}') + + # wait for Flow Log creation to propagate. Exits on timeout (no need to check results) + wait_for_flowlogs(ec2_client, wait_interval, max_retries, vpc_flow_loggroup) + + # wait_for_flowlogs will exit if unsuccessful after max_retries * wait_interval (60 seconds by default) + return { + "response": { + "message": f'VPC Flow Logs enabled for {event["vpc"]} to {vpc_flow_loggroup}', + "status": "Success" + } + }", + }, + "isEnd": true, + "name": "Remediation", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "Remediation.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "KMSKeyArn": { + "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 ASR for remediations requiring encryption", + "type": "String", + }, + "RemediationRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "The ARN of the role that will allow VPC Flow Logs to log to CloudWatch logs", + "type": "String", + }, + "VPC": { + "allowedPattern": "^vpc-[0-9a-f]{8,17}", + "description": "The VPC ID of the VPC", + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-RemoveVPCDefaultSecurityGroupRules", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "Name": "ASR-EnableVPCFlowLogs", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASREncryptRDSSnapshot": { + "Properties": { + "Content": { + "assumeRole": "{{AutomationAssumeRole}}", + "description": "### Document Name - ASR-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\` +", + "mainSteps": [ + { + "action": "aws:branch", + "inputs": { + "Choices": [ + { + "NextStep": "CopyRdsSnapshotToEncryptedRdsSnapshot", + "StringEquals": "snapshot", + "Variable": "{{DBSnapshotType}}", + }, + { + "NextStep": "CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot", + "Or": [ + { + "StringEquals": "cluster-snapshot", + "Variable": "{{DBSnapshotType}}", + }, + { + "StringEquals": "dbclustersnapshot", + "Variable": "{{DBSnapshotType}}", + }, + ], + }, + ], + }, + "name": "ChooseSnapshotOrClusterSnapshot", + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "CopyDBSnapshot", + "CopyTags": true, + "KmsKeyId": "{{KmsKeyId}}", + "Service": "rds", + "SourceDBSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", + "TargetDBSnapshotIdentifier": "{{TargetDBSnapshotIdentifier}}", + }, + "name": "CopyRdsSnapshotToEncryptedRdsSnapshot", + "outputs": [ + { + "Name": "EncryptedSnapshotId", + "Selector": "$.DBSnapshot.DBSnapshotIdentifier", + "Type": "String", + }, + ], + }, + { + "action": "aws:waitForAwsResourceProperty", + "inputs": { + "Api": "DescribeDBSnapshots", + "DesiredValues": [ + "available", + ], + "Filters": [ + { + "Name": "db-snapshot-id", + "Values": [ + "{{CopyRdsSnapshotToEncryptedRdsSnapshot.EncryptedSnapshotId}}", + ], + }, + ], + "PropertySelector": "$.DBSnapshots[0].Status", + "Service": "rds", }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "name": "VerifyRdsEncryptedSnapshot", + "timeoutSeconds": 14400, + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "DeleteDBSnapshot", + "DBSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", + "Service": "rds", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isEnd": true, + "name": "DeleteUnencryptedRdsSnapshot", + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "CopyDBClusterSnapshot", + "CopyTags": true, + "KmsKeyId": "{{KmsKeyId}}", + "Service": "rds", + "SourceDBClusterSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", + "TargetDBClusterSnapshotIdentifier": "{{TargetDBSnapshotIdentifier}}", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "name": "CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot", + "outputs": [ + { + "Name": "EncryptedClusterSnapshotId", + "Selector": "$.DBClusterSnapshot.DBClusterSnapshotIdentifier", + "Type": "String", + }, + ], + }, + { + "action": "aws:waitForAwsResourceProperty", + "inputs": { + "Api": "DescribeDBClusterSnapshots", + "DesiredValues": [ + "available", + ], + "Filters": [ + { + "Name": "db-cluster-snapshot-id", + "Values": [ + "{{CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot.EncryptedClusterSnapshotId}}", + ], + }, + ], + "PropertySelector": "$.DBClusterSnapshots[0].Status", + "Service": "rds", + }, + "name": "VerifyRdsEncryptedClusterSnapshot", + "timeoutSeconds": 14400, + }, + { + "action": "aws:executeAwsApi", + "inputs": { + "Api": "DeleteDBClusterSnapshot", + "DBSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", + "Service": "rds", + }, + "isEnd": true, + "name": "DeleteUnencryptedRdsClusterSnapshot", + }, ], + "outputs": [ + "CopyRdsSnapshotToEncryptedRdsSnapshot.EncryptedSnapshotId", + "CopyRdsClusterSnapshotToEncryptedRdsClusterSnapshot.EncryptedClusterSnapshotId", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "DBSnapshotType": { + "allowedValues": [ + "snapshot", + "cluster-snapshot", + "dbclustersnapshot", + ], + "type": "String", + }, + "KmsKeyId": { + "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": "alias/aws/rds", + "description": "(Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use to encrypt the snapshot.", + "type": "String", + }, + "SourceDBSnapshotIdentifier": { + "allowedPattern": "^(?:rds:)?(?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}$", + "description": "(Required) The name of the unencrypted RDS snapshot or cluster snapshot to copy.", + "type": "String", + }, + "TargetDBSnapshotIdentifier": { + "allowedPattern": "^(?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}$", + "description": "(Required) The name of the encrypted RDS snapshot or cluster snapshot to create.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-EncryptRDSSnapshot", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRReplaceCodeBuildClearTextCredentials": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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: |- - #!/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' + "ASRMakeEBSSnapshotsPrivate": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - ASR-MakeEBSSnapshotPrivate + +## What does this document do? +This runbook works an the account level to remove public share on all EBS snapshots + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: EC2.1 +* CIS v1.2.0: n/a +* PCI: EC2.1 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "get_public_snapshots", + "InputPayload": { + "account_id": "{{AccountId}}", + "region": "{{global:REGION}}", + "testmode": "{{TestMode}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + +def connect_to_ec2(boto_config): + return boto3.client('ec2', config=boto_config) + +def get_public_snapshots(event, context): + account_id = event['account_id'] + + if 'testmode' in event and event['testmode']: + return [ + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345", + "snap-12341234123412345" ] - - 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 - } - ] + + return list_public_snapshots(account_id) + +def list_public_snapshots(account_id): + ec2 = connect_to_ec2(boto_config) + control_token = 'start' + try: + + public_snapshot_ids = [] + + while control_token: + + if control_token == 'start': # needed a value to start the loop. Now reset it + control_token = '' + + kwargs = { + 'MaxResults': 100, + 'OwnerIds': [ account_id ], + 'RestorableByUserIds': [ 'all' ] } - 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 + if control_token: + kwargs['NextToken'] = control_token + + response = ec2.describe_snapshots( + **kwargs ) - 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 + for snapshot in response['Snapshots']: + public_snapshot_ids.append(snapshot['SnapshotId']) + + if 'NextToken' in response: + control_token = response['NextToken'] 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 - } - - 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 + control_token = '' -", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-ReplaceCodeBuildClearTextCredentials", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + return public_snapshot_ids + + except Exception as e: + print(e) + exit('Failed to describe_snapshots')", }, - ":", - Object { - "Ref": "AWS::AccountId", + "name": "GetPublicSnapshotIds", + "outputs": [ + { + "Name": "Snapshots", + "Selector": "$.Payload", + "Type": "StringList", + }, + ], + }, + { + "action": "aws:executeScript", + "inputs": { + "Handler": "make_snapshots_private", + "InputPayload": { + "region": "{{global:REGION}}", + "snapshots": "{{GetPublicSnapshotIds.Snapshots}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_ec2(boto_config): + return boto3.client('ec2', config=boto_config) + +def make_snapshots_private(event, context): + boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + ec2 = connect_to_ec2(boto_config) + + remediated = [] + snapshots = event['snapshots'] + + success_count = 0 + + for snapshot_id in snapshots: + try: + ec2.modify_snapshot_attribute( + Attribute='CreateVolumePermission', + CreateVolumePermission={ + 'Remove': [{'Group': 'all'}] + }, + SnapshotId=snapshot_id + ) + print(f'Snapshot {snapshot_id} permissions set to private') + + remediated.append(snapshot_id) + success_count += 1 + except Exception as e: + print(e) + print(f'FAILED to remediate Snapshot {snapshot_id}') + + result=json.dumps(ec2.describe_snapshots( + SnapshotIds=remediated + ), indent=2, default=str) + print(result) + + return { + "response": { + "message": f'{success_count} of {len(snapshots)} Snapshot permissions set to private', + "status": "Success" + } + }", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "name": "Remediation", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "Remediation.Output", ], + "parameters": { + "AccountId": { + "allowedPattern": "^[0-9]{12}$", + "description": "Account ID of the account for which snapshots are to be checked.", + "type": "String", + }, + "AutomationAssumeRole": { + "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", + }, + "TestMode": { + "default": false, + "description": "Enables test mode, which generates a list of fake volume Ids", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-MakeEBSSnapshotsPrivate", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRRevokeUnrotatedKeys": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document Name - SHARR-RevokeUnrotatedKeys - - ## What does this document do? - This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. It will disabled keys that have been used within the previous 90 days by have not been rotated by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html). Please note, this automation document requires AWS Config to be enabled. - - ## Input Parameters - * Finding: (Required) Security Hub finding details JSON - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * MaxCredentialUsageAge: (Optional) Maximum number of days a key is allowed to be unrotated before revoking it. DEFAULT: 90 - - ## Output Parameters - * RevokeUnrotatedKeys.Output - -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - AutomationAssumeRole: - type: String - description: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - allowedPattern: '^arn:(?:aws|aws-us-gov|aws-cn):iam::\\\\d{12}:role/[\\\\w+=,.@-]+$' - IAMResourceId: - type: String - description: (Required) IAM resource unique identifier. - allowedPattern: ^[\\\\w+=,.@_-]{1,128}$ - MaxCredentialUsageAge: - type: String - description: (Required) Maximum number of days within which a credential must be used. The default value is 90 days. - allowedPattern: ^[1-9][0-9]{0,3}|10000$ - default: \\"90\\" -outputs: - - RevokeUnrotatedKeys.Output -mainSteps: - - name: RevokeUnrotatedKeys - action: aws:executeScript - timeoutSeconds: 600 - isEnd: true - description: | - ## RevokeUnrotatedKeys - - This step deactivates IAM user access keys that have not been rotated in more than MaxCredentialUsageAge days - ## Outputs - * Output: Success message or failure Exception. - inputs: - Runtime: python3.8 - Handler: unrotated_key_handler - InputPayload: - IAMResourceId: \\"{{ IAMResourceId }}\\" - MaxCredentialUsageAge: \\"{{ MaxCredentialUsageAge }}\\" - 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. # - ############################################################################### - from datetime import datetime, timezone, timedelta - import boto3 - from botocore.config import Config - - boto_config = Config( - retries ={ - 'mode': 'standard' - } - ) - - responses = {} - responses[\\"DeactivateUnusedKeysResponse\\"] = [] - - def connect_to_iam(boto_config): - return boto3.client('iam', config=boto_config) - - def connect_to_config(boto_config): - return boto3.client('config', config=boto_config) - - def get_user_name(resource_id): - config_client = connect_to_config(boto_config) - list_discovered_resources_response = config_client.list_discovered_resources( - resourceType='AWS::IAM::User', - resourceIds=[resource_id] + "ASRMakeRDSSnapshotPrivate": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - ASR-MakeRDSSnapshotPrivate + +## What does this document do? +This runbook removes public access to an RDS Snapshot + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* DBSnapshotId: identifier of the public snapshot +* DBSnapshotType: snapshot or cluster-snapshot + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: RDS.1 +* CIS v1.2.0: n/a +* PCI: RDS.1 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "make_snapshot_private", + "InputPayload": { + "DBSnapshotId": "{{DBSnapshotId}}", + "DBSnapshotType": "{{DBSnapshotType}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +def connect_to_rds(): + boto_config = Config( + retries ={ + 'mode': 'standard' + } + ) + return boto3.client('rds', config=boto_config) + +def make_snapshot_private(event, context): + + rds_client = connect_to_rds() + snapshot_id = event['DBSnapshotId'] + snapshot_type = event['DBSnapshotType'] + try: + if (snapshot_type == 'snapshot'): + rds_client.modify_db_snapshot_attribute( + DBSnapshotIdentifier=snapshot_id, + AttributeName='restore', + ValuesToRemove=['all'] ) - resource_name = list_discovered_resources_response.get(\\"resourceIdentifiers\\")[0].get(\\"resourceName\\") - return resource_name - - def list_access_keys(user_name, include_inactive=False): - iam_client = connect_to_iam(boto_config) - active_keys = [] - keys = iam_client.list_access_keys(UserName=user_name).get(\\"AccessKeyMetadata\\", []) - for key in keys: - if include_inactive or key.get('Status') == 'Active': - active_keys.append(key) - return active_keys - - def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): - iam_client = connect_to_iam(boto_config) - for key in access_keys: - print(key) - last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get(\\"AccessKeyId\\")).get(\\"AccessKeyLastUsed\\") - deactivate = False - - 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 - - if deactivate: - deactivate_key(user_name, key.get(\\"AccessKeyId\\")) - - def deactivate_key(user_name, access_key): - iam_client = connect_to_iam(boto_config) - responses[\\"DeactivateUnusedKeysResponse\\"].append({\\"AccessKeyId\\": access_key, \\"Response\\": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status=\\"Inactive\\")}) - - def verify_expired_credentials_revoked(responses, user_name): - if responses.get(\\"DeactivateUnusedKeysResponse\\"): - for key in responses.get(\\"DeactivateUnusedKeysResponse\\"): - key_data = next(filter(lambda x: x.get(\\"AccessKeyId\\") == key.get(\\"AccessKeyId\\"), list_access_keys(user_name, True))) - if key_data.get(\\"Status\\") != \\"Inactive\\": - error_message = \\"VERIFICATION FAILED. ACCESS KEY {} NOT DEACTIVATED\\".format(key_data.get(\\"AccessKeyId\\")) - raise Exception(error_message) - - return { - \\"output\\": \\"Verification of unrotated access keys is successful.\\", - \\"http_responses\\": responses + elif (snapshot_type == 'cluster-snapshot'): + rds_client.modify_db_cluster_snapshot_attribute( + DBClusterSnapshotIdentifier=snapshot_id, + AttributeName='restore', + ValuesToRemove=['all'] + ) + else: + exit(f'Unrecognized snapshot_type {snapshot_type}') + + print(f'Remediation completed: {snapshot_id} public access removed.') + return { + "response": { + "message": f'Snapshot {snapshot_id} permissions set to private', + "status": "Success" } - - def unrotated_key_handler(event, context): - user_name = get_user_name(event.get(\\"IAMResourceId\\")) - max_credential_usage_age = int(event.get(\\"MaxCredentialUsageAge\\")) - access_keys = list_access_keys(user_name) - deactivate_unused_keys(access_keys, max_credential_usage_age, user_name) - return verify_expired_credentials_revoked(responses, user_name) - + } + except Exception as e: + exit(f'Remediation failed for {snapshot_id}: {str(e)}')", + }, + "name": "MakeRDSSnapshotPrivate", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "MakeRDSSnapshotPrivate.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "DBSnapshotId": { + "allowedPattern": "^[a-zA-Z](?:[0-9a-zA-Z]+[-]{1})*[0-9a-zA-Z]{1,}$", + "type": "String", + }, + "DBSnapshotType": { + "allowedValues": [ + "cluster-snapshot", + "snapshot", + ], + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-MakeRDSSnapshotPrivate", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASRRemoveLambdaPublicAccess": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - ASR-RemoveLambdaPublicAccess + +## 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. + +## 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 - outputs: - - Name: Output - Selector: $.Payload - Type: StringMap +* RemoveLambdaPublicAccess.Output - stdout messages from the remediation +## Security Standards / Controls +* AFSBP v1.0.0: Lambda.1 +* CIS v1.2.0: n/a +* PCI: Lambda.1 ", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "remove_lambda_public_access", + "InputPayload": { + "FunctionName": "{{FunctionName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } +) + +def connect_to_lambda(boto_config): + return boto3.client('lambda', config=boto_config) + +def print_policy_before(policy): + print('Resource Policy to be deleted:') + print(json.dumps(policy, indent=2, default=str)) + +def remove_resource_policy(functionname, sid, client): + try: + client.remove_permission( + FunctionName=functionname, + StatementId=sid + ) + print(f'SID {sid} removed from Lambda function {functionname}') + except Exception as e: + exit(f'FAILED: SID {sid} was NOT removed from Lambda function {functionname} - {str(e)}') + +def remove_public_statement(client, functionname, statement, principal_source): + for principal in list(principal_source): + if principal == "*" or (isinstance(principal, dict) and principal.get("AWS","") == "*"): + print_policy_before(statement) + remove_resource_policy(functionname, statement['Sid'], client) + break # there will only be one that matches + +def remove_lambda_public_access(event, context): + + client = connect_to_lambda(boto_config) + + functionname = event['FunctionName'] + try: + response = client.get_policy(FunctionName=functionname) + policy = response['Policy'] + policy_json = json.loads(policy) + statements = policy_json['Statement'] + + print('Scanning for public resource policies in ' + functionname) + + for statement in statements: + remove_public_statement(client, functionname, statement, list(statement['Principal'])) + + client.get_policy(FunctionName=functionname) + + verify(functionname) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + if exception_type in ['ResourceNotFoundException']: + print("Remediation completed. Resource policy is now empty.") + else: + exit(f'ERROR: Remediation failed for RemoveLambdaPublicAccess: {str(ex)}') + except Exception as e: + exit(f'ERROR: Remediation failed for RemoveLambdaPublicAccess: {str(e)}') + +def verify(function_name_to_check): + + client = connect_to_lambda(boto_config) + + try: + response = client.get_policy(FunctionName=function_name_to_check) + + print("Remediation executed successfully. Policy after:") + print(json.dumps(response, indent=2, default=str)) + + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + if exception_type in ['ResourceNotFoundException']: + print("Remediation completed. Resource policy is now empty.") + else: + exit(f'ERROR: {exception_type} on get_policy') + except Exception as e: + exit(f'Exception while retrieving lambda function policy: {str(e)}')", + }, + "name": "RemoveLambdaPublicAccess", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "RemoveLambdaPublicAccess.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "FunctionName": { + "allowedPattern": "^[a-zA-Z0-9\\-_]{1,64}$", + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-RevokeUnrotatedKeys", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "Name": "ASR-RemoveLambdaPublicAccess", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASRRemoveVPCDefaultSecurityGroupRules": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules + +## What does this document do? +This document removes all inbound and outbound rules from the default security group in an Amazon VPC. A default security group is defined as any security group whose name is \`default\`. If the security group ID passed to this automation document belongs to a non-default security group, this document does not perform any changes to the AWS account. + +## Input Parameters +* GroupId: (Required) The unique ID of the security group. +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. + +## Output Parameters +* RemoveRulesAndVerify.Output - Success message or failure exception. +", + "mainSteps": [ + { + "action": "aws:assertAwsResourceProperty", + "description": "## CheckDefaultSecurityGroup +Verifies that the security group name does match \`default\`. If the group name does match \`default\`, go to the next step: DescribeSecurityGroups. +", + "inputs": { + "Api": "DescribeSecurityGroups", + "DesiredValues": [ + "default", + ], + "GroupIds": [ + "{{ GroupId }}", + ], + "PropertySelector": "$.SecurityGroups[0].GroupName", + "Service": "ec2", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isCritical": true, + "maxAttempts": 3, + "name": "CheckDefaultSecurityGroup", + "nextStep": "RemoveRulesAndVerify", + "onFailure": "Abort", + "timeoutSeconds": 20, + }, + { + "action": "aws:executeScript", + "description": "## RemoveRulesAndVerify +Removes all rules from the default security group. +## Outputs +* Output: Success message or failure exception. +", + "inputs": { + "Handler": "handler", + "InputPayload": { + "GroupId": "{{ GroupId }}", + }, + "Runtime": "python3.8", + "Script": "import boto3 +from botocore.exceptions import ClientError +from time import sleep + + +ec2_client = boto3.client("ec2") + + +def get_permissions(group_id): + default_group = ec2_client.describe_security_groups(GroupIds=[group_id]).get("SecurityGroups")[0] + return default_group.get("IpPermissions"), default_group.get("IpPermissionsEgress") + + +def handler(event, context): + group_id = event.get("GroupId") + ingress_permissions, egress_permissions = get_permissions(group_id) + + if ingress_permissions: + ec2_client.revoke_security_group_ingress(GroupId=group_id, IpPermissions=ingress_permissions) + if egress_permissions: + ec2_client.revoke_security_group_egress(GroupId=group_id, IpPermissions=egress_permissions) + + ingress_permissions, egress_permissions = get_permissions(group_id) + if ingress_permissions or egress_permissions: + raise Exception(f"VERIFICATION FAILED. SECURITY GROUP {group_id} NOT CLOSED.") + + return { + "output": "Security group closed successfully." + }", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isCritical": true, + "isEnd": true, + "maxAttempts": 3, + "name": "RemoveRulesAndVerify", + "onFailure": "Abort", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "String", + }, + ], + "timeoutSeconds": 180, + }, ], + "outputs": [ + "RemoveRulesAndVerify.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "GroupId": { + "allowedPattern": "sg-[a-z0-9]+$", + "description": "(Required) The unique ID of the security group.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-RemoveVPCDefaultSecurityGroupRules", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRRevokeUnusedIAMUserCredentials": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document Name - AWSConfigRemediation-RevokeUnusedIAMUserCredentials - - ## What does this document do? - This document revokes unused IAM passwords and active access keys. This document will deactivate expired access keys by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html) and delete expired login profiles by using the [DeleteLoginProfile API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteLoginProfile.html). Please note, this automation document requires AWS Config to be enabled. - - ## Input Parameters - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - * IAMResourceId: (Required) IAM resource unique identifier. - * MaxCredentialUsageAge: (Required) Maximum number of days within which a credential must be used. The default value is 90 days. - - ## Output Parameters - * RevokeUnusedIAMUserCredentialsAndVerify.Output - Success message or failure Exception. - -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+=,.@-]+$' - IAMResourceId: - type: String - description: (Required) IAM resource unique identifier. - allowedPattern: ^[\\\\w+=,.@_-]{1,128}$ - MaxCredentialUsageAge: - type: String - description: (Required) Maximum number of days within which a credential must be used. The default value is 90 days. - allowedPattern: ^(\\\\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\\\\b)$ - default: \\"90\\" -outputs: - - RevokeUnusedIAMUserCredentialsAndVerify.Output -mainSteps: - - name: RevokeUnusedIAMUserCredentialsAndVerify - action: aws:executeScript - timeoutSeconds: 600 - isEnd: true - description: | - ## RevokeUnusedIAMUserCredentialsAndVerify - This step deactivates expired IAM User access keys, deletes expired login profiles and verifies credentials were revoked - ## Outputs - * Output: Success message or failure Exception. - inputs: - Runtime: python3.8 - Handler: unused_iam_credentials_handler - InputPayload: - IAMResourceId: \\"{{ IAMResourceId }}\\" - MaxCredentialUsageAge: \\"{{ MaxCredentialUsageAge }}\\" - Script: |- - import boto3 - from datetime import datetime - from datetime import timedelta - - iam_client = boto3.client(\\"iam\\") - config_client = boto3.client(\\"config\\") - - responses = {} - responses[\\"DeactivateUnusedKeysResponse\\"] = [] - - def list_access_keys(user_name): - return iam_client.list_access_keys(UserName=user_name).get(\\"AccessKeyMetadata\\") - - def deactivate_key(user_name, access_key): - responses[\\"DeactivateUnusedKeysResponse\\"].append({\\"AccessKeyId\\": access_key, \\"Response\\": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status=\\"Inactive\\")}) - - def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): - for key in access_keys: - last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get(\\"AccessKeyId\\")).get(\\"AccessKeyLastUsed\\") - if last_used.get(\\"LastUsedDate\\"): - last_used_date = last_used.get(\\"LastUsedDate\\").replace(tzinfo=None) - last_used_days = (datetime.now() - last_used_date).days - if last_used_days >= max_credential_usage_age: - deactivate_key(user_name, key.get(\\"AccessKeyId\\")) - else: - create_date = key.get(\\"CreateDate\\").replace(tzinfo=None) - days_since_creation = (datetime.now() - create_date).days - if days_since_creation >= max_credential_usage_age: - deactivate_key(user_name, key.get(\\"AccessKeyId\\")) - - def get_login_profile(user_name): - try: - return iam_client.get_login_profile(UserName=user_name)[\\"LoginProfile\\"] - except iam_client.exceptions.NoSuchEntityException: - return False - - def delete_unused_password(user_name, max_credential_usage_age): - user = iam_client.get_user(UserName=user_name).get(\\"User\\") - 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 - 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\\"): - for key in responses.get(\\"DeactivateUnusedKeysResponse\\"): - key_data = next(filter(lambda x: x.get(\\"AccessKeyId\\") == key.get(\\"AccessKeyId\\"), list_access_keys(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) - if responses.get(\\"DeleteUnusedPasswordResponse\\"): - try: - iam_client.get_login_profile(UserName=user_name) - error_message = \\"VERIFICATION FAILED. IAM USER {} LOGIN PROFILE NOT DELETED\\".format(user_name) - raise Exception(error_message) - except iam_client.exceptions.NoSuchEntityException: - pass - return { - \\"output\\": \\"Verification of unused IAM User credentials is successful.\\", - \\"http_responses\\": responses - } - - def get_user_name(resource_id): - list_discovered_resources_response = config_client.list_discovered_resources( - resourceType='AWS::IAM::User', - resourceIds=[resource_id] - ) - resource_name = list_discovered_resources_response.get(\\"resourceIdentifiers\\")[0].get(\\"resourceName\\") - return resource_name - - def 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) - - delete_unused_password(user_name, max_credential_usage_age) - - return verify_expired_credentials_revoked(responses, user_name) - outputs: - - Name: Output - Selector: $.Payload - Type: StringMap + "ASRReplaceCodeBuildClearTextCredentials": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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 ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-RevokeUnusedIAMUserCredentials", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", + "mainSteps": [ + { + "action": "aws:executeAwsApi", + "description": "## BatchGetProjects +Gets information about one or more build projects. +", + "inputs": { + "Api": "BatchGetProjects", + "Service": "codebuild", + "names": [ + "{{ ProjectName }}", + ], }, - ":lambda:", - Object { - "Ref": "AWS::Region", + "isCritical": true, + "maxAttempts": 2, + "name": "BatchGetProjects", + "outputs": [ + { + "Name": "ProjectInfo", + "Selector": "$.projects[0]", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + { + "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). +", + "inputs": { + "Handler": "replace_credentials", + "InputPayload": { + "ProjectInfo": "{{ BatchGetProjects.ProjectInfo }}", + }, + "Runtime": "python3.8", + "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. # +############################################################################### +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 + }", }, - ":", - Object { - "Ref": "AWS::AccountId", + "isCritical": true, + "name": "CreateParameters", + "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", + }, + ], + "timeoutSeconds": 600, + }, + { + "action": "aws:executeAwsApi", + "description": "## UpdateProject +Changes the settings of a build project. +", + "inputs": { + "Api": "UpdateProject", + "Service": "codebuild", + "environment": "{{ CreateParameters.UpdatedProjectEnv }}", + "name": "{{ ProjectName }}", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isCritical": true, + "isEnd": true, + "maxAttempts": 2, + "name": "UpdateProject", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "CreateParameters.Parameters", + "CreateParameters.Policy", + "CreateParameters.AttachResponse", + "UpdateProject.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "ProjectName": { + "allowedPattern": "^[A-Za-z0-9][A-Za-z0-9\\-_]{1,254}$", + "description": "(Required) The project name (not the ARN).", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-ReplaceCodeBuildClearTextCredentials", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRS3BlockDenylist": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "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: |- - #!/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}') - - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap - + "ASRRevokeUnrotatedKeys": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-RevokeUnrotatedKeys + +## What does this document do? +This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. It will disabled keys that have been used within the previous 90 days by have not been rotated by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html). Please note, this automation document requires AWS Config to be enabled. + +## Input Parameters +* Finding: (Required) Security Hub finding details JSON +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* MaxCredentialUsageAge: (Optional) Maximum number of days a key is allowed to be unrotated before revoking it. DEFAULT: 90 + +## Output Parameters +* RevokeUnrotatedKeys.Output ", - "DocumentFormat": "YAML", - "DocumentType": "Automation", - "Name": "SHARR-S3BlockDenylist", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + "mainSteps": [ + { + "action": "aws:executeScript", + "description": "## RevokeUnrotatedKeys + +This step deactivates IAM user access keys that have not been rotated in more than MaxCredentialUsageAge days +## Outputs +* Output: Success message or failure Exception. +", + "inputs": { + "Handler": "unrotated_key_handler", + "InputPayload": { + "IAMResourceId": "{{ IAMResourceId }}", + "MaxCredentialUsageAge": "{{ MaxCredentialUsageAge }}", + }, + "Runtime": "python3.8", + "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. # +############################################################################### +from datetime import datetime, timezone, timedelta +import boto3 +from botocore.config import Config + +boto_config = Config( + retries ={ + 'mode': 'standard' + } +) + +responses = {} +responses["DeactivateUnusedKeysResponse"] = [] + +def connect_to_iam(boto_config): + return boto3.client('iam', config=boto_config) + +def connect_to_config(boto_config): + return boto3.client('config', config=boto_config) + +def get_user_name(resource_id): + config_client = connect_to_config(boto_config) + list_discovered_resources_response = config_client.list_discovered_resources( + resourceType='AWS::IAM::User', + resourceIds=[resource_id] + ) + resource_name = list_discovered_resources_response.get("resourceIdentifiers")[0].get("resourceName") + return resource_name + +def list_access_keys(user_name, include_inactive=False): + iam_client = connect_to_iam(boto_config) + active_keys = [] + keys = iam_client.list_access_keys(UserName=user_name).get("AccessKeyMetadata", []) + for key in keys: + if include_inactive or key.get('Status') == 'Active': + active_keys.append(key) + return active_keys + +def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): + iam_client = connect_to_iam(boto_config) + for key in access_keys: + print(key) + last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get("AccessKeyId")).get("AccessKeyLastUsed") + deactivate = False + + 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 + + if deactivate: + deactivate_key(user_name, key.get("AccessKeyId")) + +def deactivate_key(user_name, access_key): + iam_client = connect_to_iam(boto_config) + responses["DeactivateUnusedKeysResponse"].append({"AccessKeyId": access_key, "Response": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status="Inactive")}) + +def verify_expired_credentials_revoked(responses, user_name): + if responses.get("DeactivateUnusedKeysResponse"): + for key in responses.get("DeactivateUnusedKeysResponse"): + key_data = next(filter(lambda x: x.get("AccessKeyId") == key.get("AccessKeyId"), list_access_keys(user_name, True))) + if key_data.get("Status") != "Inactive": + error_message = "VERIFICATION FAILED. ACCESS KEY {} NOT DEACTIVATED".format(key_data.get("AccessKeyId")) + raise Exception(error_message) + + return { + "output": "Verification of unrotated access keys is successful.", + "http_responses": responses + } + +def unrotated_key_handler(event, context): + user_name = get_user_name(event.get("IAMResourceId")) + max_credential_usage_age = int(event.get("MaxCredentialUsageAge")) + access_keys = list_access_keys(user_name) + deactivate_unused_keys(access_keys, max_credential_usage_age, user_name) + return verify_expired_credentials_revoked(responses, user_name)", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "isEnd": true, + "name": "RevokeUnrotatedKeys", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "RevokeUnrotatedKeys.Output", ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "IAMResourceId": { + "allowedPattern": "^[\\w+=,.@_-]{1,128}$", + "description": "(Required) IAM resource unique identifier.", + "type": "String", + }, + "MaxCredentialUsageAge": { + "allowedPattern": "^[1-9][0-9]{0,3}|10000$", + "default": "90", + "description": "(Required) Maximum number of days within which a credential must be used. The default value is 90 days.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-RevokeUnrotatedKeys", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRSetIAMPasswordPolicy": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "description: | - ### Document name - AWSConfigRemediation-SetIAMPasswordPolicy - - ## What does this document do? - This document sets the AWS Identity and Access Management (IAM) user password policy for the AWS account using the [UpdateAccountPasswordPolicy](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccountPasswordPolicy.html) API. - - ## Input Parameters - * AllowUsersToChangePassword: (Optional) Allows all IAM users in your account to use the AWS Management Console to change their own passwords. - * HardExpiry: (Optional) Prevents IAM users from setting a new password after their password has expired. - * MaxPasswordAge: (Optional) The number of days that an IAM user password is valid. - * MinimumPasswordLength: (Optional) The minimum number of characters allowed in an IAM user password. - * PasswordReusePrevention: (Optional) Specifies the number of previous passwords that IAM users are prevented from reusing. - * RequireLowercaseCharacters: (Optional) Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z). - * RequireNumbers: (Optional) Specifies whether IAM user passwords must contain at least one numeric character (0 to 9). - * RequireSymbols: (Optional) pecifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters :! @ \\\\# $ % ^ * ( ) _ + - = [ ] { } | ' - * RequireUppercaseCharacters: (Optional) Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z). - * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. - ## Output Parameters - * UpdateAndVerifyIamUserPasswordPolicy.Output -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+=,.@-]+$' - 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. - default: false - MaxPasswordAge: - type: Integer - description: (Optional) The number of days that an IAM user password is valid. - allowedPattern: ^\\\\d{0,3}$|^10[0-8]\\\\d$|^109[0-5]$ - default: 0 - MinimumPasswordLength: - type: Integer - description: (Optional) The minimum number of characters allowed in an IAM user password. - allowedPattern: ^[6-9]$|^[1-9]\\\\d$|^1[01]\\\\d$|^12[0-8]$ - default: 6 - PasswordReusePrevention: - type: Integer - description: (Optional) Specifies the number of previous passwords that IAM users are prevented from reusing. - allowedPattern: ^\\\\d{0,1}$|^1\\\\d$|^2[0-4]$ - default: 0 - RequireLowercaseCharacters: - type: Boolean - description: (Optional) Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z). - default: false - RequireNumbers: - type: Boolean - description: (Optional) Specifies whether IAM user passwords must contain at least one numeric character (0 to 9). - default: false - RequireSymbols: - type: Boolean - description: (Optional) Specifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters :! @ \\\\# $ % ^ * ( ) _ + - = [ ] { } | '. - default: false - RequireUppercaseCharacters: - type: Boolean - description: (Optional) Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z). - default: false -outputs: - - UpdateAndVerifyIamUserPasswordPolicy.Output -mainSteps: - - name: UpdateAndVerifyIamUserPasswordPolicy - action: \\"aws:executeScript\\" - timeoutSeconds: 600 - isEnd: true - description: | - ## UpdateAndVerifyIamUserPasswordPolicy - Sets or updates the AWS account password policy using input parameters using UpdateAccountPasswordPolicy API. - Verify AWS account password policy using GetAccountPasswordPolicy API. - ## Outputs - * Output: Success message with HTTP Response from GetAccountPasswordPolicy API call or failure exception. - inputs: - Runtime: python3.8 - Handler: update_and_verify_iam_user_password_policy - InputPayload: - AllowUsersToChangePassword: \\"{{ AllowUsersToChangePassword }}\\" - HardExpiry: \\"{{ HardExpiry }}\\" - MaxPasswordAge: \\"{{ MaxPasswordAge }}\\" - MinimumPasswordLength: \\"{{ MinimumPasswordLength }}\\" - PasswordReusePrevention: \\"{{ PasswordReusePrevention }}\\" - RequireLowercaseCharacters: \\"{{ RequireLowercaseCharacters }}\\" - RequireNumbers: \\"{{ RequireNumbers }}\\" - RequireSymbols: \\"{{ RequireSymbols }}\\" - RequireUppercaseCharacters: \\"{{ RequireUppercaseCharacters }}\\" - Script: |- - import boto3 - - - def update_and_verify_iam_user_password_policy(event, context): - iam_client = boto3.client('iam') - - try: - params = dict() - params[\\"AllowUsersToChangePassword\\"] = event[\\"AllowUsersToChangePassword\\"] - if \\"HardExpiry\\" in event: - params[\\"HardExpiry\\"] = event[\\"HardExpiry\\"] - if event[\\"MaxPasswordAge\\"]: - params[\\"MaxPasswordAge\\"] = event[\\"MaxPasswordAge\\"] - if event[\\"PasswordReusePrevention\\"]: - params[\\"PasswordReusePrevention\\"] = event[\\"PasswordReusePrevention\\"] - params[\\"MinimumPasswordLength\\"] = event[\\"MinimumPasswordLength\\"] - params[\\"RequireLowercaseCharacters\\"] = event[\\"RequireLowercaseCharacters\\"] - params[\\"RequireNumbers\\"] = event[\\"RequireNumbers\\"] - params[\\"RequireSymbols\\"] = event[\\"RequireSymbols\\"] - params[\\"RequireUppercaseCharacters\\"] = event[\\"RequireUppercaseCharacters\\"] - - update_api_response = iam_client.update_account_password_policy(**params) - - # Verifies IAM Password Policy configuration for AWS account using GetAccountPasswordPolicy() api call. - response = iam_client.get_account_password_policy() - if all([response[\\"PasswordPolicy\\"][\\"AllowUsersToChangePassword\\"] == event[\\"AllowUsersToChangePassword\\"], - response[\\"PasswordPolicy\\"][\\"MinimumPasswordLength\\"] == event[\\"MinimumPasswordLength\\"], - response[\\"PasswordPolicy\\"][\\"RequireLowercaseCharacters\\"] == event[\\"RequireLowercaseCharacters\\"], - response[\\"PasswordPolicy\\"][\\"RequireNumbers\\"] == event[\\"RequireNumbers\\"], - response[\\"PasswordPolicy\\"][\\"RequireUppercaseCharacters\\"] == event[\\"RequireUppercaseCharacters\\"], - ((response[\\"PasswordPolicy\\"][\\"HardExpiry\\"] == event[\\"HardExpiry\\"]) if \\"HardExpiry\\" in event else True), - ((response[\\"PasswordPolicy\\"][\\"MaxPasswordAge\\"] == event[\\"MaxPasswordAge\\"]) if event[\\"MaxPasswordAge\\"] else True), - ((response[\\"PasswordPolicy\\"][\\"PasswordReusePrevention\\"] == event[\\"PasswordReusePrevention\\"]) if event[\\"PasswordReusePrevention\\"] else True)]): - return { - \\"output\\": { - \\"Message\\": \\"AWS Account Password Policy setting is SUCCESSFUL.\\", - \\"UpdatePolicyHTTPResponse\\": update_api_response, - \\"GetPolicyHTTPResponse\\": response - } - } - raise Exception(\\"VERIFICATION FAILED. AWS ACCOUNT PASSWORD POLICY NOT UPDATED.\\") - - except iam_client.exceptions.NoSuchEntityException: - raise Exception(\\"VERIFICATION FAILED. UNABLE TO UPDATE AWS ACCOUNT PASSWORD POLICY.\\") - - outputs: - - Name: Output - Selector: $.Payload.output - Type: StringMap + "ASRRevokeUnusedIAMUserCredentials": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - AWSConfigRemediation-RevokeUnusedIAMUserCredentials + +## What does this document do? +This document revokes unused IAM passwords and active access keys. This document will deactivate expired access keys by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html) and delete expired login profiles by using the [DeleteLoginProfile API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteLoginProfile.html). Please note, this automation document requires AWS Config to be enabled. + +## Input Parameters +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +* IAMResourceId: (Required) IAM resource unique identifier. +* MaxCredentialUsageAge: (Required) Maximum number of days within which a credential must be used. The default value is 90 days. + +## Output Parameters +* RevokeUnusedIAMUserCredentialsAndVerify.Output - Success message or failure Exception. ", + "mainSteps": [ + { + "action": "aws:executeScript", + "description": "## RevokeUnusedIAMUserCredentialsAndVerify +This step deactivates expired IAM User access keys, deletes expired login profiles and verifies credentials were revoked +## Outputs +* Output: Success message or failure Exception. +", + "inputs": { + "Handler": "unused_iam_credentials_handler", + "InputPayload": { + "IAMResourceId": "{{ IAMResourceId }}", + "MaxCredentialUsageAge": "{{ MaxCredentialUsageAge }}", + }, + "Runtime": "python3.8", + "Script": "import boto3 +from datetime import datetime +from datetime import timedelta + +iam_client = boto3.client("iam") +config_client = boto3.client("config") + +responses = {} +responses["DeactivateUnusedKeysResponse"] = [] + +def list_access_keys(user_name): + return iam_client.list_access_keys(UserName=user_name).get("AccessKeyMetadata") + +def deactivate_key(user_name, access_key): + responses["DeactivateUnusedKeysResponse"].append({"AccessKeyId": access_key, "Response": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status="Inactive")}) + +def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name): + for key in access_keys: + last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get("AccessKeyId")).get("AccessKeyLastUsed") + if last_used.get("LastUsedDate"): + last_used_date = last_used.get("LastUsedDate").replace(tzinfo=None) + last_used_days = (datetime.now() - last_used_date).days + if last_used_days >= max_credential_usage_age: + deactivate_key(user_name, key.get("AccessKeyId")) + else: + create_date = key.get("CreateDate").replace(tzinfo=None) + days_since_creation = (datetime.now() - create_date).days + if days_since_creation >= max_credential_usage_age: + deactivate_key(user_name, key.get("AccessKeyId")) + +def get_login_profile(user_name): + try: + return iam_client.get_login_profile(UserName=user_name)["LoginProfile"] + except iam_client.exceptions.NoSuchEntityException: + return False + +def delete_unused_password(user_name, max_credential_usage_age): + user = iam_client.get_user(UserName=user_name).get("User") + 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 + 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"): + for key in responses.get("DeactivateUnusedKeysResponse"): + key_data = next(filter(lambda x: x.get("AccessKeyId") == key.get("AccessKeyId"), list_access_keys(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) + if responses.get("DeleteUnusedPasswordResponse"): + try: + iam_client.get_login_profile(UserName=user_name) + error_message = "VERIFICATION FAILED. IAM USER {} LOGIN PROFILE NOT DELETED".format(user_name) + raise Exception(error_message) + except iam_client.exceptions.NoSuchEntityException: + pass + return { + "output": "Verification of unused IAM User credentials is successful.", + "http_responses": responses + } + +def get_user_name(resource_id): + list_discovered_resources_response = config_client.list_discovered_resources( + resourceType='AWS::IAM::User', + resourceIds=[resource_id] + ) + resource_name = list_discovered_resources_response.get("resourceIdentifiers")[0].get("resourceName") + return resource_name + +def 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) + + delete_unused_password(user_name, max_credential_usage_age) + + return verify_expired_credentials_revoked(responses, user_name)", + }, + "isEnd": true, + "name": "RevokeUnusedIAMUserCredentialsAndVerify", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "RevokeUnusedIAMUserCredentialsAndVerify.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "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", + }, + "IAMResourceId": { + "allowedPattern": "^[\\w+=,.@_-]{1,128}$", + "description": "(Required) IAM resource unique identifier.", + "type": "String", + }, + "MaxCredentialUsageAge": { + "allowedPattern": "^(\\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\\b)$", + "default": "90", + "description": "(Required) Maximum number of days within which a credential must be used. The default value is 90 days.", + "type": "String", + }, + }, + "schemaVersion": "0.3", + }, "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-SetIAMPasswordPolicy", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + "Name": "ASR-RevokeUnusedIAMUserCredentials", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASRS3BlockDenylist": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document Name - ASR-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 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "description": "## PutS3BucketPolicyDeny +Adds an explicit deny to the bucket policy for specific restricted permissions. +", + "inputs": { + "Handler": "update_bucket_policy", + "InputPayload": { + "accountid": "{{global:ACCOUNT_ID}}", + "bucket": "{{BucketName}}", + "denylist": "{{DenyList}}", + }, + "Runtime": "python3.8", + "Script": "#!/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}')", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "name": "PutS3BucketPolicyDeny", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, ], + "outputs": [ + "PutS3BucketPolicyDeny.Output", + ], + "parameters": { + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "(Required) The bucket name (not the ARN).", + "type": "String", + }, + "DenyList": { + "allowedPattern": ".*", + "description": "(Required) Comma-delimited list (string) of permissions to be explicitly denied when the Principal contains a role or user in another account.", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-S3BlockDenylist", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, - "SHARRSetSSLBucketPolicy": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "Content": "schemaVersion: \\"0.3\\" -description: | - ### Document name - SHARR-SetSSLBucketPolicy - - ## What does this document do? - This document adds a bucket policy to require transmission over HTTPS for the given S3 bucket by adding a policy statement to the bucket policy. - - ## Input Parameters - * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. - * BucketName: (Required) Name of the bucket to modify. - * AccountId: (Required) Account to which the bucket belongs - - ## Output Parameters - - * Remediation.Output - stdout messages from the remediation - - ## Security Standards / Controls - * AFSBP v1.0.0: S3.5 - * CIS v1.2.0: n/a - * PCI: S3.5 - -assumeRole: \\"{{ AutomationAssumeRole }}\\" -parameters: - AccountId: - type: String - description: Account ID of the account for the finding - allowedPattern: ^[0-9]{12}$ - AutomationAssumeRole: - type: String - description: (Required) The 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])$) - -outputs: - - Remediation.Output -mainSteps: - - name: Remediation - action: 'aws:executeScript' - outputs: - - Name: Output - Selector: $.Payload.response - Type: StringMap - inputs: - InputPayload: - accountid: '{{AccountId}}' - bucket: '{{BucketName}}' - Runtime: python3.8 - Handler: add_ssl_bucket_policy - Script: |- - #!/usr/bin/python - ############################################################################### - # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # - # # - # Licensed under the Apache License Version 2.0 (the \\"License\\"). You may not # - # use this file except in compliance with the License. A copy of the License # - # is located at # - # # - # http://www.apache.org/licenses/LICENSE-2.0/ # - # # - # or in the \\"license\\" file accompanying this file. This file is distributed # - # on an \\"AS IS\\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # - # or implied. See the License for the specific language governing permis- # - # sions and limitations under the License. # - ############################################################################### - - import json - import boto3 - from botocore.config import Config - from botocore.exceptions import ClientError - - boto_config = Config( - retries = { - 'mode': 'standard', - 'max_attempts': 10 - } - ) - - def connect_to_s3(): - return boto3.client('s3', config=boto_config) - - def policy_to_add(bucket): - return { - \\"Sid\\": \\"AllowSSLRequestsOnly\\", - \\"Action\\": \\"s3:*\\", - \\"Effect\\": \\"Deny\\", - \\"Resource\\": [ - f'arn:aws:s3:::{bucket}', - f'arn:aws:s3:::{bucket}/*' - ], - \\"Condition\\": { - \\"Bool\\": { - \\"aws:SecureTransport\\": \\"false\\" - } + "ASRSetIAMPasswordPolicy": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - AWSConfigRemediation-SetIAMPasswordPolicy + +## What does this document do? +This document sets the AWS Identity and Access Management (IAM) user password policy for the AWS account using the [UpdateAccountPasswordPolicy](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccountPasswordPolicy.html) API. + +## Input Parameters +* AllowUsersToChangePassword: (Optional) Allows all IAM users in your account to use the AWS Management Console to change their own passwords. +* HardExpiry: (Optional) Prevents IAM users from setting a new password after their password has expired. +* MaxPasswordAge: (Optional) The number of days that an IAM user password is valid. +* MinimumPasswordLength: (Optional) The minimum number of characters allowed in an IAM user password. +* PasswordReusePrevention: (Optional) Specifies the number of previous passwords that IAM users are prevented from reusing. +* RequireLowercaseCharacters: (Optional) Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z). +* RequireNumbers: (Optional) Specifies whether IAM user passwords must contain at least one numeric character (0 to 9). +* RequireSymbols: (Optional) pecifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters :! @ \\# $ % ^ * ( ) _ + - = [ ] { } | ' +* RequireUppercaseCharacters: (Optional) Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z). +* AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. +## Output Parameters +* UpdateAndVerifyIamUserPasswordPolicy.Output +", + "mainSteps": [ + { + "action": "aws:executeScript", + "description": "## UpdateAndVerifyIamUserPasswordPolicy +Sets or updates the AWS account password policy using input parameters using UpdateAccountPasswordPolicy API. +Verify AWS account password policy using GetAccountPasswordPolicy API. +## Outputs +* Output: Success message with HTTP Response from GetAccountPasswordPolicy API call or failure exception. +", + "inputs": { + "Handler": "update_and_verify_iam_user_password_policy", + "InputPayload": { + "AllowUsersToChangePassword": "{{ AllowUsersToChangePassword }}", + "HardExpiry": "{{ HardExpiry }}", + "MaxPasswordAge": "{{ MaxPasswordAge }}", + "MinimumPasswordLength": "{{ MinimumPasswordLength }}", + "PasswordReusePrevention": "{{ PasswordReusePrevention }}", + "RequireLowercaseCharacters": "{{ RequireLowercaseCharacters }}", + "RequireNumbers": "{{ RequireNumbers }}", + "RequireSymbols": "{{ RequireSymbols }}", + "RequireUppercaseCharacters": "{{ RequireUppercaseCharacters }}", }, - \\"Principal\\": \\"*\\" - } - def new_policy(): + "Runtime": "python3.8", + "Script": "import boto3 + + +def update_and_verify_iam_user_password_policy(event, context): + iam_client = boto3.client('iam') + + try: + params = dict() + params["AllowUsersToChangePassword"] = event["AllowUsersToChangePassword"] + if "HardExpiry" in event: + params["HardExpiry"] = event["HardExpiry"] + if event["MaxPasswordAge"]: + params["MaxPasswordAge"] = event["MaxPasswordAge"] + if event["PasswordReusePrevention"]: + params["PasswordReusePrevention"] = event["PasswordReusePrevention"] + params["MinimumPasswordLength"] = event["MinimumPasswordLength"] + params["RequireLowercaseCharacters"] = event["RequireLowercaseCharacters"] + params["RequireNumbers"] = event["RequireNumbers"] + params["RequireSymbols"] = event["RequireSymbols"] + params["RequireUppercaseCharacters"] = event["RequireUppercaseCharacters"] + + update_api_response = iam_client.update_account_password_policy(**params) + + # Verifies IAM Password Policy configuration for AWS account using GetAccountPasswordPolicy() api call. + response = iam_client.get_account_password_policy() + if all([response["PasswordPolicy"]["AllowUsersToChangePassword"] == event["AllowUsersToChangePassword"], + response["PasswordPolicy"]["MinimumPasswordLength"] == event["MinimumPasswordLength"], + response["PasswordPolicy"]["RequireLowercaseCharacters"] == event["RequireLowercaseCharacters"], + response["PasswordPolicy"]["RequireNumbers"] == event["RequireNumbers"], + response["PasswordPolicy"]["RequireUppercaseCharacters"] == event["RequireUppercaseCharacters"], + ((response["PasswordPolicy"]["HardExpiry"] == event["HardExpiry"]) if "HardExpiry" in event else True), + ((response["PasswordPolicy"]["MaxPasswordAge"] == event["MaxPasswordAge"]) if event["MaxPasswordAge"] else True), + ((response["PasswordPolicy"]["PasswordReusePrevention"] == event["PasswordReusePrevention"]) if event["PasswordReusePrevention"] else True)]): return { - \\"Id\\": \\"BucketPolicy\\", - \\"Version\\": \\"2012-10-17\\", - \\"Statement\\": [] + "output": { + "Message": "AWS Account Password Policy setting is SUCCESSFUL.", + "UpdatePolicyHTTPResponse": update_api_response, + "GetPolicyHTTPResponse": response + } } - - def add_ssl_bucket_policy(event, context): - bucket_name = event['bucket'] - account_id = event['accountid'] - s3 = connect_to_s3() - bucket_policy = {} - try: - existing_policy = s3.get_bucket_policy( - Bucket=bucket_name, - ExpectedBucketOwner=account_id - ) - bucket_policy = json.loads(existing_policy['Policy']) - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - # delivery channel already exists - return - if exception_type not in [\\"NoSuchBucketPolicy\\"]: - exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') - except Exception as e: - exit(f'ERROR getting bucket policy for {bucket_name}: {str(e)}') - - if not bucket_policy: - bucket_policy = new_policy() - - print(f'Existing policy: {bucket_policy}') - bucket_policy['Statement'].append(policy_to_add(bucket_name)) - - try: - result = s3.put_bucket_policy( - Bucket=bucket_name, - Policy=json.dumps(bucket_policy, indent=4, default=str), - ExpectedBucketOwner=account_id - ) - print(result) - except ClientError as ex: - exception_type = ex.response['Error']['Code'] - exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') - except Exception as e: - exit(f'ERROR putting bucket policy for {bucket_name}: {str(e)}') - - print(f'New policy: {bucket_policy}') - + raise Exception("VERIFICATION FAILED. AWS ACCOUNT PASSWORD POLICY NOT UPDATED.") -", + except iam_client.exceptions.NoSuchEntityException: + raise Exception("VERIFICATION FAILED. UNABLE TO UPDATE AWS ACCOUNT PASSWORD POLICY.")", + }, + "isEnd": true, + "name": "UpdateAndVerifyIamUserPasswordPolicy", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.output", + "Type": "StringMap", + }, + ], + "timeoutSeconds": 600, + }, + ], + "outputs": [ + "UpdateAndVerifyIamUserPasswordPolicy.Output", + ], + "parameters": { + "AllowUsersToChangePassword": { + "default": false, + "description": "(Optional) Allows all IAM users in your AWS account to use the AWS Management Console to change their own passwords.", + "type": "Boolean", + }, + "AutomationAssumeRole": { + "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", + }, + "HardExpiry": { + "default": false, + "description": "(Optional) Prevents IAM users from setting a new password after their password has expired.", + "type": "Boolean", + }, + "MaxPasswordAge": { + "allowedPattern": "^\\d{0,3}$|^10[0-8]\\d$|^109[0-5]$", + "default": 0, + "description": "(Optional) The number of days that an IAM user password is valid.", + "type": "Integer", + }, + "MinimumPasswordLength": { + "allowedPattern": "^[6-9]$|^[1-9]\\d$|^1[01]\\d$|^12[0-8]$", + "default": 6, + "description": "(Optional) The minimum number of characters allowed in an IAM user password.", + "type": "Integer", + }, + "PasswordReusePrevention": { + "allowedPattern": "^\\d{0,1}$|^1\\d$|^2[0-4]$", + "default": 0, + "description": "(Optional) Specifies the number of previous passwords that IAM users are prevented from reusing.", + "type": "Integer", + }, + "RequireLowercaseCharacters": { + "default": false, + "description": "(Optional) Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z).", + "type": "Boolean", + }, + "RequireNumbers": { + "default": false, + "description": "(Optional) Specifies whether IAM user passwords must contain at least one numeric character (0 to 9).", + "type": "Boolean", + }, + "RequireSymbols": { + "default": false, + "description": "(Optional) Specifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters :! @ \\# $ % ^ * ( ) _ + - = [ ] { } | '.", + "type": "Boolean", + }, + "RequireUppercaseCharacters": { + "default": false, + "description": "(Optional) Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z).", + "type": "Boolean", + }, + }, + "schemaVersion": "0.3", + }, "DocumentFormat": "YAML", "DocumentType": "Automation", - "Name": "SHARR-SetSSLBucketPolicy", - "ServiceToken": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":lambda:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", + "Name": "ASR-SetIAMPasswordPolicy", + "UpdateMethod": "NewVersion", + }, + "Type": "AWS::SSM::Document", + }, + "ASRSetSSLBucketPolicy": { + "Properties": { + "Content": { + "assumeRole": "{{ AutomationAssumeRole }}", + "description": "### Document name - ASR-SetSSLBucketPolicy + +## What does this document do? +This document adds a bucket policy to require transmission over HTTPS for the given S3 bucket by adding a policy statement to the bucket policy. + +## Input Parameters +* AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. +* BucketName: (Required) Name of the bucket to modify. +* AccountId: (Required) Account to which the bucket belongs + +## Output Parameters + +* Remediation.Output - stdout messages from the remediation + +## Security Standards / Controls +* AFSBP v1.0.0: S3.5 +* CIS v1.2.0: n/a +* PCI: S3.5 +", + "mainSteps": [ + { + "action": "aws:executeScript", + "inputs": { + "Handler": "add_ssl_bucket_policy", + "InputPayload": { + "accountid": "{{AccountId}}", + "bucket": "{{BucketName}}", + }, + "Runtime": "python3.8", + "Script": "#!/usr/bin/python +############################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License Version 2.0 (the "License"). You may not # +# use this file except in compliance with the License. A copy of the License # +# is located at # +# # +# http://www.apache.org/licenses/LICENSE-2.0/ # +# # +# or in the "license" file accompanying this file. This file is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # +# or implied. See the License for the specific language governing permis- # +# sions and limitations under the License. # +############################################################################### + +import json +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +boto_config = Config( + retries = { + 'mode': 'standard', + 'max_attempts': 10 + } + ) + +def connect_to_s3(): + return boto3.client('s3', config=boto_config) + +def policy_to_add(bucket): + return { + "Sid": "AllowSSLRequestsOnly", + "Action": "s3:*", + "Effect": "Deny", + "Resource": [ + f'arn:aws:s3:::{bucket}', + f'arn:aws:s3:::{bucket}/*' + ], + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Principal": "*" + } +def new_policy(): + return { + "Id": "BucketPolicy", + "Version": "2012-10-17", + "Statement": [] + } + +def add_ssl_bucket_policy(event, context): + bucket_name = event['bucket'] + account_id = event['accountid'] + s3 = connect_to_s3() + bucket_policy = {} + try: + existing_policy = s3.get_bucket_policy( + Bucket=bucket_name, + ExpectedBucketOwner=account_id + ) + bucket_policy = json.loads(existing_policy['Policy']) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + # delivery channel already exists - return + if exception_type not in ["NoSuchBucketPolicy"]: + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR getting bucket policy for {bucket_name}: {str(e)}') + + if not bucket_policy: + bucket_policy = new_policy() + + print(f'Existing policy: {bucket_policy}') + bucket_policy['Statement'].append(policy_to_add(bucket_name)) + + try: + result = s3.put_bucket_policy( + Bucket=bucket_name, + Policy=json.dumps(bucket_policy, indent=4, default=str), + ExpectedBucketOwner=account_id + ) + print(result) + except ClientError as ex: + exception_type = ex.response['Error']['Code'] + exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') + except Exception as e: + exit(f'ERROR putting bucket policy for {bucket_name}: {str(e)}') + + print(f'New policy: {bucket_policy}')", }, - ":function:SO0111-SHARR-updatableRunbookProvider", - ], + "name": "Remediation", + "outputs": [ + { + "Name": "Output", + "Selector": "$.Payload.response", + "Type": "StringMap", + }, + ], + }, + ], + "outputs": [ + "Remediation.Output", ], + "parameters": { + "AccountId": { + "allowedPattern": "^[0-9]{12}$", + "description": "Account ID of the account for the finding", + "type": "String", + }, + "AutomationAssumeRole": { + "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", + "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", + "type": "String", + }, + "BucketName": { + "allowedPattern": "(?=^.{3,63}$)(?!^(\\d+\\.)+\\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$)", + "description": "Name of the bucket to have a policy added", + "type": "String", + }, + }, + "schemaVersion": "0.3", }, + "DocumentFormat": "YAML", + "DocumentType": "Automation", + "Name": "ASR-SetSSLBucketPolicy", + "UpdateMethod": "NewVersion", }, - "Type": "Custom::UpdatableRunbook", - "UpdateReplacePolicy": "Delete", + "Type": "AWS::SSM::Document", }, }, } diff --git a/source/test/__snapshots__/solution_deploy.test.ts.snap b/source/test/__snapshots__/solution_deploy.test.ts.snap index 57ccf0ba..4f9bf164 100644 --- a/source/test/__snapshots__/solution_deploy.test.ts.snap +++ b/source/test/__snapshots__/solution_deploy.test.ts.snap @@ -1,82 +1,82 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Test if the Stack has all the resources. 1`] = ` -Object { - "Mappings": Object { - "SourceCode": Object { - "General": Object { +{ + "Mappings": { + "SourceCode": { + "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.0.0", "S3Bucket": "solutions", }, }, - "mappings": Object { - "sendAnonymousMetrics": Object { + "mappings": { + "sendAnonymousMetrics": { "data": "Yes", }, }, }, - "Metadata": Object { - "AWS::CloudFormation::Interface": Object { - "ParameterGroups": Array [ - Object { - "Label": Object { + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { "default": "Security Standard Playbooks", }, - "Parameters": Array [], + "Parameters": [], }, ], }, }, - "Parameters": Object { - "ReuseOrchestratorLogGroup": Object { - "AllowedValues": Array [ + "Parameters": { + "ReuseOrchestratorLogGroup": { + "AllowedValues": [ "yes", "no", ], "Default": "no", - "Description": "Reuse existing Orchestrator Log Group? Choose \\"yes\\" if the log group already exists, else \\"no\\"", + "Description": "Reuse existing Orchestrator Log Group? Choose "yes" if the log group already exists, else "no"", "Type": "String", }, }, - "Resources": Object { - "CreateCustomActionE7A973F5": Object { - "DependsOn": Array [ + "Resources": { + "CreateCustomActionE7A973F5": { + "DependsOn": [ "createCustomActionRoleF0047414", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. the lambda role allows write to CW Logs", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency due to low request rate", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/createCustomAction.py.zip", }, "Description": "Custom resource to create an action target in Security Hub", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", "SOLUTION_VERSION": "v1.0.0", "log_level": "info", - "sendAnonymousMetrics": Object { - "Fn::FindInMap": Array [ + "sendAnonymousMetrics": { + "Fn::FindInMap": [ "mappings", "sendAnonymousMetrics", "data", @@ -86,35 +86,35 @@ Object { }, "FunctionName": "SO0111-SHARR-CustomAction", "Handler": "createCustomAction.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "createCustomActionRoleF0047414", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", }, - "RemediateWithSharrCustomActionABE4122A": Object { + "RemediateWithSharrCustomActionABE4122A": { "DeletionPolicy": "Delete", - "DependsOn": Array [ + "DependsOn": [ "CreateCustomActionE7A973F5", "createCustomActionPolicyE424E925", ], - "Properties": Object { + "Properties": { "Description": "Submit the finding to AWS Security Hub Automated Response and Remediation", "Id": "SHARRRemediation", "Name": "Remediate with SHARR", - "ServiceToken": Object { - "Fn::GetAtt": Array [ + "ServiceToken": { + "Fn::GetAtt": [ "CreateCustomActionE7A973F5", "Arn", ], @@ -123,14 +123,14 @@ Object { "Type": "Custom::ActionTarget", "UpdateReplacePolicy": "Delete", }, - "RemediateWithSharrEventsRuleRole4BE0B6FF": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "RemediateWithSharrEventsRuleRole4BE0B6FF": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "events.amazonaws.com", }, }, @@ -140,14 +140,14 @@ Object { }, "Type": "AWS::IAM::Role", }, - "RemediateWithSharrEventsRuleRoleDefaultPolicy44783695": Object { - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "RemediateWithSharrEventsRuleRoleDefaultPolicy44783695": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "states:StartExecution", "Effect": "Allow", - "Resource": Object { + "Resource": { "Ref": "orchestratorStateMachine77C3F8FB", }, }, @@ -155,53 +155,53 @@ Object { "Version": "2012-10-17", }, "PolicyName": "RemediateWithSharrEventsRuleRoleDefaultPolicy44783695", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "RemediateWithSharrEventsRuleRole4BE0B6FF", }, ], }, "Type": "AWS::IAM::Policy", }, - "RemediateWithSharrRemediateCustomAction40B496D2": Object { - "Properties": Object { + "RemediateWithSharrRemediateCustomAction40B496D2": { + "Properties": { "Description": "Remediate with SHARR", - "EventPattern": Object { - "detail": Object { - "findings": Object { - "Compliance": Object { - "Status": Array [ + "EventPattern": { + "detail": { + "findings": { + "Compliance": { + "Status": [ "FAILED", "WARNING", ], }, }, }, - "detail-type": Array [ + "detail-type": [ "Security Hub Findings - Custom Action", ], - "resources": Array [ - Object { - "Fn::GetAtt": Array [ + "resources": [ + { + "Fn::GetAtt": [ "RemediateWithSharrCustomActionABE4122A", "Arn", ], }, ], - "source": Array [ + "source": [ "aws.securityhub", ], }, "Name": "Remediate_with_SHARR_CustomAction", "State": "ENABLED", - "Targets": Array [ - Object { - "Arn": Object { + "Targets": [ + { + "Arn": { "Ref": "orchestratorStateMachine77C3F8FB", }, "Id": "Target0", - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "RemediateWithSharrEventsRuleRole4BE0B6FF", "Arn", ], @@ -211,13 +211,13 @@ Object { }, "Type": "AWS::Events::Rule", }, - "SHARRKeyC551FE02": Object { - "Properties": Object { + "SHARRKeyC551FE02": { + "Properties": { "Description": "KMS Customer Managed Key that SHARR will use to encrypt data", "Name": "/Solutions/SO0111/CMK_ARN", "Type": "String", - "Value": Object { - "Fn::GetAtt": Array [ + "Value": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], @@ -225,24 +225,24 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SHARRSNSTopicB940F479": Object { - "Properties": Object { + "SHARRSNSTopicB940F479": { + "Properties": { "Description": "SNS Topic ARN where SHARR will send status messages. This topic can be useful for driving additional actions, such as email notifications, trouble ticket updates.", "Name": "/Solutions/SO0111/SNS_Topic_ARN", "Type": "String", - "Value": Object { + "Value": { "Ref": "SHARRTopic229CFB9E", }, }, "Type": "AWS::SSM::Parameter", }, - "SHARRSendAnonymousMetricsCDAE439D": Object { - "Properties": Object { + "SHARRSendAnonymousMetricsCDAE439D": { + "Properties": { "Description": "Flag to enable or disable sending anonymous metrics.", "Name": "/Solutions/SO0111/sendAnonymousMetrics", "Type": "String", - "Value": Object { - "Fn::FindInMap": Array [ + "Value": { + "Fn::FindInMap": [ "mappings", "sendAnonymousMetrics", "data", @@ -251,11 +251,11 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SHARRTopic229CFB9E": Object { - "Properties": Object { + "SHARRTopic229CFB9E": { + "Properties": { "DisplayName": "SHARR Playbook Topic (SO0111)", - "KmsMasterKeyId": Object { - "Fn::GetAtt": Array [ + "KmsMasterKeyId": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], @@ -264,11 +264,11 @@ Object { }, "Type": "AWS::SNS::Topic", }, - "SHARRkeyAlias37E34763": Object { - "Properties": Object { + "SHARRkeyAlias37E34763": { + "Properties": { "AliasName": "alias/SO0111-SHARR-Key", - "TargetKeyId": Object { - "Fn::GetAtt": Array [ + "TargetKeyId": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], @@ -276,28 +276,28 @@ Object { }, "Type": "AWS::KMS::Alias", }, - "SHARRkeyE6BD0F56": Object { + "SHARRkeyE6BD0F56": { "DeletionPolicy": "Retain", - "Properties": Object { + "Properties": { "EnableKeyRotation": true, - "KeyPolicy": Object { - "Statement": Array [ - Object { - "Action": Array [ + "KeyPolicy": { + "Statement": [ + { + "Action": [ "kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Describe*", ], - "Condition": Object { - "ArnEquals": Object { - "kms:EncryptionContext:aws:logs:arn": Object { - "Fn::Join": Array [ + "Condition": { + "ArnEquals": { + "kms:EncryptionContext:aws:logs:arn": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":logs:eu-west-1:111111111111:log-group:SO0111-SHARR-*", @@ -307,15 +307,15 @@ Object { }, }, "Effect": "Allow", - "Principal": Object { - "Service": Array [ + "Principal": { + "Service": [ "sns.amazonaws.com", - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "logs.", - Object { + { "Ref": "AWS::URLSuffix", }, ], @@ -325,16 +325,16 @@ Object { }, "Resource": "*", }, - Object { + { "Action": "kms:*", "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::111111111111:root", @@ -344,8 +344,8 @@ Object { }, "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "kms:Create*", "kms:Describe*", "kms:Enable*", @@ -363,13 +363,13 @@ Object { "kms:UntagResource", ], "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ + "Principal": { + "AWS": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::111111111111:root", @@ -386,8 +386,8 @@ Object { "Type": "AWS::KMS::Key", "UpdateReplacePolicy": "Retain", }, - "SHARRversionAC0E4F96": Object { - "Properties": Object { + "SHARRversionAC0E4F96": { + "Properties": { "Description": "Solution version for metrics.", "Name": "/Solutions/SO0111/version", "Type": "String", @@ -395,14 +395,12 @@ Object { }, "Type": "AWS::SSM::Parameter", }, - "SharrLambdaLayer5BF8F147": Object { - "Properties": Object { - "CompatibleRuntimes": Array [ - "python3.6", - "python3.7", - "python3.8", + "SharrLambdaLayer5BF8F147": { + "Properties": { + "CompatibleRuntimes": [ + "python3.9", ], - "Content": Object { + "Content": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/layer.zip", }, @@ -411,37 +409,37 @@ Object { }, "Type": "AWS::Lambda::LayerVersion", }, - "checkSSMDocState06AC440F": Object { - "DependsOn": Array [ + "checkSSMDocState06AC440F": { + "DependsOn": [ "orchestratorRole46A9F242", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/check_ssm_doc_state.py.zip", }, "Description": "Checks the status of an SSM Automation Document in the target account", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", @@ -451,52 +449,52 @@ Object { }, "FunctionName": "SO0111-SHARR-checkSSMDocState", "Handler": "check_ssm_doc_state.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "orchestratorRole46A9F242", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", }, - "createCustomActionPolicyE424E925": Object { - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "createCustomActionPolicyE424E925": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-IAM5", "reason": "Resource * is required for CloudWatch Logs policies used on Lambda functions.", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W12", "reason": "Resource * is required for CloudWatch Logs policies used on Lambda functions.", }, ], }, }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "PolicyDocument": { + "Statement": [ + { "Action": "cloudwatch:PutMetricData", "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", @@ -504,27 +502,27 @@ Object { "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "securityhub:CreateActionTarget", "securityhub:DeleteActionTarget", ], "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "ssm:GetParameter", "ssm:GetParameters", "ssm:PutParameter", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:111111111111:parameter/Solutions/SO0111/*", @@ -536,32 +534,32 @@ Object { "Version": "2012-10-17", }, "PolicyName": "SO0111-SHARR_Custom_Action", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "createCustomActionRoleF0047414", }, ], }, "Type": "AWS::IAM::Policy", }, - "createCustomActionRoleF0047414": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "createCustomActionRoleF0047414": { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W28", "reason": "Static names chosen intentionally to provide easy integration with playbook templates", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "lambda.amazonaws.com", }, }, @@ -572,37 +570,37 @@ Object { }, "Type": "AWS::IAM::Role", }, - "execAutomation5D89E251": Object { - "DependsOn": Array [ + "execAutomation5D89E251": { + "DependsOn": [ "orchestratorRole46A9F242", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/exec_ssm_doc.py.zip", }, "Description": "Executes an SSM Automation Document in a target account", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", @@ -612,54 +610,54 @@ Object { }, "FunctionName": "SO0111-SHARR-execAutomation", "Handler": "exec_ssm_doc.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "orchestratorRole46A9F242", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", }, - "getApprovalRequirementE7F50E54": Object { - "DependsOn": Array [ + "getApprovalRequirementE7F50E54": { + "DependsOn": [ "orchestratorRole46A9F242", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/get_approval_requirement.py.zip", }, "Description": "Determines if a manual approval is required for remediation", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", @@ -670,54 +668,54 @@ Object { }, "FunctionName": "SO0111-SHARR-getApprovalRequirement", "Handler": "get_approval_requirement.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "orchestratorRole46A9F242", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", }, - "monitorSSMExecStateB496B8AF": Object { - "DependsOn": Array [ + "monitorSSMExecStateB496B8AF": { + "DependsOn": [ "orchestratorRole46A9F242", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/check_ssm_execution.py.zip", }, "Description": "Checks the status of an SSM automation document execution", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", @@ -727,51 +725,51 @@ Object { }, "FunctionName": "SO0111-SHARR-monitorSSMExecState", "Handler": "check_ssm_execution.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "orchestratorRole46A9F242", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", }, - "notifyPolicy320847DC": Object { - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "notifyPolicy320847DC": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-IAM5", "reason": "Resource * is required for CloudWatch Logs and Security Hub policies used by core solution Lambda function for notifications.", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W12", "reason": "Resource * is required for CloudWatch Logs and Security Hub policies used by core solution Lambda function for notifications.", }, - Object { + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, ], }, }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", @@ -779,23 +777,23 @@ Object { "Effect": "Allow", "Resource": "*", }, - Object { + { "Action": "securityhub:BatchUpdateFindings", "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "ssm:GetParameter", "ssm:PutParameter", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:eu-west-1:111111111111:parameter/Solutions/SO0111/*", @@ -803,29 +801,29 @@ Object { ], }, }, - Object { - "Action": Array [ + { + "Action": [ "kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", ], "Effect": "Allow", - "Resource": Object { - "Fn::GetAtt": Array [ + "Resource": { + "Fn::GetAtt": [ "SHARRkeyE6BD0F56", "Arn", ], }, }, - Object { + { "Action": "sns:Publish", "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":sns:eu-west-1:111111111111:SO0111-SHARR_Topic", @@ -837,35 +835,35 @@ Object { "Version": "2012-10-17", }, "PolicyName": "SO0111-SHARR_Orchestrator_Notifier", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "orchestratorRole46A9F242", }, - Object { + { "Ref": "notifyRole40298120", }, ], }, "Type": "AWS::IAM::Policy", }, - "notifyRole40298120": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "notifyRole40298120": { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W28", "reason": "Static names chosen intentionally to provide easy integration with playbook orchestrator step functions.", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "lambda.amazonaws.com", }, }, @@ -876,34 +874,34 @@ Object { }, "Type": "AWS::IAM::Role", }, - "orchestratorNestedLogStack4DD66790": Object { - "Properties": Object { - "Parameters": Object { - "KmsKeyArn": Object { - "Fn::GetAtt": Array [ + "orchestratorNestedLogStack4DD66790": { + "Properties": { + "Parameters": { + "KmsKeyArn": { + "Fn::GetAtt": [ "SHARRKeyC551FE02", "Value", ], }, - "ReuseOrchestratorLogGroup": Object { + "ReuseOrchestratorLogGroup": { "Ref": "ReuseOrchestratorLogGroup", }, }, - "TemplateURL": Object { - "Fn::Join": Array [ + "TemplateURL": { + "Fn::Join": [ "", - Array [ + [ "https://", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket", ], }, "-reference.s3.amazonaws.com/", - Object { - "Fn::FindInMap": Array [ + { + "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix", @@ -916,30 +914,30 @@ Object { }, "Type": "AWS::CloudFormation::Stack", }, - "orchestratorPolicy8045810D": Object { - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "orchestratorPolicy8045810D": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-IAM5", "reason": "Resource * is required for read-only policies used by orchestrator Lambda functions.", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W12", "reason": "Resource * is required for read-only policies used by orchestrator Lambda functions.", }, ], }, }, - "Properties": Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", @@ -947,19 +945,19 @@ Object { "Effect": "Allow", "Resource": "*", }, - Object { - "Action": Array [ + { + "Action": [ "ssm:GetParameter", "ssm:GetParameters", "ssm:PutParameter", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":ssm:*:111111111111:parameter/Solutions/SO0111/*", @@ -967,15 +965,15 @@ Object { ], }, }, - Object { + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":iam::*:role/SO0111-SHARR-Orchestrator-Member", @@ -983,7 +981,7 @@ Object { ], }, }, - Object { + { "Action": "organizations:ListTagsForResource", "Effect": "Allow", "Resource": "*", @@ -992,53 +990,53 @@ Object { "Version": "2012-10-17", }, "PolicyName": "SO0111-SHARR_Orchestrator", - "Roles": Array [ - Object { + "Roles": [ + { "Ref": "orchestratorRole46A9F242", }, ], }, "Type": "AWS::IAM::Policy", }, - "orchestratorRole12B410FD": Object { + "orchestratorRole12B410FD": { "DeletionPolicy": "Retain", - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-IAM5", "reason": "CloudWatch Logs permissions require resource * except for DescribeLogGroups, except for GovCloud, which only works with resource *", }, ], }, - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W11", "reason": "CloudWatch Logs permissions require resource * except for DescribeLogGroups, except for GovCloud, which only works with resource *", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "states.eu-west-1.amazonaws.com", }, }, ], "Version": "2012-10-17", }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": Array [ + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "logs:CreateLogDelivery", "logs:GetLogDelivery", "logs:UpdateLogDelivery", @@ -1051,27 +1049,27 @@ Object { "Effect": "Allow", "Resource": "*", }, - Object { + { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": Array [ - Object { - "Fn::Join": Array [ + "Resource": [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:eu-west-1:111111111111:function:", - Object { - "Fn::Select": Array [ + { + "Fn::Select": [ 6, - Object { - "Fn::Split": Array [ + { + "Fn::Split": [ ":", - Object { - "Fn::GetAtt": Array [ + { + "Fn::GetAtt": [ "checkSSMDocState06AC440F", "Arn", ], @@ -1083,23 +1081,23 @@ Object { ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:eu-west-1:111111111111:function:", - Object { - "Fn::Select": Array [ + { + "Fn::Select": [ 6, - Object { - "Fn::Split": Array [ + { + "Fn::Split": [ ":", - Object { - "Fn::GetAtt": Array [ + { + "Fn::GetAtt": [ "execAutomation5D89E251", "Arn", ], @@ -1111,23 +1109,23 @@ Object { ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:eu-west-1:111111111111:function:", - Object { - "Fn::Select": Array [ + { + "Fn::Select": [ 6, - Object { - "Fn::Split": Array [ + { + "Fn::Split": [ ":", - Object { - "Fn::GetAtt": Array [ + { + "Fn::GetAtt": [ "monitorSSMExecStateB496B8AF", "Arn", ], @@ -1139,23 +1137,23 @@ Object { ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:eu-west-1:111111111111:function:", - Object { - "Fn::Select": Array [ + { + "Fn::Select": [ 6, - Object { - "Fn::Split": Array [ + { + "Fn::Split": [ ":", - Object { - "Fn::GetAtt": Array [ + { + "Fn::GetAtt": [ "sendNotifications1367638A", "Arn", ], @@ -1167,23 +1165,23 @@ Object { ], ], }, - Object { - "Fn::Join": Array [ + { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":lambda:eu-west-1:111111111111:function:", - Object { - "Fn::Select": Array [ + { + "Fn::Select": [ 6, - Object { - "Fn::Split": Array [ + { + "Fn::Split": [ ":", - Object { - "Fn::GetAtt": Array [ + { + "Fn::GetAtt": [ "getApprovalRequirementE7F50E54", "Arn", ], @@ -1197,19 +1195,19 @@ Object { }, ], }, - Object { - "Action": Array [ + { + "Action": [ "kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", ], "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ + "Resource": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":kms:eu-west-1:111111111111:alias/SO0111-SHARR-Key", @@ -1227,24 +1225,24 @@ Object { "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Retain", }, - "orchestratorRole46A9F242": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "orchestratorRole46A9F242": { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W28", "reason": "Static names chosen intentionally to provide easy integration with playbook orchestrator step functions.", }, ], }, }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", "Effect": "Allow", - "Principal": Object { + "Principal": { "Service": "lambda.amazonaws.com", }, }, @@ -1256,121 +1254,121 @@ Object { }, "Type": "AWS::IAM::Role", }, - "orchestratorSHARROrchestratorArn0ACC7B05": Object { - "Properties": Object { + "orchestratorSHARROrchestratorArn0ACC7B05": { + "Properties": { "Description": "Arn of the SHARR Orchestrator Step Function. This step function routes findings to remediation runbooks.", "Name": "/Solutions/SO0111/OrchestratorArn", "Type": "String", - "Value": Object { + "Value": { "Ref": "orchestratorStateMachine77C3F8FB", }, }, "Type": "AWS::SSM::Parameter", }, - "orchestratorStateMachine77C3F8FB": Object { - "DependsOn": Array [ + "orchestratorStateMachine77C3F8FB": { + "DependsOn": [ "orchestratorNestedLogStack4DD66790", "orchestratorRole12B410FD", ], - "Metadata": Object { - "cdk_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { "id": "AwsSolutions-SF1", "reason": "False alarm. Logging configuration is overridden to log ALL.", }, - Object { + { "id": "AwsSolutions-SF2", "reason": "X-Ray is not needed for this use case.", }, ], }, }, - "Properties": Object { - "DefinitionString": Object { - "Fn::Join": Array [ + "Properties": { + "DefinitionString": { + "Fn::Join": [ "", - Array [ - "{\\"StartAt\\":\\"Get Finding Data from Input\\",\\"States\\":{\\"Get Finding Data from Input\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Extract top-level data needed for remediation\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.detail-type\\",\\"Findings.$\\":\\"$.detail.findings\\"},\\"Next\\":\\"Process Findings\\"},\\"Process Findings\\":{\\"Type\\":\\"Map\\",\\"Comment\\":\\"Process all findings in CloudWatch Event\\",\\"Next\\":\\"EOJ\\",\\"Parameters\\":{\\"Finding.$\\":\\"$$.Map.Item.Value\\",\\"EventType.$\\":\\"$.EventType\\"},\\"Iterator\\":{\\"StartAt\\":\\"Finding Workflow State NEW?\\",\\"States\\":{\\"Finding Workflow State NEW?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Or\\":[{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Custom Action\\"},{\\"And\\":[{\\"Variable\\":\\"$.Finding.Workflow.Status\\",\\"StringEquals\\":\\"NEW\\"},{\\"Variable\\":\\"$.EventType\\",\\"StringEquals\\":\\"Security Hub Findings - Imported\\"}]}],\\"Next\\":\\"Get Remediation Approval Requirement\\"}],\\"Default\\":\\"Finding Workflow State is not NEW\\"},\\"Finding Workflow State is not NEW\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)\\",\\"State.$\\":\\"States.Format('NOTNEW')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"notify\\":{\\"End\\":true,\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notifications\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"Resource\\":\\"arn:", - Object { + [ + "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","Parameters":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType"},"Iterator":{"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOTNEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "sendNotifications1367638A", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Automation Document is not Active\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)\\",\\"State.$\\":\\"States.Format('REMEDIATIONNOTACTIVE')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"Automation Doc Active?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"ACTIVE\\",\\"Next\\":\\"Execute Remediation\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTACTIVE\\",\\"Next\\":\\"Automation Document is not Active\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTENABLED\\",\\"Next\\":\\"Security Standard is not enabled\\"},{\\"Variable\\":\\"$.AutomationDocument.DocState\\",\\"StringEquals\\":\\"NOTFOUND\\",\\"Next\\":\\"No Remediation for Control\\"}],\\"Default\\":\\"check_ssm_doc_state Error\\"},\\"Get Automation Document State\\":{\\"Next\\":\\"Automation Doc Active?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Get the status of the remediation automation document in the target account\\",\\"TimeoutSeconds\\":60,\\"ResultPath\\":\\"$.AutomationDocument\\",\\"ResultSelector\\":{\\"DocState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"SecurityStandard.$\\":\\"$.Payload.securitystandard\\",\\"SecurityStandardVersion.$\\":\\"$.Payload.securitystandardversion\\",\\"SecurityStandardSupported.$\\":\\"$.Payload.standardsupported\\",\\"ControlId.$\\":\\"$.Payload.controlid\\",\\"AccountId.$\\":\\"$.Payload.accountid\\",\\"RemediationRole.$\\":\\"$.Payload.remediationrole\\",\\"AutomationDocId.$\\":\\"$.Payload.automationdocid\\",\\"ResourceRegion.$\\":\\"$.Payload.resourceregion\\"},\\"Resource\\":\\"arn:", - Object { + "","Payload.$":"$"}},"Automation Document is not Active":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Automation Document ({}) is not active ({}) in the member account({}).', $.AutomationDocId, $.AutomationDocument.DocState, $.Finding.AwsAccountId)","State.$":"States.Format('REMEDIATIONNOTACTIVE')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"},"Automation Doc Active?":{"Type":"Choice","Choices":[{"Variable":"$.AutomationDocument.DocState","StringEquals":"ACTIVE","Next":"Execute Remediation"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTACTIVE","Next":"Automation Document is not Active"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTENABLED","Next":"Security Standard is not enabled"},{"Variable":"$.AutomationDocument.DocState","StringEquals":"NOTFOUND","Next":"No Remediation for Control"}],"Default":"check_ssm_doc_state Error"},"Get Automation Document State":{"Next":"Automation Doc Active?","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Get the status of the remediation automation document in the target account","TimeoutSeconds":60,"ResultPath":"$.AutomationDocument","ResultSelector":{"DocState.$":"$.Payload.status","Message.$":"$.Payload.message","SecurityStandard.$":"$.Payload.securitystandard","SecurityStandardVersion.$":"$.Payload.securitystandardversion","SecurityStandardSupported.$":"$.Payload.standardsupported","ControlId.$":"$.Payload.controlid","AccountId.$":"$.Payload.accountid","RemediationRole.$":"$.Payload.remediationrole","AutomationDocId.$":"$.Payload.automationdocid","ResourceRegion.$":"$.Payload.resourceregion"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "checkSSMDocState06AC440F", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Get Remediation Approval Requirement\\":{\\"Next\\":\\"Get Automation Document State\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Determine whether the selected remediation requires manual approval\\",\\"TimeoutSeconds\\":300,\\"ResultPath\\":\\"$.Workflow\\",\\"ResultSelector\\":{\\"WorkflowDocument.$\\":\\"$.Payload.workflowdoc\\",\\"WorkflowAccount.$\\":\\"$.Payload.workflowaccount\\",\\"WorkflowRole.$\\":\\"$.Payload.workflowrole\\",\\"WorkflowConfig.$\\":\\"$.Payload.workflow_data\\"},\\"Resource\\":\\"arn:", - Object { + "","Payload.$":"$"}},"Get Remediation Approval Requirement":{"Next":"Get Automation Document State","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Determine whether the selected remediation requires manual approval","TimeoutSeconds":300,"ResultPath":"$.Workflow","ResultSelector":{"WorkflowDocument.$":"$.Payload.workflowdoc","WorkflowAccount.$":"$.Payload.workflowaccount","WorkflowRole.$":"$.Payload.workflowrole","WorkflowConfig.$":"$.Payload.workflow_data"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "getApprovalRequirementE7F50E54", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Orchestrator Failed\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Orchestrator failed: {}', $.Error)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\",\\"Details.$\\":\\"States.Format('Cause: {}', $.Cause)\\"},\\"Payload.$\\":\\"$\\"},\\"Next\\":\\"notify\\"},\\"Execute Remediation\\":{\\"Next\\":\\"Remediation Queued\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Execute the SSM Automation Document in the target account\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.SSMExecution\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"Account.$\\":\\"$.Payload.executionaccount\\",\\"Region.$\\":\\"$.Payload.executionregion\\"},\\"Resource\\":\\"arn:", - Object { + "","Payload.$":"$"}},"Orchestrator Failed":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Orchestrator failed: {}', $.Error)","State.$":"States.Format('LAMBDAERROR')","Details.$":"States.Format('Cause: {}', $.Cause)"},"Payload.$":"$"},"Next":"notify"},"Execute Remediation":{"Next":"Remediation Queued","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Execute the SSM Automation Document in the target account","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.SSMExecution","ResultSelector":{"ExecState.$":"$.Payload.status","Message.$":"$.Payload.message","ExecId.$":"$.Payload.executionid","Account.$":"$.Payload.executionaccount","Region.$":"$.Payload.executionregion"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "execAutomation5D89E251", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Remediation Queued\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)\\",\\"State.$\\":\\"States.Format('QUEUED')\\",\\"ExecId.$\\":\\"$.SSMExecution.ExecId\\"}},\\"Next\\":\\"Queued Notification\\"},\\"Queued Notification\\":{\\"Next\\":\\"execMonitor\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Send notification that a remediation has queued\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.notificationResult\\",\\"Resource\\":\\"arn:", - Object { + "","Payload.$":"$"}},"Remediation Queued":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","AutomationDocument.$":"$.AutomationDocument","SSMExecution.$":"$.SSMExecution","Notification":{"Message.$":"States.Format('Remediation queued for {} control {} in account {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId)","State.$":"States.Format('QUEUED')","ExecId.$":"$.SSMExecution.ExecId"}},"Next":"Queued Notification"},"Queued Notification":{"Next":"execMonitor","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notification that a remediation has queued","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.notificationResult","Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "sendNotifications1367638A", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"execMonitor\\":{\\"Next\\":\\"Remediation completed?\\",\\"Retry\\":[{\\"ErrorEquals\\":[\\"Lambda.ServiceException\\",\\"Lambda.AWSLambdaException\\",\\"Lambda.SdkClientException\\"],\\"IntervalSeconds\\":2,\\"MaxAttempts\\":6,\\"BackoffRate\\":2}],\\"Catch\\":[{\\"ErrorEquals\\":[\\"States.ALL\\"],\\"Next\\":\\"Orchestrator Failed\\"}],\\"Type\\":\\"Task\\",\\"Comment\\":\\"Monitor the remediation execution until done\\",\\"TimeoutSeconds\\":300,\\"HeartbeatSeconds\\":60,\\"ResultPath\\":\\"$.Remediation\\",\\"ResultSelector\\":{\\"ExecState.$\\":\\"$.Payload.status\\",\\"ExecId.$\\":\\"$.Payload.executionid\\",\\"RemediationState.$\\":\\"$.Payload.remediation_status\\",\\"Message.$\\":\\"$.Payload.message\\",\\"LogData.$\\":\\"$.Payload.logdata\\",\\"AffectedObject.$\\":\\"$.Payload.affected_object\\"},\\"Resource\\":\\"arn:", - Object { + "","Payload.$":"$"}},"execMonitor":{"Next":"Remediation completed?","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"Orchestrator Failed"}],"Type":"Task","Comment":"Monitor the remediation execution until done","TimeoutSeconds":300,"HeartbeatSeconds":60,"ResultPath":"$.Remediation","ResultSelector":{"ExecState.$":"$.Payload.status","ExecId.$":"$.Payload.executionid","RemediationState.$":"$.Payload.remediation_status","Message.$":"$.Payload.message","LogData.$":"$.Payload.logdata","AffectedObject.$":"$.Payload.affected_object"},"Resource":"arn:", + { "Ref": "AWS::Partition", }, - ":states:::lambda:invoke\\",\\"Parameters\\":{\\"FunctionName\\":\\"", - Object { - "Fn::GetAtt": Array [ + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ "monitorSSMExecStateB496B8AF", "Arn", ], }, - "\\",\\"Payload.$\\":\\"$\\"}},\\"Wait for Remediation\\":{\\"Type\\":\\"Wait\\",\\"Seconds\\":15,\\"Next\\":\\"execMonitor\\"},\\"Remediation completed?\\":{\\"Type\\":\\"Choice\\",\\"Choices\\":[{\\"Variable\\":\\"$.Remediation.RemediationState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Success\\",\\"Next\\":\\"Remediation Succeeded\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"TimedOut\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelling\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Cancelled\\",\\"Next\\":\\"Remediation Failed\\"},{\\"Variable\\":\\"$.Remediation.ExecState\\",\\"StringEquals\\":\\"Failed\\",\\"Next\\":\\"Remediation Failed\\"}],\\"Default\\":\\"Wait for Remediation\\"},\\"Remediation Failed\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"SSMExecution.$\\":\\"$.SSMExecution\\",\\"AutomationDocument.$\\":\\"$.AutomationDocument\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"$.Remediation.ExecState\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"Remediation Succeeded\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"Set parameters for notification\\",\\"Parameters\\":{\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\",\\"Notification\\":{\\"Message.$\\":\\"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)\\",\\"State.$\\":\\"States.Format('SUCCESS')\\",\\"Details.$\\":\\"$.Remediation.LogData\\",\\"ExecId.$\\":\\"$.Remediation.ExecId\\",\\"AffectedObject.$\\":\\"$.Remediation.AffectedObject\\"}},\\"Next\\":\\"notify\\"},\\"check_ssm_doc_state Error\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)\\",\\"State.$\\":\\"States.Format('LAMBDAERROR')\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\"},\\"Next\\":\\"notify\\"},\\"Security Standard is not enabled\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)\\",\\"State.$\\":\\"States.Format('STANDARDNOTENABLED')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"},\\"No Remediation for Control\\":{\\"Type\\":\\"Pass\\",\\"Parameters\\":{\\"Notification\\":{\\"Message.$\\":\\"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)\\",\\"State.$\\":\\"States.Format('NOREMEDIATION')\\",\\"updateSecHub\\":\\"yes\\"},\\"EventType.$\\":\\"$.EventType\\",\\"Finding.$\\":\\"$.Finding\\",\\"AccountId.$\\":\\"$.AutomationDocument.AccountId\\",\\"AutomationDocId.$\\":\\"$.AutomationDocument.AutomationDocId\\",\\"RemediationRole.$\\":\\"$.AutomationDocument.RemediationRole\\",\\"ControlId.$\\":\\"$.AutomationDocument.ControlId\\",\\"SecurityStandard.$\\":\\"$.AutomationDocument.SecurityStandard\\",\\"SecurityStandardVersion.$\\":\\"$.AutomationDocument.SecurityStandardVersion\\"},\\"Next\\":\\"notify\\"}}},\\"ItemsPath\\":\\"$.Findings\\"},\\"EOJ\\":{\\"Type\\":\\"Pass\\",\\"Comment\\":\\"END-OF-JOB\\",\\"End\\":true}},\\"TimeoutSeconds\\":900}", + "","Payload.$":"$"}},"Wait for Remediation":{"Type":"Wait","Seconds":15,"Next":"execMonitor"},"Remediation completed?":{"Type":"Choice","Choices":[{"Variable":"$.Remediation.RemediationState","StringEquals":"Failed","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Success","Next":"Remediation Succeeded"},{"Variable":"$.Remediation.ExecState","StringEquals":"TimedOut","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Cancelling","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Cancelled","Next":"Remediation Failed"},{"Variable":"$.Remediation.ExecState","StringEquals":"Failed","Next":"Remediation Failed"}],"Default":"Wait for Remediation"},"Remediation Failed":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","SSMExecution.$":"$.SSMExecution","AutomationDocument.$":"$.AutomationDocument","Notification":{"Message.$":"States.Format('Remediation failed for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)","State.$":"$.Remediation.ExecState","Details.$":"$.Remediation.LogData","ExecId.$":"$.Remediation.ExecId","AffectedObject.$":"$.Remediation.AffectedObject"}},"Next":"notify"},"Remediation Succeeded":{"Type":"Pass","Comment":"Set parameters for notification","Parameters":{"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion","Notification":{"Message.$":"States.Format('Remediation succeeded for {} control {} in account {}: {}', $.AutomationDocument.SecurityStandard, $.AutomationDocument.ControlId, $.AutomationDocument.AccountId, $.Remediation.Message)","State.$":"States.Format('SUCCESS')","Details.$":"$.Remediation.LogData","ExecId.$":"$.Remediation.ExecId","AffectedObject.$":"$.Remediation.AffectedObject"}},"Next":"notify"},"check_ssm_doc_state Error":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('check_ssm_doc_state returned an error: {}', $.AutomationDocument.Message)","State.$":"States.Format('LAMBDAERROR')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"Security Standard is not enabled":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Security Standard ({}) v{} is not enabled.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion)","State.$":"States.Format('STANDARDNOTENABLED')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"},"No Remediation for Control":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Security Standard {} v{} control {} has no automated remediation.', $.AutomationDocument.SecurityStandard, $.AutomationDocument.SecurityStandardVersion, $.AutomationDocument.ControlId)","State.$":"States.Format('NOREMEDIATION')","updateSecHub":"yes"},"EventType.$":"$.EventType","Finding.$":"$.Finding","AccountId.$":"$.AutomationDocument.AccountId","AutomationDocId.$":"$.AutomationDocument.AutomationDocId","RemediationRole.$":"$.AutomationDocument.RemediationRole","ControlId.$":"$.AutomationDocument.ControlId","SecurityStandard.$":"$.AutomationDocument.SecurityStandard","SecurityStandardVersion.$":"$.AutomationDocument.SecurityStandardVersion"},"Next":"notify"}}},"ItemsPath":"$.Findings"},"EOJ":{"Type":"Pass","Comment":"END-OF-JOB","End":true}},"TimeoutSeconds":900}", ], ], }, - "LoggingConfiguration": Object { - "Destinations": Array [ - Object { - "CloudWatchLogsLogGroup": Object { - "LogGroupArn": Object { - "Fn::Join": Array [ + "LoggingConfiguration": { + "Destinations": [ + { + "CloudWatchLogsLogGroup": { + "LogGroupArn": { + "Fn::Join": [ "", - Array [ + [ "arn:", - Object { + { "Ref": "AWS::Partition", }, ":logs:eu-west-1:111111111111:log-group:ORCH_LOG_GROUP:*", @@ -1383,8 +1381,8 @@ Object { "IncludeExecutionData": true, "Level": "ALL", }, - "RoleArn": Object { - "Fn::GetAtt": Array [ + "RoleArn": { + "Fn::GetAtt": [ "orchestratorRole12B410FD", "Arn", ], @@ -1393,37 +1391,37 @@ Object { }, "Type": "AWS::StepFunctions::StateMachine", }, - "sendNotifications1367638A": Object { - "DependsOn": Array [ + "sendNotifications1367638A": { + "DependsOn": [ "notifyRole40298120", ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { "id": "W58", "reason": "False positive. Access is provided via a policy", }, - Object { + { "id": "W89", "reason": "There is no need to run this lambda in a VPC", }, - Object { + { "id": "W92", "reason": "There is no need for Reserved Concurrency due to low request rate", }, ], }, }, - "Properties": Object { - "Code": Object { + "Properties": { + "Code": { "S3Bucket": "solutions-eu-west-1", "S3Key": "aws-security-hub-automated-response-and-remediation/v1.0.0/lambda/send_notifications.py.zip", }, "Description": "Sends notifications and log messages", - "Environment": Object { - "Variables": Object { - "AWS_PARTITION": Object { + "Environment": { + "Variables": { + "AWS_PARTITION": { "Ref": "AWS::Partition", }, "SOLUTION_ID": "SO0111", @@ -1433,19 +1431,19 @@ Object { }, "FunctionName": "SO0111-SHARR-sendNotifications", "Handler": "send_notifications.lambda_handler", - "Layers": Array [ - Object { + "Layers": [ + { "Ref": "SharrLambdaLayer5BF8F147", }, ], "MemorySize": 256, - "Role": Object { - "Fn::GetAtt": Array [ + "Role": { + "Fn::GetAtt": [ "notifyRole40298120", "Arn", ], }, - "Runtime": "python3.8", + "Runtime": "python3.9", "Timeout": 600, }, "Type": "AWS::Lambda::Function", diff --git a/source/test/member_stack.test.ts b/source/test/member_stack.test.ts index 45187244..08055997 100644 --- a/source/test/member_stack.test.ts +++ b/source/test/member_stack.test.ts @@ -28,7 +28,7 @@ function getCatStack(): cdk.Stack { solutionVersion: 'v1.1.1', solutionDistBucket: 'sharrbukkit', solutionTMN: 'aws-security-hub-automated-response-and-remediation', - runtimePython: lambda.Runtime.PYTHON_3_8 + runtimePython: lambda.Runtime.PYTHON_3_9 }); Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})) return stack; diff --git a/source/test/regex_registry.ts b/source/test/regex_registry.ts index 34da1c4b..92192d78 100644 --- a/source/test/regex_registry.ts +++ b/source/test/regex_registry.ts @@ -341,13 +341,11 @@ export function getRegexRegistry(): RegexRegistry { )); const autoScalingGroupNameTestCase: RegexTestCase = new RegexTestCase( - String.raw`^[\u0020-\uD7FF\uE000-\uFFFD\uD800\uDC00-\uDBFF\uDFFF]{1,255}$`, + String.raw`^.{1,255}$`, 'AutoScaling Group Name', - [], - [] + ['my-group'], + ['', 'a'.repeat(256)], ); - // TODO: This regex has an out-of-order group, it's probably a bug copied from the doc - autoScalingGroupNameTestCase.disable(); registry.addCase(autoScalingGroupNameTestCase); registry.addCase(new RegexTestCase( @@ -406,7 +404,7 @@ export function getRegexRegistry(): RegexRegistry { )); registry.addCase(new RegexTestCase( - String.raw`^[^"'\\ ]{0,512}$`, + String.raw`^[^"'\\ ]{0,512}$`, 'The prefix applied to the log file names.', [], [] @@ -456,13 +454,14 @@ function addIamMatchTestCases(registry: RegexRegistry) { function addAutoScalingMatchTestCases(registry: RegexRegistry) { const autoScalingGroupNameTestCase: RegexMatchTestCase = new RegexMatchTestCase( - String.raw`^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/(.*)$`, + String.raw`^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\d):\d{12}:autoScalingGroup:(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}):autoScalingGroupName/(.{1,255})$`, 'EC2 AutoScaling Group ARN, capture name', - [], - [] + ['arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:00000000-0000-0000-0000-000000000000:autoScalingGroupName/my-group'], + ['arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:00000000-0000-0000-0000-000000000000:autoScalingGroupName/'] ); - // TODO: ES regex engine doesn't support case-insensitive non-capturing groups, just include capitals - autoScalingGroupNameTestCase.disable(); + autoScalingGroupNameTestCase.addMatchTestCase( + 'arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:00000000-0000-0000-0000-000000000000:autoScalingGroupName/my-group', + ['my-group']); registry.addCase(autoScalingGroupNameTestCase); } diff --git a/source/test/runbook_validator.test.ts b/source/test/runbook_validator.test.ts index d1390a30..07abb903 100644 --- a/source/test/runbook_validator.test.ts +++ b/source/test/runbook_validator.test.ts @@ -206,16 +206,16 @@ test.each(runbooks)('%s has correct schema version', (runbook: RunbookTestHelper function getExpectedDocumentName(runbook: RunbookTestHelper): string { if (runbook.isRemediationRunbook()) { - return `SHARR-${runbook.getDocumentName()}`; + return `ASR-${runbook.getDocumentName()}`; } const standard: string = runbook.getStandardName(); switch(standard) { case 'AFSBP': - return `SHARR-AFSBP_1.0.0_${runbook.getControlName()}`; + return `ASR-AFSBP_1.0.0_${runbook.getControlName()}`; case 'CIS120': - return `SHARR-CIS_1.2.0_${runbook.getControlName()}`; + return `ASR-CIS_1.2.0_${runbook.getControlName()}`; case 'PCI321': - return `SHARR-PCI_3.2.1_${runbook.getControlName()}`; + return `ASR-PCI_3.2.1_${runbook.getControlName()}`; default: throw Error(`Unrecognized standard: ${standard}`); } diff --git a/source/test/solution_deploy.test.ts b/source/test/solution_deploy.test.ts index 78e7ece2..2ef38c7d 100644 --- a/source/test/solution_deploy.test.ts +++ b/source/test/solution_deploy.test.ts @@ -23,16 +23,16 @@ import { Aspects } from '@aws-cdk/core' function getTestStack(): cdk.Stack { const envEU = { account: '111111111111', region: 'eu-west-1' }; const app = new cdk.App(); - const stack = new SolutionDeploy.SolutionDeployStack(app, 'stack', { + const stack = new SolutionDeploy.SolutionDeployStack(app, 'stack', { env: envEU, solutionId: 'SO0111', solutionVersion: 'v1.0.0', solutionDistBucket: 'solutions', solutionTMN: 'aws-security-hub-automated-response-and-remediation', solutionName: 'AWS Security Hub Automated Response & Remediation', - runtimePython: lambda.Runtime.PYTHON_3_8, + runtimePython: lambda.Runtime.PYTHON_3_9, orchLogGroup: 'ORCH_LOG_GROUP' - + }) Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})) return stack; diff --git a/source/test/ssmplaybook.test.ts b/source/test/ssmplaybook.test.ts index 2d4c7c6c..129cc9ec 100644 --- a/source/test/ssmplaybook.test.ts +++ b/source/test/ssmplaybook.test.ts @@ -49,7 +49,7 @@ function getSsmPlaybook(): Stack { test('Test SsmPlaybook Generation', () => { expectCDK(getSsmPlaybook()).to(haveResourceLike("AWS::SSM::Document", { "Content": { - "description": "### Document Name - SHARR-SECTEST_1.2.3_TEST.1\n", + "description": "### Document Name - ASR-SECTEST_1.2.3_TEST.1\n", "schemaVersion": "0.3", "assumeRole": "{{ AutomationAssumeRole }}", "outputs": [ @@ -117,7 +117,7 @@ function getSsmRemediationRunbook(): Stack { test('Test Shared Remediation Generation', () => { expectCDK(getSsmRemediationRunbook()).to(haveResourceLike("AWS::SSM::Document", { "Content": { - "description": "### Document Name - SHARR-CIS_1.2.0_2.9\n", + "description": "### Document Name - ASR-CIS_1.2.0_2.9\n", "schemaVersion": "0.3", "assumeRole": "{{ AutomationAssumeRole }}", "outputs": [ diff --git a/source/test/test_data/tstest-cis29.yaml b/source/test/test_data/tstest-cis29.yaml index 04063781..698194af 100644 --- a/source/test/test_data/tstest-cis29.yaml +++ b/source/test/test_data/tstest-cis29.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-CIS_1.2.0_2.9 + ### Document Name - ASR-CIS_1.2.0_2.9 schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: @@ -12,4 +12,3 @@ parameters: type: String description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. default: '' - \ No newline at end of file diff --git a/source/test/test_data/tstest-rds1.yaml b/source/test/test_data/tstest-rds1.yaml index 995f7ff7..965ff08f 100644 --- a/source/test/test_data/tstest-rds1.yaml +++ b/source/test/test_data/tstest-rds1.yaml @@ -1,5 +1,5 @@ description: | - ### Document Name - SHARR-SECTEST_1.2.3_TEST.1 + ### Document Name - ASR-SECTEST_1.2.3_TEST.1 schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' outputs: @@ -12,4 +12,3 @@ parameters: type: String description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. default: '' - \ No newline at end of file diff --git a/source/test/test_data/tstest-runbook.yaml b/source/test/test_data/tstest-runbook.yaml index b52ee8c1..ee1ce3e8 100644 --- a/source/test/test_data/tstest-runbook.yaml +++ b/source/test/test_data/tstest-runbook.yaml @@ -1,8 +1,8 @@ description: | - ### Document Name - SHARR-EnableCloudTrailToCloudWatchLogging + ### Document Name - ASR-EnableCloudTrailToCloudWatchLogging ## What does this document do? Creates a CloudWatch logs group for CloudTrail data. - + ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * KMSKeyArn (from SSM): Arn of the KMS key to be used to encrypt data @@ -26,16 +26,16 @@ 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. - allowedPattern: '^[A-Za-z0-9_-\/.]{1,512}$' + allowedPattern: '^[A-Za-z0-9_-\/.]{1,512}$' outputs: - UpdateTrailToCWLogs.Output mainSteps: - - + - name: CreateLogGroup action: 'aws:executeAPI' inputs: @@ -47,11 +47,11 @@ mainSteps: - Name: Output Selector: $ Type: StringMap - - + - name: WaitForCreation action: 'aws:executeScript' inputs: - InputPayload: + InputPayload: LogGroup: '{{LogGroupName}}' Runtime: python3.8 Handler: wait_for_loggroup @@ -64,7 +64,7 @@ mainSteps: isEnd: false - - + - name: UpdateTrailToCWLogs action: 'aws:executeAPI' inputs: @@ -78,4 +78,3 @@ mainSteps: - Name: Output Selector: $ Type: StringMap - \ No newline at end of file