// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`default stack 1`] = ` { "Description": "test;", "Mappings": { "SourceCode": { "General": { "KeyPrefix": "aws-security-hub-automated-response-and-remediation/v1.1.1", "S3Bucket": "sharrbukkit", }, }, }, "Parameters": { "CIS12011AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for CIS 1.2.0 1.1", "Type": "String", }, "CIS12012AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for CIS 1.2.0 1.2", "Type": "String", }, "CIS12013AutoTrigger": { "AllowedValues": [ "ENABLED", "DISABLED", ], "Default": "DISABLED", "Description": "This will fully enable automated remediation for CIS 1.2.0 1.3", "Type": "String", }, "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/Solutions/SO0111/OrchestratorArn", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "CIS11AutoEventRule5178D649": { "Properties": { "Description": "Remediate CIS 1.2.0 1.1 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.1", ], ], }, ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "CIS_1.2.0_1.1_AutoTrigger", "State": { "Ref": "CIS12011AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "CIS11EventsRuleRoleB8D228E0", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "CIS11EventsRuleRoleB8D228E0": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "CIS11EventsRuleRoleDefaultPolicy66E09676": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "CIS11EventsRuleRoleDefaultPolicy66E09676", "Roles": [ { "Ref": "CIS11EventsRuleRoleB8D228E0", }, ], }, "Type": "AWS::IAM::Policy", }, "CIS12AutoEventRuleD40B64BC": { "Properties": { "Description": "Remediate CIS 1.2.0 1.2 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.2", ], ], }, ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "CIS_1.2.0_1.2_AutoTrigger", "State": { "Ref": "CIS12012AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "CIS12EventsRuleRoleA8389A61", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "CIS12EventsRuleRoleA8389A61": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "CIS12EventsRuleRoleDefaultPolicyA48EF9DD": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "CIS12EventsRuleRoleDefaultPolicyA48EF9DD", "Roles": [ { "Ref": "CIS12EventsRuleRoleA8389A61", }, ], }, "Type": "AWS::IAM::Policy", }, "CIS13AutoEventRuleC6C84C81": { "Properties": { "Description": "Remediate CIS 1.2.0 1.3 automatic remediation trigger event rule.", "EventPattern": { "detail": { "findings": { "Compliance": { "Status": [ "FAILED", "WARNING", ], }, "GeneratorId": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0/rule/1.3", ], ], }, ], "RecordState": [ "ACTIVE", ], "Workflow": { "Status": [ "NEW", ], }, }, }, "detail-type": [ "Security Hub Findings - Imported", ], "source": [ "aws.securityhub", ], }, "Name": "CIS_1.2.0_1.3_AutoTrigger", "State": { "Ref": "CIS12013AutoTrigger", }, "Targets": [ { "Arn": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ "CIS13EventsRuleRoleFEBE574F", "Arn", ], }, }, ], }, "Type": "AWS::Events::Rule", }, "CIS13EventsRuleRoleDefaultPolicy2E3E119B": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "states:StartExecution", "Effect": "Allow", "Resource": { "Ref": "SsmParameterValueSolutionsSO0111OrchestratorArnC96584B6F00A464EAD1953AFF4B05118Parameter", }, }, ], "Version": "2012-10-17", }, "PolicyName": "CIS13EventsRuleRoleDefaultPolicy2E3E119B", "Roles": [ { "Ref": "CIS13EventsRuleRoleFEBE574F", }, ], }, "Type": "AWS::IAM::Policy", }, "CIS13EventsRuleRoleFEBE574F": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "CISShortNameB8C8810A": { "Properties": { "Description": "Provides a short (1-12) character abbreviation for the standard.", "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/1.2.0/shortname", "Type": "String", "Value": "CIS", }, "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/cis-aws-foundations-benchmark/1.2.0/status", "Type": "String", "Value": "enabled", }, "Type": "AWS::SSM::Parameter", }, }, } `; exports[`default stack 2`] = ` { "Conditions": { "Enable13Condition": { "Fn::Equals": [ { "Ref": "Enable13", }, "Available", ], }, "Enable15Condition": { "Fn::Equals": [ { "Ref": "Enable15", }, "Available", ], }, "Enable21Condition": { "Fn::Equals": [ { "Ref": "Enable21", }, "Available", ], }, }, "Description": "test;", "Parameters": { "Enable13": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 1.3 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "Enable15": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 1.5 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "Enable21": { "AllowedValues": [ "Available", "NOT Available", ], "Default": "Available", "Description": "Enable/disable availability of remediation for CIS version 1.2.0 Control 2.1 in Security Hub Console Custom Actions. If NOT Available the remediation cannot be triggered from the Security Hub console in the Security Hub Admin account.", "Type": "String", }, "SecHubAdminAccount": { "AllowedPattern": "^\\d{12}$", "Description": "Admin account number", "Type": "String", }, "WaitProviderServiceToken": { "Type": "String", }, }, "Resources": { "ControlCIS13": { "Condition": "Enable13Condition", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_1.3 ## What does this document do? This document ensures that credentials unused for 90 days or greater are disabled. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output - Output of DescribeAutoScalingGroups API. ## Documentation Links * [CIS v1.2.0 1.3](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.3) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{Finding}}", "expected_control_id": [ "1.3", ], "parse_id_pattern": "^arn:(?:aws|aws-cn|aws-us-gov):iam::\\d{12}:user(?:(?:\\u002F)|(?:\\u002F[\\u0021-\\u007F]{1,510}\\u002F))([\\w+=,.@-]{1,64})$", }, "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 }", }, "isEnd": false, "name": "ParseInput", "outputs": [ { "Name": "IAMUser", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "IAMResourceId", "Selector": "$.Payload.details.AwsIamUser.UserId", "Type": "String", }, { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-RevokeUnusedIAMUserCredentials", "RuntimeParameters": { "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-RevokeUnusedIAMUserCredentials", "IAMResourceId": "{{ ParseInput.IAMResourceId }}", }, }, "isEnd": false, "name": "Remediation", }, { "action": "aws:executeAwsApi", "description": "Update finding", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ParseInput.FindingId}}", "ProductArn": "{{ParseInput.ProductArn}}", }, ], "Note": { "Text": "Deactivated unused keys and expired logins for {{ ParseInput.IAMUser }}.", "UpdatedBy": "ASR-CIS_1.2.0_1.3", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "ParseInput.AffectedObject", "Remediation.Output", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the 1.3 finding", "type": "StringMap", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-CIS_1.2.0_1.3", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlCIS15": { "Condition": "Enable15Condition", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_1.5 ## What does this document do? This document establishes a default password policy. ## Security Standards and Controls * CIS 1.5 - 1.11 ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Output Parameters * Remediation.Output ## Documentation Links * [CIS v1.2.0 1.5](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.5) * [CIS v1.2.0 1.6](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.6) * [CIS v1.2.0 1.7](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.7) * [CIS v1.2.0 1.8](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.8) * [CIS v1.2.0 1.9](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.9) * [CIS v1.2.0 1.10](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.10) * [CIS v1.2.0 1.11](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.11) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{Finding}}", "expected_control_id": [ "1.5", "1.6", "1.7", "1.8", "1.9", "1.10", "1.11", ], "parse_id_pattern": "", }, "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 }", }, "isEnd": false, "name": "ParseInput", "outputs": [ { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-SetIAMPasswordPolicy", "RuntimeParameters": { "AllowUsersToChangePassword": true, "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-SetIAMPasswordPolicy", "HardExpiry": true, "MaxPasswordAge": 90, "MinimumPasswordLength": 14, "PasswordReusePrevention": 24, "RequireLowercaseCharacters": true, "RequireNumbers": true, "RequireSymbols": true, "RequireUppercaseCharacters": true, }, }, "isEnd": false, "name": "Remediation", }, { "action": "aws:executeAwsApi", "description": "Update finding", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ParseInput.FindingId}}", "ProductArn": "{{ParseInput.ProductArn}}", }, ], "Note": { "Text": "Established a baseline password policy using the AWSConfigRemediation-SetIAMPasswordPolicy runbook.", "UpdatedBy": "ASR-CIS_1.2.0_1.5", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "ParseInput.AffectedObject", "Remediation.Output", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the 1.5 finding", "type": "StringMap", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-CIS_1.2.0_1.5", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "ControlCIS21": { "Condition": "Enable21Condition", "DependsOn": [ "CreateWait0", ], "Properties": { "Content": { "assumeRole": "{{ AutomationAssumeRole }}", "description": "### Document Name - ASR-CIS_1.2.0_2.1 ## What does this document do? Creates a multi-region trail with KMS encryption and enables CloudTrail Note: this remediation will create a NEW trail. ## Input Parameters * Finding: (Required) Security Hub finding details JSON * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf. ## Documentation Links * [CIS v1.2.0 2.1](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-2.1) ", "mainSteps": [ { "action": "aws:executeScript", "inputs": { "Handler": "parse_event", "InputPayload": { "Finding": "{{Finding}}", "expected_control_id": [ "2.1", ], "parse_id_pattern": "", }, "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 }", }, "isEnd": false, "name": "ParseInput", "outputs": [ { "Name": "ResourceId", "Selector": "$.Payload.resource_id", "Type": "String", }, { "Name": "FindingId", "Selector": "$.Payload.finding_id", "Type": "String", }, { "Name": "ProductArn", "Selector": "$.Payload.product_arn", "Type": "String", }, { "Name": "AffectedObject", "Selector": "$.Payload.object", "Type": "StringMap", }, { "Name": "AWSPartition", "Selector": "$.Payload.partition", "Type": "String", }, ], }, { "action": "aws:executeAutomation", "inputs": { "DocumentName": "ASR-CreateCloudTrailMultiRegionTrail", "RuntimeParameters": { "AWSPartition": "{{global:AWS_PARTITION}}", "AutomationAssumeRole": "arn:{{global:AWS_PARTITION}}:iam::{{global:ACCOUNT_ID}}:role/SO0111-CreateCloudTrailMultiRegionTrail", }, }, "isEnd": false, "name": "Remediation", }, { "action": "aws:executeAwsApi", "description": "Update finding", "inputs": { "Api": "BatchUpdateFindings", "FindingIdentifiers": [ { "Id": "{{ParseInput.FindingId}}", "ProductArn": "{{ParseInput.ProductArn}}", }, ], "Note": { "Text": "Multi-region, encrypted AWS CloudTrail successfully created", "UpdatedBy": "ASR-CIS_1.2.0_2.11", }, "Service": "securityhub", "Workflow": { "Status": "RESOLVED", }, }, "isEnd": true, "name": "UpdateFinding", }, ], "outputs": [ "Remediation.Output", "ParseInput.AffectedObject", ], "parameters": { "AutomationAssumeRole": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):iam::\\d{12}:role/[\\w+=,.@-]+$", "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf.", "type": "String", }, "Finding": { "description": "The input from the Orchestrator Step function for the 2.1 finding", "type": "StringMap", }, "KMSKeyArn": { "allowedPattern": "^arn:(?:aws|aws-us-gov|aws-cn):kms:(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d):\\d{12}:(?:(?:alias/[A-Za-z0-9/-_])|(?:key/(?i:[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})))$", "default": "{{ssm:/Solutions/SO0111/CMK_REMEDIATION_ARN}}", "description": "The ARN of the KMS key created by ASR for this remediation", "type": "String", }, }, "schemaVersion": "0.3", }, "DocumentFormat": "YAML", "DocumentType": "Automation", "Name": "ASR-CIS_1.2.0_2.1", "UpdateMethod": "NewVersion", }, "Type": "AWS::SSM::Document", }, "CreateWait0": { "DeletionPolicy": "Delete", "Properties": { "CreateIntervalSeconds": 1, "DeleteIntervalSeconds": 0, "DocumentPropertiesHash": "c275c0d9e95945c9a2e73ba060457a2ea175ae65c3cb302192fc92a2c23d215f", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 1, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, "DeleteIntervalSeconds": 0.5, "DocumentPropertiesHash": "c275c0d9e95945c9a2e73ba060457a2ea175ae65c3cb302192fc92a2c23d215f", "ServiceToken": { "Ref": "WaitProviderServiceToken", }, "UpdateIntervalSeconds": 0, }, "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, "Gate0": { "Metadata": { "ControlCIS13Ready": { "Fn::If": [ "Enable13Condition", { "Ref": "ControlCIS13", }, "", ], }, "ControlCIS15Ready": { "Fn::If": [ "Enable15Condition", { "Ref": "ControlCIS15", }, "", ], }, "ControlCIS21Ready": { "Fn::If": [ "Enable21Condition", { "Ref": "ControlCIS21", }, "", ], }, }, "Type": "AWS::CloudFormation::WaitConditionHandle", }, "RemapCIS4245EB49A0": { "Properties": { "Description": "Remap the CIS 4.2 finding to CIS 4.1 remediation", "Name": "/Solutions/SO0111/cis-aws-foundations-benchmark/1.2.0-4.2", "Type": "String", "Value": "4.1", }, "Type": "AWS::SSM::Parameter", }, }, } `;