// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Global Roles Stack 1`] = ` { "Description": "test;", "Parameters": { "SecHubAdminAccount": { "AllowedPattern": "^\\d{12}$", "Description": "Admin account number", "Type": "String", }, }, "Resources": { "OrchestratorMemberRoleMemberAccountRoleBE9AD9D5": { "Metadata": { "cfn_nag": { "rules_to_suppress": [ { "id": "W11", "reason": "Resource * is required due to the administrative nature of the solution.", }, { "id": "W28", "reason": "Static names chosen intentionally to provide integration in cross-account permissions", }, ], }, }, "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "AWS": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::", { "Ref": "SecHubAdminAccount", }, ":role/SO0111-SHARR-Orchestrator-Admin", ], ], }, }, }, { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ssm.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "iam:PassRole", "iam:GetRole", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::", { "Ref": "AWS::AccountId", }, ":role/SO0111-*", ], ], }, }, { "Action": "ssm:StartAutomationExecution", "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*:", { "Ref": "AWS::AccountId", }, ":document/ASR-*", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*:", { "Ref": "AWS::AccountId", }, ":automation-definition/*", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*::automation-definition/*", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*:", { "Ref": "AWS::AccountId", }, ":automation-execution/*", ], ], }, ], }, { "Action": [ "ssm:DescribeAutomationExecutions", "ssm:GetAutomationExecution", ], "Effect": "Allow", "Resource": "*", }, { "Action": "ssm:DescribeDocument", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*:*:document/*", ], ], }, }, { "Action": [ "ssm:GetParameters", "ssm:GetParameter", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:*:*:parameter/Solutions/SO0111/*", ], ], }, }, { "Action": "config:DescribeConfigRules", "Effect": "Allow", "Resource": "*", }, { "Action": [ "cloudwatch:PutMetricData", "securityhub:BatchUpdateFindings", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "member_orchestrator", }, ], "RoleName": "SO0111-SHARR-Orchestrator-Member", }, "Type": "AWS::IAM::Role", }, }, } `; exports[`Regional Documents 1`] = ` { "Description": "test;", "Parameters": { "WaitProviderServiceToken": { "Type": "String", }, }, "Resources": { "ASRConfigureS3BucketPublicAccessBlock": { "DependsOn": [ "CreateWait4", ], "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. ", "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", }, "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)", }, "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": "AWS::SSM::Document", }, "ASRConfigureS3PublicAccessBlock": { "DependsOn": [ "CreateWait5", ], "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. ", "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", }, "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)", }, "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": "AWS::SSM::Document", }, "ASRConfigureSNSTopicForStack": { "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document name - ASR-ConfigureSNSTopicForStack ## What does this document do? This document creates an SNS topic if it does not already exist, then updates the stack to notify the topic on changes ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. * StackArn: (Required) The ARN of the stack. ## Security Standards / Controls * AFSBP v1.0.0: CloudFormation.1 ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "lambda_handler", "InputPayload": { "stack_arn": "{{ StackArn }}", "topic_name": "SO0111-ASR-CloudFormationNotifications", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ Configure a CloudFormation stack with an SNS topic for notifications, creating the topic if it does not already exist """ from time import time, sleep import boto3 from botocore.config import Config boto_config = Config(retries={ 'mode': 'standard' }) def lambda_handler(event, _): """ Configure a CloudFormation stack with an SNS topic for notifications, creating the topic if it does not already exist \`event\` should have the following keys and values: \`stack_arn\`: the ARN of the CloudFormation stack to be updated \`topic_name\`: the name of the SQS Queue to create and configure for notifications \`context\` is ignored """ stack_arn = event['stack_arn'] topic_name = event['topic_name'] topic_arn = get_or_create_topic(topic_name) configure_notifications(stack_arn, topic_arn) wait_for_update(stack_arn) return assert_stack_configured(stack_arn, topic_arn) def get_or_create_topic(topic_name: str): """Get the SQS topic arn for the given topic name, creating it if it does not already exist""" sns = boto3.client('sns', config=boto_config) response = sns.create_topic(Name=topic_name) return response['TopicArn'] def configure_notifications(stack_arn: str, topic_arn: str): """Configure the stack with ARN \`stack_arn\` to notify the queue with ARN \`topic_arn\`""" cloudformation = boto3.resource('cloudformation', config=boto_config) stack = cloudformation.Stack(stack_arn) kwargs = { 'UsePreviousTemplate': True, 'NotificationARNs': [topic_arn]} if stack.parameters: kwargs['Parameters'] = [{ 'ParameterKey': param['ParameterKey'], 'UsePreviousValue': True } for param in stack.parameters] if stack.capabilities: kwargs['Capabilities'] = stack.capabilities stack.update(**kwargs) class UpdateTimeoutException(Exception): """Timed out waiting for the CloudFormation stack to update""" def wait_for_update(stack_arn: str): """Wait for the stack with ARN \`stack_arn\` to be in status \`UPDATE_COMPLETE\`""" wait_interval_seconds = 10 timeout_seconds = 300 start = time() while get_stack_status(stack_arn) != 'UPDATE_COMPLETE': if time() - start > timeout_seconds: raise UpdateTimeoutException('Timed out waiting for stack update') wait_seconds(wait_interval_seconds) wait_interval_seconds = wait_interval_seconds * 2 def get_stack_status(stack_arn): """Get the status of the CloudFormation stack with ARN \`stack_arn\`""" cloudformation = boto3.client('cloudformation', config=boto_config) response = cloudformation.describe_stacks(StackName=stack_arn) return response['Stacks'][0]['StackStatus'] def wait_seconds(seconds): """Wait for \`seconds\` seconds""" sleep(seconds) def assert_stack_configured(stack_arn, topic_arn): """ Verify that the CloudFormation stack with ARN \`stack_arn\` is configured to update the SQS topic with ARN \`topic_arn\` """ cloudformation = boto3.resource('cloudformation', config=boto_config) stack = cloudformation.Stack(stack_arn) wait_interval_seconds = 10 timeout_seconds = 300 start = time() while stack.notification_arns != [topic_arn]: if time() - start > timeout_seconds: raise StackConfigurationFailedException( 'Timed out waiting for stack configuration to take effect') wait_seconds(wait_interval_seconds) wait_interval_seconds = wait_interval_seconds * 2 stack.reload() return { 'NotificationARNs': stack.notification_arns } class StackConfigurationFailedException(Exception): """An error occurred updating the CloudFormation stack to notify the SQS topic"""", }, "isEnd": true, "name": "ConfigureSNSTopic", "outputs": [ { "Name": "Output", "Selector": "$.Payload.output", "Type": "StringMap", }, ], "timeoutSeconds": 600, }, ], "outputs": [ "ConfigureSNSTopic.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", }, "StackArn": { "allowedPattern": "^(arn:(?:aws|aws-us-gov|aws-cn):cloudformation:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:stack/[a-zA-Z][a-zA-Z0-9-]{0,127}/[a-fA-F0-9]{8}-(?:[a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12})$", "description": "(Required) The ARN of the CloudFormation stack.", "type": "String", }, "TopicName": { "allowedPattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]{0,255}$", "default": "SO0111-ASR-CloudFormationNotifications", "description": "(Optional) The name of the SNS topic to create and configure for notifications.", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-ConfigureSNSTopicForStack", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASRCreateAccessLoggingBucket": { "DependsOn": [ "CreateWait1", ], "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import boto3 from botocore.exceptions import ClientError from botocore.config import Config from typing import TYPE_CHECKING, Dict if TYPE_CHECKING: from mypy_boto3_s3 import S3Client from aws_lambda_powertools.utilities.typing import LambdaContext else: S3Client = object LambdaContext = object def connect_to_s3(boto_config: Config) -> S3Client: return boto3.client("s3", config=boto_config) def create_logging_bucket(event: Dict, _: LambdaContext) -> Dict: 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", "ObjectOwnership": "ObjectWriter", } 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"] != "BucketAlreadyOwnedByYou": exit(str(error)) else: return { "output": { "Message": f'Bucket {event["BucketName"]} already exists and is owned by you' } } except Exception as e: print(e) exit(str(e))", }, "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": "AWS::SSM::Document", }, "ASRCreateCloudTrailMultiRegionTrail": { "DependsOn": [ "CreateWait0", ], "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import boto3 from botocore.config import Config from botocore.exceptions import ClientError from typing import TYPE_CHECKING, Dict if TYPE_CHECKING: from mypy_boto3_s3 import S3Client from aws_lambda_powertools.utilities.typing import LambdaContext else: S3Client = object LambdaContext = object def connect_to_s3() -> S3Client: return boto3.client("s3", config=Config(retries={"mode": "standard"})) def create_logging_bucket(event: Dict, _: LambdaContext) -> Dict: s3 = connect_to_s3() kms_key_arn: str = event["kms_key_arn"] aws_account: str = event["account"] aws_region: str = 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: S3Client, bucket_name: str, aws_region: str) -> str: try: kwargs = { "Bucket": bucket_name, "ACL": "private", "ObjectOwnership": "ObjectWriter", } if aws_region != "us-east-1": kwargs["CreateBucketConfiguration"] = {"LocationConstraint": aws_region} s3.create_bucket(**kwargs) return "success" except ClientError as ex: exception_type = ex.response["Error"]["Code"] # bucket already exists - return if exception_type == "BucketAlreadyOwnedByYou": print("Bucket " + bucket_name + " already exists and is owned by you") 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: S3Client, bucket_name: str, kms_key_arn: str) -> None: 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: S3Client, bucket_name: str) -> None: 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: S3Client, bucket_name: str) -> None: 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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): 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 == "BucketAlreadyOwnedByYou": print("Bucket " + bucket_name + " already exists and is owned by you") return "bucket_exists" else: exit("Error creating bucket " + bucket_name + " " + str(client_ex)) except Exception as e: exit("Error creating bucket " + bucket_name + " " + str(e)) def put_bucket_encryption(s3, bucket_name, kms_key_arn): try: s3.put_bucket_encryption( Bucket=bucket_name, ServerSideEncryptionConfiguration={ "Rules": [ { "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "aws:kms", "KMSMasterKeyID": kms_key_arn.split("key/")[1], } } ] }, ) except Exception as e: print(e) exit( "Error applying encryption to bucket " + bucket_name + " with key " + kms_key_arn ) def put_public_access_block(s3, bucket_name): try: s3.put_public_access_block( Bucket=bucket_name, PublicAccessBlockConfiguration={ "BlockPublicAcls": True, "IgnorePublicAcls": True, "BlockPublicPolicy": True, "RestrictPublicBuckets": True, }, ) except Exception as e: exit(f"Error setting public access block for bucket {bucket_name}: {str(e)}") def put_bucket_logging(s3, bucket_name, logging_bucket): try: s3.put_bucket_logging( Bucket=bucket_name, BucketLoggingStatus={ "LoggingEnabled": { "TargetBucket": logging_bucket, "TargetPrefix": "cloudtrail-access-logs", } }, ) except Exception as e: print(e) exit("Error setting public access block for bucket " + bucket_name)", }, "isEnd": false, "name": "CreateCloudTrailBucket", "outputs": [ { "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): 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" }, } }, { "Sid": "AllowSSLRequestsOnly", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:" + aws_partition + ":s3:::" + cloudtrail_bucket ,"arn:" + aws_partition + ":s3:::" + cloudtrail_bucket + "/*"], "Condition": { "Bool": { "aws:SecureTransport": "false" } } } ] } s3.put_bucket_policy( Bucket=cloudtrail_bucket, Policy=json.dumps(bucket_policy) ) return { "output": { "Message": f'Set bucket policy for bucket {cloudtrail_bucket}' } } except Exception as e: print(e) exit('PutBucketPolicy failed: ' + str(e))", }, "isEnd": false, "name": "CreateCloudTrailBucketPolicy", }, { "action": "aws:executeScript", "inputs": { "Handler": "enable_cloudtrail", "InputPayload": { "cloudtrail_bucket": "{{CreateCloudTrailBucket.CloudTrailBucketName}}", "kms_key_arn": "{{KMSKeyArn}}", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): boto_config = Config( retries ={ 'mode': 'standard' } ) ct = connect_to_cloudtrail(boto_config) try: ct.create_trail( Name='multi-region-cloud-trail', S3BucketName=event['cloudtrail_bucket'], IncludeGlobalServiceEvents=True, EnableLogFileValidation=True, IsMultiRegionTrail=True, KmsKeyId=event['kms_key_arn'] ) ct.start_logging( Name='multi-region-cloud-trail' ) return { "output": { "Message": f'CloudTrail Trail multi-region-cloud-trail created' } } except Exception as e: exit('Error enabling AWS Config: ' + str(e))", }, "isEnd": false, "name": "EnableCloudTrail", "outputs": [ { "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 def process_results(event, _): 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": "AWS::SSM::Document", }, "ASRCreateIAMSupportRole": { "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CreateIAMSupportRole ## What does this document do? This document creates a role to allow AWS Support access. ## Input Parameters * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * CreateIAMRole.Output ", "mainSteps": [ { "action": "aws:executeScript", "description": "## CreateIAMSupportRole 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": "create_iam_role", "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json from botocore.config import Config import boto3 BOTO_CONFIG = Config(retries={"mode": "standard"}) responses = {} responses["CreateIAMRoleResponse"] = [] def connect_to_iam(boto_config): return boto3.client("iam", config=boto_config) def get_account(boto_config): return boto3.client('sts', config=boto_config).get_caller_identity()['Account'] def get_partition(boto_config): return boto3.client('sts', config=boto_config).get_caller_identity()['Arn'].split(':')[1] def create_iam_role(_, __): account = get_account(BOTO_CONFIG) partition = get_partition(BOTO_CONFIG) aws_support_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRole", "Principal": {"AWS": f"arn:{partition}:iam::{account}:root"}, } ], } role_name = "aws_incident_support_role" iam = connect_to_iam(BOTO_CONFIG) if not does_role_exist(iam, role_name): iam.create_role( RoleName=role_name, AssumeRolePolicyDocument=json.dumps(aws_support_policy), Description="Created by ASR security hub remediation 1.20 rule", Tags=[ {"Key": "Name", "Value": "CIS 1.20 aws support access role"}, ], ) iam.attach_role_policy( RoleName=role_name, PolicyArn=f"arn:{partition}:iam::aws:policy/AWSSupportAccess", ) responses["CreateIAMRoleResponse"].append( {"Account": account, "RoleName": role_name} ) return {"output": "IAM role creation is successful.", "http_responses": responses} def does_role_exist(iam, role_name): """Check if the role name exists. Parameters ---------- iam: iam client, required role_name: string, required Returns ------ bool: returns if the role exists """ role_exists = False try: response = iam.get_role(RoleName=role_name) if "Role" in response: role_exists = True except iam.exceptions.NoSuchEntityException: role_exists = False return role_exists", }, "isEnd": true, "name": "CreateIAMSupportRole", "outputs": [ { "Name": "Output", "Selector": "$.Payload", "Type": "StringMap", }, ], "timeoutSeconds": 300, }, ], "outputs": [ "CreateIAMSupportRole.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", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-CreateIAMSupportRole", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASRCreateLogMetricFilterAndAlarm": { "DependsOn": [ "CreateWait0", ], "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): kms_key_arn = event['kms_key_arn'] new_topic = False topic_arn = '' topic_name = event['topic_name'] try: sns = connect_to_sns() topic_arn = sns.create_topic( Name=topic_name, Attributes={ 'KmsMasterKeyId': kms_key_arn.split('key/')[1] } )['TopicArn'] new_topic = True except ClientError as client_exception: exception_type = client_exception.response['Error']['Code'] if exception_type == 'InvalidParameter': print(f'Topic {topic_name} already exists. This remediation may have been run before.') print('Ignoring exception - remediation continues.') topic_arn = sns.create_topic( Name=topic_name )['TopicArn'] else: exit(f'ERROR: Unhandled client exception: {client_exception}') except Exception as e: exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') if new_topic: try: ssm = connect_to_ssm() ssm.put_parameter( Name='/Solutions/SO0111/SNS_Topic_CIS3.x', Description='SNS Topic for AWS Config updates', Type='String', Overwrite=True, Value=topic_arn ) except Exception as e: exit(f'ERROR: could not create SNS Topic {topic_name}: {str(e)}') create_topic_policy(topic_arn) return {"topic_arn": topic_arn} def create_topic_policy(topic_arn): sns = connect_to_sns() try: topic_policy = { "Id": "Policy_ID", "Statement": [ { "Sid": "AWSConfigSNSPolicy", "Effect": "Allow", "Principal": { "Service": "cloudwatch.amazonaws.com" }, "Action": "SNS:Publish", "Resource": topic_arn, }] } sns.set_topic_attributes( TopicArn=topic_arn, AttributeName='Policy', AttributeValue=json.dumps(topic_policy) ) except Exception as e: exit(f'ERROR: Failed to SetTopicAttributes for {topic_arn}: {str(e)}')", }, "name": "CreateTopic", "outputs": [ { "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): 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": "AWS::SSM::Document", }, "ASRDisablePublicAccessToRDSInstance": { "DependsOn": [ "CreateWait7", ], "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. ## Troubleshooting * ModifyDBInstance isn't supported for a DB instance in a Multi-AZ DB Cluster. - This remediation will not work on an instance within a MySQL or PostgreSQL Multi-AZ Cluster due to limitations with the RDS API. ", "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", }, "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", }, "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", }, "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": "AWS::SSM::Document", }, "ASRDisablePublicAccessToRedshiftCluster": { "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document name - ASR-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. ", "mainSteps": [ { "action": "aws:executeAwsApi", "description": "## DisableRedshiftPubliclyAccessible Disables public accessibility for the cluster specified in the ClusterIdentifer parameter. ## Outputs * Response: The standard HTTP response from the ModifyCluster API call. ", "inputs": { "Api": "ModifyCluster", "ClusterIdentifier": "{{ ClusterIdentifier }}", "PubliclyAccessible": false, "Service": "redshift", }, "isEnd": false, "name": "DisableRedshiftPubliclyAccessible", "outputs": [ { "Name": "Response", "Selector": "$", "Type": "StringMap", }, ], "timeoutSeconds": 600, }, { "action": "aws:waitForAwsResourceProperty", "description": "## WaitForRedshiftClusterAvailability Waits for the state of the cluster to change to available. ", "inputs": { "Api": "DescribeClusters", "ClusterIdentifier": "{{ ClusterIdentifier }}", "DesiredValues": [ "available", ], "PropertySelector": "$.Clusters[0].ClusterStatus", "Service": "redshift", }, "isEnd": false, "name": "WaitForRedshiftClusterAvailability", "timeoutSeconds": 600, }, { "action": "aws:assertAwsResourceProperty", "description": "## VerifyRedshiftPubliclyAccessible Confirms the public accessibility setting is disabled on the cluster. ", "inputs": { "Api": "DescribeClusters", "ClusterIdentifier": "{{ ClusterIdentifier }}", "DesiredValues": [ "False", ], "PropertySelector": "$.Clusters[0].PubliclyAccessible", "Service": "redshift", }, "isEnd": true, "name": "VerifyRedshiftPubliclyAccessible", "timeoutSeconds": 600, }, ], "outputs": [ "DisableRedshiftPubliclyAccessible.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}(?= 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_seconds(wait_interval): time.sleep(wait_interval) def wait_for_loggroup(client, wait_interval, max_retries, loggroup): attempts = 1 while not log_group_exists(client, loggroup): wait_for_seconds(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): wait_for_seconds(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, _): """ remediates CloudTrail.2 by enabling SSE-KMS On success returns a string map On failure returns NoneType """ max_retries = event.get('retries', 12) # max number of waits for actions to complete. wait_interval = event.get('wait', 5) # how many seconds between attempts boto_config_args = { 'retries': { 'mode': 'standard' } } boto_config = Config(**boto_config_args) if 'vpc' not in event or 'remediation_role' not in event or 'kms_key_arn' not in event: exit('Error: missing vpc from input') logs_client = connect_to_logs(boto_config) ec2_client = connect_to_ec2(boto_config) kms_key_arn = event['kms_key_arn'] # for logs encryption at rest # set dynamic variable for CW Log Group for VPC Flow Logs vpc_flow_loggroup = "VPCFlowLogs/" + event['vpc'] # create cloudwatch log group try: logs_client.create_log_group( logGroupName=vpc_flow_loggroup, kmsKeyId=kms_key_arn ) except ClientError as client_error: exception_type = client_error.response['Error']['Code'] if exception_type in ["ResourceAlreadyExistsException"]: print(f'CloudWatch Logs group {vpc_flow_loggroup} already exists') else: exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') except Exception as e: exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(e)}') # wait for CWL creation to propagate wait_for_loggroup(logs_client, wait_interval, max_retries, vpc_flow_loggroup) # create VPC Flow Logging try: ec2_client.create_flow_logs( DryRun=False, DeliverLogsPermissionArn=event['remediation_role'], LogGroupName=vpc_flow_loggroup, ResourceIds=[event['vpc']], ResourceType='VPC', TrafficType='REJECT', LogDestinationType='cloud-watch-logs' ) except ClientError as client_error: exception_type = client_error.response['Error']['Code'] if exception_type in ["FlowLogAlreadyExists"]: return { "response": { "message": f'VPC Flow Logs for {event["vpc"]} already enabled', "status": "Success" } } else: exit(f'ERROR CREATING LOGGROUP {vpc_flow_loggroup}: {str(exception_type)}') except Exception as e: exit(f'create_flow_logs failed {str(e)}') # wait for Flow Log creation to propagate. Exits on timeout (no need to check results) wait_for_flowlogs(ec2_client, wait_interval, max_retries, vpc_flow_loggroup) # wait_for_flowlogs will exit if unsuccessful after max_retries * wait_interval (60 seconds by default) return { "response": { "message": f'VPC Flow Logs enabled for {event["vpc"]} to {vpc_flow_loggroup}', "status": "Success" } }", }, "isEnd": true, "name": "Remediation", "outputs": [ { "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": "ASR-EnableVPCFlowLogs", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASREncryptRDSSnapshot": { "DependsOn": [ "CreateWait3", ], "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", }, "name": "VerifyRdsEncryptedSnapshot", "timeoutSeconds": 14400, }, { "action": "aws:executeAwsApi", "inputs": { "Api": "DeleteDBSnapshot", "DBSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", "Service": "rds", }, "isEnd": true, "name": "DeleteUnencryptedRdsSnapshot", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "CopyDBClusterSnapshot", "CopyTags": true, "KmsKeyId": "{{KmsKeyId}}", "Service": "rds", "SourceDBClusterSnapshotIdentifier": "{{SourceDBSnapshotIdentifier}}", "TargetDBClusterSnapshotIdentifier": "{{TargetDBSnapshotIdentifier}}", }, "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": "AWS::SSM::Document", }, "ASRMakeEBSSnapshotsPrivate": { "DependsOn": [ "CreateWait1", ], "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): account_id = event['account_id'] if 'testmode' in event and event['testmode']: return [ "snap-12341234123412345", "snap-12341234123412345", "snap-12341234123412345", "snap-12341234123412345", "snap-12341234123412345" ] return list_public_snapshots(account_id) def list_public_snapshots(account_id): ec2 = connect_to_ec2(boto_config) control_token = 'start' try: public_snapshot_ids = [] while control_token: if control_token == 'start': # needed a value to start the loop. Now reset it control_token = '' kwargs = { 'MaxResults': 100, 'OwnerIds': [ account_id ], 'RestorableByUserIds': [ 'all' ] } if control_token: kwargs['NextToken'] = control_token response = ec2.describe_snapshots( **kwargs ) for snapshot in response['Snapshots']: public_snapshot_ids.append(snapshot['SnapshotId']) if 'NextToken' in response: control_token = response['NextToken'] else: control_token = '' return public_snapshot_ids except Exception as e: print(e) exit('Failed to describe_snapshots')", }, "name": "GetPublicSnapshotIds", "outputs": [ { "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): boto_config = Config( retries = { 'mode': 'standard', 'max_attempts': 10 } ) ec2 = connect_to_ec2(boto_config) remediated = [] snapshots = event['snapshots'] success_count = 0 for snapshot_id in snapshots: try: ec2.modify_snapshot_attribute( Attribute='CreateVolumePermission', CreateVolumePermission={ 'Remove': [{'Group': 'all'}] }, SnapshotId=snapshot_id ) print(f'Snapshot {snapshot_id} permissions set to private') remediated.append(snapshot_id) success_count += 1 except Exception as e: print(e) print(f'FAILED to remediate Snapshot {snapshot_id}') result=json.dumps(ec2.describe_snapshots( SnapshotIds=remediated ), indent=2, default=str) print(result) return { "response": { "message": f'{success_count} of {len(snapshots)} Snapshot permissions set to private', "status": "Success" } }", }, "name": "Remediation", "outputs": [ { "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": "AWS::SSM::Document", }, "ASRMakeRDSSnapshotPrivate": { "DependsOn": [ "CreateWait2", ], "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, _): 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)}')", }, "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": { "DependsOn": [ "CreateWait2", ], "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 * 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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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 public_s3_statement_check(statement, principal): """ This function checks if the user has given access to an S3 bucket without providing an AWS account. """ try: empty_source_account_check = False if ("StringEquals" in statement["Condition"]): empty_source_account_check = ("AWS:SourceAccount" not in statement["Condition"]["StringEquals"]) else: empty_source_account_check = True return principal.get("Service", "") == "s3.amazonaws.com" and empty_source_account_check except KeyError: return principal.get("Service", "") == "s3.amazonaws.com" 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): if principal == "*" or (isinstance(principal, dict) and (principal.get("AWS","") == "*" or public_s3_statement_check(statement, principal))): print_policy_before(statement) remove_resource_policy(functionname, statement['Sid'], client) def remove_lambda_public_access(event, _): 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, 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": "ASR-RemoveLambdaPublicAccess", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASRRemoveVPCDefaultSecurityGroupRules": { "DependsOn": [ "CreateWait6", ], "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", }, "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." }", }, "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": "AWS::SSM::Document", }, "ASRReplaceCodeBuildClearTextCredentials": { "DependsOn": [ "CreateWait2", ], "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 ", "mainSteps": [ { "action": "aws:executeAwsApi", "description": "## BatchGetProjects Gets information about one or more build projects. ", "inputs": { "Api": "BatchGetProjects", "Service": "codebuild", "names": [ "{{ ProjectName }}", ], }, "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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 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 ASR', 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 ASR", 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, _): 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 }", }, "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 }}", }, "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": "AWS::SSM::Document", }, "ASRRevokeUnrotatedKeys": { "DependsOn": [ "CreateWait2", ], "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 ", "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": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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 RuntimeError(error_message) return { "output": "Verification of unrotated access keys is successful.", "http_responses": responses } def unrotated_key_handler(event, _): 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)", }, "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]\\d{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": "AWS::SSM::Document", }, "ASRRevokeUnusedIAMUserCredentials": { "DependsOn": [ "CreateWait7", ], "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": "ASR-RevokeUnusedIAMUserCredentials", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASRS3BlockDenylist": { "DependsOn": [ "CreateWait3", ], "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": "# 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, bucket_name): self.bucket_name = bucket_name 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 principal_arn not 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 resource_arn not in self.deny_statement_json.Resource: self.deny_statement_json["Resource"].append(resource_arn) def update_bucket_policy(event, _): 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}')", }, "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": "AWS::SSM::Document", }, "ASRSetIAMPasswordPolicy": { "DependsOn": [ "CreateWait7", ], "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 }}", }, "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 { "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.")", }, "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": "ASR-SetIAMPasswordPolicy", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ASRSetSSLBucketPolicy": { "DependsOn": [ "CreateWait2", ], "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}}", "partition": "{{global:AWS_PARTITION}}", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 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, partition): return { "Sid": "AllowSSLRequestsOnly", "Action": "s3:*", "Effect": "Deny", "Resource": [ f'arn:{partition}:s3:::{bucket}', f'arn:{partition}: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, _): bucket_name = event['bucket'] account_id = event['accountid'] aws_partition = event['partition'] 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, aws_partition)) try: result = s3.put_bucket_policy( Bucket=bucket_name, Policy=json.dumps(bucket_policy, indent=4, default=str), ExpectedBucketOwner=account_id ) print(result) except ClientError as ex: exception_type = ex.response['Error']['Code'] exit(f'ERROR: Boto3 s3 ClientError: {exception_type} - {str(ex)}') except Exception as e: exit(f'ERROR putting bucket policy for {bucket_name}: {str(e)}') print(f'New policy: {bucket_policy}')", }, "name": "Remediation", "outputs": [ { "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": "AWS::SSM::Document", }, "CreateWait0": { "DeletionPolicy": "Delete", "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "745c05bc80a76c7b162d697b58125375f920159a74e215966a32f7ae82f8707b", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait1": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait0", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "ed6f0fed953f12a30fce8949d3584bea716efea4cf8d55d2b8d6bff413b7c3ee", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait2": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait1", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "d9d70554583f2debde0daaee77134a4f27f295524be0a9fbc1f5896899b12862", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait3": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait2", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "d8cb9fbe52d6cb8d12cf3f418251ea9ecf2a823155b7aeceaefc304e2999d008", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait4": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait3", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "cbb21589711741582f016da0204b949e6bb393744d53b01dcc5383669d106dea", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait5": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait4", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "6beab64cd1e1e29ec0dfac301f88b9a6eb2cf699bf743fbecf0fb88eca6f4f5c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait6": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait5", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "76ac9234ed21512ab7274d9f95c6a1ef847748fa697d122f7c3e1d7a1d74f010", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait7": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait6", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "971be703d38efdd2d75a17325953419f3244f959e839b5a0668b12146968ac0c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "CreateWait8": { "DeletionPolicy": "Delete", "DependsOn": [ "CreateWait7", ], "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "dff26ffe5bc8a2eb7a84297f591aa46181b17efa8b7c977636db9cf3beeec61c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRCreateCloudTrailMultiRegionTrail", "ASRCreateLogMetricFilterAndAlarm", "ASREnableAutoScalingGroupELBHealthCheck", "ASREnableAWSConfig", "ASREnableCloudTrailToCloudWatchLogging", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "745c05bc80a76c7b162d697b58125375f920159a74e215966a32f7ae82f8707b", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait1": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRCreateAccessLoggingBucket", "ASREnableCloudTrailEncryption", "ASREnableDefaultEncryptionS3", "ASREnableVPCFlowLogs", "ASRMakeEBSSnapshotsPrivate", "DeletWait0", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "ed6f0fed953f12a30fce8949d3584bea716efea4cf8d55d2b8d6bff413b7c3ee", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait2": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRMakeRDSSnapshotPrivate", "ASRRemoveLambdaPublicAccess", "ASRReplaceCodeBuildClearTextCredentials", "ASRRevokeUnrotatedKeys", "ASRSetSSLBucketPolicy", "DeletWait1", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "d9d70554583f2debde0daaee77134a4f27f295524be0a9fbc1f5896899b12862", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait3": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRDisablePublicAccessToRedshiftCluster", "ASREnableAutomaticVersionUpgradeOnRedshiftCluster", "ASREnableRedshiftClusterAuditLogging", "ASREncryptRDSSnapshot", "ASRS3BlockDenylist", "DeletWait2", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "d8cb9fbe52d6cb8d12cf3f418251ea9ecf2a823155b7aeceaefc304e2999d008", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait4": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRConfigureS3BucketPublicAccessBlock", "ASRConfigureSNSTopicForStack", "ASRCreateIAMSupportRole", "ASREnableAutomaticSnapshotsOnRedshiftCluster", "ASREnableEncryptionForSQSQueue", "DeletWait3", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "cbb21589711741582f016da0204b949e6bb393744d53b01dcc5383669d106dea", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait5": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRConfigureS3PublicAccessBlock", "ASREnableCloudTrailLogFileValidation", "ASREnableEbsEncryptionByDefault", "ASREnableEnhancedMonitoringOnRDSInstance", "ASREnableKeyRotation", "DeletWait4", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "6beab64cd1e1e29ec0dfac301f88b9a6eb2cf699bf743fbecf0fb88eca6f4f5c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait6": { "DeletionPolicy": "Delete", "DependsOn": [ "ASREnableCopyTagsToSnapshotOnRDSCluster", "ASREnableMultiAZOnRDSInstance", "ASREnableRDSClusterDeletionProtection", "ASREnableRDSInstanceDeletionProtection", "ASRRemoveVPCDefaultSecurityGroupRules", "DeletWait5", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "76ac9234ed21512ab7274d9f95c6a1ef847748fa697d122f7c3e1d7a1d74f010", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait7": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRDisablePublicAccessToRDSInstance", "ASREnableEncryptionForSNSTopic", "ASREnableMinorVersionUpgradeOnRDSDBInstance", "ASRRevokeUnusedIAMUserCredentials", "ASRSetIAMPasswordPolicy", "DeletWait6", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "971be703d38efdd2d75a17325953419f3244f959e839b5a0668b12146968ac0c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait8": { "DeletionPolicy": "Delete", "DependsOn": [ "ASRDisablePublicIPAutoAssign", "ASREnableDeliveryStatusLoggingForSNSTopic", "DeletWait7", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "dff26ffe5bc8a2eb7a84297f591aa46181b17efa8b7c977636db9cf3beeec61c", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, }, } `;