# ------------------------------------------------------------------------------------------------- # CloudFormation Template 1 of 2 - Real Time Automated Remediation for Foundational Security Findings based on Security Hub # # Provisions Systems Manager Automation Documents for AWS Foundations Security Benchmarks # Each document is invoked from a AWS Security Hub Custom Action # # # @author Kanishk Mahajan # --------------------------------------------------------------------------------------------------- AWSTemplateFormatVersion: '2010-09-09' Description: CloudFormation Template 1 of 2 - Real Time Automated Remediation for Foundational Security Findings based on Security Hub. (qs-1t0eilb5g) Outputs: AutomationAssumeRoleArn: Description: Arn for AutomationAssumeRole Value: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}' Export: # added to export Name: FSBP-AutomationAssumeRoleArn CloudTrailLogGroupArn: Description: Arn for CloudTrail CloudWatch Logs Value: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudTrailLogGroup}:*" Export: # added to export Name: FSBP-CloudTrailLogGroupArn S3LoggingBucketFullName: Description: S3 Logging Bucket - CIS 2.6 Value: !Ref S3LoggingBucket Export: # added to export Name: FSBP-S3LoggingBucketFullName CISS3CloudTrailBucket: Description: CIS S3 CloudTrail Bucket Value: !Ref CISS3CloudTrailBucket Export: # added to export Name: FSBP-CISS3CloudTrailBucket CloudTrailLogGroup: Description: CIS CloudTrail CloudWatch Log Group Value: !Ref CloudTrailLogGroup Export: # added to export Name: FSBP-CloudTrailLogGroup SSMInstanceProfileRoleArn: Description: Arn for SSMInstanceProfileRole Value: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${SSMInstanceProfileRole}' Export: # added to export Name: FSBP-SSMInstanceProfileRoleArn CISCloudTrail: Description: CIS CloudTrail Value: 'remediation-cis-trail' Export: # added to export Name: FSBP-CISCloudTrail CloudWatchRoleArn: Description: Arn for CloudTrail CloudWatch IAM Role Value: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${CloudWatchRole}" Export: # added to export Name: FSBP-CloudWatchRoleArn KMSKeyArn: Description: Arn for KMS CMK Value: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KmsKeyId}" Export: # added to export Name: FSBP-KMSKeyArn Resources: # ------------------------------------------------------------------------------------------ # FSBP AWS Foundations Benchmark - Pre-requesites # # Provisions all pre-req AWS Services for FSBP remediations # # @kanishk.mahajan # --------------------------------------------------------------------------------------------- # Bucket Policy for CloudTrail S3 Bucket. Restrict to allow access to only SSL transport. CISBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref CISS3CloudTrailBucket PolicyDocument: Statement: - Action: - "s3:*" Effect: "Allow" Resource: - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket} - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket}/* Principal: AWS: - !Ref AWS::AccountId - Action: - "s3:*" Effect: "Allow" Resource: - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket} - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket}/* Principal: Service: - cloudtrail.amazonaws.com - Effect: Deny Principal: "*" Action: "*" Resource: - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket} - !Sub arn:${AWS::Partition}:s3:::${CISS3CloudTrailBucket}/* Condition: Bool: aws:SecureTransport: 'false' # S3 Bucket to store CloudTrail logs CISS3CloudTrailBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "fsbp-s3-cis-${AWS::AccountId}-${AWS::Region}" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 AccessControl: BucketOwnerFullControl LifecycleConfiguration: Rules: - AbortIncompleteMultipartUpload: DaysAfterInitiation: 3 NoncurrentVersionExpirationInDays: 3 Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Description Value: S3 Bucket for CloudTrail Logs VersioningConfiguration: Status: Enabled # S3 Bucket to store CloudTrail logs CISS3CloudTrailBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "fsbp-s3-cis-${AWS::AccountId}-${AWS::Region}" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 AccessControl: BucketOwnerFullControl LifecycleConfiguration: Rules: - AbortIncompleteMultipartUpload: DaysAfterInitiation: 3 NoncurrentVersionExpirationInDays: 3 Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Description Value: S3 Bucket for CloudTrail Logs VersioningConfiguration: Status: Enabled # Bucket Policy for S3 Bucket used for Bucket Logging. Restrict to allow access to only SSL transport. S3LoggingBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3LoggingBucket PolicyDocument: Statement: - Action: - "s3:*" Effect: "Allow" Resource: - !Sub arn:${AWS::Partition}:s3:::${S3LoggingBucket} - !Sub arn:${AWS::Partition}:s3:::${S3LoggingBucket}/* Principal: AWS: - !Ref AWS::AccountId - Effect: Deny Principal: "*" Action: "*" Resource: - !Sub arn:${AWS::Partition}:s3:::${S3LoggingBucket} - !Sub arn:${AWS::Partition}:s3:::${S3LoggingBucket}/* Condition: Bool: aws:SecureTransport: 'false' # S3 Bucket for Bucket Logging S3LoggingBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "fsbp-s3-logging-${AWS::AccountId}-${AWS::Region}" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 AccessControl: BucketOwnerFullControl LifecycleConfiguration: Rules: - AbortIncompleteMultipartUpload: DaysAfterInitiation: 3 NoncurrentVersionExpirationInDays: 3 Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Description Value: S3 Bucket for Logging VersioningConfiguration: Status: Enabled #CloudTrail CW Log Group CloudTrailLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub FSBP-DefaultLogGroup-${AWS::Region} RetentionInDays: 1827 # SSM Automation Role AutomationAssumeRole: Type: 'AWS::IAM::Role' Properties: RoleName: !Sub fsbp-automationassumerole-${AWS::Region} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ssm.amazonaws.com - events.amazonaws.com - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" # SSM Instance Profile Role SSMInstanceProfileRole: Type: 'AWS::IAM::Role' Properties: RoleName: !Sub fsbp-ssminstanceprofilerole-${AWS::Region} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" # Cloud Watch Role CloudWatchRole: Type: AWS::IAM::Role Properties: RoleName: !Sub FSBP-CloudTrail_CloudWatchLogs_Role-${AWS::Region} Policies: - PolicyName: CloudWatchLogsRolePolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cloudwatch:PutMetricData Resource: '*' - Effect: Allow Action: - ssm:StartAutomationExecution - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:DescribeLogGroups - logs:DescribeLogStreams - iam:PassRole Resource: '*' - Effect: Allow Action: - cloudtrail:UpdateTrail - securityhub:UpdateFindings Resource: '*' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - cloudtrail.amazonaws.com Action: - sts:AssumeRole # CIS KMS CMK KmsKeyId: Type: 'AWS::KMS::Key' Properties: EnableKeyRotation: true Enabled: true KeyUsage: ENCRYPT_DECRYPT KeyPolicy: Version: '2012-10-17' Statement: - Sid: Enable IAM User Permissions Effect: Allow Principal: AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" Action: 'kms:*' Resource: '*' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [CloudTrail.2] CloudTrail logs should be encrypted at rest using AWS KMS CMKs # # SSM Automation Document that leverages context passed from AWS Security Hub action # Similar pattern is repeated for each automation document # # @kanishk.mahajan # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomCloudTrailEncryptionAutomationCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-CloudTrailEncryptionCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" CloudTrailLogGroupArn: type: String default: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudTrailLogGroup}:*" CloudWatchRoleArn: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${CloudWatchRole}" TrailName: type: String KMSKeyArn: type: String default: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KmsKeyId}" mainSteps: - name: EncryptCloudTrail action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: encrypttrail_handler Script: "def encrypttrail_handler(events, context):\r\n import boto3\r\n cloudtrail = boto3.client('cloudtrail')\r\n\r\n CloudTrailLogGroupArn = events['CloudTrailLogGroupArn']\r\n CloudWatchRoleArn = events['CloudWatchRoleArn']\r\n TrailName = events['TrailName']\r\n KMSKeyArn = events['KMSKeyArn']\r\n\r\n response = cloudtrail.update_trail(\r\n Name=TrailName,\r\n IncludeGlobalServiceEvents=True,\r\n IsMultiRegionTrail=True,\r\n EnableLogFileValidation=True,\r\n CloudWatchLogsLogGroupArn=CloudTrailLogGroupArn,\r\n CloudWatchLogsRoleArn=CloudWatchRoleArn,\r\n KmsKeyId=KMSKeyArn\r\n ) " InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' CloudTrailLogGroupArn: '{{CloudTrailLogGroupArn}}' CloudWatchRoleArn: '{{CloudWatchRoleArn}}' TrailName: '{{TrailName}}' KMSKeyArn: '{{KMSKeyArn}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [EC2.2] The VPC default security group should not allow inbound and outbound traffic # ------------------------------------------------------------------------------------------------------------------------------------------------------- RestrictSecurityGroupPublicAccess: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-RestrictSecurityGroup Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: groupId: type: String IpAddressToBlock: type: String default: '0.0.0.0/0' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: RestrictSecurityGroup action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: restrict_sg Script: "def restrict_sg(events, context):\r\n import boto3\r\n import json\r\n import os\r\n ec2 = boto3.resource('ec2')\r\n defaultSecGroupId = events['groupId']\r\n try:\r\n defaultSG = ec2.SecurityGroup(defaultSecGroupId)\r\n defaultIngress = defaultSG.ip_permissions\r\n defaultEgress = defaultSG.ip_permissions_egress\r\n revokeIngress = defaultSG.revoke_ingress(IpPermissions=defaultIngress)\r\n revokeEgress = defaultSG.revoke_egress(IpPermissions=defaultEgress)\r\n except Exception as e:\r\n print(e)" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' groupId: '{{groupId}}' IpAddressToBlock: '{{IpAddressToBlock}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.7] Password policies for IAM users should have strong configurations # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMPasswordUpdateCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMPasswordUpdateCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: updatepasswordpolicy action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: updateiampolicy_handler Script: | def updateiampolicy_handler(events, context): import boto3 iam = boto3.client('iam') response = iam.update_account_password_policy( AllowUsersToChangePassword=True, HardExpiry=True, MaxPasswordAge=90 , MinimumPasswordLength=14, PasswordReusePrevention=24, RequireLowercaseCharacters=True, RequireNumbers=True, RequireSymbols=True, RequireUppercaseCharacters=True) InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.3] IAM users' access keys should be rotated every 90 days or less # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMKeyRotate90DaysCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMKeyRotate90DaysCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: username: type: String default: 'cisadmin' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: rotateiam90days action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: rotateiam90days_handler Script: | def rotateiam90days_handler(events, context): import boto3 import datetime import json import os iam = boto3.client('iam') securityhub = boto3.client('securityhub') iam_resource = boto3.resource('iam') try: username = events['username'] todaysDatetime = datetime.datetime.now(datetime.timezone.utc) paginator = iam.get_paginator('list_access_keys') for response in paginator.paginate(UserName=username): for keyMetadata in response['AccessKeyMetadata']: accessKeyId = str(keyMetadata['AccessKeyId']) keyAgeFinder = todaysDatetime - keyMetadata['CreateDate'] if keyAgeFinder <= datetime.timedelta(days=90): print("Access key: " + accessKeyId + " is compliant") else: print("Access key over 90 days old found!") access_key = iam_resource.AccessKey(username, accessKeyId) access_key.deactivate() except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' username: '{{username}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.4] IAM root user access key should not exist # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomDeactivateRootIAMAccessKeyCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-DeactivateRootIAMAccessKeyCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: username: type: String default: 'root' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: deactivaterootiamaccesskey action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: deactivaterootiamaccesskey_handler Script: | def deactivaterootiamaccesskey_handler(events, context): import boto3 import json import os iam = boto3.client('iam') config = boto3.client('config') iam_resource = boto3.resource('iam') try: resourceid = events['username'] response_config = config.list_discovered_resources( resourceType='AWS::IAM::User', resourceIds=[ resourceid ] ) userName = response_config['resourceIdentifiers'][0]['resourceName'] access_key = iam_resource.AccessKey(userName, accessKeyId) access_key.deactivate() except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' username: '{{username}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.2] IAM users should not have IAM policies attached # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMUserPolicyDetachCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMUserPolicyDetachCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: userresourceid: type: String default: 'cisadmin' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: detachiamuserpolicy action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: detachiamuserpolicy_handler Script: | def detachiamuserpolicy_handler(events, context): import boto3 import json import os import string import random client = boto3.client('iam') iam = boto3.resource('iam') config = boto3.client('config') try: resourceid = events['userresourceid'] response_config = config.list_discovered_resources( resourceType='AWS::IAM::User', resourceIds=[ resourceid ] ) userName = response_config['resourceIdentifiers'][0]['resourceName'] letters = string.ascii_lowercase findingid = ''.join(random.choice(letters) for i in range(4)) userpolicyresponse = client.list_attached_user_policies(UserName=userName) userpolicies =[] userpolicies = userpolicyresponse.get("AttachedPolicies") newgroup = "CISGroup" + "-" + userName + "-" + findingid grouplist = client.list_groups() for group in grouplist: try: if group['GroupName'] == newgroup: print("foundGroup") break else: response = client.create_group( GroupName = newgroup ) except Exception as e: print(e) group = iam.Group(newgroup) for policy in userpolicies: try: response = group.attach_policy( PolicyArn = policy['PolicyArn'] ) print(response) except Exception as e: print(e) for policy in userpolicies: try: response_1 = client.detach_user_policy( UserName=userName, PolicyArn=policy['PolicyArn'] ) except Exception as e: print(e) try: response_2 = client.add_user_to_group( GroupName = newgroup, UserName = userName ) except Exception as e: print(e) except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' userresourceid: '{{userresourceid}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.7] Password policies for IAM users should have strong configurations # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMPasswordUpdateCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMPasswordUpdateCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: updatepasswordpolicy action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: updateiampolicy_handler Script: | def updateiampolicy_handler(events, context): import boto3 iam = boto3.client('iam') response = iam.update_account_password_policy( AllowUsersToChangePassword=True, HardExpiry=True, MaxPasswordAge=90 , MinimumPasswordLength=14, PasswordReusePrevention=24, RequireLowercaseCharacters=True, RequireNumbers=True, RequireSymbols=True, RequireUppercaseCharacters=True) InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # IAM.3 – Disable and Rotate IAM Access Key that is older than 90 days # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMKeyRotate90DaysCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMKeyRotate90DaysCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: username: type: String default: 'cisadmin' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: rotateiam90days action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: rotateiam90days_handler Script: | def rotateiam90days_handler(events, context): import boto3 import datetime import json import os iam = boto3.client('iam') securityhub = boto3.client('securityhub') iam_resource = boto3.resource('iam') try: username = events['username'] todaysDatetime = datetime.datetime.now(datetime.timezone.utc) paginator = iam.get_paginator('list_access_keys') for response in paginator.paginate(UserName=username): for keyMetadata in response['AccessKeyMetadata']: accessKeyId = str(keyMetadata['AccessKeyId']) keyAgeFinder = todaysDatetime - keyMetadata['CreateDate'] if keyAgeFinder <= datetime.timedelta(days=90): print("Access key: " + accessKeyId + " is compliant") else: print("Access key over 90 days old found!") access_key = iam_resource.AccessKey(username, accessKeyId) access_key.deactivate() except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' username: '{{username}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [IAM.1] IAM policies should not allow full "*" administrative privileges # ------------------------------------------------------------------------------------------------------------------------------------------------------- CustomIAMFullAdminPolicyDetachCF: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-IAMFullAdminPolicyDetachCF Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: policyresourceid: type: String accountid: type: String AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: detachiamfulladminpolicy action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: detachiamfulladminpolicy_handler Script: | def detachiamfulladminpolicy_handler(events, context): import boto3 import json import os config = boto3.client('config') iam = boto3.resource('iam') try: policyresourceid = events['policyresourceid'] accountid = events['accountid'] response = config.list_discovered_resources( resourceType='AWS::IAM::Policy', resourceIds=[ resourceid ] ) resourcename = response['resourceIdentifiers'][0]['resourceName'] policyarn = "arn:aws:iam::" + accountid + ":policy/" + resourcename response_iam = iam.list_entities_for_policy( PolicyArn=policyarn, EntityFilter='User' ) policyusers = response_iam['PolicyUsers'] for user in policyusers: try: username = user['UserName'] response = client.detach_user_policy( UserName=username, PolicyArn=policyarn ) except Exception as e: print(e) response_iam2 = iam.delete_policy( PolicyArn=policyarn ) except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' policyresourceid: '{{policyresourceid}}' accountid: '{{accountid}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [EC2.2] VPC default security group should prohibit inbound and outbound traffic # ------------------------------------------------------------------------------------------------------------------------------------------------------- RestrictSecurityGroupPublicAccess: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-RestrictSecurityGroup Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: groupId: type: String IpAddressToBlock: type: String default: '0.0.0.0/0' AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" mainSteps: - name: RestrictSecurityGroup action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: restrict_sg Script: "def restrict_sg(events, context):\r\n import boto3\r\n import json\r\n import os\r\n ec2 = boto3.resource('ec2')\r\n defaultSecGroupId = events['groupId']\r\n try:\r\n defaultSG = ec2.SecurityGroup(defaultSecGroupId)\r\n defaultIngress = defaultSG.ip_permissions\r\n defaultEgress = defaultSG.ip_permissions_egress\r\n revokeIngress = defaultSG.revoke_ingress(IpPermissions=defaultIngress)\r\n revokeEgress = defaultSG.revoke_egress(IpPermissions=defaultEgress)\r\n except Exception as e:\r\n print(e)" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' groupId: '{{groupId}}' IpAddressToBlock: '{{IpAddressToBlock}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [CodeBuild.2] CodeBuild project environment variables should not contain clear text credentials # ------------------------------------------------------------------------------------------------------------------------------------------------------- CodeBuildUpdateProject: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-CodeBuildUpdateProject Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" projectName: type: String mainSteps: - name: CodeBuildUpdateProject action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n client = boto3.client('codebuild')\r\n projectName = events['projectName']\r\n\r\n response = client.batch_get_projects(\r\n names=[\r\n projectName\r\n ]\r\n )\r\n \r\n projectenv = response['projects'][0]['environment']\r\n projectenv1_type = \"projectenv type: \" + projectenv['type']\r\n projectenvvars = projectenv['environmentVariables']\r\n \r\n for i in range(len(projectenvvars)):\r\n if projectenvvars[i]['name'] == 'AWS_ACCESS_KEY_ID':\r\n del (projectenvvars[i])\r\n break\r\n \r\n for i in range(len(projectenvvars)):\r\n if projectenvvars[i]['name'] == 'AWS_SECRET_ACCESS_KEY':\r\n del (projectenvvars[i])\r\n break\r\n \r\n response['projects'][0]['environment']['environmentVariables'] = projectenvvars\r\n response1 = client.update_project(name=projectName, environment=response['projects'][0]['environment'])\r\n" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' projectName: '{{projectName}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [RDS.3] RDS DB instances should have encryption at-rest enabled # ------------------------------------------------------------------------------------------------------------------------------------------------------- RDSEncryptDBInstance: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-EncryptRDSDBInstance Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" dbresourceid: type: String kmskeyArn: type: String mainSteps: - name: EncryptRDSDBInstance action: 'aws:executeScript' timeoutSeconds: 4000 inputs: Runtime: python3.6 Handler: script_handler Script: | def script_handler(events, context): import boto3 import json import os import time client = boto3.client('rds') config = boto3.client('config') dbresourceid = events['dbresourceid'] kmskeyArn = events['kmskeyArn'] response = config.list_discovered_resources( resourceType='AWS::RDS::DBInstance', resourceIds=[ dbresourceid ] ) dbinstanceId = response['resourceIdentifiers'][0]['resourceName'] response_snapshot = client.create_db_snapshot( DBSnapshotIdentifier="fsbp-snapshot-" + dbinstanceId, DBInstanceIdentifier=dbinstanceId ) response_snapshotA = client.get_waiter('db_snapshot_available').wait( DBSnapshotIdentifier='fsbp-snapshot-' + dbinstanceId, DBInstanceIdentifier=dbinstanceId ) response_snapshotCopy = client.copy_db_snapshot( SourceDBSnapshotIdentifier="fsbp-snapshot-" + dbinstanceId, TargetDBSnapshotIdentifier="fsbp-snapshot-encrypted-" + dbinstanceId, KmsKeyId=kmskeyArn, CopyTags=True ) response_snapshotB = client.get_waiter('db_snapshot_available').wait( DBSnapshotIdentifier='fsbp-snapshot-encrypted-' + dbinstanceId, DBInstanceIdentifier=dbinstanceId ) response_restore = client.restore_db_instance_from_db_snapshot( DBInstanceIdentifier='fsbp-encrypted-' + dbinstanceId, DBSnapshotIdentifier='fsbp-snapshot-encrypted-' + dbinstanceId ) response_snapshotC = client.get_waiter('db_instance_available').wait( DBInstanceIdentifier='fsbp-encrypted-' + dbinstanceId ) response_delete1 = client.delete_db_snapshot( DBSnapshotIdentifier='fsbp-snapshot-' + dbinstanceId ) response_delete2 = client.get_waiter('db_snapshot_deleted').wait( DBSnapshotIdentifier='fsbp-snapshot-' + dbinstanceId, WaiterConfig={ 'Delay': 5, 'MaxAttempts': 30 } ) response_delete3 = client.delete_db_instance( DBInstanceIdentifier=dbinstanceId, SkipFinalSnapshot=True ) response_wait = client.get_waiter('db_instance_deleted').wait( DBInstanceIdentifier=dbinstanceId ) InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' dbresourceid: '{{dbresourceid}}' kmskeyArn: '{{kmskeyArn}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [RDS.1] RDS snapshots should prohibit public access # ------------------------------------------------------------------------------------------------------------------------------------------------------- RDSPublicNonRestoreSnapshot: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-ModifyRDSSnapshot Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" snapshotId: type: String snapshotType: type: String mainSteps: - name: ModifyRDSSnapshot action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n client = boto3.client('rds')\r\n snapshotId = events['snapshotId']\r\n snapshotType = events['snapshotType']\r\n\r\n if snapshotType == \"AwsRdsDBClusterSnapshot\":\r\n response = client.modify_db_cluster_snapshot_attribute(\r\n DBClusterSnapshotIdentifier=snapshotId,\r\n AttributeName='restore',\r\n ValuesToRemove=[\r\n 'all',\r\n ]\r\n )\r\n else:\r\n response = client.modify_db_snapshot_attribute(\r\n DBSnapshotIdentifier=snapshotId,\r\n AttributeName='restore',\r\n ValuesToRemove=[\r\n 'all',\r\n ]\r\n )\r\n" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' snapshotId: '{{snapshotId}}' snapshotType: '{{snapshotType}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [RDS.2] RDS DB Instances should prohibit public access # ------------------------------------------------------------------------------------------------------------------------------------------------------- RDSNonPublicDBInstance: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-ModifyRDSDBInstance Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" dbresourceid: type: String mainSteps: - name: ModifyRDSDBInstance action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: | def script_handler(events, context): import boto3 import json import os rds = boto3.client('rds') config = boto3.client('config') try: resourceid = events['dbresourceid'] response = config.list_discovered_resources( resourceType='AWS::RDS::DBInstance', resourceIds=[ resourceid ] ) resourcename = response['resourceIdentifiers'][0]['resourceName'] response = rds.modify_db_instance( ApplyImmediately=True, DBInstanceIdentifier=resourcename, PubliclyAccessible= False ) except Exception as e: print(e) raise InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' dbresourceid: '{{dbresourceid}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [EC2.1] Amazon EBS snapshots should not be public, determined by the ability to be restorable by anyone # ------------------------------------------------------------------------------------------------------------------------------------------------------- PublicNonRestoreSnapshot: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-ModifySnapshot Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" snapshotId: type: String mainSteps: - name: ModifySnapshot action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n client = boto3.client('ec2')\r\n\r\n snapshotId = events['snapshotId']\r\n \r\n response = client.modify_snapshot_attribute(\r\n Attribute='createVolumePermission',\r\n GroupNames=[\r\n 'all',\r\n ],\r\n OperationType='remove',\r\n SnapshotId=snapshotId\r\n )\r\n" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' snapshotId: '{{snapshotId}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [EC2.3] Attached EBS volumes should be encrypted at-rest # ------------------------------------------------------------------------------------------------------------------------------------------------------- EncryptEBSVolume: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-EncryptEBSVolume Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" ebsvolumeId: type: String sourceregion: type: String kmskeyArn: type: String mainSteps: - name: EncryptEBSVolume action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: "def script_handler(events, context):\r\n # TODO implement\r\n import json\r\n import boto3\r\n \r\n client = boto3.client('ec2')\r\n ebsvolumeId = events['ebsvolumeId']\r\n kmskeyArn = events['kmskeyArn']\r\n sourceregion = events['sourceregion']\r\n \r\n print('0. Describe Volume')\r\n \r\n response_volume = client.describe_volumes(\r\n VolumeIds=[\r\n ebsvolumeId\r\n ] \r\n )\r\n instanceid = response_volume['Volumes'][0]['Attachments'][0]['InstanceId']\r\n size= response_volume['Volumes'][0]['Size']\r\n availabilityzone = response_volume['Volumes'][0]['AvailabilityZone']\r\n \r\n response_snapshot = client.create_snapshot(\r\n Description='New FSBP snapshot',\r\n VolumeId=ebsvolumeId\r\n )\r\n \r\n snapshotid = response_snapshot['SnapshotId']\r\n \r\n response_snapshotA = client.get_waiter('snapshot_completed').wait(\r\n SnapshotIds=[snapshotid]\r\n )\r\n\r\n print('2. Copy and Encrypt. Creating encrypted snapshot from unencrypted copy')\r\n \r\n response_snapshotCopy = client.copy_snapshot(\r\n Description='New FSBP Encrypted snapshot.',\r\n DestinationRegion=sourceregion,\r\n SourceRegion=sourceregion,\r\n SourceSnapshotId=snapshotid,\r\n KmsKeyId=kmskeyArn,\r\n Encrypted=True\r\n )\r\n \r\n snapshotencryptedId = response_snapshot['SnapshotId']\r\n \r\n response_snapshotB = client.get_waiter('snapshot_completed').wait(\r\n SnapshotIds=[snapshotencryptedId]\r\n )\r\n \r\n print('3. Create volume from encrypted snapshot')\r\n \r\n response_volume_encrypted = client.create_volume(\r\n AvailabilityZone=availabilityzone,\r\n Size=size,\r\n VolumeType='gp2',\r\n KmsKeyId=kmskeyArn,\r\n Encrypted=True\r\n )\r\n\r\n encryptedVolumeId = response_volume_encrypted['VolumeId']\r\n \r\n response_snapshotC = client.get_waiter('volume_available').wait(\r\n VolumeIds=[encryptedVolumeId]\r\n )\r\n\r\n print('4. Stop original instance or terminate original instance if instance in asg')\r\n \r\n asgclient = boto3.client('autoscaling')\r\n \r\n response_asg = asgclient.describe_auto_scaling_instances(\r\n InstanceIds=[\r\n instanceid\r\n ]\r\n )\r\n \r\n if not response_asg['AutoScalingInstances']:\r\n response_terminateinstance = client.terminate_instances(\r\n InstanceIds=[\r\n instanceid\r\n ]\r\n )\r\n else:\r\n response_stopinstance = client.stop_instances(\r\n InstanceIds=[\r\n instanceid\r\n ]\r\n )\r\n \r\n response_instanceA = client.get_waiter('instance_stopped').wait(\r\n InstanceIds=[instanceid]\r\n )\r\n \r\n print('5. Detach original volume')\r\n \r\n response_detach_volume = client.detach_volume(\r\n VolumeId=ebsvolumeId\r\n )\r\n\r\n response_snapshotC = client.get_waiter('volume_available').wait(\r\n VolumeIds=[ebsvolumeId]\r\n )\r\n \r\n print('6. Delete original volume')\r\n \r\n response = client.delete_volume(\r\n VolumeId=ebsvolumeId\r\n )\r\n \r\n response_volumeA = client.get_waiter('volume_deleted').wait(\r\n VolumeIds=[ebsvolumeId]\r\n )\r\n \r\n print('7. Delete original snapshot')\r\n \r\n response = client.delete_snapshot(\r\n SnapshotId=snapshotid\r\n )" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' ebsvolumeId: '{{ebsvolumeId}}' sourceregion: '{{sourceregion}}' kmskeyArn: '{{kmskeyArn}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [GuardDuty.1] GuardDuty should be enabled # ------------------------------------------------------------------------------------------------------------------------------------------------------- EnableGuardDuty: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-EnableGuardDuty Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" findingpublishingfrequency: type: String accountid: type: String mainSteps: - name: EnableGuardDuty action: 'aws:executeScript' inputs: Runtime: python3.7 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n client = boto3.client('guardduty')\r\n findingpublishingfrequency= events['findingpublishingfrequency']\r\n\r\n response = client.create_detector(\r\n Enable=True,\r\n FindingPublishingFrequency=findingpublishingfrequency\r\n )\r\n" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' findingpublishingfrequency: '{{findingpublishingfrequency}}' accountid: '{{accountid}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [Lambda.2] Lambda functions should use latest runtimes # ------------------------------------------------------------------------------------------------------------------------------------------------------- LatestRuntimeLambda: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-LatestRuntimeLambda Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" functionname: type: String mainSteps: - name: LatestRuntime action: 'aws:executeScript' inputs: Runtime: python3.7 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n client = boto3.client('lambda')\r\n functionname = events['functionname']\r\n \r\n response = client.get_function_configuration(\r\n FunctionName=functionname\r\n )\r\n runtime = response['Runtime']\r\n \r\n if 'python' in runtime:\r\n runtime = 'python3.8'\r\n if 'node' in runtime:\r\n runtime = 'nodejs12.x'\r\n if 'java' in runtime:\r\n runtime = 'java11'\r\n if 'dotnet' in runtime:\r\n runtime = 'dotnetcore3.1'\r\n if 'ruby' in runtime:\r\n runtime = 'ruby2.7'\r\n if 'go' in runtime:\r\n runtime = 'go1.x'\r\n \r\n response = client.update_function_configuration(\r\n FunctionName=functionname,\r\n Runtime=runtime\r\n )\r\n \r\n\r\n" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' functionname: '{{functionname}}' # ------------------------------------------------------------------------------------------------------------------------------------------------------- # [Lambda.1] Lambda functions should prohibit public access by other accounts # ------------------------------------------------------------------------------------------------------------------------------------------------------- RestrictPublicLambda: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: Custom-FSBP-RestrictPublicLambda Content: schemaVersion: '0.3' assumeRole: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" parameters: AutomationAssumeRole: type: String default: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AutomationAssumeRole}" accountID: type: String functionname: type: String mainSteps: - name: RestrictLambda action: 'aws:executeScript' inputs: Runtime: python3.6 Handler: script_handler Script: "def script_handler(events, context):\r\n import boto3\r\n import json\r\n \r\n client = boto3.client('lambda')\r\n functionname = events['functionname']\r\n accountID = events['accountID']\r\n response = client.get_policy(FunctionName=functionname)\r\n policy = response['Policy']\r\n policy_json = json.loads(policy)\r\n statements = policy_json['Statement']\r\n \r\n for statement in statements:\r\n Principal = str(statement['Principal']).replace(\"{'Service': '\",\"\")[:-2]\r\n Action = statement['Action']\r\n Resource = statement['Resource']\r\n StatementId = statement ['Sid']\r\n NewStatementId = \"New\" + StatementId\r\n \r\n response_old = client.remove_permission(\r\n FunctionName=functionname,\r\n StatementId=StatementId\r\n )\r\n\r\n response = client.add_permission(\r\n FunctionName=functionname,\r\n StatementId=NewStatementId,\r\n Action=Action,\r\n Principal=Principal,\r\n SourceAccount= accountID\r\n )" InputPayload: AutomationAssumeRole: '{{AutomationAssumeRole}}' accountID: '{{accountID}}' functionname: '{{functionname}}'