# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Parameters: pEmailNotification: Type: String Default: email@domain.com Description: Enter the email address to receive GuardDuty and Security Hub findings. pSecurityHubEmailsRate: Type: String Default: cron(0 17 * * ? *) Description: Enter the frequency (cron syntax) to receive the top 10 critical and high Security Hub findings. The default cron syntax is daily at 10am PDT. pEnableSecurityServicesRate: Type: String Default: cron(0 15 * * ? *) Description: Enter the frequency (cron syntax) to check and enable the security services included in the solution. The default cron syntax is daily at 8am PDT. Resources: kmsKMSKey: Type: AWS::KMS::Key Properties: EnableKeyRotation: true Enabled: true KeyPolicy: Version: 2012-10-17 Id: key-default-1 Statement: - Sid: Enable IAM User Permissions Effect: Allow Principal: AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' Action: 'kms:*' Resource: '*' - Sid: Allow CloudWatch Events to use KMS key Effect: Allow Principal: Service: - events.amazonaws.com Action: - kms:GenerateDataKey* - kms:Decrypt Resource: '*' KeySpec: SYMMETRIC_DEFAULT KeyUsage: ENCRYPT_DECRYPT MultiRegion: false PendingWindowInDays: 7 iamSecurityHubLambdaAlertsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: !Sub 'securityhub-enabled-role-${AWS::Region}' PolicyDocument: Version: 2012-10-17 Statement: - Sid: AdministerSecurityHub Effect: Allow Action: - 'securityhub:GetFindings' Resource: - !Sub 'arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:hub/default' - Sid: SendNotifications Effect: Allow Action: - 'sns:Publish' Resource: !Ref snsNotifications - Sid: AccessKMSKey Effect: Allow Action: - 'kms:GenerateDataKey' - 'kms:Decrypt' Resource: !GetAtt kmsKMSKey.Arn - Sid: CloudWatchCreateGroup Effect: Allow Action: - 'logs:CreateLogGroup' Resource: - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' - Sid: CloudWatchLogging Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*:*' iamEventBridgeInvokeStepFunction: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: events.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Sub ${AWS::AccountId} Path: / Policies: - PolicyName: eventbridge-stepfunctions PolicyDocument: Version: 2012-10-17 Statement: - Sid: InvokeStepFunctions Effect: Allow Action: - 'states:StartExecution' Resource: - !Ref stepEnableGuardDuty - !Ref stepEnableSecurityHubAndConfig - !Ref stepEnableCloudTrail iamStepFunctionGuardDutyRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: Resource required to be *. Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Sub ${AWS::AccountId} Path: / Policies: - PolicyName: stepfunction-guardduty PolicyDocument: Version: 2012-10-17 Statement: - Sid: AdministerGuardDuty Effect: Allow Action: - 'guardduty:CreateDetector' - 'guardduty:ListDetectors' Resource: - '*' - Sid: IAMAccess Effect: Allow Action: - 'iam:CreateServiceLinkedRole' Resource: - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty' - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/malware-protection.guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDutyMalwareProtection' - Sid: IAMPermissionsForMalwareProtection Effect: Allow Action: - 'iam:GetRole' Resource: - 'arn:aws:iam::*:role/*AWSServiceRoleForAmazonGuardDutyMalwareProtection' iamStepFunctionSecurityHubRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: Config Actions don't allow a resource specified. Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Sub ${AWS::AccountId} Path: / Policies: - PolicyName: stepfunction-securityhub PolicyDocument: Version: 2012-10-17 Statement: - Sid: AdministerSecurityHub Effect: Allow Action: - 'securityhub:EnableSecurityHub' - 'securityhub:DescribeHub' Resource: - !Sub 'arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:/accounts' - !Sub 'arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:hub/default' - Sid: IAMRoles Effect: Allow Action: - 'iam:GetRole' - 'iam:CreateServiceLinkedRole' Resource: - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/securityhub.amazonaws.com/AWSServiceRoleForSecurityHub' - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig' - Sid: ConfigRole Effect: Allow Action: - 'iam:PassRole' Resource: - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig' - Sid: S3access Effect: Allow Action: - 's3:ListBucket' - 's3:PutBucketPolicy' - 's3:PutBucketPublicAccessBlock' - 's3:CreateBucket' Resource: - !Sub 'arn:aws:s3:::config-bucket-${AWS::AccountId}' - Sid: AdministerConfig Effect: Allow Action: - 'config:DescribeConfigurationRecorders' - 'config:DescribeConfigurationRecorderStatus' - 'config:PutConfigurationRecorder' - 'config:PutDeliveryChannel' - 'config:StartConfigurationRecorder' Resource: '*' iamStepFunctionCloudTrailRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: Actions don't allow a resource specified. Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Sub ${AWS::AccountId} Path: / Policies: - PolicyName: stepfunction-cloudtrail PolicyDocument: Version: 2012-10-17 Statement: - Sid: AdministerCloudTrail Effect: Allow Action: - 'cloudtrail:GetTrailStatus' - 'cloudtrail:DescribeTrails' - 'cloudtrail:StartLogging' - 'cloudtrail:CreateTrail' Resource: - '*' - Sid: S3access Effect: Allow Action: - 's3:ListBucket' - 's3:PutBucketPolicy' - 's3:PutBucketPublicAccessBlock' - 's3:CreateBucket' Resource: - !Sub 'arn:aws:s3:::cloudtrail-bucket-${AWS::AccountId}' stepEnableGuardDuty: Type: AWS::StepFunctions::StateMachine Properties: Definition: StartAt: ListDetectors States: ListDetectors: Type: Task Parameters: {} Resource: arn:aws:states:::aws-sdk:guardduty:listDetectors Next: Choice Choice: Type: Choice Choices: - Variable: $.DetectorIds[0] IsPresent: false Next: GuardDutyNotEnabled Default: GuardDutyEnabled GuardDutyNotEnabled: Type: Pass Next: CreateDetector CreateDetector: Type: Task End: true Parameters: Enable: 'true' Resource: arn:aws:states:::aws-sdk:guardduty:createDetector GuardDutyEnabled: Type: Pass End: true RoleArn: !GetAtt iamStepFunctionGuardDutyRole.Arn stepEnableSecurityHubAndConfig: Type: AWS::StepFunctions::StateMachine Properties: Definition: StartAt: DescribeConfigurationRecorders States: DescribeConfigurationRecorders: Type: Task Next: Choice Parameters: {} Resource: arn:aws:states:::aws-sdk:config:describeConfigurationRecorders Choice: Type: Choice Choices: - Variable: $.ConfigurationRecorders[0] IsPresent: true Next: ConfigRecorderExists Default: NoConfigRecorder ConfigRecorderExists: Type: Pass Next: DescribeConfigurationRecorderStatus DescribeConfigurationRecorderStatus: Type: Task Parameters: {} Resource: arn:aws:states:::aws-sdk:config:describeConfigurationRecorderStatus Next: isConfigRecorderRecording isConfigRecorderRecording: Type: Choice Choices: - Variable: $.ConfigurationRecordersStatus[0].Recording BooleanEquals: true Next: ConfigRecorderCreatedandRecording Default: ConfigRecorderCreatedNotRecording ConfigRecorderCreatedandRecording: Type: Pass Next: DescribeHub ConfigRecorderCreatedNotRecording: Type: Pass Next: CreateBucket StartConfigurationRecorder: Type: Task Parameters: ConfigurationRecorderName: default Resource: arn:aws:states:::aws-sdk:config:startConfigurationRecorder Retry: - ErrorEquals: - States.TaskFailed BackoffRate: 2 IntervalSeconds: 1 MaxAttempts: 2 Next: DescribeHub DescribeHub: Type: Task Parameters: {} Resource: arn:aws:states:::aws-sdk:securityhub:describeHub Catch: - ErrorEquals: - States.ALL Next: EnableSecurityHub End: true EnableSecurityHub: Type: Task End: true Parameters: ControlFindingGenerator: SECURITY_CONTROL EnableDefaultStandards: true Resource: arn:aws:states:::aws-sdk:securityhub:enableSecurityHub CreateBucket: Type: Task Next: PutPublicAccessBlock Parameters: Bucket.$: >- States.Format('config-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Resource: arn:aws:states:::aws-sdk:s3:createBucket Catch: - ErrorEquals: - States.ALL Next: PutDeliveryChannel PutPublicAccessBlock: Type: Task Parameters: Bucket.$: >- States.Format('config-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Resource: arn:aws:states:::aws-sdk:s3:putPublicAccessBlock Next: PutBucketPolicy PutBucketPolicy: Type: Task Next: PutDeliveryChannel Parameters: Bucket.$: >- States.Format('config-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Policy: Version: '2012-10-17' Statement: - Sid: AWSConfigBucketPermissionsCheck Effect: Allow Principal: Service: config.amazonaws.com Action: s3:GetBucketAcl Resource: !Sub arn:aws:s3:::config-bucket-${AWS::AccountId} - Sid: AWSConfigBucketDelivery Effect: Allow Principal: Service: config.amazonaws.com Action: s3:PutObject Resource: !Sub >- arn:aws:s3:::config-bucket-${AWS::AccountId}/AWSLogs/${AWS::AccountId}/Config/* Condition: StringEquals: s3:x-amz-acl: bucket-owner-full-control Resource: arn:aws:states:::aws-sdk:s3:putBucketPolicy PutDeliveryChannel: Type: Task Parameters: DeliveryChannel: Name: default S3BucketName.$: >- States.Format('config-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Resource: arn:aws:states:::aws-sdk:config:putDeliveryChannel Next: StartConfigurationRecorder NoConfigRecorder: Type: Pass Next: GetRole GetRole: Type: Task Next: PutConfigurationRecorder Parameters: RoleName: AWSServiceRoleForConfig Resource: arn:aws:states:::aws-sdk:iam:getRole Catch: - ErrorEquals: - States.ALL Next: CreateServiceLinkedRole CreateServiceLinkedRole: Type: Task Next: PutConfigurationRecorder Parameters: AwsServiceName: config.amazonaws.com Resource: arn:aws:states:::aws-sdk:iam:createServiceLinkedRole PutConfigurationRecorder: Type: Task Parameters: ConfigurationRecorder: Name: default RoleARN.$: $.Role.Arn RecordingGroup: AllSupported: 'true' IncludeGlobalResourceTypes: 'true' Resource: arn:aws:states:::aws-sdk:config:putConfigurationRecorder Next: DescribeConfigurationRecorderStatus RoleArn: !GetAtt iamStepFunctionSecurityHubRole.Arn stepEnableCloudTrail: Type: AWS::StepFunctions::StateMachine Properties: Definition: StartAt: DescribeTrails States: DescribeTrails: Type: Task Next: IsMultiRegionTrail Parameters: {} Resource: arn:aws:states:::aws-sdk:cloudtrail:describeTrails IsMultiRegionTrail: Type: Pass Next: Choice InputPath: $.TrailList[?(@.IsMultiRegionTrail==true)] Choice: Type: Choice Choices: - Variable: $[0].TrailARN IsPresent: true Next: MultiRegionCloudTrailEnabled Default: MultiRegionCloudTrailNotEnabled MultiRegionCloudTrailEnabled: Type: Pass End: true MultiRegionCloudTrailNotEnabled: Type: Pass Next: HeadBucket HeadBucket: Type: Task Parameters: Bucket.$: >- States.Format('cloudtrail-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Resource: arn:aws:states:::aws-sdk:s3:headBucket Catch: - ErrorEquals: - States.ALL Next: CreateBucket Next: GetTrailStatus GetTrailStatus: Type: Task Next: StartLogging Parameters: Name: sasa-cloudtrail Resource: arn:aws:states:::aws-sdk:cloudtrail:getTrailStatus Catch: - ErrorEquals: - States.ALL Next: CreateTrail CreateBucket: Type: Task Parameters: Bucket.$: >- States.Format('cloudtrail-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Resource: arn:aws:states:::aws-sdk:s3:createBucket Next: PutPublicAccessBlock PutPublicAccessBlock: Type: Task Parameters: Bucket.$: >- States.Format('cloudtrail-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Resource: arn:aws:states:::aws-sdk:s3:putPublicAccessBlock Next: PutBucketPolicy PutBucketPolicy: Type: Task Parameters: Bucket.$: >- States.Format('cloudtrail-bucket-{}',States.ArrayGetItem(States.StringSplit($$.Execution.Id,':'),4)) Policy: Version: '2012-10-17' Statement: - Sid: AWSCloudTrailAclCheck20150319 Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:GetBucketAcl Resource: !Sub arn:aws:s3:::cloudtrail-bucket-${AWS::AccountId} Condition: StringEquals: aws:SourceArn: !Sub >- arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/sasa-cloudtrail - Sid: AWSCloudTrailWrite20150319 Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:PutObject Resource: !Sub arn:aws:s3:::cloudtrail-bucket-${AWS::AccountId}/AWSLogs/${AWS::AccountId}/* Condition: StringEquals: s3:x-amz-acl: bucket-owner-full-control aws:SourceArn: !Sub >- arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/sasa-cloudtrail Resource: arn:aws:states:::aws-sdk:s3:putBucketPolicy Next: CreateTrail CreateTrail: Type: Task Parameters: Name: sasa-cloudtrail S3BucketName: !Sub cloudtrail-bucket-${AWS::AccountId} EnableLogFileValidation: true IncludeGlobalServiceEvents: true IsMultiRegionTrail: true Resource: arn:aws:states:::aws-sdk:cloudtrail:createTrail Next: StartLogging StartLogging: Type: Task End: true Parameters: Name: sasa-cloudtrail Resource: arn:aws:states:::aws-sdk:cloudtrail:startLogging RoleArn: !GetAtt iamStepFunctionCloudTrailRole.Arn fnSecurityHubLambdaAlert: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: This function has access to CloudWatch Logs. - id: W89 reason: Lambda does not need access to anything in the VPC. https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/aws-lambda.html - id: W92 reason: This function is used for Security Hub reports. ReservedConcurrentExecutions can be set based on requirements. Properties: Handler: index.lambda_handler Role: !GetAtt iamSecurityHubLambdaAlertsRole.Arn Architectures: - arm64 Runtime: python3.10 RuntimeManagementConfig: UpdateRuntimeOn: Auto MemorySize: 128 Timeout: 180 Environment: Variables: SNS_TOPIC_ARN: !Ref snsNotifications Code: ZipFile: | import boto3 import os SNS_TOPIC = os.environ['SNS_TOPIC_ARN'] def lambda_handler(event,context): sh = boto3.client('securityhub') sh.get_findings() sns = boto3.client('sns') response = sh.get_findings( Filters={ 'SeverityLabel': [ { 'Value': 'CRITICAL', 'Comparison': 'EQUALS' }, { 'Value': 'HIGH', 'Comparison': 'EQUALS' }], 'RecordState': [ { 'Value': 'ACTIVE', 'Comparison': 'EQUALS' }, ] }, SortCriteria=[ { 'Field': 'SeverityLabel', 'SortOrder': 'asc' }, ], MaxResults=10 ) message = "" if len(response["Findings"])>0: for finding in response["Findings"]: severity = finding["Severity"]["Label"] title = finding["Title"] message = message + f"{severity:<15}{title}\n" elif(): message = "There are no critical or high findings. Please check the console for other severities." messageTitle = """Top findings from AWS Security Hub.\n--------------------------------------\nUp to 10 critical and high findings will be shown. Please review the summary and remediate. Navigate to the AWS Security Hub console (https://console.aws.amazon.com/securityhub) for more information and other findings.\n\n""" message = messageTitle + message response = sns.publish( TargetArn=SNS_TOPIC, Message=message, Subject='Top findings from AWS Security Hub.' ) return response snsNotifications: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: !GetAtt kmsKMSKey.Arn Subscription: - Endpoint: !Ref pEmailNotification Protocol: email snsNotificationsPolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Id: snsNotificationsPolicy Version: '2012-10-17' Statement: - Sid: Allow events Effect: Allow Principal: Service: 'events.amazonaws.com' Action: - sns:Publish Resource: !Ref snsNotifications Topics: - !Ref snsNotifications ruleGuardDutyAlerts: Type: AWS::Events::Rule Properties: Name: "GuardDutyAlerts" EventPattern: source: - aws.guardduty detail-type: - GuardDuty Finding detail: severity: - numeric: - '>=' - 4 - < - 9 Targets: - Arn: !Ref snsNotifications Id: snsNotifications InputTransformer: InputPathsMap: Account_ID: $.detail.accountId Finding_ID: $.detail.id Finding_Type: $.detail.type Finding_description: $.detail.description region: $.region severity: $.detail.severity InputTemplate: >- "AWS account has a severity GuardDuty finding type in the region." "Finding Description: " "For more details navigate to the GuardDuty console at https://console.aws.amazon.com/guardduty/home?region=#/findings?search=id%3D" ruleSecurityHubAlerts: Type: AWS::Events::Rule Properties: Name: "SecurityHubAlerts" ScheduleExpression: !Ref pSecurityHubEmailsRate State: ENABLED Targets: - Arn: !GetAtt fnSecurityHubLambdaAlert.Arn Id: fnSecurityHubLambdaAlert ruleScheduleEnableSecurityServices: Type: AWS::Events::Rule Properties: EventBusName: default ScheduleExpression: !Ref pEnableSecurityServicesRate State: ENABLED Targets: - Id: stepEnableGuardDuty Arn: !GetAtt stepEnableGuardDuty.Arn RoleArn: !GetAtt iamEventBridgeInvokeStepFunction.Arn - Id: stepEnableSecurityHubAndConfig Arn: !GetAtt stepEnableSecurityHubAndConfig.Arn RoleArn: !GetAtt iamEventBridgeInvokeStepFunction.Arn - Id: stepEnableCloudTrail Arn: !GetAtt stepEnableCloudTrail.Arn RoleArn: !GetAtt iamEventBridgeInvokeStepFunction.Arn permRuleSecurityHubAlerts: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt fnSecurityHubLambdaAlert.Arn Principal: events.amazonaws.com SourceArn: !GetAtt ruleSecurityHubAlerts.Arn