// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`admin stack 1`] = ` { "Description": "test;", "Parameters": { "SC200Example1AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for SC 2.0.0 Example.1", "Type": "String", }, "SC200Example3AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for SC 2.0.0 Example.3", "Type": "String", }, "SC200Example5AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for SC 2.0.0 Example.5", "Type": "String", }, "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/Solutions/SO0111/OrchestratorArn", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "SCExample1AutoEventRule34C9623C": { "Properties": { "Description": "Remediate SC 2.0.0 Example.1 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ "security-control/Example.1", ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "SC_2.0.0_Example.1_AutoTrigger", "State": { "Ref": "SC200Example1AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "SCExample1EventsRuleRole4E88085F", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "SCExample1EventsRuleRole4E88085F": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "SCExample1EventsRuleRoleDefaultPolicyCA403208": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "SCExample1EventsRuleRoleDefaultPolicyCA403208", "Roles": [ { "Ref": "SCExample1EventsRuleRole4E88085F", }, ], }, "Type": "AWS::IAM::Policy", }, "SCExample3AutoEventRule21EE1ACB": { "Properties": { "Description": "Remediate SC 2.0.0 Example.3 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ "security-control/Example.3", ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "SC_2.0.0_Example.3_AutoTrigger", "State": { "Ref": "SC200Example3AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "SCExample3EventsRuleRole1761FED0", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "SCExample3EventsRuleRole1761FED0": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "SCExample3EventsRuleRoleDefaultPolicy6D141B89": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "SCExample3EventsRuleRoleDefaultPolicy6D141B89", "Roles": [ { "Ref": "SCExample3EventsRuleRole1761FED0", }, ], }, "Type": "AWS::IAM::Policy", }, "SCExample5AutoEventRule174B7EFF": { "Properties": { "Description": "Remediate SC 2.0.0 Example.5 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ "security-control/Example.5", ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "SC_2.0.0_Example.5_AutoTrigger", "State": { "Ref": "SC200Example5AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "SCExample5EventsRuleRole568252F1", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "SCExample5EventsRuleRole568252F1": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "SCExample5EventsRuleRoleDefaultPolicy179ECE2B": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "SCExample5EventsRuleRoleDefaultPolicy179ECE2B", "Roles": [ { "Ref": "SCExample5EventsRuleRole568252F1", }, ], }, "Type": "AWS::IAM::Policy", }, "SCShortName2FDDCF16": { "Properties": { "Description": "Provides a short (1-12) character abbreviation for the standard.", "Name": "/Solutions/SO0111/security-control/2.0.0/shortname", "Type": "String", "Value": "SC", }, "Type": "AWS::SSM::Parameter", }, "StandardVersionCB2C6951": { "Properties": { "Description": "This parameter controls whether the SHARR step function will process findings for this version of the standard.", "Name": "/Solutions/SO0111/security-control/2.0.0/status", "Type": "String", "Value": "enabled", }, "Type": "AWS::SSM::Parameter", }, }, } `; exports[`member stack 1`] = ` { "Conditions": { "ControlRunbooksEnableAutoScaling1ConditionD5DF4981": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableAutoScaling1851AF8B0", }, "Available", ], }, "ControlRunbooksEnableCloudFormation1ConditionD8D32097": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudFormation1B75725BB", }, "Available", ], }, "ControlRunbooksEnableCloudTrail1ConditionB7EBAA86": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail1F0F927F7", }, "Available", ], }, "ControlRunbooksEnableCloudTrail2ConditionC182A10F": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail28CC248AB", }, "Available", ], }, "ControlRunbooksEnableCloudTrail4Condition587734A2": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail4040C6EAB", }, "Available", ], }, "ControlRunbooksEnableCloudTrail5Condition17B6B536": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail52CBFD019", }, "Available", ], }, "ControlRunbooksEnableCloudTrail6Condition486CC2C3": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail63394AC2B", }, "Available", ], }, "ControlRunbooksEnableCloudTrail7ConditionA4FF88B2": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudTrail7FFC8DAB9", }, "Available", ], }, "ControlRunbooksEnableCloudWatch1ConditionAB0DF2E5": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCloudWatch19BE65F2B", }, "Available", ], }, "ControlRunbooksEnableCodeBuild2ConditionB01F473D": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableCodeBuild26FB6E539", }, "Available", ], }, "ControlRunbooksEnableConfig1Condition8CEB8627": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableConfig19F6E6FE3", }, "Available", ], }, "ControlRunbooksEnableEC213Condition567EA275": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC21349FA0A79", }, "Available", ], }, "ControlRunbooksEnableEC215Condition52A7DE4B": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC215DA64A549", }, "Available", ], }, "ControlRunbooksEnableEC21ConditionD4F1277B": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC21395C7891", }, "Available", ], }, "ControlRunbooksEnableEC22ConditionB9E0D42E": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC22F9B66A60", }, "Available", ], }, "ControlRunbooksEnableEC26ConditionF1F880B0": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC265685AB83", }, "Available", ], }, "ControlRunbooksEnableEC27ConditionC77CF056": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableEC27108F6303", }, "Available", ], }, "ControlRunbooksEnableIAM18ConditionC6288150": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableIAM18A4548D88", }, "Available", ], }, "ControlRunbooksEnableIAM22Condition387158E7": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableIAM22E05F6A1E", }, "Available", ], }, "ControlRunbooksEnableIAM3Condition3AA0E892": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableIAM35D05519D", }, "Available", ], }, "ControlRunbooksEnableIAM7ConditionDF8E776B": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableIAM766CB4E0A", }, "Available", ], }, "ControlRunbooksEnableIAM8Condition9CA5CB4B": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableIAM834577BE3", }, "Available", ], }, "ControlRunbooksEnableKMS4Condition710C0C5C": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableKMS415F4485B", }, "Available", ], }, "ControlRunbooksEnableLambda1Condition077CECAF": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableLambda11AAE99FF", }, "Available", ], }, "ControlRunbooksEnableRDS13Condition0E8A44B3": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS13F10477DD", }, "Available", ], }, "ControlRunbooksEnableRDS16ConditionCB5C3E8F": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS16F428962C", }, "Available", ], }, "ControlRunbooksEnableRDS1ConditionFAE5B7EA": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS18380A289", }, "Available", ], }, "ControlRunbooksEnableRDS2Condition4FD00FE6": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS2004A67EB", }, "Available", ], }, "ControlRunbooksEnableRDS4Condition2E89346E": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS4E2A98B6D", }, "Available", ], }, "ControlRunbooksEnableRDS5ConditionEC2574C3": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS59E051E8F", }, "Available", ], }, "ControlRunbooksEnableRDS6Condition4A60A39B": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS6C46B2207", }, "Available", ], }, "ControlRunbooksEnableRDS7ConditionE53509B0": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS7CEA605AE", }, "Available", ], }, "ControlRunbooksEnableRDS8Condition8F460AB5": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRDS8FBE41D2B", }, "Available", ], }, "ControlRunbooksEnableRedshift1Condition3449D560": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRedshift1E5BFAC24", }, "Available", ], }, "ControlRunbooksEnableRedshift3ConditionC65BAEF6": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRedshift39346F065", }, "Available", ], }, "ControlRunbooksEnableRedshift4Condition2377F6B5": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRedshift40FBDF0D8", }, "Available", ], }, "ControlRunbooksEnableRedshift6Condition5A51FC97": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableRedshift648AC3FBB", }, "Available", ], }, "ControlRunbooksEnableS31Condition25C33B3F": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableS3116A23B93", }, "Available", ], }, "ControlRunbooksEnableS32ConditionD6F8CCE9": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableS325CF1F81C", }, "Available", ], }, "ControlRunbooksEnableS34ConditionC23F6623": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableS348078AE21", }, "Available", ], }, "ControlRunbooksEnableS35ConditionD5E024B6": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableS35B965D7F6", }, "Available", ], }, "ControlRunbooksEnableS36ConditionD22273E2": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableS36B92F84BB", }, "Available", ], }, "ControlRunbooksEnableSNS1Condition7720D1CC": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableSNS1B5923950", }, "Available", ], }, "ControlRunbooksEnableSNS2Condition69621468": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableSNS232380485", }, "Available", ], }, "ControlRunbooksEnableSQS1Condition3065B4F2": { "Fn::Equals": [ { "Ref": "ControlRunbooksEnableSQS1A400C913", }, "Available", ], }, }, "Description": "test;", "Parameters": { "ControlRunbooksEnableAutoScaling1851AF8B0": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control AutoScaling.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudFormation1B75725BB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudFormation.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail1F0F927F7": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail28CC248AB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail4040C6EAB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.4 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail52CBFD019": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.5 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail63394AC2B": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudTrail7FFC8DAB9": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudTrail.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCloudWatch19BE65F2B": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CloudWatch.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableCodeBuild26FB6E539": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control CodeBuild.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableConfig19F6E6FE3": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Config.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC21349FA0A79": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.13 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC21395C7891": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC215DA64A549": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.15 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC22F9B66A60": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC265685AB83": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableEC27108F6303": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control EC2.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableIAM18A4548D88": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control IAM.18 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableIAM22E05F6A1E": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control IAM.22 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableIAM35D05519D": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control IAM.3 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableIAM766CB4E0A": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control IAM.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableIAM834577BE3": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control IAM.8 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableKMS415F4485B": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control KMS.4 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableLambda11AAE99FF": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Lambda.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS13F10477DD": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.13 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS16F428962C": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.16 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS18380A289": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS2004A67EB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS4E2A98B6D": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.4 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS59E051E8F": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.5 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS6C46B2207": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS7CEA605AE": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.7 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRDS8FBE41D2B": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control RDS.8 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRedshift1E5BFAC24": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Redshift.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRedshift39346F065": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Redshift.3 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRedshift40FBDF0D8": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Redshift.4 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableRedshift648AC3FBB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control Redshift.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableS3116A23B93": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control S3.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableS325CF1F81C": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control S3.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableS348078AE21": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control S3.4 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableS35B965D7F6": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control S3.5 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableS36B92F84BB": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control S3.6 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableSNS1B5923950": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control SNS.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableSNS232380485": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control SNS.2 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "ControlRunbooksEnableSQS1A400C913": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for pci-dss version 3.2.1 Control SQS.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "SecHubAdminAccount": { "AllowedPattern": "^\\d{12}$", "Description": "Admin account number", "Type": "String", }, "WaitProviderServiceToken": { "Type": "String", }, }, "Resources": { "ControlRunbooksAutoScaling1BA109277": { "Condition": "ControlRunbooksEnableAutoScaling1ConditionD5DF4981", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_AutoScaling.1 ## What does this document do? This document enables ELB healthcheck on a given AutoScaling Group using the [UpdateAutoScalingGroup] API. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * HealthCheckGracePeriod: (Optional) Health check grace period when ELB health check is Enabled Default: 30 seconds * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP AutoScaling.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-autoscaling-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "AutoScaling.1", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):autoscaling:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:autoScalingGroup:(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}):autoScalingGroupName/(.{1,255})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "AutoScalingGroupName", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableAutoScalingGroupELBHealthCheck", "RuntimeParameters": { "AutoScalingGroupName": "{{ ParseInput.AutoScalingGroupName }}", "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "ASG health check type updated to ELB", "UpdatedBy": "ASR-PCI_3.2.1_AutoScaling.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the AutoScaling.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableAutoScalingGroupELBHealthCheck", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_AutoScaling.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudFormation12CB945DB": { "Condition": "ControlRunbooksEnableCloudFormation1ConditionD8D32097", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CloudFormation.1 ## What does this document do? This document configures an SNS topic for notifications from a CloudFormation stack by calling another document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP v1.0.0 CloudFormation.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudformation-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudFormation.1", ], "parse_id_pattern": "^(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})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "StackArn", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-ConfigureSNSTopicForStack", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "StackArn": "{{ ParseInput.StackArn }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Configured SNS topic for notifications", "UpdatedBy": "ASR-PCI_3.2.1_CloudFormation.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudFormation.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-ConfigureSNSTopicForStack", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudFormation.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail1B15F1A13": { "Condition": "ControlRunbooksEnableCloudTrail1ConditionB7EBAA86", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CloudTrail.1 ## What does this document do? Creates a multi-region trail with KMS encryption and enables CloudTrail Note: this remediation will create a NEW trail. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP CloudTrail.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.1", "CloudTrail.3", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-CreateCloudTrailMultiRegionTrail", "RuntimeParameters": { "AWSPartition": "{{ global:AWS_PARTITION }}", "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Multi-region, encrypted AWS CloudTrail successfully created", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-CreateCloudTrailMultiRegionTrail", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail2979D0B5D": { "Condition": "ControlRunbooksEnableCloudTrail2ConditionC182A10F", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CloudTrail.2 ## What does this document do? This document enables SSE KMS encryption for log files using the ASR remediation KMS CMK ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output from the remediation ## Documentation Links * [AFSBP CloudTrail.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-2) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.2", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "TrailArn", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableCloudTrailEncryption", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "KMSKeyArn": "{{ KMSKeyArn }}", "TrailArn": "{{ ParseInput.TrailArn }}", "TrailRegion": "{{ ParseInput.RemediationRegion }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Encryption enabled on CloudTrail", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.2", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.2 finding", "type": "StringMap", }, "KMSKeyArn": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias\\/[A-Za-z0-9/_-])|(?:key\\/(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})))$", "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", "type": "String", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableCloudTrailEncryption", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.2", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail4057F669F": { "Condition": "ControlRunbooksEnableCloudTrail4Condition587734A2", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CloudTrail.4 ## What does this document do? This document enables CloudTrail log file validation. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP v1.0.0 CloudTrail.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-4) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.4", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:trail\\/([A-Za-z0-9._-]{3,128})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "TrailName", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableCloudTrailLogFileValidation", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "TrailName": "{{ ParseInput.TrailName }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Enabled CloudTrail log file validation.", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.4", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.4 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableCloudTrailLogFileValidation", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.4", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail54F5ED8E4": { "Condition": "ControlRunbooksEnableCloudTrail5Condition17B6B536", "DependsOn": [ "CreateWait1", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CloudTrail.5 ## What does this document do? This document configures CloudTrail to log to CloudWatch Logs. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Remediation results ## Documentation Links * [AFSBP v1.0.0 CloudTrail.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-cloudtrail-5) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.5", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):cloudtrail:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:trail\\/([A-Za-z0-9._-]{3,128})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "TrailName", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableCloudTrailToCloudWatchLogging", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "CloudWatchLogsRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-CloudTrailToCloudWatchLogs", "LogGroupName": "CloudTrail/{{ ParseInput.TrailName }}", "TrailName": "{{ ParseInput.TrailName }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Configured CloudTrail logging to CloudWatch Logs Group CloudTrail/{{ ParseInput.TrailName }}", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.5", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.5 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableCloudTrailToCloudWatchLogging", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.5", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail6526C5643": { "Condition": "ControlRunbooksEnableCloudTrail6Condition486CC2C3", "DependsOn": [ "CreateWait1", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_2.3 ## What does this document do? This document blocks public access to the CloudTrail S3 bucket. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [CIS v1.2.0 2.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.3) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.6", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "BucketName", "Selector": "$.Payload.resource_id", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-ConfigureS3BucketPublicAccessBlock", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "BlockPublicAcls": true, "BlockPublicPolicy": true, "BucketName": "{{ ParseInput.BucketName }}", "IgnorePublicAcls": true, "RestrictPublicBuckets": true, }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Disabled public access to CloudTrail logs bucket.", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.6", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.6 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-ConfigureS3BucketPublicAccessBlock", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.6", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudTrail7C6D85038": { "Condition": "ControlRunbooksEnableCloudTrail7ConditionA4FF88B2", "DependsOn": [ "CreateWait1", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_2.6 ## What does this document do? Configures access logging for a CloudTrail S3 bucket. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Remediation results ## Documentation Links * [CIS v1.2.0 2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.6) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudTrail.7", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):s3:::([A-Za-z0-9.-]{3,63})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "BucketName", "Selector": "$.Payload.resource_id", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-CreateAccessLoggingBucket", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-CreateAccessLoggingBucket", "BucketName": "so0111-cloudtrailaccesslogs-{{ global:ACCOUNT_ID }}-{{ global:REGION }}", }, }, "name": "CreateAccessLoggingBucket", }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "AWS-ConfigureS3BucketLogging", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "BucketName": "{{ ParseInput.BucketName }}", "GrantedPermission": [ "READ", ], "GranteeType": [ "Group", ], "GranteeUri": [ "http://acs.amazonaws.com/groups/s3/LogDelivery", ], "TargetBucket": [ "so0111-cloudtrailaccesslogs-{{ global:ACCOUNT_ID }}-{{ global:REGION }}", ], "TargetPrefix": [ "{{ ParseInput.BucketName }}", ], }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Created S3 bucket so0111-cloudtrailaccesslogs-{{ global:ACCOUNT_ID }}-{{ global:REGION }} for logging access to {{ ParseInput.BucketName }}", "UpdatedBy": "ASR-PCI_3.2.1_CloudTrail.7", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudTrail.7 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-ConfigureS3BucketLogging", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudTrail.7", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCloudWatch1A05F543A": { "Condition": "ControlRunbooksEnableCloudWatch1ConditionAB0DF2E5", "DependsOn": [ "CreateWait1", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_3.x ## What does this document do? Remediates the following CIS findings: 3.1 - Creates a log metric filter and alarm for unauthorized API calls 3.2 - Creates a log metric filter and alarm for AWS Management Console sign-in without MFA 3.3 - Creates a log metric filter and alarm for usage of "root" account 3.4 - Creates a log metric filter and alarm for for IAM policy changes 3.5 - Creates a log metric filter and alarm for CloudTrail configuration changes 3.6 - Creates a log metric filter and alarm for AWS Management Console authentication failures 3.7 - Creates a log metric filter and alarm for disabling or scheduled deletion of customer created CMKs 3.8 - Creates a log metric filter and alarm for S3 bucket policy changes 3.9 - Creates a log metric filter and alarm for AWS Config configuration changes 3.10 - Creates a log metric filter and alarm for security group changes 3.11 - Creates a log metric filter and alarm for changes to Network Access Control Lists (NACL) 3.12 - Creates a log metric filter and alarm for changes to network gateways 3.13 - Creates a log metric filter and alarm for route table changes 3.14 - Creates a log metric filter and alarm for VPC changes ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of remediation runbook. ## Documentation Links [CIS v1.2.0 3.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.1) [CIS v1.2.0 3.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.2) [CIS v1.2.0 3.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.3) [CIS v1.2.0 3.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.4) [CIS v1.2.0 3.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.5) [CIS v1.2.0 3.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.6) [CIS v1.2.0 3.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.7) [CIS v1.2.0 3.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.8) [CIS v1.2.0 3.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.9) [CIS v1.2.0 3.10](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.10) [CIS v1.2.0 3.11](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.11) [CIS v1.2.0 3.12](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.12) [CIS v1.2.0 3.13](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.13) [CIS v1.2.0 3.14](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-3.14) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CloudWatch.1", "CloudWatch.2", "CloudWatch.3", "CloudWatch.4", "CloudWatch.5", "CloudWatch.6", "CloudWatch.7", "CloudWatch.8", "CloudWatch.9", "CloudWatch.10", "CloudWatch.11", "CloudWatch.12", "CloudWatch.13", "CloudWatch.14", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "ControlId", "Selector": "$.Payload.control_id", "Type": "String", }, ], }, { "action": "aws:executeScript", "inputs": { "Handler": "verify", "InputPayload": { "ControlId": "{{ ParseInput.ControlId }}", "StandardLongName": "pci-dss", "StandardVersion": "3.2.1", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 unauthorizedAPICallsFilter = { "filter_name": "UnauthorizedAPICalls", "filter_pattern": '{($.errorCode="*UnauthorizedOperation") || ($.errorCode="AccessDenied*")}', "metric_name": "UnauthorizedAPICalls", "metric_value": 1, "alarm_name": "UnauthorizedAPICalls", "alarm_desc": "Alarm for UnauthorizedAPICalls > 0", "alarm_threshold": 1 } consoleSignInWithoutMFAFilter = { "filter_name": "ConsoleSigninWithoutMFA", "filter_pattern": '{($.eventName="ConsoleLogin") && ($.additionalEventData.MFAUsed !="Yes")}', "metric_name": "ConsoleSigninWithoutMFA", "metric_value": 1, "alarm_name": "ConsoleSigninWithoutMFA", "alarm_desc": "Alarm for ConsoleSigninWithoutMFA > 0", "alarm_threshold": 1 } rootAccountUsageFilter = { "filter_name": "RootAccountUsage", "filter_pattern": '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}', "metric_name": "RootAccountUsage", "metric_value": 1, "alarm_name": "RootAccountUsage", "alarm_desc": "Alarm for RootAccountUsage > 0", "alarm_threshold": 1 } iamPolicyChangesFilter = { "filter_name": "IAMPolicyChanges", "filter_pattern": '{($.eventName=DeleteGroupPolicy) || ($.eventName=DeleteRolePolicy) || ($.eventName=DeleteUserPolicy) || ($.eventName=PutGroupPolicy) || ($.eventName=PutRolePolicy) || ($.eventName=PutUserPolicy) || ($.eventName=CreatePolicy) || ($.eventName=DeletePolicy) || ($.eventName=CreatePolicyVersion) || ($.eventName=DeletePolicyVersion) || ($.eventName=AttachRolePolicy) || ($.eventName=DetachRolePolicy) || ($.eventName=AttachUserPolicy) || ($.eventName=DetachUserPolicy) || ($.eventName=AttachGroupPolicy) || ($.eventName=DetachGroupPolicy)}', "metric_name": "IAMPolicyChanges", "metric_value": 1, "alarm_name": "IAMPolicyChanges", "alarm_desc": "Alarm for IAMPolicyChanges > 0", "alarm_threshold": 1 } cloudtrailChangesFilter = { "filter_name": "CloudTrailChanges", "filter_pattern": '{($.eventName=CreateTrail) || ($.eventName=UpdateTrail) || ($.eventName=DeleteTrail) || ($.eventName=StartLogging) || ($.eventName=StopLogging)}', "metric_name": "CloudTrailChanges", "metric_value": 1, "alarm_name": "CloudTrailChanges", "alarm_desc": "Alarm for CloudTrailChanges > 0", "alarm_threshold": 1 } consoleAuthenticationFailureFilter = { "filter_name": "ConsoleAuthenticationFailure", "filter_pattern": '{($.eventName=ConsoleLogin) && ($.errorMessage="Failed authentication")}', "metric_name": "ConsoleAuthenticationFailure", "metric_value": 1, "alarm_name": "ConsoleAuthenticationFailure", "alarm_desc": "Alarm for ConsoleAuthenticationFailure > 0", "alarm_threshold": 1 } disableOrDeleteCMKFilter = { "filter_name": "DisableOrDeleteCMK", "filter_pattern": '{($.eventSource=kms.amazonaws.com) && (($.eventName=DisableKey) || ($.eventName=ScheduleKeyDeletion))}', "metric_name": "DisableOrDeleteCMK", "metric_value": 1, "alarm_name": "DisableOrDeleteCMK", "alarm_desc": "Alarm for DisableOrDeleteCMK > 0", "alarm_threshold": 1 } s3BucketPolicyChangesFilter = { "filter_name": "S3BucketPolicyChanges", "filter_pattern": '{($.eventSource=s3.amazonaws.com) && (($.eventName=PutBucketAcl) || ($.eventName=PutBucketPolicy) || ($.eventName=PutBucketCors) || ($.eventName=PutBucketLifecycle) || ($.eventName=PutBucketReplication) || ($.eventName=DeleteBucketPolicy) || ($.eventName=DeleteBucketCors) || ($.eventName=DeleteBucketLifecycle) || ($.eventName=DeleteBucketReplication))}', "metric_name": "S3BucketPolicyChanges", "metric_value": 1, "alarm_name": "S3BucketPolicyChanges", "alarm_desc": "Alarm for S3BucketPolicyChanges > 0", "alarm_threshold": 1 } awsConfigChangesFilter = { "filter_name": "AWSConfigChanges", "filter_pattern": '{($.eventSource=config.amazonaws.com) && (($.eventName=StopConfigurationRecorder) || ($.eventName=DeleteDeliveryChannel) || ($.eventName=PutDeliveryChannel) || ($.eventName=PutConfigurationRecorder))}', "metric_name": "AWSConfigChanges", "metric_value": 1, "alarm_name": "AWSConfigChanges", "alarm_desc": "Alarm for AWSConfigChanges > 0", "alarm_threshold": 1 } securityGroupChangesFilter = { "filter_name": "SecurityGroupChanges", "filter_pattern": '{($.eventName=AuthorizeSecurityGroupIngress) || ($.eventName=AuthorizeSecurityGroupEgress) || ($.eventName=RevokeSecurityGroupIngress) || ($.eventName=RevokeSecurityGroupEgress) || ($.eventName=CreateSecurityGroup) || ($.eventName=DeleteSecurityGroup)}', "metric_name": "SecurityGroupChanges", "metric_value": 1, "alarm_name": "SecurityGroupChanges", "alarm_desc": "Alarm for SecurityGroupChanges > 0", "alarm_threshold": 1 } networkACLChangesFilter = { "filter_name": "NetworkACLChanges", "filter_pattern": '{($.eventName=CreateNetworkAcl) || ($.eventName=CreateNetworkAclEntry) || ($.eventName=DeleteNetworkAcl) || ($.eventName=DeleteNetworkAclEntry) || ($.eventName=ReplaceNetworkAclEntry) || ($.eventName=ReplaceNetworkAclAssociation)}', "metric_name": "NetworkACLChanges", "metric_value": 1, "alarm_name": "NetworkACLChanges", "alarm_desc": "Alarm for NetworkACLChanges > 0", "alarm_threshold": 1 } networkGatewayChangesFilter = { "filter_name": "NetworkGatewayChanges", "filter_pattern": '{($.eventName=CreateCustomerGateway) || ($.eventName=DeleteCustomerGateway) || ($.eventName=AttachInternetGateway) || ($.eventName=CreateInternetGateway) || ($.eventName=DeleteInternetGateway) || ($.eventName=DetachInternetGateway)}', "metric_name": "NetworkGatewayChanges", "metric_value": 1, "alarm_name": "NetworkGatewayChanges", "alarm_desc": "Alarm for NetworkGatewayChanges > 0", "alarm_threshold": 1 } routeTableChangesFilter = { "filter_name": "RouteTableChanges", "filter_pattern": '{($.eventName=CreateRoute) || ($.eventName=CreateRouteTable) || ($.eventName=ReplaceRoute) || ($.eventName=ReplaceRouteTableAssociation) || ($.eventName=DeleteRouteTable) || ($.eventName=DeleteRoute) || ($.eventName=DisassociateRouteTable)}', "metric_name": "RouteTableChanges", "metric_value": 1, "alarm_name": "RouteTableChanges", "alarm_desc": "Alarm for RouteTableChanges > 0", "alarm_threshold": 1 } vpcChangesFilter = { "filter_name": "VPCChanges", "filter_pattern": '{($.eventName=CreateVpc) || ($.eventName=DeleteVpc) || ($.eventName=ModifyVpcAttribute) || ($.eventName=AcceptVpcPeeringConnection) || ($.eventName=CreateVpcPeeringConnection) || ($.eventName=DeleteVpcPeeringConnection) || ($.eventName=RejectVpcPeeringConnection) || ($.eventName=AttachClassicLinkVpc) || ($.eventName=DetachClassicLinkVpc) || ($.eventName=DisableVpcClassicLink) || ($.eventName=EnableVpcClassicLink)}', "metric_name": "VPCChanges", "metric_value": 1, "alarm_name": "VPCChanges", "alarm_desc": "Alarm for VPCChanges > 0", "alarm_threshold": 1 } Cloudwatch_mappings = { 'cis-aws-foundations-benchmark': { '1.2.0': { '3.1': unauthorizedAPICallsFilter, '3.2': consoleSignInWithoutMFAFilter, '3.3': rootAccountUsageFilter, '3.4': iamPolicyChangesFilter, '3.5': cloudtrailChangesFilter, '3.6': consoleAuthenticationFailureFilter, '3.7': disableOrDeleteCMKFilter, '3.8': s3BucketPolicyChangesFilter, '3.9': awsConfigChangesFilter, '3.10': securityGroupChangesFilter, '3.11': networkACLChangesFilter, '3.12': networkGatewayChangesFilter, '3.13': routeTableChangesFilter, '3.14': vpcChangesFilter }, '1.4.0': { '4.3': rootAccountUsageFilter, '4.4': iamPolicyChangesFilter, '4.5': cloudtrailChangesFilter, '4.6': consoleAuthenticationFailureFilter, '4.7': disableOrDeleteCMKFilter, '4.8': s3BucketPolicyChangesFilter, '4.9': awsConfigChangesFilter, '4.10': securityGroupChangesFilter, '4.11': networkACLChangesFilter, '4.12': networkGatewayChangesFilter, '4.13': routeTableChangesFilter, '4.14': vpcChangesFilter } }, 'security-control': { '2.0.0': { "CloudWatch.1": rootAccountUsageFilter, "CloudWatch.2": unauthorizedAPICallsFilter, "CloudWatch.3": consoleSignInWithoutMFAFilter, "CloudWatch.4": iamPolicyChangesFilter, "CloudWatch.5": cloudtrailChangesFilter, "CloudWatch.6": consoleAuthenticationFailureFilter, "CloudWatch.7": disableOrDeleteCMKFilter, "CloudWatch.8": s3BucketPolicyChangesFilter, "CloudWatch.9": awsConfigChangesFilter, "CloudWatch.10": securityGroupChangesFilter, "CloudWatch.11": networkACLChangesFilter, "CloudWatch.12": networkGatewayChangesFilter, "CloudWatch.13": routeTableChangesFilter, "CloudWatch.14": vpcChangesFilter } } } def verify(event, _): try: standard_mapping = Cloudwatch_mappings.get(event['StandardLongName']).get(event['StandardVersion']) return standard_mapping.get(event['ControlId'], None) except KeyError as ex: exit(f'ERROR: Could not find associated metric filter. Missing parameter: {str(ex)}') ", }, "name": "GetMetricFilterAndAlarmInputValue", "outputs": [ { "Name": "FilterName", "Selector": "$.Payload.filter_name", "Type": "String", }, { "Name": "FilterPattern", "Selector": "$.Payload.filter_pattern", "Type": "String", }, { "Name": "MetricName", "Selector": "$.Payload.metric_name", "Type": "String", }, { "Name": "MetricValue", "Selector": "$.Payload.metric_value", "Type": "Integer", }, { "Name": "AlarmName", "Selector": "$.Payload.alarm_name", "Type": "String", }, { "Name": "AlarmDesc", "Selector": "$.Payload.alarm_desc", "Type": "String", }, { "Name": "AlarmThreshold", "Selector": "$.Payload.alarm_threshold", "Type": "Integer", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-CreateLogMetricFilterAndAlarm", "RuntimeParameters": { "AlarmDesc": "{{ GetMetricFilterAndAlarmInputValue.AlarmDesc }}", "AlarmName": "{{ GetMetricFilterAndAlarmInputValue.AlarmName }}", "AlarmThreshold": "{{ GetMetricFilterAndAlarmInputValue.AlarmThreshold }}", "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "FilterName": "{{ GetMetricFilterAndAlarmInputValue.FilterName }}", "FilterPattern": "{{ GetMetricFilterAndAlarmInputValue.FilterPattern }}", "KMSKeyArn": "{{ KMSKeyArn }}", "LogGroupName": "{{ LogGroupName }}", "MetricName": "{{ GetMetricFilterAndAlarmInputValue.MetricName }}", "MetricNamespace": "{{ MetricNamespace }}", "MetricValue": "{{ GetMetricFilterAndAlarmInputValue.MetricValue }}", "SNSTopicName": "SO0111-SHARR-LocalAlarmNotification", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Added metric filter to the log group and notifications to SNS topic SO0111-ASR-LocalAlarmNotification.", "UpdatedBy": "ASR-PCI_3.2.1_CloudWatch.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CloudWatch.1 finding", "type": "StringMap", }, "KMSKeyArn": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias\\/[A-Za-z0-9/-_])|(?:key\\/(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})))$", "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", "description": "The ARN of the KMS key created by ASR for remediations", "type": "String", }, "LogGroupName": { "allowedPattern": ".*", "default": "{{ssm:/Solutions/SO0111/Metrics_LogGroupName}}", "description": "The name of the Log group to be used to create filters and metric alarms", "type": "String", }, "MetricNamespace": { "allowedPattern": ".*", "default": "LogMetrics", "description": "The name of the metric namespace where the metrics will be logged", "type": "String", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-CreateLogMetricFilterAndAlarm", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CloudWatch.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksCodeBuild2A2751671": { "Condition": "ControlRunbooksEnableCodeBuild2ConditionB01F473D", "DependsOn": [ "CreateWait1", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_CodeBuild.2 ## What does this document do? This document removes CodeBuild project environment variables containing clear text credentials and replaces them with Amazon EC2 Systems Manager Parameters. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP v1.0.0 CodeBuild.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-codebuild-2) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "CodeBuild.2", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):codebuild:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:project\\/([A-Za-z0-9][A-Za-z0-9\\-_]{1,254})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "ProjectName", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-ReplaceCodeBuildClearTextCredentials", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "ProjectName": "{{ ParseInput.ProjectName }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Replaced clear text credentials with SSM parameters.", "UpdatedBy": "ASR-PCI_3.2.1_CodeBuild.2", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the CodeBuild.2 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-ReplaceCodeBuildClearTextCredentials", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_CodeBuild.2", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksConfig1512B566F": { "Condition": "ControlRunbooksEnableConfig1Condition8CEB8627", "DependsOn": [ "CreateWait2", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_Config.1 ## What does this document do? Enables AWS Config: * Turns on recording for all resources. * Creates an encrypted bucket for Config logging. * Creates a logging bucket for access logs for the config bucket * Creates an SNS topic for Config notifications * Creates a service-linked role ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP Config.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-config-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "Config.1", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableAWSConfig", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "KMSKeyArn": "{{ KMSKeyArn }}", "SNSTopicName": "SO0111-SHARR-AWSConfigNotification", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "AWS Config enabled", "UpdatedBy": "ASR-PCI_3.2.1_Config.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the Config.1 finding", "type": "StringMap", }, "KMSKeyArn": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias\\/[A-Za-z0-9/-_])|(?:key\\/(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})))$", "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", "description": "The ARN of the KMS key created by ASR for remediations", "type": "String", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableAWSConfig", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_Config.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC213D7C9C1EB": { "Condition": "ControlRunbooksEnableEC213Condition567EA275", "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-PCI_3.2.1_EC2.5 ## What does this document do? Removes public access to remove server administrative ports from an EC2 Security Group ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of AWS-DisablePublicAccessForSecurityGroup runbook. ## Documentation Links * [PCI v3.2.1 EC2.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-ec2-5) * [CIS v1.2.0 4.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-4.1) * [CIS v1.2.0 4.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-4.2) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.13", "EC2.14", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:security-group\\/(sg-[a-f\\d]{8,17})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "GroupId", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "AWS-DisablePublicAccessForSecurityGroup", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "GroupId": "{{ ParseInput.GroupId }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Disabled public access to administrative ports in the security group {{ ParseInput.GroupId }}.", "UpdatedBy": "ASR-PCI_3.2.1_EC2.13", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.13 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-DisablePublicAccessForSecurityGroup", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.13", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC214D3BB404": { "Condition": "ControlRunbooksEnableEC21ConditionD4F1277B", "DependsOn": [ "CreateWait2", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_EC2.1 ## What does this document do? This document changes all public EC2 snapshots to private ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP EC2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.1", ], "parse_id_pattern": "", "resource_index": 2, }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "TestMode", "Selector": "$.Payload.testmode", "Type": "Boolean", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-MakeEBSSnapshotsPrivate", "RuntimeParameters": { "AccountId": "{{ ParseInput.RemediationAccount }}", "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "TestMode": "{{ ParseInput.TestMode }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "EBS Snapshot modified to private", "UpdatedBy": "ASR-PCI_3.2.1_EC2.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-MakeEBSSnapshotsPrivate", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC2153B43E7A8": { "Condition": "ControlRunbooksEnableEC215Condition52A7DE4B", "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_EC2.15 ## What does this document do? This document disables auto assignment of public IP addresses on a subnet. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP v1.0.0 EC2.15](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-15)", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.15", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "SubnetARN", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-DisablePublicIPAutoAssign", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "SubnetARN": "{{ ParseInput.SubnetARN }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Disabled public IP auto assignment for subnet.", "UpdatedBy": "ASR-PCI_3.2.1_EC2.15", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.15 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-DisablePublicIPAutoAssign", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.15", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC22ED852ADF": { "Condition": "ControlRunbooksEnableEC22ConditionB9E0D42E", "DependsOn": [ "CreateWait2", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_EC2.2 ## What does this document do? This document deletes ingress and egress rules from default security group using the AWS SSM Runbook AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output from AWSConfigRemediation-RemoveVPCDefaultSecurityGroupRules SSM doc ## Documentation Links * [AFSBP EC2.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-2) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.2", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:security-group\\/(sg-[0-9a-f]*)$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "GroupId", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RemoveVPCDefaultSecurityGroupRules", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "GroupId": "{{ ParseInput.GroupId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Removed rules on default security group", "UpdatedBy": "ASR-PCI_3.2.1_EC2.2", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.2 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-RemoveVPCDefaultSecurityGroupRules", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.2", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC267E3087AE": { "Condition": "ControlRunbooksEnableEC26ConditionF1F880B0", "DependsOn": [ "CreateWait2", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_EC2.6 ## What does this document do? Enables VPC Flow Logs for a VPC ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Remediation results ## Documentation Links * [AFSBP EC2.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-6) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.6", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):ec2:.*:\\d{12}:vpc\\/(vpc-[0-9a-f]{8,17})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "VPC", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableVPCFlowLogs", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "RemediationRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/SO0111-EnableVPCFlowLogs-remediationRole", "VPC": "{{ ParseInput.VPC }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Removed rules on default security group", "UpdatedBy": "ASR-PCI_3.2.1_EC2.6", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.6 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableVPCFlowLogs", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.6", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksEC277719A4CD": { "Condition": "ControlRunbooksEnableEC27ConditionC77CF056", "DependsOn": [ "CreateWait2", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_EC2.7 ## What does this document do? This document enables \`EBS Encryption by default\` for an AWS account in the current region by calling another SSM document ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP EC2.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-ec2-7) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "EC2.7", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableEbsEncryptionByDefault", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Enabled EBS encryption by default", "UpdatedBy": "ASR-PCI_3.2.1_EC2.7", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the EC2.7 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableEbsEncryptionByDefault", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_EC2.7", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksIAM18ACE62321": { "Condition": "ControlRunbooksEnableIAM18ConditionC6288150", "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_1.20 ## What does this document do? Creates a support role to allow AWS Support access. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of CreateRole API. ## Documentation Links * [CIS v1.2.0 1.20](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.20) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "IAM.18", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-CreateIAMSupportRole", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Create an IAM role to allow authorized users to manage incidents with AWS Support using the ASR-CreateIAMSupportRole runbook.", "UpdatedBy": "ASR-PCI_3.2.1_IAM.18", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the IAM.18 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-CreateIAMSupportRole", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_IAM.18", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksIAM2280FCB95D": { "Condition": "ControlRunbooksEnableIAM22Condition387158E7", "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_IAM.8 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of remediation runbook SEE AWSConfigRemediation-RevokeUnusedIAMUserCredentials ## Documentation Links * [AFSBP IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-8) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "IAM.22", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "IAMResourceId", "Selector": "$.Payload.details.AwsIamUser.UserId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RevokeUnusedIAMUserCredentials", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "IAMResourceId": "{{ ParseInput.IAMResourceId }}", "MaxCredentialUsageAge": "45", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Deactivated unused keys and expired logins using the ASR-RevokeUnusedIAMUserCredentials runbook.", "UpdatedBy": "ASR-PCI_3.2.1_IAM.22", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the IAM.22 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-RevokeUnusedIAMUserCredentials", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_IAM.22", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksIAM3DC25477E": { "Condition": "ControlRunbooksEnableIAM3Condition3AA0E892", "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_IAM.3 ## What does this document do? This document disables active keys that have not been rotated for more than 90 days. Note that this remediation is **DISRUPTIVE**. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP v1.0.0 IAM.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-3) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "IAM.3", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):iam::\\d{12}:user(?:(?:\\u002F)|(?:\\u002F[\\u0021-\\u007F]{1,510}\\u002F))([\\w+=,.@-]{1,64})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "IAMUser", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "IAMResourceId", "Selector": "$.Payload.details.AwsIamUser.UserId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RevokeUnrotatedKeys", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "IAMResourceId": "{{ ParseInput.IAMResourceId }}", "MaxCredentialUsageAge": "{{ MaxCredentialUsageAge }}", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Deactivated unrotated keys for {{ ParseInput.IAMUser }}.", "UpdatedBy": "ASR-PCI_3.2.1_IAM.3", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the IAM.3 finding", "type": "StringMap", }, "MaxCredentialUsageAge": { "allowedPattern": "^(?:[1-9]\\d{0,3}|10000)$", "default": "90", "description": "(Required) Maximum number of days a key can be unrotated. The default value is 90 days.", "type": "String", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-RevokeUnrotatedKeys", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_IAM.3", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksIAM70A808F7C": { "Condition": "ControlRunbooksEnableIAM7ConditionDF8E776B", "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_IAM.7 ## What does this document do? This document establishes a default password policy. ## Security Standards and Controls * AFSBP IAM.7 ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [AFSBP IAM.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-7) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "IAM.7", "IAM.11", "IAM.12", "IAM.13", "IAM.14", "IAM.15", "IAM.16", "IAM.17", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-SetIAMPasswordPolicy", "RuntimeParameters": { "AllowUsersToChangePassword": true, "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "HardExpiry": true, "MaxPasswordAge": 90, "MinimumPasswordLength": 14, "PasswordReusePrevention": 24, "RequireLowercaseCharacters": true, "RequireNumbers": true, "RequireSymbols": true, "RequireUppercaseCharacters": true, }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Established a baseline password policy using the ASR-SetIAMPasswordPolicy runbook.", "UpdatedBy": "ASR-PCI_3.2.1_IAM.7", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the IAM.7 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-SetIAMPasswordPolicy", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_IAM.7", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksIAM8632E03ED": { "Condition": "ControlRunbooksEnableIAM8Condition9CA5CB4B", "DependsOn": [ "CreateWait3", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_IAM.8 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of remediation runbook SEE AWSConfigRemediation-RevokeUnusedIAMUserCredentials ## Documentation Links * [AFSBP IAM.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-iam-8) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "IAM.8", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "IAMResourceId", "Selector": "$.Payload.details.AwsIamUser.UserId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RevokeUnusedIAMUserCredentials", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "IAMResourceId": "{{ ParseInput.IAMResourceId }}", "MaxCredentialUsageAge": "90", }, }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Deactivated unused keys and expired logins using the ASR-RevokeUnusedIAMUserCredentials runbook.", "UpdatedBy": "ASR-PCI_3.2.1_IAM.8", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the IAM.8 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-RevokeUnusedIAMUserCredentials", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_IAM.8", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksKMS41A22BB8D": { "Condition": "ControlRunbooksEnableKMS4Condition710C0C5C", "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_2.8 ## What does this document do? Enables rotation for customer-managed KMS keys. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Remediation results ## Documentation Links * [CIS v1.2.0 2.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.8) * [PCI v3.2.1 PCI.KMS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-pci-controls.html#pcidss-kms-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "KMS.4", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:key\\/([A-Za-z0-9-]{36})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "KeyId", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableKeyRotation", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "KeyId": "{{ ParseInput.KeyId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Enabled KMS Customer Managed Key rotation for {{ ParseInput.KeyId }}", "UpdatedBy": "ASR-PCI_3.2.1_KMS.4", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the KMS.4 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableKeyRotation", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_KMS.4", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksLambda1F6ECACF8": { "Condition": "ControlRunbooksEnableLambda1Condition077CECAF", "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_Lambda.1 ## What does this document do? This document removes the public resource policy. A public resource policy contains a principal "*" or AWS: "*", which allows public access to the function. The remediation is to remove the SID of the public policy. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP Lambda.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-lambda-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "Lambda.1", ], "parse_id_pattern": "^arn:(?:aws|aws-us-gov|aws-cn):lambda:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:function:([a-zA-Z0-9\\-_]{1,64})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "FunctionName", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RemoveLambdaPublicAccess", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "FunctionName": "{{ ParseInput.FunctionName }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Lamdba {{ ParseInput.FunctionName }} policy updated to remove public access", "UpdatedBy": "ASR-PCI_3.2.1_Lambda.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the Lambda.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-RemoveLambdaPublicAccess", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_Lambda.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS13FCEA51BD": { "Condition": "ControlRunbooksEnableRDS13Condition0E8A44B3", "DependsOn": [ "CreateWait6", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.13 ## What does this document do? This document enables \`Auto minor version upgrade\` on a given Amazon RDS instance by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - The standard HTTP response from the ModifyDBInstance API. ## Documentation Links * [AFSBP RDS.13](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-13) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.13", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DBInstanceIdentifier", "Selector": "$.Payload.resource.Details.AwsRdsDbInstance.DBInstanceIdentifier", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableMinorVersionUpgradeOnRDSDBInstance", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DBInstanceIdentifier": "{{ ParseInput.DBInstanceIdentifier }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Minor Version enabled on the RDS Instance or Multi-AZ RDS Cluster.", "UpdatedBy": "ASR-PCI_3.2.1_RDS.13", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.13 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableMinorVersionUpgradeOnRDSDBInstance", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.13", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS16EB04DCBF": { "Condition": "ControlRunbooksEnableRDS16ConditionCB5C3E8F", "DependsOn": [ "CreateWait6", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.16 ## What does this document do? This document enables \`Copy tags to snapshots\` on a given Amazon RDS cluster by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - The standard HTTP response from the ModifyDBCluster API. ## Documentation Links * [AFSBP RDS.16](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-16) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.16", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.details.AwsRdsDbCluster.DbClusterResourceId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableCopyTagsToSnapshotOnRDSCluster", "RuntimeParameters": { "ApplyImmediately": true, "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DbClusterResourceId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Copy Tags to Snapshots enabled on RDS DB cluster", "UpdatedBy": "ASR-PCI_3.2.1_RDS.16", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.16 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableCopyTagsToSnapshotOnRDSCluster", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.16", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS1D73701E9": { "Condition": "ControlRunbooksEnableRDS1ConditionFAE5B7EA", "DependsOn": [ "CreateWait4", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.1 ## What does this document do? This document changes public RDS snapshot to private ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP RDS.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.1", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(cluster-snapshot|snapshot):([a-zA-Z](?:[0-9a-zA-Z]+-)*[0-9a-zA-Z]+)$", "resource_index": 2, }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "DBSnapshotId", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DBSnapshotType", "Selector": "$.Payload.matches[0]", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-MakeRDSSnapshotPrivate", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DBSnapshotId": "{{ ParseInput.DBSnapshotId }}", "DBSnapshotType": "{{ ParseInput.DBSnapshotType }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "RDS DB Snapshot modified to private", "UpdatedBy": "ASR-PCI_3.2.1_RDS.1", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.1 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-MakeRDSSnapshotPrivate", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.1", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS2FBE04686": { "Condition": "ControlRunbooksEnableRDS2Condition4FD00FE6", "DependsOn": [ "CreateWait5", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.2 ## What does this document do? This document disables public access to RDS instances by calling another SSM document ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [AFSBP RDS.2](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-2) ## 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:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.2", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:db:((?!.*--.*)(?!.*-$)[a-z][a-z0-9-]{0,62})$", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-DisablePublicAccessToRDSInstance", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DbiResourceId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Disabled public access to RDS instance", "UpdatedBy": "ASR-PCI_3.2.1_RDS.2", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.2 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-DisablePublicAccessToRDSInstance", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.2", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS4C82F2410": { "Condition": "ControlRunbooksEnableRDS4Condition2E89346E", "DependsOn": [ "CreateWait5", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.4 ## What does this document do? This document encrypts an unencrypted RDS snapshot by calling another SSM document ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. * KMSKeyId: (Optional) ID, ARN or Alias for the AWS KMS Customer-Managed Key (CMK) to use to encrypt the snapshot. ## Documentation Links * [AFSBP RDS.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-4) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.4", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):rds:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:((?:cluster-)?snapshot|dbclustersnapshot):((?:rds:)?((?!.*--.*)(?!.*-$)[a-zA-Z][a-zA-Z0-9-]{0,254}))$", "resource_index": 2, }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "SourceDBSnapshotIdentifier", "Selector": "$.Payload.matches[1]", "Type": "String", }, { "Name": "SourceDBSnapshotIdentifierNoPrefix", "Selector": "$.Payload.matches[2]", "Type": "String", }, { "Name": "DBSnapshotType", "Selector": "$.Payload.matches[0]", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EncryptRDSSnapshot", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DBSnapshotType": "{{ ParseInput.DBSnapshotType }}", "KmsKeyId": "{{ KMSKeyId }}", "SourceDBSnapshotIdentifier": "{{ ParseInput.SourceDBSnapshotIdentifier }}", "TargetDBSnapshotIdentifier": "{{ ParseInput.SourceDBSnapshotIdentifierNoPrefix }}-encrypted", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Encrypted RDS snapshot", "UpdatedBy": "ASR-PCI_3.2.1_RDS.4", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.4 finding", "type": "StringMap", }, "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\\/(?:[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-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", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EncryptRDSSnapshot", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.4", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS5CECD9314": { "Condition": "ControlRunbooksEnableRDS5ConditionEC2574C3", "DependsOn": [ "CreateWait5", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.RDS.5 ## What does this document do? This document configures an RDS DB instance for multiple Availability Zones by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. ## Documentation Links * [AFSBP RDS.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-5) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.5", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableMultiAZOnRDSInstance", "RuntimeParameters": { "ApplyImmediately": true, "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DbiResourceId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Configured RDS cluster for multiple Availability Zones", "UpdatedBy": "ASR-PCI_3.2.1_RDS.5", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.5 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableMultiAZOnRDSInstance", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.5", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS6082B0D6B": { "Condition": "ControlRunbooksEnableRDS6Condition4A60A39B", "DependsOn": [ "CreateWait5", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.6 ## What does this document do? This document enables \`Enhanced Monitoring\` on a given Amazon RDS instance by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * VerifyRemediation.Output - The standard HTTP response from the ModifyDBInstance API. ## Documentation Links * [AFSBP RDS.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-6) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.6", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId", "Type": "String", }, ], }, { "action": "aws:executeAwsApi", "inputs": { "Api": "GetRole", "RoleName": "SO0111-RDSMonitoring-remediationRole", "Service": "iam", }, "name": "GetMonitoringRoleArn", "outputs": [ { "Name": "Arn", "Selector": "$.Role.Arn", "Type": "String", }, ], "timeoutSeconds": 600, }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableEnhancedMonitoringOnRDSInstance", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "MonitoringRoleArn": "{{ GetMonitoringRoleArn.Arn }}", "ResourceId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Enhanced Monitoring enabled on RDS DB cluster", "UpdatedBy": "ASR-PCI_3.2.1_RDS.6", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.6 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableEnhancedMonitoringOnRDSInstance", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.6", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS715C0A01A": { "Condition": "ControlRunbooksEnableRDS7ConditionE53509B0", "DependsOn": [ "CreateWait5", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_RDS.7 ## What does this document do? This document enables \`Deletion Protection\` on a given Amazon RDS cluster by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - The standard HTTP response from the ModifyDBCluster API. ## Documentation Links * [AFSBP RDS.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-7) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.7", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.details.AwsRdsDbCluster.DbClusterResourceId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableRDSClusterDeletionProtection", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "ClusterId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Deletion protection enabled on RDS DB cluster", "UpdatedBy": "ASR-PCI_3.2.1_RDS.7", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.7 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableRDSClusterDeletionProtection", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.7", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRDS89256480A": { "Condition": "ControlRunbooksEnableRDS8Condition8F460AB5", "DependsOn": [ "CreateWait6", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.RDS.8 ## What does this document do? This document enables \`Deletion Protection\` on a given Amazon RDS cluster by calling another SSM document. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. ## Documentation Links * [AFSBP RDS.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-rds-8) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "RDS.8", ], "parse_id_pattern": "", }, "Runtime": "python3.8", "Script": "# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re import json import boto3 from botocore.config import Config def connect_to_config(boto_config): return boto3.client('config', config=boto_config) def connect_to_ssm(boto_config): return boto3.client('ssm', config=boto_config) def get_solution_id(): return 'SO0111' def get_solution_version(): ssm = connect_to_ssm( Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/unknown' ) ) solution_version = 'unknown' try: ssm_parm_value = ssm.get_parameter( Name=f'/Solutions/{get_solution_id()}/member-version' )['Parameter'].get('Value', 'unknown') solution_version = ssm_parm_value except Exception as e: print(e) print(f'ERROR getting solution version') return solution_version def get_shortname(long_name): short_name = { 'aws-foundational-security-best-practices': 'AFSBP', 'cis-aws-foundations-benchmark': 'CIS', 'pci-dss': 'PCI', 'security-control': 'SC' } return short_name.get(long_name, None) def get_config_rule(rule_name): boto_config = Config( retries = { 'mode': 'standard' }, user_agent_extra = f'AwsSolution/{get_solution_id()}/{get_solution_version()}' ) config_rule = None try: configsvc = connect_to_config(boto_config) config_rule = configsvc.describe_config_rules( ConfigRuleNames=[ rule_name ] ).get('ConfigRules', [])[0] except Exception as e: print(e) exit(f'ERROR getting config rule {rule_name}') return config_rule class FindingEvent: """ Finding object returns the parse fields from an input finding json object """ def _get_resource_id(self, parse_id_pattern, resource_index): identifier_raw = self.finding_json['Resources'][0]['Id'] self.resource_id = identifier_raw self.resource_id_matches = [] if parse_id_pattern: identifier_match = re.match( parse_id_pattern, identifier_raw ) if identifier_match: for group in range(1, len(identifier_match.groups())+1): self.resource_id_matches.append(identifier_match.group(group)) self.resource_id = identifier_match.group(resource_index) else: exit(f'ERROR: Invalid resource Id {identifier_raw}') def _get_sc_check(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'security-control/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname('security-control') self.control_id = match_finding_id.group(1) return match_finding_id def _get_standard_info(self): match_finding_id = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:'+ 'subscription/(.*?)/v/(\\d+\\.\\d+\\.\\d+)/(.*)/finding/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})$', self.finding_json['Id'] ) if match_finding_id: self.standard_id = get_shortname(match_finding_id.group(1)) self.standard_version = match_finding_id.group(2) self.control_id = match_finding_id.group(3) else: match_sc_finding_id = self._get_sc_check() if not match_sc_finding_id: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]}' def _get_aws_config_rule(self): # config_rule_id refers to the AWS Config Rule that produced the finding if "RelatedAWSResources:0/type" in self.finding_json['ProductFields'] and self.finding_json['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule': self.aws_config_rule_id = self.finding_json['ProductFields']['RelatedAWSResources:0/name'] self.aws_config_rule = get_config_rule(self.aws_config_rule_id) def _get_region_from_resource_id(self): check_for_region = re.match( r'^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9]+:([a-z]{2}(?:-gov)?-[a-z]+-\\d):.*:.*$', self.finding_json['Resources'][0]['Id'] ) if check_for_region: self.resource_region = check_for_region.group(1) def __init__(self, finding_json, parse_id_pattern, expected_control_id, resource_index): self.valid_finding = True self.resource_region = None self.control_id = None self.aws_config_rule_id = None self.aws_config_rule = {} """Populate fields""" # v1.5 self.finding_json = finding_json self._get_resource_id(parse_id_pattern, resource_index) # self.resource_id, self.resource_id_matches self._get_standard_info() # self.standard_id, self.standard_version, self.control_id # V1.4 self.account_id = self.finding_json.get('AwsAccountId', None) # deprecate - get Finding.AwsAccountId if not re.match(r'^\\d{12}$', self.account_id) and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'AwsAccountId is invalid: {self.account_id}' self.finding_id = self.finding_json.get('Id', None) # deprecate self.product_arn = self.finding_json.get('ProductArn', None) if not re.match(r'^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\\d::product/aws/securityhub$', self.product_arn): if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'ProductArn is invalid: {self.product_arn}' self.details = self.finding_json['Resources'][0].get('Details', {}) # Test mode is used with fabricated finding data to tell the # remediation runbook to run in test more (where supported) # Currently not widely-used and perhaps should be deprecated. self.testmode = bool('testmode' in self.finding_json) self.resource = self.finding_json['Resources'][0] self._get_region_from_resource_id() self._get_aws_config_rule() self.affected_object = {'Type': self.resource['Type'], 'Id': self.resource_id, 'OutputKey': 'Remediation.Output'} # Validate control_id if not self.control_id: if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Finding Id is invalid: {self.finding_json["Id"]} - missing Control Id' elif self.control_id not in expected_control_id: # ControlId is the expected value if self.valid_finding: self.valid_finding = False self.invalid_finding_reason = f'Control Id from input ({self.control_id}) does not match {str(expected_control_id)}' if not self.resource_id and self.valid_finding: self.valid_finding = False self.invalid_finding_reason = 'Resource Id is missing from the finding json Resources (Id)' if not self.valid_finding: # Error message and return error data msg = f'ERROR: {self.invalid_finding_reason}' exit(msg) def __str__(self): return json.dumps(self.__dict__) ''' MAIN ''' def parse_event(event, _): finding_event = FindingEvent(event['Finding'], event['parse_id_pattern'], event['expected_control_id'], event.get('resource_index', 1)) if not finding_event.valid_finding: exit('ERROR: Finding is not valid') return { "account_id": finding_event.account_id, "resource_id": finding_event.resource_id, "finding_id": finding_event.finding_id, # Deprecate v1.5.0+ "control_id": finding_event.control_id, "product_arn": finding_event.product_arn, # Deprecate v1.5.0+ "object": finding_event.affected_object, "matches": finding_event.resource_id_matches, "details": finding_event.details, # Deprecate v1.5.0+ "testmode": finding_event.testmode, # Deprecate v1.5.0+ "resource": finding_event.resource, "resource_region": finding_event.resource_region, "finding": finding_event.finding_json, "aws_config_rule": finding_event.aws_config_rule } ", }, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "RemediationAccount", "Selector": "$.Payload.account_id", "Type": "String", }, { "Name": "RemediationRegion", "Selector": "$.Payload.resource_region", "Type": "String", }, { "Name": "DbiResourceId", "Selector": "$.Payload.resource.Details.AwsRdsDbInstance.DbiResourceId", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-EnableRDSInstanceDeletionProtection", "RuntimeParameters": { "ApplyImmediately": true, "AutomationAssumeRole": "arn:{{ global:AWS_PARTITION }}:iam::{{ global:ACCOUNT_ID }}:role/{{ RemediationRoleName }}", "DbInstanceResourceId": "{{ ParseInput.DbiResourceId }}", }, "TargetLocations": [ { "Accounts": [ "{{ ParseInput.RemediationAccount }}", ], "ExecutionRoleName": "{{ RemediationRoleName }}", "Regions": [ "{{ ParseInput.RemediationRegion }}", ], }, ], }, "name": "Remediation", }, { "action": "aws:executeAwsApi", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ ParseInput.FindingId }}", "ProductArn": "{{ ParseInput.ProductArn }}", }, ], "Note": { "Text": "Enabled deletion protection on RDS instance", "UpdatedBy": "ASR-PCI_3.2.1_RDS.8", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role\\/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the RDS.8 finding", "type": "StringMap", }, "RemediationRoleName": { "allowedPattern": "^[\\w+=,.@-]+$", "default": "SO0111-EnableRDSInstanceDeletionProtection", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-PCI_3.2.1_RDS.8", "Tags": [ { "Key": "CdkGenerated", "Value": "true", }, ], "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlRunbooksRedshift1789871EB": { "Condition": "ControlRunbooksEnableRedshift1Condition3449D560", "DependsOn": [ "CreateWait6", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-AFSBP_1.0.0_Redshift.1 ## What does this document do? This document disables public access to a Redshift cluster by calling another SSM document ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. * RemediationRoleName: (Optional) The name of the role that allows Automation to remediate the finding on your behalf. ## Documentation Links * [AFSBP Redshift.4](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-fsbp-controls.html#fsbp-redshift-1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{ Finding }}", "expected_control_id": [ "Redshift.1", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):redshift:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:cluster:(?!.*--)([a-z][a-z0-9-]{0,62})(?