diff --git a/CHANGELOG.md b/CHANGELOG.md index cf79c858..4789e995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.1] - 2025-08-06 + +### Added + +- AWS Lambda Powertools Logger & Tracer support for all services +- Added the SNS topic name to the logs +- Added missing ECR.1 remediation in SC list + +### Fixed + +- Remove tag for EventSourceMapping +- Added missing condition on log group in Admin stack to skip creation on solution re-deployment + ## [2.3.0] - 2025-07-16 ### Added diff --git a/README.md b/README.md index 5c6b7cf6..8a75778f 100644 --- a/README.md +++ b/README.md @@ -444,8 +444,7 @@ For example: ParameterKey=UseCloudWatchMetrics,ParameterValue=yes \ ParameterKey=UseCloudWatchMetricsAlarms,ParameterValue=yes \ ParameterKey=RemediationFailureAlarmThreshold,ParameterValue=5 \ - ParameterKey=EnableEnhancedCloudWatchMetrics,ParameterValue=no \ - ParameterKey=TicketGenFunctionName,ParameterValue= + ParameterKey=EnableEnhancedCloudWatchMetrics,ParameterValue=no export NAMESPACE=$(date +%s | tail -c 9) export MEMBER_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member.template @@ -464,7 +463,7 @@ For example: ParameterKey=CreateS3BucketForRedshiftAuditLogging,ParameterValue=no \ ParameterKey=LogGroupName,ParameterValue=random-log-group-123456789012 \ ParameterKey=Namespace,ParameterValue=$NAMESPACE \ - ParameterKey=SecHubAdminAccount,ParameterValue=123456789012 + ParameterKey=SecHubAdminAccount,ParameterValue={SecHubAdminAccount} export MEMBER_ROLES_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member-roles.template aws cloudformation create-stack \ @@ -473,7 +472,7 @@ For example: --template-url $MEMBER_ROLES_TEMPLATE_URL \ --parameters \ ParameterKey=Namespace,ParameterValue=$NAMESPACE \ - ParameterKey=SecHubAdminAccount,ParameterValue=123456789012 + ParameterKey=SecHubAdminAccount,ParameterValue={SecHubAdminAccount} ``` ## Directory structure @@ -487,6 +486,7 @@ For example: |-lib/ [ Solution CDK ] |-cdk-helper/ [ CDK helper functions ] |-member/ [ Member stack helper functions ] + |-parameters/ [ Stack common parameters ] |-tags/ [ Resource tagging helper functions ] |-Orchestrator/ [ Orchestrator Step Function Lambda Functions ] |-playbooks/ [ Playbooks ] @@ -499,6 +499,7 @@ For example: |-bin/ [ Playbook CDK App ] |-ssmdocs/ [ Control runbooks ] |-PCI321/ [ PCI-DSS v3.2.1 playbook ] + |-NIST80053/ [ NIST80053 playbook ] |-SC/ [ Security Control playbook ] |-remediation_runbooks/ [ Shared remediation runbooks ] |-scripts/ [ Scripts used by remediation runbooks ] diff --git a/pyproject.toml b/pyproject.toml index eec3fd3d..2c91f3ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "automated_security_response_on_aws" -version = "2.3.0" +version = "2.3.1" [tool.setuptools] package-dir = {"" = "source"} diff --git a/solution-manifest.yaml b/solution-manifest.yaml index 87d0f5ca..4dcff0e2 100644 --- a/solution-manifest.yaml +++ b/solution-manifest.yaml @@ -1,6 +1,6 @@ id: SO0111 name: automated-security-response-on-aws -version: v2.3.0 +version: v2.3.1 cloudformation_templates: - template: automated-security-response-admin.template main_template: true diff --git a/sonar-project.properties b/sonar-project.properties index 75a1530a..57d4945a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -34,6 +34,13 @@ sonar.python.coverage.reportPaths = deployment/test/coverage-reports/*.coverage. sonar.javascript.lcov.reportPaths = source/coverage/lcov.info +sonar.cpd.exclusions= \ + source/playbooks/**/lib/*_remediations.ts, \ + source/playbooks/**/lib/*construct.ts, \ + source/playbooks/**/ssmdocs/**, \ + source/lib/*-stack.ts + sonar.issue.ignore.multicriteria = ts1 sonar.issue.ignore.multicriteria.ts1.ruleKey = typescript:S1848 sonar.issue.ignore.multicriteria.ts1.resourceKey = **/*.ts + diff --git a/source/Orchestrator/check_ssm_doc_state.py b/source/Orchestrator/check_ssm_doc_state.py index ae83cf2b..e221d1fc 100644 --- a/source/Orchestrator/check_ssm_doc_state.py +++ b/source/Orchestrator/check_ssm_doc_state.py @@ -1,39 +1,37 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import os +from typing import Any, Dict import boto3 from botocore.exceptions import ClientError -from layer import tracer_utils, utils +from layer import utils from layer.awsapi_cached_client import BotoSession from layer.cloudwatch_metrics import CloudWatchMetrics -from layer.logger import Logger +from layer.powertools_logger import get_logger from layer.sechub_findings import Finding +from layer.tracer_utils import init_tracer ORCH_ROLE_NAME = "SO0111-ASR-Orchestrator-Member" # role to use for cross-account -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +logger = get_logger("check_ssm_doc_state") +tracer = init_tracer() + session = boto3.session.Session() AWS_REGION = session.region_name -tracer = tracer_utils.init_tracer() - def _get_ssm_client(account, role, region=""): """ Create a client for ssm """ kwargs = {} - if region: kwargs["region_name"] = region return BotoSession(account, f"{role}").client("ssm", **kwargs) -def _add_doc_state_to_answer(doc, account, region, answer): +def _add_doc_state_to_answer(doc: str, account: str, region: str, answer: Any) -> None: try: # Connect to APIs ssm = _get_ssm_client(account, ORCH_ROLE_NAME, region) @@ -50,7 +48,7 @@ def _add_doc_state_to_answer(doc, account, region, answer): "message": 'Document Type is not "Automation": ' + str(doctype), } ) - LOGGER.error(answer.message) + logger.error(answer.message) docstate = docinfo.get("Status", "unknown") if docstate != "Active": @@ -60,7 +58,7 @@ def _add_doc_state_to_answer(doc, account, region, answer): "message": 'Document Status is not "Active": ' + str(docstate), } ) - LOGGER.error(answer.message) + logger.error(answer.message) answer.update({"status": "ACTIVE"}) @@ -70,7 +68,7 @@ def _add_doc_state_to_answer(doc, account, region, answer): answer.update( {"status": "NOTFOUND", "message": f"Document {doc} does not exist."} ) - LOGGER.error(answer.message) + logger.error(answer.message) elif exception_type == "AccessDenied": answer.update( { @@ -78,7 +76,7 @@ def _add_doc_state_to_answer(doc, account, region, answer): "message": f"Could not assume role for {doc} in {account} in {region}", } ) - LOGGER.error(answer.message) + logger.error(answer.message) try: cloudwatch_metrics = CloudWatchMetrics() cloudwatch_metric = { @@ -88,7 +86,7 @@ def _add_doc_state_to_answer(doc, account, region, answer): } cloudwatch_metrics.send_metric(cloudwatch_metric) except Exception: - LOGGER.debug("Did not send Cloudwatch metric") + logger.debug("Did not send Cloudwatch metric") else: answer.update( { @@ -96,25 +94,25 @@ def _add_doc_state_to_answer(doc, account, region, answer): "message": "An unhandled client error occurred: " + exception_type, } ) - LOGGER.error(answer.message) + logger.error(answer.message) except Exception as e: answer.update( {"status": "ERROR", "message": "An unhandled error occurred: " + str(e)} ) - LOGGER.error(answer.message) + logger.error(answer.message) -@tracer.capture_lambda_handler -def lambda_handler(event, _): - answer = utils.StepFunctionLambdaAnswer() # holds the response to the step function - LOGGER.info(event) +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: + answer = utils.StepFunctionLambdaAnswer() + logger.info("Processing SSM doc state check", **event) if "Finding" not in event or "EventType" not in event: answer.update( {"status": "ERROR", "message": "Missing required data in request"} ) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] product_name = ( event["Finding"] @@ -146,7 +144,7 @@ def lambda_handler(event, _): } ) answer.update({"status": "ACTIVE"}) - return answer.json() + return answer.json() # type: ignore[no-any-return] finding = Finding(event["Finding"]) @@ -174,7 +172,7 @@ def lambda_handler(event, _): "message": f'Security Standard is not enabled": "{finding.standard_name} version {finding.standard_version}"', } ) - return answer.json() + return answer.json() # type: ignore[no-any-return] # Is there alt workflow configuration? alt_workflow_doc = event.get("Workflow", {}).get("WorkflowDocument", None) @@ -195,4 +193,4 @@ def lambda_handler(event, _): automation_docid, finding.account_id, finding.resource_region, answer ) - return answer.json() + return answer.json() # type: ignore[no-any-return] diff --git a/source/Orchestrator/check_ssm_execution.py b/source/Orchestrator/check_ssm_execution.py index 4e085614..17f5636e 100644 --- a/source/Orchestrator/check_ssm_execution.py +++ b/source/Orchestrator/check_ssm_execution.py @@ -1,14 +1,14 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json -import os import re from json.decoder import JSONDecodeError -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional -from layer import tracer_utils, utils +from layer import utils from layer.awsapi_cached_client import BotoSession -from layer.logger import Logger +from layer.powertools_logger import get_logger +from layer.tracer_utils import init_tracer if TYPE_CHECKING: from mypy_boto3_ssm.client import SSMClient @@ -17,14 +17,11 @@ ORCH_ROLE_NAME = "SO0111-ASR-Orchestrator-Member" # role to use for cross-account -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +logger = get_logger("check_ssm_execution") +tracer = init_tracer() -tracer = tracer_utils.init_tracer() - -def _get_ssm_client(account: str, role: str, region: str = "") -> SSMClient: +def _get_ssm_client(account: str, role: str, region: str = "") -> Any: """ Create a client for ssm """ @@ -33,7 +30,7 @@ def _get_ssm_client(account: str, role: str, region: str = "") -> SSMClient: if region: kwargs["region_name"] = region - ssm: SSMClient = BotoSession(account, f"{role}").client("ssm", **kwargs) + ssm = BotoSession(account, f"{role}").client("ssm", **kwargs) return ssm @@ -78,7 +75,7 @@ def __init__(self, exec_id, account, role_base_name, region): def get_execution_state(self): automation_exec_info = self._ssm_client.describe_automation_executions( - Filters=[{"Key": "ExecutionId", "Values": [self.exec_id]}] # type: ignore[list-item] + Filters=[{"Key": "ExecutionId", "Values": [self.exec_id]}] ) self.status = automation_exec_info["AutomationExecutionMetadataList"][0].get( @@ -160,7 +157,7 @@ def get_remediation_message(response_data, remediation_status): return message -def get_remediation_output(response_data: dict[str, str]) -> str: +def get_remediation_output(response_data: Dict[str, str]) -> str: message_key = next((key for key in response_data if key.lower() == "message"), "") if message_key: response_data.pop( @@ -186,8 +183,8 @@ def get_remediation_response(remediation_response_raw): return remediation_response -@tracer.capture_lambda_handler -def lambda_handler(event, _): +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: answer = utils.StepFunctionLambdaAnswer() automation_doc = event["AutomationDocument"] @@ -199,8 +196,8 @@ def lambda_handler(event, _): + json.dumps(automation_doc), } ) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] SSM_EXEC_ID = event["SSMExecution"]["ExecId"] SSM_ACCOUNT = event["SSMExecution"].get("Account") @@ -216,7 +213,7 @@ def lambda_handler(event, _): SSM_EXEC_ID, SSM_ACCOUNT, ORCH_ROLE_NAME, SSM_REGION ) except Exception as e: - LOGGER.error(f"Unable to retrieve AutomationExecution data: {str(e)}") + logger.error(f"Unable to retrieve AutomationExecution data: {str(e)}") raise e # Terminal states - get log data from AutomationExecutionMetadataList @@ -298,4 +295,4 @@ def lambda_handler(event, _): } ) - return answer.json() + return answer.json() # type: ignore[no-any-return] diff --git a/source/Orchestrator/exec_ssm_doc.py b/source/Orchestrator/exec_ssm_doc.py index 0ca888ac..f07e478c 100644 --- a/source/Orchestrator/exec_ssm_doc.py +++ b/source/Orchestrator/exec_ssm_doc.py @@ -3,22 +3,21 @@ import json import os import re +from typing import Any, Dict from botocore.exceptions import ClientError -from layer import tracer_utils, utils +from layer import utils from layer.awsapi_cached_client import BotoSession -from layer.logger import Logger +from layer.powertools_logger import get_logger +from layer.tracer_utils import init_tracer AWS_PARTITION = os.getenv("AWS_PARTITION") AWS_REGION = os.getenv("AWS_REGION") SOLUTION_ID = os.getenv("SOLUTION_ID", "SO0111") SOLUTION_ID = re.sub(r"^DEV-", "", SOLUTION_ID) -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) - -tracer = tracer_utils.init_tracer() +logger = get_logger("exec_ssm_doc") +tracer = init_tracer() def _get_ssm_client(account, role, region=""): @@ -26,7 +25,6 @@ def _get_ssm_client(account, role, region=""): Create a client for ssm """ kwargs = {} - if region: kwargs["region_name"] = region @@ -40,14 +38,14 @@ def _get_iam_client(accountid, role): return BotoSession(accountid, role).client("iam") -def lambda_role_exists(account, rolename): +def lambda_role_exists(account: str, rolename: str) -> bool: iam = _get_iam_client(account, SOLUTION_ID + "-ASR-Orchestrator-Member") try: iam.get_role(RoleName=rolename) return True except ClientError as ex: exception_type = ex.response["Error"]["Code"] - if exception_type in "NoSuchEntity": + if exception_type == "NoSuchEntity": return False else: exit("An unhandled client error occurred: " + exception_type) @@ -55,8 +53,8 @@ def lambda_role_exists(account, rolename): exit("An unhandled error occurred: " + str(e)) -@tracer.capture_lambda_handler -def lambda_handler(event, _): +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: # Expected: # { # Finding: { @@ -74,13 +72,13 @@ def lambda_handler(event, _): # executionid: { '' | string } # } answer = utils.StepFunctionLambdaAnswer() - LOGGER.info(event) + logger.info("Processing SSM execution request", **event) if "Finding" not in event or "EventType" not in event: answer.update( {"status": "ERROR", "message": "Missing required data in request"} ) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] automation_doc = event["AutomationDocument"] alt_workflow_doc = event.get("Workflow", {}).get("WorkflowDocument", None) @@ -112,8 +110,8 @@ def lambda_handler(event, _): + json.dumps(automation_doc), } ) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] # Execution role will be, in order of precedence # 1) remote_workflow_role @@ -174,6 +172,6 @@ def lambda_handler(event, _): } ) - LOGGER.info(answer.message) + logger.info(answer.message) - return answer.json() + return answer.json() # type: ignore[no-any-return] diff --git a/source/Orchestrator/get_approval_requirement.py b/source/Orchestrator/get_approval_requirement.py index a1abfabf..23c5fb72 100644 --- a/source/Orchestrator/get_approval_requirement.py +++ b/source/Orchestrator/get_approval_requirement.py @@ -12,21 +12,21 @@ import json import os import re +from typing import Any, Dict import boto3 from botocore.config import Config from botocore.exceptions import ClientError -from layer import tracer_utils, utils +from layer import utils from layer.awsapi_cached_client import BotoSession -from layer.logger import Logger +from layer.powertools_logger import get_logger from layer.sechub_findings import Finding from layer.simple_validation import extract_safe_product_name, safe_ssm_path +from layer.tracer_utils import init_tracer -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +logger = get_logger("get_approval_requirement") +tracer = init_tracer() -tracer = tracer_utils.init_tracer() # If env WORKFLOW_RUNBOOK is set and not blank then all remediations will be # executed through this runbook, if it is present and enabled in the member # account. @@ -95,7 +95,7 @@ def _get_alternate_workflow(accountid): WORKFLOW_RUNBOOK_ACCOUNT = running_account else: # log an error - bad config - LOGGER.error( + logger.error( f'WORKFLOW_RUNBOOK_ACCOUNT config error: "{WORKFLOW_RUNBOOK_ACCOUNT}" is not valid. Must be "member" or "admin"' ) return (None, None, None) @@ -107,7 +107,7 @@ def _get_alternate_workflow(accountid): return (None, None, None) -def _doc_is_active(doc, account): +def _doc_is_active(doc: str, account: str) -> bool: try: ssm = _get_ssm_client(account, SOLUTION_ID + "-ASR-Orchestrator-Member") docinfo = ssm.describe_document(Name=doc)["Document"] @@ -125,16 +125,16 @@ def _doc_is_active(doc, account): if exception_type in "InvalidDocument": return False else: - LOGGER.error("An unhandled client error occurred: " + exception_type) + logger.error("An unhandled client error occurred: " + exception_type) return False except Exception as e: - LOGGER.error("An unhandled error occurred: " + str(e)) + logger.error("An unhandled error occurred: " + str(e)) return False -@tracer.capture_lambda_handler -def lambda_handler(event, _): +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: answer = utils.StepFunctionLambdaAnswer() answer.update( { @@ -144,13 +144,13 @@ def lambda_handler(event, _): "workflow_data": {"impact": "nondestructive", "approvalrequired": "false"}, } ) - LOGGER.info(event) + logger.info("Processing approval requirement request", **event) if "Finding" not in event or "EventType" not in event: answer.update( {"status": "ERROR", "message": "Missing required data in request"} ) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] # # Check to see if this is a non-sechub finding that we are remediating @@ -184,11 +184,11 @@ def lambda_handler(event, _): }, } ) - return answer.json() + return answer.json() # type: ignore[no-any-return] except Exception as error: answer.update({"status": "ERROR", "message": error}) - LOGGER.error(answer.message) - return answer.json() + logger.error(answer.message) + return answer.json() # type: ignore[no-any-return] finding = Finding(event["Finding"]) @@ -214,7 +214,7 @@ def lambda_handler(event, _): # ---------------------------------------------------------------------------------- # Is there an alternative workflow configured? - (alt_workflow, alt_account, alt_role) = _get_alternate_workflow(finding.account_id) + alt_workflow, alt_account, alt_role = _get_alternate_workflow(finding.account_id) # If so, update workflow_data # --------------------------- @@ -235,4 +235,4 @@ def lambda_handler(event, _): } ) - return answer.json() + return answer.json() # type: ignore[no-any-return] diff --git a/source/Orchestrator/schedule_remediation.py b/source/Orchestrator/schedule_remediation.py index 45c4f714..728358ba 100644 --- a/source/Orchestrator/schedule_remediation.py +++ b/source/Orchestrator/schedule_remediation.py @@ -3,31 +3,29 @@ import json import os from datetime import datetime, timezone +from typing import Any, Dict import boto3 from botocore.config import Config -from layer import tracer_utils -from layer.logger import Logger +from layer.powertools_logger import get_logger +from layer.tracer_utils import init_tracer -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) - -tracer = tracer_utils.init_tracer() +logger = get_logger("schedule_remediation") +tracer = init_tracer() boto_config = Config(retries={"mode": "standard", "max_attempts": 10}) -def connect_to_dynamodb(): +def connect_to_dynamodb() -> Any: return boto3.client("dynamodb", config=boto_config) -def connect_to_sfn(): +def connect_to_sfn() -> Any: return boto3.client("stepfunctions", config=boto_config) -@tracer.capture_lambda_handler -def lambda_handler(event, _): +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Dict[str, Any], _: Any) -> str: """ Schedules a remediation for execution. @@ -64,9 +62,11 @@ def lambda_handler(event, _): found_timestamp_string = result["Item"]["LastExecutedTimestamp"]["S"] found_timestamp = int(found_timestamp_string) + is_within_threshold = found_time_is_within_wait_threshold(found_timestamp) + new_timestamp = ( found_timestamp + wait_threshold - if found_time_is_within_wait_threshold(found_timestamp) + if is_within_threshold else current_timestamp ) new_timestamp_ttl = new_timestamp + wait_threshold @@ -86,11 +86,9 @@ def lambda_handler(event, _): ) else: put_initial_in_dynamodb(table_name, table_key, current_timestamp) + return send_success_to_step_function( - sfn_client, - task_token, - current_timestamp, - remediation_details, + sfn_client, task_token, current_timestamp, remediation_details ) except Exception as e: sfn_client = connect_to_sfn() @@ -99,6 +97,7 @@ def lambda_handler(event, _): error=e.__class__.__name__, cause=str(e), ) + return f"Remediation scheduling failed: {str(e)}" def get_wait_threshold() -> int: @@ -139,8 +138,11 @@ def put_initial_in_dynamodb( def send_success_to_step_function( - sfn_client, task_token, new_timestamp, remediation_details -): + sfn_client: Any, + task_token: str, + new_timestamp: int, + remediation_details: Dict[str, Any], +) -> str: # Formatting for expected State Machine time planned_timestamp = datetime.fromtimestamp(new_timestamp, timezone.utc).strftime( "%Y-%m-%dT%H:%M:%SZ" diff --git a/source/Orchestrator/send_notifications.py b/source/Orchestrator/send_notifications.py index 21774ed8..b496ee6e 100644 --- a/source/Orchestrator/send_notifications.py +++ b/source/Orchestrator/send_notifications.py @@ -5,11 +5,11 @@ from json.decoder import JSONDecodeError from typing import Any, NotRequired, TypedDict, Union -from aws_lambda_powertools.utilities.typing import LambdaContext -from layer import sechub_findings, tracer_utils +from layer import sechub_findings from layer.cloudwatch_metrics import CloudWatchMetrics -from layer.logger import Logger from layer.metrics import Metrics +from layer.powertools_logger import get_logger +from layer.tracer_utils import init_tracer from layer.utils import get_account_alias # Get AWS region from Lambda environment. If not present then we're not @@ -24,14 +24,11 @@ "aws": "aws.amazon", } -# initialise loggers -LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +logger = get_logger("send_notifications") +tracer = init_tracer() -tracer = tracer_utils.init_tracer() - -def format_details_for_output(details): +def format_details_for_output(details: Any) -> list[str]: """Handle various possible formats in the details""" details_formatted = [] if isinstance(details, list): @@ -88,9 +85,8 @@ class Event(TypedDict): ControlId: NotRequired[str] -# powertools tracer decorator is not typed @tracer.capture_lambda_handler # type: ignore[misc] -def lambda_handler(event: Event, _: LambdaContext) -> None: +def lambda_handler(event: Event, _: Any) -> None: message_prefix, message_suffix = set_message_prefix_and_suffix(event) status_from_event = event.get("Notification", {}).get("State", "").upper() @@ -254,4 +250,4 @@ def create_and_send_cloudwatch_metrics( } cloudwatch_metrics.send_metric(cloudwatch_metric) except Exception as e: - LOGGER.debug(f"Encountered error sending Cloudwatch metric: {str(e)}") + logger.debug(f"Encountered error sending Cloudwatch metric: {str(e)}") diff --git a/source/Orchestrator/test/test_check_ssm_doc_state.py b/source/Orchestrator/test/test_check_ssm_doc_state.py index 7a9eb828..b3d5ae7f 100644 --- a/source/Orchestrator/test/test_check_ssm_doc_state.py +++ b/source/Orchestrator/test/test_check_ssm_doc_state.py @@ -1,9 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Unit Test: check_ssm_doc_state.py -Run from /deployment/build/Orchestrator after running build-s3-dist.sh -""" import os import botocore.session @@ -12,6 +8,8 @@ from check_ssm_doc_state import lambda_handler from layer.awsapi_cached_client import AWSCachedClient +from .test_orc_utils import create_lambda_context + def get_region(): return os.getenv("AWS_DEFAULT_REGION") @@ -202,7 +200,7 @@ def test_sunny_day(mocker): ssmc_stub.activate() mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) - assert lambda_handler(test_input, {}) == expected_good_response + assert lambda_handler(test_input, create_lambda_context()) == expected_good_response ssmc_stub.deactivate() @@ -301,7 +299,7 @@ def test_doc_not_active(mocker): mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) mocker.patch("check_ssm_doc_state.CloudWatchMetrics.send_metric", return_value=None) - assert lambda_handler(test_input, {}) == expected_good_response + assert lambda_handler(test_input, create_lambda_context()) == expected_good_response ssmc_stub.deactivate() @@ -390,7 +388,7 @@ def test_client_error(mocker): ssmc_stub.activate() mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) - assert lambda_handler(test_input, {}) == expected_good_response + assert lambda_handler(test_input, create_lambda_context()) == expected_good_response ssmc_stub.deactivate() @@ -543,7 +541,7 @@ def test_control_remap(mocker): ssmc_stub.activate() mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) - assert lambda_handler(test_input, {}) == expected_good_response + assert lambda_handler(test_input, create_lambda_context()) == expected_good_response ssmc_stub.deactivate() @@ -632,6 +630,147 @@ def test_alt_workflow_with_role(mocker): mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm) mocker.patch("layer.sechub_findings.get_ssm_connection", return_value=ssm) - result = lambda_handler(test_input, {}) + result = lambda_handler(test_input, create_lambda_context()) assert result == expected_good_response + + +def test_missing_finding(): + test_input = { + "EventType": "Security Hub Findings - Custom Action", + } + + result = lambda_handler(test_input, create_lambda_context()) + assert result["status"] == "ERROR" + assert result["message"] == "Missing required data in request" + + +def test_missing_event_type(): + test_input = {"Finding": {"Id": "test-finding-123", "AwsAccountId": "111111111111"}} + + result = lambda_handler(test_input, create_lambda_context()) + assert result["status"] == "ERROR" + assert result["message"] == "Missing required data in request" + + +def test_non_security_hub_product(): + test_input = { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "Id": "test-finding-123", + "AwsAccountId": "111111111111", + "ProductFields": {"aws/securityhub/ProductName": "Custom Product"}, + "Resources": [{"Type": "AwsS3Bucket", "Region": "us-west-2"}], + }, + "Workflow": { + "WorkflowDocument": "CustomWorkflowDoc", + "WorkflowRole": "CustomRole", + }, + } + + result = lambda_handler(test_input, create_lambda_context()) + + assert result["status"] == "ACTIVE" + assert result["securitystandard"] == "N/A" + assert result["securitystandardversion"] == "N/A" + assert result["controlid"] == "N/A" + assert result["playbookenabled"] == "N/A" + assert result["automationdocid"] == "CustomWorkflowDoc" + assert result["remediationrole"] == "CustomRole" + assert result["resourceregion"] == "us-west-2" + + +def test_non_security_hub_product_default_role(): + test_input = { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "Id": "test-finding-123", + "AwsAccountId": "111111111111", + "ProductFields": {"aws/securityhub/ProductName": "Custom Product"}, + "Resources": [{"Type": "AwsS3Bucket", "Region": "us-west-2"}], + }, + "Workflow": {"WorkflowDocument": "CustomWorkflowDoc", "WorkflowRole": ""}, + } + + result = lambda_handler(test_input, create_lambda_context()) + + assert result["status"] == "ACTIVE" + assert result["remediationrole"] == "SO0111-UseDefaultRole" + + +def test_playbook_not_enabled(mocker): + """Test when playbook is not enabled""" + test_input = { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1/finding/635ceb5d-3dfd-4458-804e-48a42cd723e4", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "GeneratorId": "aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "AwsAccountId": "111111111111", + "ProductFields": { + "StandardsArn": "arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0", + "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0", + "ControlId": "AutoScaling.1", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "aws/securityhub/ProductName": "Security Hub", + }, + "Resources": [ + { + "Type": "AwsAccount", + "Id": "arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:785df3481e1-cd66-435d-96de-d6ed5416defd:autoScalingGroupName/sharr-test-autoscaling-1", + "Partition": "aws", + "Region": "us-east-1", + } + ], + "WorkflowState": "NEW", + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + }, + } + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + + ssmc_stub = Stubber(ssm_c) + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "disabled", # Not enabled + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.activate() + mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(test_input, create_lambda_context()) + + assert result["status"] == "NOTENABLED" + assert "Security Standard is not enabled" in result["message"] + + ssmc_stub.deactivate() diff --git a/source/Orchestrator/test/test_check_ssm_execution.py b/source/Orchestrator/test/test_check_ssm_execution.py index 2c29a7ca..320af96c 100644 --- a/source/Orchestrator/test/test_check_ssm_execution.py +++ b/source/Orchestrator/test/test_check_ssm_execution.py @@ -1,9 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Unit Test: check_ssm_execution.py -Run from /deployment/temp/source/Orchestrator after running build-s3-dist.sh -""" import os from typing import Any @@ -13,6 +9,8 @@ from check_ssm_execution import AutomationExecution, lambda_handler from layer.awsapi_cached_client import AWSCachedClient +from .test_orc_utils import create_lambda_context + def get_region(): return os.getenv("AWS_DEFAULT_REGION") @@ -197,7 +195,7 @@ def test_failed_remediation(mocker): ssmc_stub.activate() mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) - response = lambda_handler(test_event, {}) + response = lambda_handler(test_event, create_lambda_context()) assert response == expected_result ssmc_stub.deactivate() @@ -239,7 +237,7 @@ def test_successful_remediation(mocker): mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) - response = lambda_handler(test_event, {}) + response = lambda_handler(test_event, create_lambda_context()) assert response == expected_result ssmc_stub.deactivate() @@ -355,7 +353,7 @@ def test_missing_account_id(mocker): mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) with pytest.raises(SystemExit) as response: - lambda_handler(test_event, {}) + lambda_handler(test_event, create_lambda_context()) assert ( response.value.code @@ -392,7 +390,7 @@ def test_missing_region(mocker): mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) with pytest.raises(SystemExit) as response: - lambda_handler(test_event, {}) + lambda_handler(test_event, create_lambda_context()) assert ( response.value.code diff --git a/source/Orchestrator/test/test_exec_ssm_doc.py b/source/Orchestrator/test/test_exec_ssm_doc.py index 9ab2ff40..b1d0a9b9 100644 --- a/source/Orchestrator/test/test_exec_ssm_doc.py +++ b/source/Orchestrator/test/test_exec_ssm_doc.py @@ -1,15 +1,13 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Unit Test: exec_ssm_doc.py -Run from /deployment/temp/source/Orchestrator after running build-s3-dist.sh -""" from typing import Any import boto3 from botocore.stub import ANY, Stubber from exec_ssm_doc import lambda_handler +from .test_orc_utils import create_lambda_context + def test_exec_runbook(mocker): """ @@ -123,9 +121,9 @@ def test_exec_runbook(mocker): ssmc_stub.activate() mocker.patch("exec_ssm_doc._get_ssm_client", return_value=ssm_c) mocker.patch("exec_ssm_doc._get_iam_client", return_value=iam_c) - mocker.patch("sechub_findings.ASRNotification.notify") + mocker.patch("layer.sechub_findings.ASRNotification.notify") - response = lambda_handler(step_input, {}) + response = lambda_handler(step_input, create_lambda_context()) assert response["executionid"] == expected_result["executionid"] assert response["remediation_status"] == expected_result["remediation_status"] assert response["status"] == expected_result["status"] diff --git a/source/Orchestrator/test/test_get_approval_requirement.py b/source/Orchestrator/test/test_get_approval_requirement.py index b0987230..8cffba44 100644 --- a/source/Orchestrator/test/test_get_approval_requirement.py +++ b/source/Orchestrator/test/test_get_approval_requirement.py @@ -1,12 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os +from unittest.mock import Mock import pytest from botocore.stub import Stubber from get_approval_requirement import lambda_handler from layer.awsapi_cached_client import AWSCachedClient +from .test_orc_utils import create_lambda_context + def get_region(): return os.getenv("AWS_DEFAULT_REGION") @@ -233,7 +236,7 @@ def test_get_approval_req(mocker): ssmc_stub.activate() mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) - response = lambda_handler(step_input(), {}) + response = lambda_handler(step_input(), create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -344,6 +347,399 @@ def test_non_security_hub_product_other(mocker): ssmc_stub.deactivate() +def test_invalid_workflow_runbook_account(mocker): + os.environ["WORKFLOW_RUNBOOK"] = "ASR-TestWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "invalid_account_type" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.activate() + mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + assert result["workflow_data"]["impact"] == "nondestructive" + assert result["workflow_data"]["approvalrequired"] == "false" + + ssmc_stub.deactivate() + + +def test_workflow_document_not_active(mocker): + """Test when workflow document is not active""" + os.environ["WORKFLOW_RUNBOOK"] = "ASR-InactiveWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "member" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.add_response( + "describe_document", + { + "Document": { + "Name": "ASR-InactiveWorkflow", + "Status": "Creating", # Not Active + "DocumentType": "Automation", + "Owner": "111111111111", + "CreatedDate": "2021-05-13T09:01:20.399000-04:00", + "DocumentVersion": "1", + "Description": "Test document", + "Parameters": [], + "PlatformTypes": ["Windows", "Linux", "MacOS"], + "SchemaVersion": "0.3", + "LatestVersion": "1", + "DefaultVersion": "1", + "DocumentFormat": "JSON", + "Tags": [], + } + }, + {"Name": "ASR-InactiveWorkflow"}, + ) + + ssmc_stub.activate() + mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + + ssmc_stub.deactivate() + + +def test_workflow_document_wrong_type(mocker): + os.environ["WORKFLOW_RUNBOOK"] = "ASR-WrongTypeWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "member" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.add_response( + "describe_document", + { + "Document": { + "Name": "ASR-WrongTypeWorkflow", + "Status": "Active", + "DocumentType": "Command", # Not Automation + "Owner": "111111111111", + "CreatedDate": "2021-05-13T09:01:20.399000-04:00", + "DocumentVersion": "1", + "Description": "Test document", + "Parameters": [], + "PlatformTypes": ["Windows", "Linux", "MacOS"], + "SchemaVersion": "0.3", + "LatestVersion": "1", + "DefaultVersion": "1", + "DocumentFormat": "JSON", + "Tags": [], + } + }, + {"Name": "ASR-WrongTypeWorkflow"}, + ) + + ssmc_stub.activate() + mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + + ssmc_stub.deactivate() + + +def test_workflow_document_not_found(mocker): + os.environ["WORKFLOW_RUNBOOK"] = "ASR-NonExistentWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "member" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.add_client_error("describe_document", "InvalidDocument") + + ssmc_stub.activate() + mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + + ssmc_stub.deactivate() + + +def test_workflow_document_access_denied(mocker): + os.environ["WORKFLOW_RUNBOOK"] = "ASR-AccessDeniedWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "member" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.add_client_error("describe_document", "AccessDenied") + + ssmc_stub.activate() + mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + + ssmc_stub.deactivate() + + +def test_workflow_document_general_exception(mocker): + os.environ["WORKFLOW_RUNBOOK"] = "ASR-ExceptionWorkflow" + os.environ["WORKFLOW_RUNBOOK_ACCOUNT"] = "member" + + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + + ssmc_stub.activate() + + def mock_get_ssm_client(account, role, region=""): + raise Exception("Network error") + + mocker.patch( + "get_approval_requirement._get_ssm_client", side_effect=mock_get_ssm_client + ) + + result = lambda_handler(step_input(), create_lambda_context()) + + assert result["workflowdoc"] == "" + assert result["workflowaccount"] == "" + assert result["workflowrole"] == "" + + ssmc_stub.deactivate() + + +def test_non_security_hub_product_exception(mocker): + """Test exception handling for non-Security Hub products""" + test_input = step_input_config() + + # Mock SSM client to raise exception + ssm_c = Mock() + ssm_c.get_parameter.side_effect = Exception("SSM Parameter error") + + mocker.patch("boto3.client", return_value=ssm_c) + + result = lambda_handler(test_input, create_lambda_context()) + + assert result["status"] == "ERROR" + assert "message" in result + + def test_get_approval_req_no_fanout(mocker): """ Verifies that it does not return workflow_status at all @@ -434,7 +830,7 @@ def test_get_approval_req_no_fanout(mocker): ssmc_stub.activate() mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) - response = lambda_handler(step_input(), {}) + response = lambda_handler(step_input(), create_lambda_context()) print(response) assert response["workflow_data"] == expected_result["workflow_data"] @@ -538,7 +934,7 @@ def test_workflow_in_admin(mocker): ssmc_stub.activate() mocker.patch("get_approval_requirement._get_ssm_client", return_value=ssm_c) - response = lambda_handler(step_input(), {}) + response = lambda_handler(step_input(), create_lambda_context()) print(response) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -587,7 +983,7 @@ def test_get_approval_config(mocker): ssmc_stub.activate() mocker.patch("boto3.client", return_value=ssm_c) - response = lambda_handler(step_input_config(), {}) + response = lambda_handler(step_input_config(), create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -635,7 +1031,7 @@ def test_get_approval_config_no_role(mocker): ssmc_stub.activate() mocker.patch("boto3.client", return_value=ssm_c) - response = lambda_handler(step_input_config(), {}) + response = lambda_handler(step_input_config(), create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -687,7 +1083,7 @@ def test_get_approval_health(mocker): ssmc_stub.activate() mocker.patch("boto3.client", return_value=ssm_c) - response = lambda_handler(step_input_health, {}) + response = lambda_handler(step_input_health, create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -742,7 +1138,7 @@ def test_get_approval_guardduty(mocker): ssmc_stub.activate() mocker.patch("boto3.client", return_value=ssm_c) - response = lambda_handler(step_input_guardduty, {}) + response = lambda_handler(step_input_guardduty, create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] @@ -794,7 +1190,7 @@ def test_get_approval_inspector(mocker): ssmc_stub.activate() mocker.patch("boto3.client", return_value=ssm_c) - response = lambda_handler(step_input_inspector, {}) + response = lambda_handler(step_input_inspector, create_lambda_context()) assert response["workflow_data"] == expected_result["workflow_data"] assert response["workflowdoc"] == expected_result["workflowdoc"] diff --git a/source/Orchestrator/test/test_orc_utils.py b/source/Orchestrator/test/test_orc_utils.py new file mode 100644 index 00000000..d4aca927 --- /dev/null +++ b/source/Orchestrator/test/test_orc_utils.py @@ -0,0 +1,22 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from unittest.mock import Mock + +MOTO_ACCOUNT_ID = "123456789012" + + +def create_lambda_context(): + context = Mock() + context.function_name = "test-function" + context.function_version = "$LATEST" + context.invoked_function_arn = ( + "arn:aws:lambda:us-east-1:123456789012:function:test-function" + ) + context.memory_limit_in_mb = 128 + context.remaining_time_in_millis = lambda: 30000 + context.aws_request_id = "test-request-id-123" + context.log_group_name = "/aws/lambda/test-function" + context.log_stream_name = "2021/01/01/[$LATEST]test-stream" + context.identity = Mock() + context.client_context = Mock() + return context diff --git a/source/Orchestrator/test/test_schedule_remediation.py b/source/Orchestrator/test/test_schedule_remediation.py index 9d624e2c..a3650dd9 100644 --- a/source/Orchestrator/test/test_schedule_remediation.py +++ b/source/Orchestrator/test/test_schedule_remediation.py @@ -1,9 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Unit Test: schedule_remediation.py -Run from /deployment/temp/source/Orchestrator after running build-s3-dist.sh -""" import json import os from datetime import datetime, timezone @@ -15,6 +11,8 @@ from moto import mock_aws from schedule_remediation import lambda_handler +from .test_orc_utils import create_lambda_context + os.environ["SchedulingTableName"] = "TestTable" os.environ["RemediationWaitTime"] = "3" timestampFormat = "%Y-%m-%dT%H:%M:%SZ" @@ -94,7 +92,7 @@ def test_new_account_remediation(mocker): sfn_stub.activate() with patch(client, side_effect=lambda service, **_: clients[service]): - response = lambda_handler(event, {}) + response = lambda_handler(event, create_lambda_context()) final_item = dynamodb_client.get_item( TableName=table_name, Key={"AccountID-Region": {"S": table_key}} ) @@ -145,7 +143,7 @@ def test_no_recent_remediation(mocker): sfn_stub.activate() with patch(client, side_effect=lambda service, **_: clients[service]): - response = lambda_handler(event, {}) + response = lambda_handler(event, create_lambda_context()) final_item = dynamodb_client.get_item( TableName=table_name, Key={"AccountID-Region": {"S": table_key}} ) @@ -197,7 +195,7 @@ def test_recent_remediation(mocker): sfn_stub.activate() with patch(client, side_effect=lambda service, **_: clients[service]): - response = lambda_handler(event, {}) + response = lambda_handler(event, create_lambda_context()) final_item = dynamodb_client.get_item( TableName=table_name, Key={"AccountID-Region": {"S": table_key}} ) @@ -238,7 +236,7 @@ def test_account_missing_last_executed(mocker): sfn_stub.activate() with patch(client, side_effect=lambda service, **_: clients[service]): - response = lambda_handler(event, {}) + response = lambda_handler(event, create_lambda_context()) final_item = dynamodb_client.get_item( TableName=table_name, Key={"AccountID-Region": {"S": table_key}} ) @@ -271,6 +269,6 @@ def test_failure(mocker): sfn_stub.activate() with patch(client, side_effect=lambda service, **_: clients[service]): - lambda_handler(event, {}) + lambda_handler(event, create_lambda_context()) sfn_stub.deactivate() diff --git a/source/Orchestrator/test/test_send_notifications.py b/source/Orchestrator/test/test_send_notifications.py index c7b7f0d7..d7213d47 100644 --- a/source/Orchestrator/test/test_send_notifications.py +++ b/source/Orchestrator/test/test_send_notifications.py @@ -1,9 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Unit Test: exec_ssm_doc.py -Run from /deployment/temp/source/Orchestrator after running build-s3-dist.sh -""" import os import boto3 diff --git a/source/blueprints/jira/cdk/jira-blueprint-stack.ts b/source/blueprints/jira/cdk/jira-blueprint-stack.ts index ec7005ed..ada27a5e 100644 --- a/source/blueprints/jira/cdk/jira-blueprint-stack.ts +++ b/source/blueprints/jira/cdk/jira-blueprint-stack.ts @@ -50,7 +50,10 @@ export class JiraBlueprintStack extends BlueprintStack { ), // Modify this configuration to build a local version of the ticket generator lambda environment: { POWERTOOLS_LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: props.solutionInfo.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'jira_ticket_generator', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', SOLUTION_ID: props.solutionInfo.solutionId, INSTANCE_URI: jiraInstanceURIParam.valueAsString, PROJECT_NAME: jiraProjectKeyParam.valueAsString, diff --git a/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap b/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap index aa62b529..3b3349c5 100644 --- a/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap +++ b/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap @@ -122,8 +122,11 @@ exports[`JiraBlueprintStack Matches snapshot 1`] = ` "INSTANCE_URI": { "Ref": "InstanceURI", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", "POWERTOOLS_LOG_LEVEL": "INFO", - "POWERTOOLS_SERVICE_NAME": "my-solution-tmn", + "POWERTOOLS_SERVICE_NAME": "jira_ticket_generator", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "PROJECT_NAME": { "Ref": "JiraProjectKey", }, diff --git a/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts b/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts index 565d5f52..ff1454d3 100644 --- a/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts +++ b/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts @@ -51,7 +51,10 @@ export class ServiceNowBlueprintStack extends BlueprintStack { ), // Modify this configuration to build a local version of the ticket generator lambda environment: { POWERTOOLS_LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: props.solutionInfo.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'servicenow_ticket_generator', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', SOLUTION_ID: props.solutionInfo.solutionId, INSTANCE_URI: serviceNowInstanceURIParam.valueAsString, TABLE_NAME: serviceNowTableName.valueAsString, diff --git a/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap b/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap index c170a466..06808e2f 100644 --- a/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap +++ b/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap @@ -123,8 +123,11 @@ exports[`ServiceNowBlueprintStack Matches snapshot 1`] = ` "INSTANCE_URI": { "Ref": "InstanceURI", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", "POWERTOOLS_LOG_LEVEL": "INFO", - "POWERTOOLS_SERVICE_NAME": "my-solution-tmn", + "POWERTOOLS_SERVICE_NAME": "servicenow_ticket_generator", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SECRET_ARN": { "Ref": "SecretArn", }, diff --git a/source/layer/cloudwatch_metrics.py b/source/layer/cloudwatch_metrics.py index 924660b4..dd5866c7 100644 --- a/source/layer/cloudwatch_metrics.py +++ b/source/layer/cloudwatch_metrics.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, cast import boto3 -from layer.logger import Logger +from layer.powertools_logger import get_logger if TYPE_CHECKING: from mypy_boto3_cloudwatch import CloudWatchClient @@ -20,7 +20,7 @@ # initialise loggers LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +LOGGER = get_logger("cloudwatch_metrics", LOG_LEVEL) class CloudWatchMetrics: diff --git a/source/layer/metrics.py b/source/layer/metrics.py index a03217a1..4f33c562 100644 --- a/source/layer/metrics.py +++ b/source/layer/metrics.py @@ -3,7 +3,7 @@ import json import urllib.parse import uuid -from datetime import datetime +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Optional, Tuple from urllib.request import Request, urlopen @@ -165,7 +165,7 @@ def send_metrics(self, metrics_data): usage_data = { "Solution": "SO0111", "UUID": self.solution_uuid, - "TimeStamp": str(datetime.utcnow().isoformat()), + "TimeStamp": str(datetime.now(UTC).isoformat()), "Data": metrics_data, "Version": self.solution_version, } diff --git a/source/layer/powertools_logger.py b/source/layer/powertools_logger.py new file mode 100644 index 00000000..d3c5f6cd --- /dev/null +++ b/source/layer/powertools_logger.py @@ -0,0 +1,83 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +import os +from typing import Any, Optional + +from aws_lambda_powertools import Logger + + +class PowertoolsLogger: + def __init__(self, service_name: Optional[str] = None, level: str = "info"): + self.service_name = service_name or os.getenv("POWERTOOLS_SERVICE_NAME", "ASR") + self._level = level.upper() + self.logger = Logger(service=self.service_name, level=self._level) + + def debug(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.debug(message, extra=kwargs) + else: + self.logger.debug(message) + + def info(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.info(message, extra=kwargs) + else: + self.logger.info(message) + + def warning(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.warning(message, extra=kwargs) + else: + self.logger.warning(message) + + def error(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.error(message, extra=kwargs) + else: + self.logger.error(message) + + def critical(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.critical(message, extra=kwargs) + else: + self.logger.critical(message) + + def exception(self, message: str, **kwargs: Any) -> None: + if kwargs: + self.logger.exception(message, extra=kwargs) + else: + self.logger.exception(message) + + def add_persistent_keys(self, **kwargs: Any) -> None: + self.logger.append_keys(**kwargs) + + def remove_keys(self, keys: list[str]) -> None: + self.logger.remove_keys(keys) + + def set_correlation_id(self, correlation_id: str) -> None: + self.logger.set_correlation_id(correlation_id) + + def inject_lambda_context( + self, lambda_context: Any, log_event: bool = False + ) -> None: + self.logger.inject_lambda_context(lambda_context, log_event) + + def config(self, level: str = "info") -> None: + self.logger.setLevel(level.upper()) + + @property + def level(self) -> int: + # Convert string level to integer using logging module + return getattr(logging, self._level, logging.INFO) + + @property + def log(self) -> Logger: + return self.logger + + +def get_logger( + service_name: Optional[str] = None, level: str = "info" +) -> PowertoolsLogger: + return PowertoolsLogger(service_name, level) diff --git a/source/layer/sechub_findings.py b/source/layer/sechub_findings.py index 8faef8be..18176a0c 100644 --- a/source/layer/sechub_findings.py +++ b/source/layer/sechub_findings.py @@ -326,12 +326,13 @@ def notify(self): sns_notify_json["Ticket_URL"] = self.ticket_url if self.send_to_sns: + topic = "SO0111-ASR_Topic" sent_id = publish_to_sns( - "SO0111-ASR_Topic", + topic, json.dumps(sns_notify_json, indent=2, default=str), self.__region, ) - print(f"Notification message ID {sent_id} sent.") + print(f"Notification message ID {sent_id} sent to {topic}") self.applogger.add_message(self.severity + ": " + self.message) if self.logdata: for line in self.logdata: diff --git a/source/layer/test/test_orchestrator_logic_preservation.py b/source/layer/test/test_orchestrator_logic_preservation.py new file mode 100644 index 00000000..04ac7c2a --- /dev/null +++ b/source/layer/test/test_orchestrator_logic_preservation.py @@ -0,0 +1,406 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json +import os +import sys +from unittest.mock import Mock, patch + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +class TestCheckSSMDocStateLogicPreservation: + + def test_document_validation_logic_unchanged(self): + from Orchestrator.check_ssm_doc_state import lambda_handler + + test_event = { + "Finding": { + "ProductFields": {"aws/securityhub/ProductName": "Security Hub"}, + "AwsAccountId": "123456789012", + "Resources": [{"Region": "us-east-1"}], + }, + "EventType": "Security Hub Findings - Imported", + } + + with patch("Orchestrator.check_ssm_doc_state._get_ssm_client") as mock_ssm: + mock_client = Mock() + mock_client.describe_document.return_value = { + "Document": {"DocumentType": "Automation", "Status": "Active"} + } + mock_ssm.return_value = mock_client + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + with patch("Orchestrator.check_ssm_doc_state.Finding") as mock_finding: + mock_finding.return_value.standard_shortname = "AFSBP" + mock_finding.return_value.standard_version = "1.0.0" + mock_finding.return_value.remediation_control = "EC2.1" + mock_finding.return_value.playbook_enabled = "True" + mock_finding.return_value.account_id = "123456789012" + mock_finding.return_value.resource_region = "us-east-1" + + result = lambda_handler(test_event, context) + + assert result["status"] == "ACTIVE" + + def test_access_denied_handling_preserved(self): + from botocore.exceptions import ClientError + from Orchestrator.check_ssm_doc_state import lambda_handler + + test_event = { + "Finding": { + "ProductFields": {"aws/securityhub/ProductName": "Security Hub"}, + "AwsAccountId": "123456789012", + }, + "EventType": "Security Hub Findings - Imported", + } + + with patch("Orchestrator.check_ssm_doc_state._get_ssm_client") as mock_ssm: + mock_client = Mock() + mock_client.describe_document.side_effect = ClientError( + {"Error": {"Code": "AccessDenied"}}, "DescribeDocument" + ) + mock_ssm.return_value = mock_client + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + with patch("Orchestrator.check_ssm_doc_state.Finding") as mock_finding: + mock_finding.return_value.playbook_enabled = "True" + mock_finding.return_value.standard_shortname = "AFSBP" + mock_finding.return_value.standard_version = "1.0.0" + mock_finding.return_value.remediation_control = "EC2.1" + + result = lambda_handler(test_event, context) + + assert result["status"] == "ACCESSDENIED" + + +class TestExecSSMDocLogicPreservation: + + def test_execution_parameter_logic_unchanged(self): + from Orchestrator.exec_ssm_doc import lambda_handler + + test_event = { + "Finding": {"AwsAccountId": "123456789012"}, + "AutomationDocument": { + "SecurityStandard": "AFSBP", + "ControlId": "EC2.1", + "AccountId": "123456789012", + "AutomationDocId": "ASR-AFSBP_1.0.0_EC2.1", + "RemediationRole": "TestRole", + "ResourceRegion": "us-east-1", + }, + "EventType": "Security Hub Findings - Imported", + } + + with patch("Orchestrator.exec_ssm_doc._get_ssm_client") as mock_ssm: + mock_client = Mock() + mock_client.start_automation_execution.return_value = { + "AutomationExecutionId": "test-execution-id" + } + mock_ssm.return_value = mock_client + + with patch( + "Orchestrator.exec_ssm_doc.lambda_role_exists", return_value=True + ): + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + result = lambda_handler(test_event, context) + + assert result["status"] == "QUEUED" + assert result["executionid"] == "test-execution-id" + + def test_role_selection_logic_preserved(self): + from Orchestrator.exec_ssm_doc import lambda_role_exists + + with patch("Orchestrator.exec_ssm_doc._get_iam_client") as mock_iam: + mock_client = Mock() + mock_client.get_role.return_value = {"Role": {"RoleName": "TestRole"}} + mock_iam.return_value = mock_client + + result = lambda_role_exists("123456789012", "TestRole") + assert result is True + + +class TestCheckSSMExecutionLogicPreservation: + + def test_execution_status_evaluation_unchanged(self): + from Orchestrator.check_ssm_execution import lambda_handler + + test_event = { + "AutomationDocument": { + "SecurityStandard": "AFSBP", + "ControlId": "EC2.1", + "AccountId": "123456789012", + }, + "SSMExecution": { + "ExecId": "12345678-1234-1234-1234-123456789012", + "Account": "123456789012", + "Region": "us-east-1", + }, + } + + with patch("Orchestrator.check_ssm_execution.AutomationExecution") as mock_exec: + mock_instance = Mock() + mock_instance.status = "Success" + mock_instance.outputs = {"Remediation.Output": ['{"status": "SUCCESS"}']} + mock_instance.failure_message = "" + mock_exec.return_value = mock_instance + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + result = lambda_handler(test_event, context) + + assert result["status"] == "Success" + + def test_output_parsing_logic_preserved(self): + from Orchestrator.check_ssm_execution import get_remediation_response + + test_response = ['{"status": "SUCCESS", "message": "Completed"}'] + result = get_remediation_response(test_response) + + assert result["status"] == "SUCCESS" + assert result["message"] == "Completed" + + +class TestGetApprovalRequirementLogicPreservation: + + def test_approval_determination_logic_unchanged(self): + from Orchestrator.get_approval_requirement import lambda_handler + + test_event = { + "Finding": { + "ProductFields": {"aws/securityhub/ProductName": "Security Hub"}, + "AwsAccountId": "123456789012", + }, + "EventType": "Security Hub Findings - Imported", + } + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + with patch("Orchestrator.get_approval_requirement.Finding") as mock_finding: + mock_finding.return_value.standard_shortname = "AFSBP" + mock_finding.return_value.standard_version = "1.0.0" + mock_finding.return_value.standard_control = "EC2.1" + mock_finding.return_value.finding_id = "test-finding" + mock_finding.return_value.account_id = "123456789012" + + with patch( + "Orchestrator.get_approval_requirement.get_running_account", + return_value="123456789012", + ): + result = lambda_handler(test_event, context) + + assert "workflow_data" in result + assert result["workflow_data"]["approvalrequired"] == "false" + + +class TestScheduleRemediationLogicPreservation: + + def test_rate_limiting_logic_unchanged(self): + from Orchestrator.schedule_remediation import lambda_handler + + test_event = { + "Records": [ + { + "body": json.dumps( + { + "ResourceRegion": "us-east-1", + "AccountId": "123456789012", + "TaskToken": "test-token", + "RemediationDetails": {"control": "EC2.1"}, + } + ) + } + ] + } + + with patch("Orchestrator.schedule_remediation.connect_to_dynamodb") as mock_ddb: + with patch("Orchestrator.schedule_remediation.connect_to_sfn") as mock_sfn: + with patch.dict( + os.environ, + {"SchedulingTableName": "test-table", "RemediationWaitTime": "300"}, + ): + mock_ddb_client = Mock() + mock_ddb_client.get_item.return_value = {} + mock_ddb.return_value = mock_ddb_client + + mock_sfn_client = Mock() + mock_sfn.return_value = mock_sfn_client + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + result = lambda_handler(test_event, context) + + assert "scheduled to execute" in result + + +class TestSendNotificationsLogicPreservation: + + def test_notification_formatting_unchanged(self): + from Orchestrator.send_notifications import lambda_handler + + test_event = { + "Notification": { + "Message": "Test remediation completed", + "State": "SUCCESS", + "ExecId": "test-exec-123", + "AffectedObject": "test-resource", + }, + "Finding": { + "Id": "test-finding-id", + "Compliance": {"SecurityControlId": "EC2.1"}, + }, + "EventType": "ASR", + } + + with patch("layer.sechub_findings.Finding") as mock_finding: + with patch("layer.sechub_findings.ASRNotification") as mock_notification: + mock_finding_instance = Mock() + mock_finding.return_value = mock_finding_instance + + mock_notification_instance = Mock() + mock_notification.return_value = mock_notification_instance + + context = Mock() + + lambda_handler(test_event, context) + + mock_notification_instance.notify.assert_called_once() + + +class TestCriticalPathsValidation: + + def test_security_hub_finding_processing_path(self): + from Orchestrator.check_ssm_doc_state import lambda_handler + + security_hub_event = { + "Finding": { + "ProductFields": {"aws/securityhub/ProductName": "Security Hub"}, + "AwsAccountId": "123456789012", + }, + "EventType": "Security Hub Findings - Imported", + } + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + with patch("Orchestrator.check_ssm_doc_state.Finding") as mock_finding: + mock_finding.return_value.playbook_enabled = "True" + mock_finding.return_value.standard_shortname = "AFSBP" + + result = lambda_handler(security_hub_event, context) + + assert "securitystandard" in result + assert "controlid" in result + + def test_non_security_hub_finding_processing_path(self): + from Orchestrator.check_ssm_doc_state import lambda_handler + + non_security_hub_event = { + "Finding": { + "ProductFields": {"aws/securityhub/ProductName": "Inspector"}, + "AwsAccountId": "123456789012", + "Resources": [{"Region": "us-east-1"}], + }, + "EventType": "Security Hub Findings - Imported", + "Workflow": {"WorkflowDocument": "CustomDoc", "WorkflowRole": "CustomRole"}, + } + + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + result = lambda_handler(non_security_hub_event, context) + + assert result["securitystandard"] == "N/A" + assert result["automationdocid"] == "CustomDoc" + + +class TestDataIntegrityValidation: + + def test_finding_id_preservation_across_services(self): + test_finding_id = "arn:aws:securityhub:us-east-1:123456789012:finding/12345678-1234-1234-1234-123456789012" + test_account_id = "123456789012" + + with patch("layer.sechub_findings.Finding") as mock_finding_class: + mock_finding_instance = Mock() + mock_finding_instance.arn = test_finding_id + mock_finding_instance.uuid = "12345678-1234-1234-1234-123456789012" + mock_finding_instance.account_id = test_account_id + mock_finding_instance.standard_shortname = "AFSBP" + mock_finding_instance.standard_version = "1.0.0" + mock_finding_instance.remediation_control = "EC2.1" + + mock_finding_class.return_value = mock_finding_instance + + finding_obj = mock_finding_class( + {"Id": test_finding_id, "AwsAccountId": test_account_id} + ) + + assert finding_obj.arn == test_finding_id + assert finding_obj.uuid == "12345678-1234-1234-1234-123456789012" + assert finding_obj.account_id == test_account_id + + mock_finding_class.assert_called_once() + call_args = mock_finding_class.call_args[0][0] + assert call_args["Id"] == test_finding_id + assert call_args["AwsAccountId"] == test_account_id + + def test_execution_id_flow_preservation(self): + from Orchestrator.exec_ssm_doc import lambda_handler + + test_event = { + "Finding": {"AwsAccountId": "123456789012"}, + "AutomationDocument": { + "SecurityStandard": "AFSBP", + "ControlId": "EC2.1", + "AccountId": "123456789012", + "AutomationDocId": "ASR-AFSBP_1.0.0_EC2.1", + "RemediationRole": "TestRole", + "ResourceRegion": "us-east-1", + }, + "EventType": "Security Hub Findings - Imported", + } + + expected_exec_id = "execution-12345" + + with patch("Orchestrator.exec_ssm_doc._get_ssm_client") as mock_ssm: + mock_client = Mock() + mock_client.start_automation_execution.return_value = { + "AutomationExecutionId": expected_exec_id + } + mock_ssm.return_value = mock_client + + with patch( + "Orchestrator.exec_ssm_doc.lambda_role_exists", return_value=False + ): + context = Mock() + context.function_name = "test-function" + context.function_version = "1" + context.aws_request_id = "test-request" + + result = lambda_handler(test_event, context) + + assert result["executionid"] == expected_exec_id diff --git a/source/layer/test/test_sechub_findings.py b/source/layer/test/test_sechub_findings.py index 257243d8..6d993144 100644 --- a/source/layer/test/test_sechub_findings.py +++ b/source/layer/test/test_sechub_findings.py @@ -1,19 +1,13 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Simple test to validate that the request format coming from the Cfn template -will turn into a valid API call. -""" import json import boto3 import layer.sechub_findings as findings import pytest from botocore.stub import Stubber -from layer.logger import Logger log_level = "info" -logger = Logger(loglevel=log_level) test_data = "test/test_json_data/" my_session = boto3.session.Session() diff --git a/source/layer/test/test_simple_powertools.py b/source/layer/test/test_simple_powertools.py new file mode 100644 index 00000000..8135f306 --- /dev/null +++ b/source/layer/test/test_simple_powertools.py @@ -0,0 +1,264 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from unittest.mock import MagicMock, patch + +import pytest +from layer.powertools_logger import PowertoolsLogger, get_logger +from layer.tracer_utils import PowertoolsTracer, get_tracer, init_tracer, tracer + + +class TestSimplePowertoolsLogger: + + def test_logger_initialization(self): + logger = PowertoolsLogger("test_service", "info") + assert logger.service_name == "test_service" + assert isinstance(logger.level, int) + + logger2 = get_logger("test_service", "debug") + assert logger2.service_name == "test_service" + + def test_all_logging_methods(self): + logger = get_logger("test_service", "debug") + + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + logger.error("Error message") + logger.critical("Critical message") + logger.exception("Exception message") + + logger.debug("Debug message", key1="value1") + logger.info("Info message", key2="value2") + logger.warning("Warning message", key3="value3") + logger.error("Error message", key4="value4") + logger.critical("Critical message", key5="value5") + logger.exception("Exception message", key6="value6") + + assert logger is not None + assert hasattr(logger, "debug") + assert hasattr(logger, "info") + assert hasattr(logger, "warning") + assert hasattr(logger, "error") + assert hasattr(logger, "critical") + assert hasattr(logger, "exception") + + def test_logger_configuration_methods(self): + logger = get_logger("test_service", "info") + + logger.add_persistent_keys(service_version="1.0.0", env="test") + logger.remove_keys(["service_version"]) + logger.config("warning") + logger.set_correlation_id("test-correlation-123") + + assert logger is not None + assert hasattr(logger, "add_persistent_keys") + assert hasattr(logger, "remove_keys") + assert hasattr(logger, "config") + assert hasattr(logger, "set_correlation_id") + + def test_lambda_context_injection(self): + logger = get_logger("test_service", "info") + + mock_context = MagicMock() + mock_context.function_name = "test-function" + mock_context.function_version = "$LATEST" + mock_context.aws_request_id = "test-request-id" + + logger.inject_lambda_context(mock_context, log_event=True) + + assert logger is not None + assert hasattr(logger, "inject_lambda_context") + + def test_logger_properties(self): + logger = get_logger("test_service", "info") + + level = logger.level + assert isinstance(level, int) + + from aws_lambda_powertools import Logger + + assert isinstance(logger.log, Logger) + + @patch.dict(os.environ, {"POWERTOOLS_SERVICE_NAME": "ASR"}) + def test_environment_variable_usage(self): + logger = PowertoolsLogger() + assert logger.service_name == "ASR" + + +class TestSimplePowertoolsTracer: + + def test_tracer_initialization(self): + tracer_instance = PowertoolsTracer("test_service") + assert tracer_instance.service_name == "test_service" + + tracer2 = init_tracer("test_service") + assert tracer2.service_name == "test_service" + + tracer3 = get_tracer() + assert tracer3 is not None + + def test_tracer_annotations_and_metadata(self): + tracer_instance = init_tracer("test_service") + + tracer_instance.put_annotation("test_key", "test_value") + tracer_instance.put_annotation("count", "42") + + tracer_instance.put_metadata("test_metadata", {"key": "value", "count": 123}) + tracer_instance.put_metadata("simple_value", "test") + + assert tracer_instance is not None + assert hasattr(tracer_instance, "put_annotation") + assert hasattr(tracer_instance, "put_metadata") + + def test_finding_context(self): + tracer_instance = init_tracer("test_service") + + complete_finding = { + "Id": "test-finding-id-123", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Title": "Test Security Finding", + "GeneratorId": "test-generator-id", + "ProductArn": "arn:aws:securityhub:us-east-1:123456789012:product/test", + } + + tracer_instance.add_finding_context(complete_finding) + + minimal_finding = {"Id": "minimal-finding-id"} + + tracer_instance.add_finding_context(minimal_finding) + + tracer_instance.add_finding_context({}) + + assert tracer_instance is not None + assert hasattr(tracer_instance, "add_finding_context") + + def test_remediation_context(self): + tracer_instance = init_tracer("test_service") + + complete_remediation = { + "security_standard": "AWS-FSBP", + "control_id": "EC2.1", + "automation_doc_id": "ASR-AWS-FSBP_1.0.0_EC2.1", + "account_id": "123456789012", + "region": "us-east-1", + } + + tracer_instance.add_remediation_context(complete_remediation) + + minimal_remediation = {"control_id": "S3.1"} + + tracer_instance.add_remediation_context(minimal_remediation) + + tracer_instance.add_remediation_context({}) + + assert tracer_instance is not None + assert hasattr(tracer_instance, "add_remediation_context") + + def test_tracer_property(self): + tracer_instance = init_tracer("test_service") + + from aws_lambda_powertools import Tracer + + assert isinstance(tracer_instance.trace, Tracer) + + @patch.dict(os.environ, {"POWERTOOLS_SERVICE_NAME": "ASR"}) + def test_environment_variable_usage(self): + tracer_instance = PowertoolsTracer() + assert tracer_instance.service_name == "ASR" + + def test_global_tracer_instance(self): + assert tracer is not None + tracer.put_annotation("global_test", "value") + tracer.put_metadata("global_metadata", {"test": True}) + + def test_capture_lambda_handler_decorator(self): + tracer_instance = init_tracer("test_service") + + @tracer_instance.capture_lambda_handler + def test_lambda_handler(event, context): + return {"statusCode": 200, "body": "success"} + + assert hasattr(tracer_instance, "capture_lambda_handler") + assert callable(tracer_instance.capture_lambda_handler) + + +class TestIntegration: + + def test_logger_and_tracer_together(self): + + logger = get_logger("integration_test", "info") + tracer_instance = init_tracer("integration_test") + + finding = { + "Id": "integration-finding-123", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Title": "Integration Test Finding", + } + + remediation = { + "security_standard": "AWS-FSBP", + "control_id": "EC2.1", + "automation_doc_id": "ASR-AWS-FSBP_1.0.0_EC2.1", + } + + tracer_instance.add_finding_context(finding) + tracer_instance.add_remediation_context(remediation) + + logger.info("Starting remediation", finding_id=finding["Id"]) + logger.info("Processing control", control_id=remediation["control_id"]) + logger.info("Remediation completed", status="SUCCESS") + + assert logger is not None + assert tracer_instance is not None + assert logger.service_name == "integration_test" + assert tracer_instance.service_name == "integration_test" + + def test_error_handling_robustness(self): + logger = get_logger("error_test", "info") + tracer_instance = init_tracer("error_test") + + try: + logger.info( + "Test message", + complex_data={"nested": {"deep": {"list": [1, 2, 3]}}}, + none_value=None, + empty_dict={}, + empty_list=[], + ) + + tracer_instance.put_annotation("test", "") + tracer_instance.put_metadata("test", None) + tracer_instance.add_finding_context({"invalid": "data"}) + tracer_instance.add_remediation_context({"missing": "fields"}) + + except Exception as e: + pytest.fail(f"Error handling should be robust: {e}") + + def test_api_compatibility(self): + logger = get_logger("compat_test", "info") + tracer_instance = init_tracer("compat_test") + + logger.add_persistent_keys(service_name="ASR", version="1.0.0") + logger.set_correlation_id("test-correlation-456") + + tracer_instance.put_annotation("service", "ASR") + tracer_instance.put_annotation("version", "1.0.0") + + logger.info( + "Processing request", + request_id="req-123", + user_id="user-456", + action="remediate", + ) + + tracer_instance.put_annotation("request_id", "req-123") + tracer_instance.put_annotation("action", "remediate") + + assert logger is not None + assert tracer_instance is not None + assert logger.service_name == "compat_test" + assert tracer_instance.service_name == "compat_test" diff --git a/source/layer/test/test_tracer_utils.py b/source/layer/test/test_tracer_utils.py new file mode 100644 index 00000000..b043a62b --- /dev/null +++ b/source/layer/test/test_tracer_utils.py @@ -0,0 +1,281 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +from unittest.mock import Mock, patch + +import pytest +from layer.tracer_utils import ( + PowertoolsTracer, + add_finding_context, + add_remediation_context, + add_trace_annotation, + add_trace_metadata, + get_tracer, + init_tracer, +) + + +class TestTracerInitialization: + @patch.dict(os.environ, {"POWERTOOLS_SERVICE_NAME": "TEST_SERVICE"}) + def test_init_tracer_with_service_name(self): + tracer_instance = init_tracer() + assert tracer_instance is not None + assert tracer_instance.service_name == "TEST_SERVICE" + + @patch.dict(os.environ, {}, clear=True) + def test_init_tracer_default_service_name(self): + tracer_instance = init_tracer() + assert tracer_instance is not None + assert tracer_instance.service_name == "ASR" + + def test_init_tracer_with_explicit_service_name(self): + tracer_instance = init_tracer("EXPLICIT_SERVICE") + assert tracer_instance is not None + assert tracer_instance.service_name == "EXPLICIT_SERVICE" + + def test_get_tracer_returns_instance(self): + tracer_instance = get_tracer() + assert tracer_instance is not None + assert isinstance(tracer_instance, PowertoolsTracer) + + +class TestPowertoolsTracerClass: + + def test_tracer_class_initialization(self): + tracer_instance = PowertoolsTracer("test_service") + assert tracer_instance.service_name == "test_service" + assert tracer_instance.tracer is not None + + def test_put_annotation_success(self): + tracer_instance = PowertoolsTracer("test_service") + tracer_instance.put_annotation("test_key", "test_value") + tracer_instance.put_annotation("count", "42") + + def test_put_metadata_success(self): + tracer_instance = PowertoolsTracer("test_service") + tracer_instance.put_metadata("test_key", {"nested": "value"}) + tracer_instance.put_metadata("simple", "value") + + def test_capture_lambda_handler_decorator(self): + tracer_instance = PowertoolsTracer("test_service") + + @tracer_instance.capture_lambda_handler + def test_handler(event, context): + return {"statusCode": 200, "body": "success"} + + result = test_handler({}, Mock()) + assert result["statusCode"] == 200 + + def test_trace_property_access(self): + tracer_instance = PowertoolsTracer("test_service") + from aws_lambda_powertools import Tracer + + assert isinstance(tracer_instance.trace, Tracer) + + +class TestStandaloneFunctions: + + def test_add_trace_metadata_with_mock_tracer(self): + mock_tracer = Mock() + test_metadata = {"key1": "value1", "key2": "value2", "key3": 123} + + add_trace_metadata(mock_tracer, **test_metadata) + + assert mock_tracer.put_metadata.call_count == 3 + mock_tracer.put_metadata.assert_any_call("key1", "value1") + mock_tracer.put_metadata.assert_any_call("key2", "value2") + mock_tracer.put_metadata.assert_any_call("key3", 123) + + def test_add_trace_annotation_with_mock_tracer(self): + mock_tracer = Mock() + test_annotations = {"status": "SUCCESS", "control_id": "S3.1", "count": 42} + + add_trace_annotation(mock_tracer, **test_annotations) + + assert mock_tracer.put_annotation.call_count == 3 + mock_tracer.put_annotation.assert_any_call("status", "SUCCESS") + mock_tracer.put_annotation.assert_any_call("control_id", "S3.1") + mock_tracer.put_annotation.assert_any_call("count", "42") + + def test_add_trace_metadata_with_real_tracer(self): + tracer_instance = init_tracer("test_service") + add_trace_metadata(tracer_instance, test_key="test_value", count=42) + + def test_add_trace_annotation_with_real_tracer(self): + tracer_instance = init_tracer("test_service") + add_trace_annotation(tracer_instance, status="SUCCESS", control_id="S3.1") + + +class TestFindingContextTracing: + + def test_add_finding_context_complete_with_mock(self): + mock_tracer = Mock() + finding = { + "Id": "test-finding-id", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Title": "Test Security Finding", + "GeneratorId": "test-generator", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + } + + add_finding_context(mock_tracer, finding) + + mock_tracer.add_finding_context.assert_called_once_with(finding) + + def test_add_finding_context_with_real_tracer(self): + tracer_instance = init_tracer("test_service") + finding = { + "Id": "test-finding-id", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Title": "Test Security Finding", + "GeneratorId": "test-generator", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + } + + add_finding_context(tracer_instance, finding) + + def test_tracer_class_add_finding_context_complete(self): + tracer_instance = PowertoolsTracer("test_service") + finding = { + "Id": "test-finding-id", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Title": "Test Security Finding", + "GeneratorId": "test-generator", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + } + + tracer_instance.add_finding_context(finding) + + def test_tracer_class_add_finding_context_partial(self): + tracer_instance = PowertoolsTracer("test_service") + finding = {"Id": "test-finding-id", "AwsAccountId": "123456789012"} + + tracer_instance.add_finding_context(finding) + + def test_tracer_class_add_finding_context_empty(self): + tracer_instance = PowertoolsTracer("test_service") + + tracer_instance.add_finding_context({}) + + +class TestRemediationContextTracing: + + def test_add_remediation_context_complete_with_mock(self): + mock_tracer = Mock() + remediation_data = { + "security_standard": "AFSBP", + "control_id": "S3.1", + "automation_doc_id": "ASR-AFSBP_1.0.0_S3.1", + } + + add_remediation_context(mock_tracer, remediation_data) + + mock_tracer.add_remediation_context.assert_called_once_with(remediation_data) + + def test_add_remediation_context_with_real_tracer(self): + tracer_instance = init_tracer("test_service") + remediation_data = { + "security_standard": "AFSBP", + "control_id": "S3.1", + "automation_doc_id": "ASR-AFSBP_1.0.0_S3.1", + } + + add_remediation_context(tracer_instance, remediation_data) + + def test_tracer_class_add_remediation_context_complete(self): + tracer_instance = PowertoolsTracer("test_service") + remediation_data = { + "security_standard": "AFSBP", + "control_id": "S3.1", + "automation_doc_id": "ASR-AFSBP_1.0.0_S3.1", + "account_id": "123456789012", + "region": "us-east-1", + } + + tracer_instance.add_remediation_context(remediation_data) + + def test_tracer_class_add_remediation_context_partial(self): + tracer_instance = PowertoolsTracer("test_service") + remediation_data = {"control_id": "S3.1"} + + tracer_instance.add_remediation_context(remediation_data) + + def test_tracer_class_add_remediation_context_empty(self): + tracer_instance = PowertoolsTracer("test_service") + + tracer_instance.add_remediation_context({}) + + +class TestBackwardCompatibility: + + def test_global_tracer_variable_exists(self): + from layer.tracer_utils import tracer + + assert tracer is not None + assert isinstance(tracer, PowertoolsTracer) + + def test_tracer_has_expected_methods(self): + tracer_instance = init_tracer() + assert hasattr(tracer_instance, "capture_lambda_handler") + assert hasattr(tracer_instance, "put_annotation") + assert hasattr(tracer_instance, "put_metadata") + assert hasattr(tracer_instance, "add_finding_context") + assert hasattr(tracer_instance, "add_remediation_context") + assert hasattr(tracer_instance, "trace") + + def test_all_expected_functions_importable(self): + from layer.tracer_utils import ( + PowertoolsTracer, + add_finding_context, + add_remediation_context, + add_trace_annotation, + add_trace_metadata, + get_tracer, + init_tracer, + ) + + assert PowertoolsTracer is not None + assert add_finding_context is not None + assert add_remediation_context is not None + assert add_trace_annotation is not None + assert add_trace_metadata is not None + assert get_tracer is not None + assert init_tracer is not None + + +class TestErrorHandling: + + def test_tracer_class_methods_handle_errors_gracefully(self): + tracer_instance = PowertoolsTracer("test_service") + + try: + tracer_instance.put_annotation("", "") + tracer_instance.put_metadata("test", None) + tracer_instance.add_finding_context({"invalid": "data"}) + tracer_instance.add_remediation_context({"missing": "fields"}) + assert tracer_instance is not None + assert hasattr(tracer_instance, "put_annotation") + assert hasattr(tracer_instance, "put_metadata") + assert hasattr(tracer_instance, "add_finding_context") + assert hasattr(tracer_instance, "add_remediation_context") + except Exception as e: + pytest.fail(f"Tracer methods should handle errors gracefully: {e}") + + def test_standalone_functions_handle_errors_gracefully(self): + tracer_instance = init_tracer("test_service") + + try: + add_trace_annotation(tracer_instance, empty_key="") + add_trace_metadata(tracer_instance, none_value=None) + add_finding_context(tracer_instance, {}) + add_remediation_context(tracer_instance, {}) + assert tracer_instance is not None + assert callable(add_trace_annotation) + assert callable(add_trace_metadata) + assert callable(add_finding_context) + assert callable(add_remediation_context) + except Exception as e: + pytest.fail(f"Standalone functions should handle errors gracefully: {e}") diff --git a/source/layer/tracer_utils.py b/source/layer/tracer_utils.py index c51c64ec..ff899937 100644 --- a/source/layer/tracer_utils.py +++ b/source/layer/tracer_utils.py @@ -1,12 +1,99 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os +from typing import Any, Dict, Optional from aws_lambda_powertools import Tracer -SERVICE_NAME = os.getenv("SOLUTION_TMN") +class PowertoolsTracer: -def init_tracer(): - tracer = Tracer(service=SERVICE_NAME) - return tracer + def __init__(self, service_name: Optional[str] = None): + self.service_name = service_name or os.getenv("POWERTOOLS_SERVICE_NAME", "ASR") + self.tracer = Tracer(service=self.service_name, auto_patch=True) + + def put_annotation(self, key: str, value: str) -> None: + try: + self.tracer.put_annotation(key, value) + except Exception: + pass + + def put_metadata(self, key: str, value: Any) -> None: + try: + self.tracer.put_metadata(key, value) + except Exception: + pass + + def add_finding_context(self, finding: Dict[str, Any]) -> None: + try: + if "Id" in finding: + self.put_annotation("finding_id", finding["Id"]) + if "AwsAccountId" in finding: + self.put_annotation("account_id", finding["AwsAccountId"]) + if "Region" in finding: + self.put_annotation("region", finding["Region"]) + + metadata = { + k: v + for k, v in finding.items() + if k in ["Title", "GeneratorId", "ProductArn"] + } + if metadata: + self.put_metadata("finding_context", metadata) + except Exception: + pass + + def add_remediation_context(self, remediation: Dict[str, Any]) -> None: + try: + if "security_standard" in remediation: + self.put_annotation( + "security_standard", remediation["security_standard"] + ) + if "control_id" in remediation: + self.put_annotation("control_id", remediation["control_id"]) + if "automation_doc_id" in remediation: + self.put_annotation("automation_doc", remediation["automation_doc_id"]) + + self.put_metadata("remediation_context", remediation) + except Exception: + pass + + def capture_lambda_handler(self, lambda_handler): + return self.tracer.capture_lambda_handler(lambda_handler) + + @property + def trace(self) -> Tracer: + return self.tracer + + +def init_tracer(service_name: Optional[str] = None) -> PowertoolsTracer: + return PowertoolsTracer(service_name) + + +def get_tracer() -> PowertoolsTracer: + return init_tracer() + + +def add_trace_annotation(tracer_instance: PowertoolsTracer, **annotations: Any) -> None: + for key, value in annotations.items(): + tracer_instance.put_annotation(key, str(value)) + + +def add_trace_metadata(tracer_instance: PowertoolsTracer, **metadata: Any) -> None: + for key, value in metadata.items(): + tracer_instance.put_metadata(key, value) + + +def add_finding_context( + tracer_instance: PowertoolsTracer, finding: Dict[str, Any] +) -> None: + tracer_instance.add_finding_context(finding) + + +def add_remediation_context( + tracer_instance: PowertoolsTracer, remediation: Dict[str, Any] +) -> None: + tracer_instance.add_remediation_context(remediation) + + +tracer = init_tracer() diff --git a/source/layer/utils.py b/source/layer/utils.py index 5bb6e88b..1894b55e 100644 --- a/source/layer/utils.py +++ b/source/layer/utils.py @@ -8,12 +8,12 @@ import boto3 from botocore.exceptions import UnknownRegionError from layer.awsapi_cached_client import AWSCachedClient -from layer.logger import Logger +from layer.powertools_logger import get_logger AWS_REGION = os.getenv("AWS_REGION", "us-east-1") LOG_LEVEL = os.getenv("log_level", "info") -LOGGER = Logger(loglevel=LOG_LEVEL) +LOGGER = get_logger("utils", LOG_LEVEL) properties = [ "status", diff --git a/source/lib/__snapshots__/member-stack.test.ts.snap b/source/lib/__snapshots__/member-stack.test.ts.snap index 32e53d04..6c8e4513 100644 --- a/source/lib/__snapshots__/member-stack.test.ts.snap +++ b/source/lib/__snapshots__/member-stack.test.ts.snap @@ -382,6 +382,11 @@ exports[`member stack snapshot matches 1`] = ` "Ref": "AWS::Partition", }, "LOG_LEVEL": "INFO", + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "deployment_metrics_custom_resource", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO9999", "SOLUTION_VERSION": "v9.9.9", }, diff --git a/source/lib/administrator-stack.ts b/source/lib/administrator-stack.ts index b28e0e41..25de8051 100644 --- a/source/lib/administrator-stack.ts +++ b/source/lib/administrator-stack.ts @@ -31,6 +31,7 @@ import { ActionLog } from './action-log'; import { addCfnGuardSuppression } from './cdk-helper/add-cfn-guard-suppression'; import AccountTargetParam from './parameters/account-target-param'; import MetricResources from './cdk-helper/metric-resources'; +import { removeEventSourceMappingTags } from './tags/applyTag'; export interface ASRStackProps extends cdk.StackProps { solutionId: string; @@ -46,7 +47,6 @@ export interface ASRStackProps extends cdk.StackProps { export class AdministratorStack extends cdk.Stack { private static readonly sendAnonymizedData: string = 'Yes'; - private readonly primarySolutionSNSTopicARN: string; constructor(scope: App, id: string, props: ASRStackProps) { super(scope, id, props); @@ -126,7 +126,6 @@ export class AdministratorStack extends cdk.Stack { topicName: props.SNSTopicName, masterKey: kmsKey, }); - this.primarySolutionSNSTopicARN = `arn:${stack.partition}:sns:${stack.region}:${stack.account}:${props.SNSTopicName}`; new StringParameter(this, 'SHARR_SNS_Topic', { description: @@ -253,6 +252,11 @@ export class AdministratorStack extends cdk.Stack { SOLUTION_ID: props.solutionId, SOLUTION_VERSION: props.solutionVersion, SOLUTION_TMN: props.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'check_ssm_doc_state', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -304,6 +308,11 @@ export class AdministratorStack extends cdk.Stack { SOLUTION_VERSION: props.solutionVersion, WORKFLOW_RUNBOOK: '', SOLUTION_TMN: props.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'get_approval_requirement', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -354,6 +363,11 @@ export class AdministratorStack extends cdk.Stack { SOLUTION_ID: props.solutionId, SOLUTION_VERSION: props.solutionVersion, SOLUTION_TMN: props.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'exec_ssm_doc', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -404,6 +418,11 @@ export class AdministratorStack extends cdk.Stack { SOLUTION_ID: props.solutionId, SOLUTION_VERSION: props.solutionVersion, SOLUTION_TMN: props.solutionTMN, + POWERTOOLS_SERVICE_NAME: 'check_ssm_execution', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -553,6 +572,11 @@ export class AdministratorStack extends cdk.Stack { SOLUTION_VERSION: props.solutionVersion, SOLUTION_TMN: props.solutionTMN, ENHANCED_METRICS: enableEnhancedCloudWatchMetrics.valueAsString, + POWERTOOLS_SERVICE_NAME: 'send_notifications', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -671,6 +695,11 @@ export class AdministratorStack extends cdk.Stack { sendAnonymizedMetrics: mapping.findInMap('sendAnonymizedMetrics', 'data'), SOLUTION_ID: props.solutionId, SOLUTION_VERSION: props.solutionVersion, + POWERTOOLS_SERVICE_NAME: 'action_target_provider', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: cdk.Duration.seconds(600), @@ -877,6 +906,11 @@ export class AdministratorStack extends cdk.Stack { environment: { SchedulingTableName: schedulingTable.tableName, RemediationWaitTime: '3', + POWERTOOLS_SERVICE_NAME: 'schedule_remediation', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 128, timeout: cdk.Duration.seconds(10), @@ -892,6 +926,8 @@ export class AdministratorStack extends cdk.Stack { schedulingLambdaTrigger.addEventSource(eventSource); + removeEventSourceMappingTags(schedulingLambdaTrigger); + new ActionLog(this, 'ActionLog', { logGroupName: props.cloudTrailLogGroupName, }); @@ -968,8 +1004,4 @@ export class AdministratorStack extends cdk.Stack { value: orchestrator.ticketGenFunctionARN, }); } - - getPrimarySolutionSNSTopicARN(): string { - return this.primarySolutionSNSTopicARN; - } } diff --git a/source/lib/cdk-helper/metric-resources.ts b/source/lib/cdk-helper/metric-resources.ts index 429c66ca..7d13d6d7 100644 --- a/source/lib/cdk-helper/metric-resources.ts +++ b/source/lib/cdk-helper/metric-resources.ts @@ -62,6 +62,11 @@ export default class MetricResources extends Construct { AWS_PARTITION: Stack.of(this).partition, SOLUTION_ID: props.solutionId, SOLUTION_VERSION: props.solutionVersion, + POWERTOOLS_SERVICE_NAME: 'deployment_metrics_custom_resource', + POWERTOOLS_LOG_LEVEL: 'INFO', + POWERTOOLS_LOGGER_LOG_EVENT: 'false', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', }, memorySize: 256, timeout: Duration.seconds(5), diff --git a/source/lib/common-orchestrator-construct.ts b/source/lib/common-orchestrator-construct.ts index 895dbabd..e3a91ab5 100644 --- a/source/lib/common-orchestrator-construct.ts +++ b/source/lib/common-orchestrator-construct.ts @@ -250,7 +250,7 @@ export class OrchestratorConstruct extends Construct { const processFindings = new sfn.Map(this, 'Process Findings', { comment: 'Process all findings in CloudWatch Event', - parameters: { + itemSelector: { 'Finding.$': '$$.Map.Item.Value', 'EventType.$': '$.EventType', 'CustomActionName.$': '$.CustomActionName', diff --git a/source/lib/member-stack.ts b/source/lib/member-stack.ts index cfcdbeff..5cd95956 100644 --- a/source/lib/member-stack.ts +++ b/source/lib/member-stack.ts @@ -30,8 +30,6 @@ export interface SolutionProps extends StackProps { } export class MemberStack extends Stack { - private readonly primarySolutionSNSTopicARN: string; - constructor(scope: App, id: string, props: SolutionProps) { super(scope, id, props); const stack = cdk.Stack.of(this); @@ -60,8 +58,6 @@ export class MemberStack extends Stack { new MemberBucketEncryption(this, 'MemberBucketEncryption', { solutionId: props.solutionId }); - this.primarySolutionSNSTopicARN = `arn:${stack.partition}:sns:${stack.region}:${adminAccountParam.value}:${props.SNSTopicName}`; - const nestedStackFactory = new SerializedNestedStackFactory(this, 'NestedStackFactory', { solutionDistBucket: props.solutionDistBucket, solutionTMN: props.solutionTradeMarkName, @@ -201,8 +197,4 @@ export class MemberStack extends Stack { }, }; } - - getPrimarySolutionSNSTopicARN(): string { - return this.primarySolutionSNSTopicARN; - } } diff --git a/source/lib/orchestrator-log-stack.ts b/source/lib/orchestrator-log-stack.ts index 3a4fa021..2144d1de 100644 --- a/source/lib/orchestrator-log-stack.ts +++ b/source/lib/orchestrator-log-stack.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as cdk from 'aws-cdk-lib'; -import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { CfnLogGroup, LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { Key } from 'aws-cdk-lib/aws-kms'; export interface OrchLogStackProps extends cdk.StackProps { @@ -30,11 +30,18 @@ export class OrchLogStack extends cdk.Stack { kmsKeyArn.overrideLogicalId(`KmsKeyArn`); const kmsKey = Key.fromKeyArn(this, 'KmsKey', kmsKeyArn.valueAsString); - new LogGroup(this, 'Orchestrator-Logs-Encrypted', { + const logGroup = new LogGroup(this, 'Orchestrator-Logs-Encrypted', { logGroupName: props.logGroupName, removalPolicy: cdk.RemovalPolicy.RETAIN, retention: RetentionDays.TEN_YEARS, encryptionKey: kmsKey, }); + + { + const childToMod = logGroup.node.defaultChild as CfnLogGroup; + childToMod.cfnOptions.condition = new cdk.CfnCondition(this, 'Encrypted Log Group', { + expression: cdk.Fn.conditionEquals(reuseOrchLogGroup.valueAsString, 'no'), + }); + } } } diff --git a/source/lib/tags/applyTag.ts b/source/lib/tags/applyTag.ts index 0b215faa..ef9384f9 100644 --- a/source/lib/tags/applyTag.ts +++ b/source/lib/tags/applyTag.ts @@ -1,14 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Tags } from 'aws-cdk-lib'; -import { IConstruct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; /** - * @description applies tag to cloudformation resources - * @param resource - * @param key - * @param value + * @description Remove tags from EventSourceMappings attached to a Lambda function + * @param lambdaFunction The Lambda function to clean EventSourceMapping tags from */ -export function applyTag(resource: IConstruct, key: string, value: string) { - Tags.of(resource).add(key, value); +export function removeEventSourceMappingTags(lambdaFunction: lambda.Function): void { + const eventSourceMappings = lambdaFunction.node.children.filter( + (child) => child.node.defaultChild instanceof lambda.CfnEventSourceMapping, + ); + + if (eventSourceMappings.length > 0) { + const cfnEventSourceMapping = eventSourceMappings[0].node.defaultChild as lambda.CfnEventSourceMapping; + cfnEventSourceMapping.addPropertyOverride('Tags', undefined); + } } diff --git a/source/package-lock.json b/source/package-lock.json index a5a0745e..bf430d82 100644 --- a/source/package-lock.json +++ b/source/package-lock.json @@ -1,12 +1,12 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aws-security-hub-automated-response-and-remediation", - "version": "2.3.0", + "version": "2.3.1", "license": "Apache-2.0", "dependencies": { "compare-versions": "^6.1.1" diff --git a/source/package.json b/source/package.json index 13fc4da7..01733490 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "2.3.0", + "version": "2.3.1", "description": "Automated remediation for AWS Security Hub (SO0111)", "bin": { "solution_deploy": "bin/solution_deploy.js" diff --git a/source/playbooks/SC/lib/sc_remediations.ts b/source/playbooks/SC/lib/sc_remediations.ts index e1deaa4f..746341ea 100644 --- a/source/playbooks/SC/lib/sc_remediations.ts +++ b/source/playbooks/SC/lib/sc_remediations.ts @@ -62,6 +62,7 @@ const remediations: IControl[] = [ { control: 'EC2.19', versionAdded: '2.1.0' }, { control: 'EC2.23', versionAdded: '2.1.0' }, { control: 'ELB.1', versionAdded: '2.3.0' }, + { control: 'ECR.1', versionAdded: '2.1.0' }, { control: 'IAM.3', versionAdded: '2.1.0' }, { control: 'IAM.7', versionAdded: '2.1.0' }, { control: 'IAM.8', versionAdded: '2.1.0' }, diff --git a/source/solution_deploy/source/action_target_provider.py b/source/solution_deploy/source/action_target_provider.py index 3b829394..d06bb161 100644 --- a/source/solution_deploy/source/action_target_provider.py +++ b/source/solution_deploy/source/action_target_provider.py @@ -20,11 +20,11 @@ import cfnresponse from botocore.config import Config from botocore.exceptions import ClientError -from layer.logger import Logger +from layer.powertools_logger import get_logger # initialize logger LOG_LEVEL = os.getenv("log_level", "info") -logger_obj = Logger(loglevel=LOG_LEVEL) +logger_obj = get_logger("action_target_provider", LOG_LEVEL) REGION = os.getenv("AWS_REGION", "us-east-1") PARTITION = os.getenv("AWS_PARTITION", default="aws") # Set by deployment template @@ -78,7 +78,7 @@ def create(self): ) return "FAILED" else: - logger_obj.error(error) + logger_obj.error(str(error)) return "FAILED" except Exception: return "FAILED" @@ -121,10 +121,10 @@ def delete(self): ) return "SUCCESS" else: - logger_obj.error(error) + logger_obj.error(str(error)) return "FAILED" except Exception as e: - logger_obj.error(e) + logger_obj.error(str(e)) return "FAILED" diff --git a/source/test/__snapshots__/orchestrator.test.ts.snap b/source/test/__snapshots__/orchestrator.test.ts.snap index 4c33a824..a4d64b03 100644 --- a/source/test/__snapshots__/orchestrator.test.ts.snap +++ b/source/test/__snapshots__/orchestrator.test.ts.snap @@ -260,7 +260,7 @@ exports[`test App Orchestrator Construct 1`] = ` "Fn::Join": [ "", [ - "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings","CustomActionName.$":"$.detail.actionName"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","Parameters":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType","CustomActionName.$":"$.CustomActionName"},"ItemsPath":"$.Findings","ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOT_NEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", + "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings","CustomActionName.$":"$.detail.actionName"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","ItemsPath":"$.Findings","ItemSelector":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType","CustomActionName.$":"$.CustomActionName"},"ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOT_NEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", { "Ref": "AWS::Partition", }, diff --git a/source/test/__snapshots__/orchestrator_logs.test.ts.snap b/source/test/__snapshots__/orchestrator_logs.test.ts.snap index 4a1a00c1..6faf24a1 100644 --- a/source/test/__snapshots__/orchestrator_logs.test.ts.snap +++ b/source/test/__snapshots__/orchestrator_logs.test.ts.snap @@ -2,6 +2,16 @@ exports[`Global Roles Stack 1`] = ` { + "Conditions": { + "EncryptedLogGroup": { + "Fn::Equals": [ + { + "Ref": "ReuseOrchestratorLogGroup", + }, + "no", + ], + }, + }, "Description": "test;", "Parameters": { "KmsKeyArn": { @@ -21,6 +31,7 @@ exports[`Global Roles Stack 1`] = ` }, "Resources": { "OrchestratorLogsEncrypted072D6E38": { + "Condition": "EncryptedLogGroup", "DeletionPolicy": "Retain", "Properties": { "KmsKeyId": { diff --git a/source/test/__snapshots__/solution_deploy.test.ts.snap b/source/test/__snapshots__/solution_deploy.test.ts.snap index f12c40c7..9d977b21 100644 --- a/source/test/__snapshots__/solution_deploy.test.ts.snap +++ b/source/test/__snapshots__/solution_deploy.test.ts.snap @@ -3438,6 +3438,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "AWS_PARTITION": { "Ref": "AWS::Partition", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "action_target_provider", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_VERSION": "v1.0.0", "log_level": "info", @@ -4749,6 +4754,86 @@ exports[`Test if the Stack has all the resources. 1`] = ` }, "Type": "AWS::CloudWatch::Alarm", }, + "ECR1remediationfailureAD78C4E8": { + "Condition": "enhancedAlarmsEnabled", + "Metadata": { + "guard": { + "SuppressedRules": [ + "CFN_NO_EXPLICIT_RESOURCE_NAMES", + ], + }, + }, + "Properties": { + "AlarmActions": [ + { + "Ref": "ASRAlarmTopic7CEFBDF9", + }, + ], + "AlarmDescription": "This alarm triggers when the percentage of remediation failures for ECR.1 reaches above the configured threshold. + This indicates that there may be a problem remediating this control ID in your AWS environment. Check the most recent failed execution of this control's runbook in the target account to identify potential issues.", + "AlarmName": "ASR-ECR.1-remediation-failure", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "DatapointsToAlarm": 1, + "EvaluationPeriods": 1, + "Metrics": [ + { + "Expression": "(m1ECR1 / (m1ECR1+m2ECR1)) * 100", + "Id": "expr_1", + "Label": "ECR.1 Failure Percentage", + "ReturnData": true, + }, + { + "Id": "m1ECR1", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "ControlId", + "Value": "ECR.1", + }, + { + "Name": "Outcome", + "Value": "FAILED", + }, + ], + "MetricName": "RemediationOutcome", + "Namespace": "ASR", + }, + "Period": 86400, + "Stat": "Average", + }, + "ReturnData": false, + }, + { + "Id": "m2ECR1", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "ControlId", + "Value": "ECR.1", + }, + { + "Name": "Outcome", + "Value": "SUCCESS", + }, + ], + "MetricName": "RemediationOutcome", + "Namespace": "ASR", + }, + "Period": 86400, + "Stat": "Average", + }, + "ReturnData": false, + }, + ], + "Threshold": { + "Ref": "RemediationFailureAlarmThreshold", + }, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, "ECS5remediationfailure9BD08802": { "Condition": "enhancedAlarmsEnabled", "Metadata": { @@ -6694,6 +6779,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "Ref": "AWS::Partition", }, "LOG_LEVEL": "INFO", + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "deployment_metrics_custom_resource", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_VERSION": "v1.0.0", }, @@ -8443,7 +8533,7 @@ exports[`Test if the Stack has all the resources. 1`] = ` { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"AutoScaling.1 Failure Percentage","expression":"(m1AutoScaling1 / (m1AutoScaling1+m2AutoScaling1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","AutoScaling.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1AutoScaling1"}],["ASR","RemediationOutcome","ControlId","AutoScaling.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2AutoScaling1"}],[{"label":"CloudFormation.1 Failure Percentage","expression":"(m1CloudFormation1 / (m1CloudFormation1+m2CloudFormation1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFormation.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFormation1"}],["ASR","RemediationOutcome","ControlId","CloudFormation.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFormation1"}],[{"label":"CloudFront.1 Failure Percentage","expression":"(m1CloudFront1 / (m1CloudFront1+m2CloudFront1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFront.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFront1"}],["ASR","RemediationOutcome","ControlId","CloudFront.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFront1"}],[{"label":"CloudFront.12 Failure Percentage","expression":"(m1CloudFront12 / (m1CloudFront12+m2CloudFront12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFront.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFront12"}],["ASR","RemediationOutcome","ControlId","CloudFront.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFront12"}],[{"label":"CloudTrail.1 Failure Percentage","expression":"(m1CloudTrail1 / (m1CloudTrail1+m2CloudTrail1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail1"}],["ASR","RemediationOutcome","ControlId","CloudTrail.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail1"}],[{"label":"CloudTrail.2 Failure Percentage","expression":"(m1CloudTrail2 / (m1CloudTrail2+m2CloudTrail2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail2"}],["ASR","RemediationOutcome","ControlId","CloudTrail.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail2"}],[{"label":"CloudTrail.3 Failure Percentage","expression":"(m1CloudTrail3 / (m1CloudTrail3+m2CloudTrail3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail3"}],["ASR","RemediationOutcome","ControlId","CloudTrail.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail3"}],[{"label":"CloudTrail.4 Failure Percentage","expression":"(m1CloudTrail4 / (m1CloudTrail4+m2CloudTrail4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail4"}],["ASR","RemediationOutcome","ControlId","CloudTrail.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail4"}],[{"label":"CloudTrail.5 Failure Percentage","expression":"(m1CloudTrail5 / (m1CloudTrail5+m2CloudTrail5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail5"}],["ASR","RemediationOutcome","ControlId","CloudTrail.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail5"}],[{"label":"CloudTrail.6 Failure Percentage","expression":"(m1CloudTrail6 / (m1CloudTrail6+m2CloudTrail6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail6"}],["ASR","RemediationOutcome","ControlId","CloudTrail.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail6"}],[{"label":"CloudTrail.7 Failure Percentage","expression":"(m1CloudTrail7 / (m1CloudTrail7+m2CloudTrail7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail7"}],["ASR","RemediationOutcome","ControlId","CloudTrail.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail7"}],[{"label":"CloudWatch.1 Failure Percentage","expression":"(m1CloudWatch1 / (m1CloudWatch1+m2CloudWatch1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch1"}],["ASR","RemediationOutcome","ControlId","CloudWatch.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch1"}],[{"label":"CloudWatch.2 Failure Percentage","expression":"(m1CloudWatch2 / (m1CloudWatch2+m2CloudWatch2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch2"}],["ASR","RemediationOutcome","ControlId","CloudWatch.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch2"}],[{"label":"CloudWatch.3 Failure Percentage","expression":"(m1CloudWatch3 / (m1CloudWatch3+m2CloudWatch3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch3"}],["ASR","RemediationOutcome","ControlId","CloudWatch.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch3"}],[{"label":"CloudWatch.4 Failure Percentage","expression":"(m1CloudWatch4 / (m1CloudWatch4+m2CloudWatch4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch4"}],["ASR","RemediationOutcome","ControlId","CloudWatch.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch4"}],[{"label":"CloudWatch.5 Failure Percentage","expression":"(m1CloudWatch5 / (m1CloudWatch5+m2CloudWatch5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch5"}],["ASR","RemediationOutcome","ControlId","CloudWatch.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch5"}],[{"label":"CloudWatch.6 Failure Percentage","expression":"(m1CloudWatch6 / (m1CloudWatch6+m2CloudWatch6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch6"}],["ASR","RemediationOutcome","ControlId","CloudWatch.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch6"}],[{"label":"CloudWatch.7 Failure Percentage","expression":"(m1CloudWatch7 / (m1CloudWatch7+m2CloudWatch7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch7"}],["ASR","RemediationOutcome","ControlId","CloudWatch.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch7"}],[{"label":"CloudWatch.8 Failure Percentage","expression":"(m1CloudWatch8 / (m1CloudWatch8+m2CloudWatch8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch8"}],["ASR","RemediationOutcome","ControlId","CloudWatch.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch8"}],[{"label":"CloudWatch.9 Failure Percentage","expression":"(m1CloudWatch9 / (m1CloudWatch9+m2CloudWatch9)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.9","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch9"}],["ASR","RemediationOutcome","ControlId","CloudWatch.9","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch9"}],[{"label":"CloudWatch.10 Failure Percentage","expression":"(m1CloudWatch10 / (m1CloudWatch10+m2CloudWatch10)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.10","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch10"}],["ASR","RemediationOutcome","ControlId","CloudWatch.10","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch10"}],[{"label":"CloudWatch.11 Failure Percentage","expression":"(m1CloudWatch11 / (m1CloudWatch11+m2CloudWatch11)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch11"}],["ASR","RemediationOutcome","ControlId","CloudWatch.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch11"}],[{"label":"CloudWatch.12 Failure Percentage","expression":"(m1CloudWatch12 / (m1CloudWatch12+m2CloudWatch12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch12"}],["ASR","RemediationOutcome","ControlId","CloudWatch.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch12"}],[{"label":"CloudWatch.13 Failure Percentage","expression":"(m1CloudWatch13 / (m1CloudWatch13+m2CloudWatch13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch13"}],["ASR","RemediationOutcome","ControlId","CloudWatch.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch13"}],[{"label":"CloudWatch.14 Failure Percentage","expression":"(m1CloudWatch14 / (m1CloudWatch14+m2CloudWatch14)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch14"}],["ASR","RemediationOutcome","ControlId","CloudWatch.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch14"}],[{"label":"CodeBuild.2 Failure Percentage","expression":"(m1CodeBuild2 / (m1CodeBuild2+m2CodeBuild2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CodeBuild.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CodeBuild2"}],["ASR","RemediationOutcome","ControlId","CodeBuild.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CodeBuild2"}],[{"label":"CodeBuild.5 Failure Percentage","expression":"(m1CodeBuild5 / (m1CodeBuild5+m2CodeBuild5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CodeBuild.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CodeBuild5"}],["ASR","RemediationOutcome","ControlId","CodeBuild.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CodeBuild5"}],[{"label":"Config.1 Failure Percentage","expression":"(m1Config1 / (m1Config1+m2Config1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Config.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Config1"}],["ASR","RemediationOutcome","ControlId","Config.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Config1"}],[{"label":"EC2.1 Failure Percentage","expression":"(m1EC21 / (m1EC21+m2EC21)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC21"}],["ASR","RemediationOutcome","ControlId","EC2.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC21"}],[{"label":"EC2.2 Failure Percentage","expression":"(m1EC22 / (m1EC22+m2EC22)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC22"}],["ASR","RemediationOutcome","ControlId","EC2.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC22"}],[{"label":"EC2.4 Failure Percentage","expression":"(m1EC24 / (m1EC24+m2EC24)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC24"}],["ASR","RemediationOutcome","ControlId","EC2.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC24"}],[{"label":"EC2.6 Failure Percentage","expression":"(m1EC26 / (m1EC26+m2EC26)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC26"}],["ASR","RemediationOutcome","ControlId","EC2.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC26"}],[{"label":"EC2.7 Failure Percentage","expression":"(m1EC27 / (m1EC27+m2EC27)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC27"}],["ASR","RemediationOutcome","ControlId","EC2.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC27"}],[{"label":"EC2.8 Failure Percentage","expression":"(m1EC28 / (m1EC28+m2EC28)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC28"}],["ASR","RemediationOutcome","ControlId","EC2.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC28"}],[{"label":"EC2.13 Failure Percentage","expression":"(m1EC213 / (m1EC213+m2EC213)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC213"}],["ASR","RemediationOutcome","ControlId","EC2.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC213"}],[{"label":"EC2.14 Failure Percentage","expression":"(m1EC214 / (m1EC214+m2EC214)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC214"}],["ASR","RemediationOutcome","ControlId","EC2.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC214"}],[{"label":"EC2.15 Failure Percentage","expression":"(m1EC215 / (m1EC215+m2EC215)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.15","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC215"}],["ASR","RemediationOutcome","ControlId","EC2.15","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC215"}],[{"label":"EC2.18 Failure Percentage","expression":"(m1EC218 / (m1EC218+m2EC218)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.18","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC218"}],["ASR","RemediationOutcome","ControlId","EC2.18","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC218"}],[{"label":"EC2.19 Failure Percentage","expression":"(m1EC219 / (m1EC219+m2EC219)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.19","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC219"}],["ASR","RemediationOutcome","ControlId","EC2.19","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC219"}],[{"label":"EC2.23 Failure Percentage","expression":"(m1EC223 / (m1EC223+m2EC223)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.23","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC223"}],["ASR","RemediationOutcome","ControlId","EC2.23","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC223"}],[{"label":"IAM.3 Failure Percentage","expression":"(m1IAM3 / (m1IAM3+m2IAM3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM3"}],["ASR","RemediationOutcome","ControlId","IAM.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM3"}],[{"label":"IAM.7 Failure Percentage","expression":"(m1IAM7 / (m1IAM7+m2IAM7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM7"}],["ASR","RemediationOutcome","ControlId","IAM.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM7"}],[{"label":"IAM.8 Failure Percentage","expression":"(m1IAM8 / (m1IAM8+m2IAM8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM8"}],["ASR","RemediationOutcome","ControlId","IAM.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM8"}],[{"label":"IAM.11 Failure Percentage","expression":"(m1IAM11 / (m1IAM11+m2IAM11)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM11"}],["ASR","RemediationOutcome","ControlId","IAM.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM11"}],[{"label":"IAM.12 Failure Percentage","expression":"(m1IAM12 / (m1IAM12+m2IAM12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM12"}],["ASR","RemediationOutcome","ControlId","IAM.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM12"}],[{"label":"IAM.13 Failure Percentage","expression":"(m1IAM13 / (m1IAM13+m2IAM13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM13"}],["ASR","RemediationOutcome","ControlId","IAM.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM13"}],[{"label":"IAM.14 Failure Percentage","expression":"(m1IAM14 / (m1IAM14+m2IAM14)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM14"}],["ASR","RemediationOutcome","ControlId","IAM.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM14"}],[{"label":"IAM.15 Failure Percentage","expression":"(m1IAM15 / (m1IAM15+m2IAM15)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.15","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM15"}],["ASR","RemediationOutcome","ControlId","IAM.15","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM15"}],[{"label":"IAM.16 Failure Percentage","expression":"(m1IAM16 / (m1IAM16+m2IAM16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM16"}],["ASR","RemediationOutcome","ControlId","IAM.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM16"}],[{"label":"IAM.17 Failure Percentage","expression":"(m1IAM17 / (m1IAM17+m2IAM17)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.17","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM17"}],["ASR","RemediationOutcome","ControlId","IAM.17","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM17"}],[{"label":"IAM.18 Failure Percentage","expression":"(m1IAM18 / (m1IAM18+m2IAM18)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.18","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM18"}],["ASR","RemediationOutcome","ControlId","IAM.18","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM18"}],[{"label":"IAM.22 Failure Percentage","expression":"(m1IAM22 / (m1IAM22+m2IAM22)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.22","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM22"}],["ASR","RemediationOutcome","ControlId","IAM.22","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM22"}],[{"label":"KMS.4 Failure Percentage","expression":"(m1KMS4 / (m1KMS4+m2KMS4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","KMS.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1KMS4"}],["ASR","RemediationOutcome","ControlId","KMS.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2KMS4"}],[{"label":"Lambda.1 Failure Percentage","expression":"(m1Lambda1 / (m1Lambda1+m2Lambda1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Lambda.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Lambda1"}],["ASR","RemediationOutcome","ControlId","Lambda.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Lambda1"}],[{"label":"RDS.1 Failure Percentage","expression":"(m1RDS1 / (m1RDS1+m2RDS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS1"}],["ASR","RemediationOutcome","ControlId","RDS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS1"}],[{"label":"RDS.2 Failure Percentage","expression":"(m1RDS2 / (m1RDS2+m2RDS2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS2"}],["ASR","RemediationOutcome","ControlId","RDS.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS2"}],[{"label":"RDS.4 Failure Percentage","expression":"(m1RDS4 / (m1RDS4+m2RDS4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS4"}],["ASR","RemediationOutcome","ControlId","RDS.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS4"}],[{"label":"RDS.5 Failure Percentage","expression":"(m1RDS5 / (m1RDS5+m2RDS5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS5"}],["ASR","RemediationOutcome","ControlId","RDS.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS5"}],[{"label":"RDS.6 Failure Percentage","expression":"(m1RDS6 / (m1RDS6+m2RDS6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS6"}],["ASR","RemediationOutcome","ControlId","RDS.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS6"}],[{"label":"RDS.7 Failure Percentage","expression":"(m1RDS7 / (m1RDS7+m2RDS7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS7"}],["ASR","RemediationOutcome","ControlId","RDS.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS7"}],[{"label":"RDS.8 Failure Percentage","expression":"(m1RDS8 / (m1RDS8+m2RDS8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS8"}],["ASR","RemediationOutcome","ControlId","RDS.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS8"}],[{"label":"RDS.13 Failure Percentage","expression":"(m1RDS13 / (m1RDS13+m2RDS13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS13"}],["ASR","RemediationOutcome","ControlId","RDS.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS13"}],[{"label":"RDS.16 Failure Percentage","expression":"(m1RDS16 / (m1RDS16+m2RDS16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS16"}],["ASR","RemediationOutcome","ControlId","RDS.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS16"}],[{"label":"Redshift.1 Failure Percentage","expression":"(m1Redshift1 / (m1Redshift1+m2Redshift1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift1"}],["ASR","RemediationOutcome","ControlId","Redshift.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift1"}],[{"label":"Redshift.3 Failure Percentage","expression":"(m1Redshift3 / (m1Redshift3+m2Redshift3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift3"}],["ASR","RemediationOutcome","ControlId","Redshift.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift3"}],[{"label":"Redshift.4 Failure Percentage","expression":"(m1Redshift4 / (m1Redshift4+m2Redshift4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift4"}],["ASR","RemediationOutcome","ControlId","Redshift.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift4"}],[{"label":"Redshift.6 Failure Percentage","expression":"(m1Redshift6 / (m1Redshift6+m2Redshift6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift6"}],["ASR","RemediationOutcome","ControlId","Redshift.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift6"}],[{"label":"S3.1 Failure Percentage","expression":"(m1S31 / (m1S31+m2S31)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S31"}],["ASR","RemediationOutcome","ControlId","S3.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S31"}],[{"label":"S3.2 Failure Percentage","expression":"(m1S32 / (m1S32+m2S32)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S32"}],["ASR","RemediationOutcome","ControlId","S3.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S32"}],[{"label":"S3.3 Failure Percentage","expression":"(m1S33 / (m1S33+m2S33)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S33"}],["ASR","RemediationOutcome","ControlId","S3.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S33"}],[{"label":"S3.4 Failure Percentage","expression":"(m1S34 / (m1S34+m2S34)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S34"}],["ASR","RemediationOutcome","ControlId","S3.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S34"}],[{"label":"S3.5 Failure Percentage","expression":"(m1S35 / (m1S35+m2S35)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S35"}],["ASR","RemediationOutcome","ControlId","S3.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S35"}],[{"label":"S3.6 Failure Percentage","expression":"(m1S36 / (m1S36+m2S36)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S36"}],["ASR","RemediationOutcome","ControlId","S3.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S36"}],[{"label":"S3.8 Failure Percentage","expression":"(m1S38 / (m1S38+m2S38)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S38"}],["ASR","RemediationOutcome","ControlId","S3.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S38"}],[{"label":"S3.9 Failure Percentage","expression":"(m1S39 / (m1S39+m2S39)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.9","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S39"}],["ASR","RemediationOutcome","ControlId","S3.9","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S39"}],[{"label":"S3.11 Failure Percentage","expression":"(m1S311 / (m1S311+m2S311)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S311"}],["ASR","RemediationOutcome","ControlId","S3.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S311"}],[{"label":"S3.13 Failure Percentage","expression":"(m1S313 / (m1S313+m2S313)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S313"}],["ASR","RemediationOutcome","ControlId","S3.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S313"}],[{"label":"SecretsManager.1 Failure Percentage","expression":"(m1SecretsManager1 / (m1SecretsManager1+m2SecretsManager1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager1"}],["ASR","RemediationOutcome","ControlId","SecretsManager.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager1"}],[{"label":"SecretsManager.3 Failure Percentage","expression":"(m1SecretsManager3 / (m1SecretsManager3+m2SecretsManager3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager3"}],["ASR","RemediationOutcome","ControlId","SecretsManager.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager3"}],[{"label":"SecretsManager.4 Failure Percentage","expression":"(m1SecretsManager4 / (m1SecretsManager4+m2SecretsManager4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager4"}],["ASR","RemediationOutcome","ControlId","SecretsManager.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager4"}],[{"label":"SNS.1 Failure Percentage","expression":"(m1SNS1 / (m1SNS1+m2SNS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SNS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SNS1"}],["ASR","RemediationOutcome","ControlId","SNS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SNS1"}],[{"label":"SNS.2 Failure Percentage","expression":"(m1SNS2 / (m1SNS2+m2SNS2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SNS.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SNS2"}],["ASR","RemediationOutcome","ControlId","SNS.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SNS2"}],[{"label":"SQS.1 Failure Percentage","expression":"(m1SQS1 / (m1SQS1+m2SQS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SQS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SQS1"}],["ASR","RemediationOutcome","ControlId","SQS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SQS1"}],[{"label":"SSM.4 Failure Percentage","expression":"(m1SSM4 / (m1SSM4+m2SSM4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SSM.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SSM4"}],["ASR","RemediationOutcome","ControlId","SSM.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SSM4"}],[{"label":"GuardDuty.1 Failure Percentage","expression":"(m1GuardDuty1 / (m1GuardDuty1+m2GuardDuty1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty1"}],["ASR","RemediationOutcome","ControlId","GuardDuty.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty1"}],[{"label":"Athena.4 Failure Percentage","expression":"(m1Athena4 / (m1Athena4+m2Athena4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Athena.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Athena4"}],["ASR","RemediationOutcome","ControlId","Athena.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Athena4"}],[{"label":"APIGateway.1 Failure Percentage","expression":"(m1APIGateway1 / (m1APIGateway1+m2APIGateway1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","APIGateway.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1APIGateway1"}],["ASR","RemediationOutcome","ControlId","APIGateway.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2APIGateway1"}],[{"label":"APIGateway.5 Failure Percentage","expression":"(m1APIGateway5 / (m1APIGateway5+m2APIGateway5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","APIGateway.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1APIGateway5"}],["ASR","RemediationOutcome","ControlId","APIGateway.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2APIGateway5"}],[{"label":"AutoScaling.3 Failure Percentage","expression":"(m1AutoScaling3 / (m1AutoScaling3+m2AutoScaling3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","AutoScaling.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1AutoScaling3"}],["ASR","RemediationOutcome","ControlId","AutoScaling.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2AutoScaling3"}],[{"label":"Autoscaling.5 Failure Percentage","expression":"(m1Autoscaling5 / (m1Autoscaling5+m2Autoscaling5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Autoscaling.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Autoscaling5"}],["ASR","RemediationOutcome","ControlId","Autoscaling.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Autoscaling5"}],[{"label":"CloudWatch.16 Failure Percentage","expression":"(m1CloudWatch16 / (m1CloudWatch16+m2CloudWatch16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch16"}],["ASR","RemediationOutcome","ControlId","CloudWatch.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch16"}],[{"label":"EC2.10 Failure Percentage","expression":"(m1EC210 / (m1EC210+m2EC210)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.10","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC210"}],["ASR","RemediationOutcome","ControlId","EC2.10","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC210"}],[{"label":"SSM.1 Failure Percentage","expression":"(m1SSM1 / (m1SSM1+m2SSM1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SSM.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SSM1"}],["ASR","RemediationOutcome","ControlId","SSM.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SSM1"}],[{"label":"GuardDuty.2 Failure Percentage","expression":"(m1GuardDuty2 / (m1GuardDuty2+m2GuardDuty2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty2"}],["ASR","RemediationOutcome","ControlId","GuardDuty.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty2"}],[{"label":"GuardDuty.4 Failure Percentage","expression":"(m1GuardDuty4 / (m1GuardDuty4+m2GuardDuty4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty4"}],["ASR","RemediationOutcome","ControlId","GuardDuty.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty4"}],[{"label":"Macie.1 Failure Percentage","expression":"(m1Macie1 / (m1Macie1+m2Macie1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Macie.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Macie1"}],["ASR","RemediationOutcome","ControlId","Macie.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Macie1"}],[{"label":"DynamoDB.1 Failure Percentage","expression":"(m1DynamoDB1 / (m1DynamoDB1+m2DynamoDB1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB1"}],["ASR","RemediationOutcome","ControlId","DynamoDB.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB1"}],[{"label":"DynamoDB.5 Failure Percentage","expression":"(m1DynamoDB5 / (m1DynamoDB5+m2DynamoDB5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB5"}],["ASR","RemediationOutcome","ControlId","DynamoDB.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB5"}],[{"label":"DynamoDB.6 Failure Percentage","expression":"(m1DynamoDB6 / (m1DynamoDB6+m2DynamoDB6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB6"}],["ASR","RemediationOutcome","ControlId","DynamoDB.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB6"}],[{"label":"ElastiCache.1 Failure Percentage","expression":"(m1ElastiCache1 / (m1ElastiCache1+m2ElastiCache1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache1"}],["ASR","RemediationOutcome","ControlId","ElastiCache.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache1"}],[{"label":"ElastiCache.2 Failure Percentage","expression":"(m1ElastiCache2 / (m1ElastiCache2+m2ElastiCache2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache2"}],["ASR","RemediationOutcome","ControlId","ElastiCache.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache2"}],[{"label":"ElastiCache.3 Failure Percentage","expression":"(m1ElastiCache3 / (m1ElastiCache3+m2ElastiCache3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache3"}],["ASR","RemediationOutcome","ControlId","ElastiCache.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache3"}],[{"label":"ECS.5 Failure Percentage","expression":"(m1ECS5 / (m1ECS5+m2ECS5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ECS.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ECS5"}],["ASR","RemediationOutcome","ControlId","ECS.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ECS5"}],[{"label":"ELB.1 Failure Percentage","expression":"(m1ELB1 / (m1ELB1+m2ELB1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ELB.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ELB1"}],["ASR","RemediationOutcome","ControlId","ELB.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ELB1"}]],"annotations":{"horizontal":[{"value":", + "","metrics":[[{"label":"AutoScaling.1 Failure Percentage","expression":"(m1AutoScaling1 / (m1AutoScaling1+m2AutoScaling1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","AutoScaling.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1AutoScaling1"}],["ASR","RemediationOutcome","ControlId","AutoScaling.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2AutoScaling1"}],[{"label":"CloudFormation.1 Failure Percentage","expression":"(m1CloudFormation1 / (m1CloudFormation1+m2CloudFormation1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFormation.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFormation1"}],["ASR","RemediationOutcome","ControlId","CloudFormation.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFormation1"}],[{"label":"CloudFront.1 Failure Percentage","expression":"(m1CloudFront1 / (m1CloudFront1+m2CloudFront1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFront.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFront1"}],["ASR","RemediationOutcome","ControlId","CloudFront.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFront1"}],[{"label":"CloudFront.12 Failure Percentage","expression":"(m1CloudFront12 / (m1CloudFront12+m2CloudFront12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudFront.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudFront12"}],["ASR","RemediationOutcome","ControlId","CloudFront.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudFront12"}],[{"label":"CloudTrail.1 Failure Percentage","expression":"(m1CloudTrail1 / (m1CloudTrail1+m2CloudTrail1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail1"}],["ASR","RemediationOutcome","ControlId","CloudTrail.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail1"}],[{"label":"CloudTrail.2 Failure Percentage","expression":"(m1CloudTrail2 / (m1CloudTrail2+m2CloudTrail2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail2"}],["ASR","RemediationOutcome","ControlId","CloudTrail.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail2"}],[{"label":"CloudTrail.3 Failure Percentage","expression":"(m1CloudTrail3 / (m1CloudTrail3+m2CloudTrail3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail3"}],["ASR","RemediationOutcome","ControlId","CloudTrail.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail3"}],[{"label":"CloudTrail.4 Failure Percentage","expression":"(m1CloudTrail4 / (m1CloudTrail4+m2CloudTrail4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail4"}],["ASR","RemediationOutcome","ControlId","CloudTrail.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail4"}],[{"label":"CloudTrail.5 Failure Percentage","expression":"(m1CloudTrail5 / (m1CloudTrail5+m2CloudTrail5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail5"}],["ASR","RemediationOutcome","ControlId","CloudTrail.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail5"}],[{"label":"CloudTrail.6 Failure Percentage","expression":"(m1CloudTrail6 / (m1CloudTrail6+m2CloudTrail6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail6"}],["ASR","RemediationOutcome","ControlId","CloudTrail.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail6"}],[{"label":"CloudTrail.7 Failure Percentage","expression":"(m1CloudTrail7 / (m1CloudTrail7+m2CloudTrail7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudTrail.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudTrail7"}],["ASR","RemediationOutcome","ControlId","CloudTrail.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudTrail7"}],[{"label":"CloudWatch.1 Failure Percentage","expression":"(m1CloudWatch1 / (m1CloudWatch1+m2CloudWatch1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch1"}],["ASR","RemediationOutcome","ControlId","CloudWatch.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch1"}],[{"label":"CloudWatch.2 Failure Percentage","expression":"(m1CloudWatch2 / (m1CloudWatch2+m2CloudWatch2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch2"}],["ASR","RemediationOutcome","ControlId","CloudWatch.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch2"}],[{"label":"CloudWatch.3 Failure Percentage","expression":"(m1CloudWatch3 / (m1CloudWatch3+m2CloudWatch3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch3"}],["ASR","RemediationOutcome","ControlId","CloudWatch.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch3"}],[{"label":"CloudWatch.4 Failure Percentage","expression":"(m1CloudWatch4 / (m1CloudWatch4+m2CloudWatch4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch4"}],["ASR","RemediationOutcome","ControlId","CloudWatch.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch4"}],[{"label":"CloudWatch.5 Failure Percentage","expression":"(m1CloudWatch5 / (m1CloudWatch5+m2CloudWatch5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch5"}],["ASR","RemediationOutcome","ControlId","CloudWatch.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch5"}],[{"label":"CloudWatch.6 Failure Percentage","expression":"(m1CloudWatch6 / (m1CloudWatch6+m2CloudWatch6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch6"}],["ASR","RemediationOutcome","ControlId","CloudWatch.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch6"}],[{"label":"CloudWatch.7 Failure Percentage","expression":"(m1CloudWatch7 / (m1CloudWatch7+m2CloudWatch7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch7"}],["ASR","RemediationOutcome","ControlId","CloudWatch.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch7"}],[{"label":"CloudWatch.8 Failure Percentage","expression":"(m1CloudWatch8 / (m1CloudWatch8+m2CloudWatch8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch8"}],["ASR","RemediationOutcome","ControlId","CloudWatch.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch8"}],[{"label":"CloudWatch.9 Failure Percentage","expression":"(m1CloudWatch9 / (m1CloudWatch9+m2CloudWatch9)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.9","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch9"}],["ASR","RemediationOutcome","ControlId","CloudWatch.9","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch9"}],[{"label":"CloudWatch.10 Failure Percentage","expression":"(m1CloudWatch10 / (m1CloudWatch10+m2CloudWatch10)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.10","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch10"}],["ASR","RemediationOutcome","ControlId","CloudWatch.10","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch10"}],[{"label":"CloudWatch.11 Failure Percentage","expression":"(m1CloudWatch11 / (m1CloudWatch11+m2CloudWatch11)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch11"}],["ASR","RemediationOutcome","ControlId","CloudWatch.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch11"}],[{"label":"CloudWatch.12 Failure Percentage","expression":"(m1CloudWatch12 / (m1CloudWatch12+m2CloudWatch12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch12"}],["ASR","RemediationOutcome","ControlId","CloudWatch.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch12"}],[{"label":"CloudWatch.13 Failure Percentage","expression":"(m1CloudWatch13 / (m1CloudWatch13+m2CloudWatch13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch13"}],["ASR","RemediationOutcome","ControlId","CloudWatch.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch13"}],[{"label":"CloudWatch.14 Failure Percentage","expression":"(m1CloudWatch14 / (m1CloudWatch14+m2CloudWatch14)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch14"}],["ASR","RemediationOutcome","ControlId","CloudWatch.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch14"}],[{"label":"CodeBuild.2 Failure Percentage","expression":"(m1CodeBuild2 / (m1CodeBuild2+m2CodeBuild2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CodeBuild.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CodeBuild2"}],["ASR","RemediationOutcome","ControlId","CodeBuild.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CodeBuild2"}],[{"label":"CodeBuild.5 Failure Percentage","expression":"(m1CodeBuild5 / (m1CodeBuild5+m2CodeBuild5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CodeBuild.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CodeBuild5"}],["ASR","RemediationOutcome","ControlId","CodeBuild.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CodeBuild5"}],[{"label":"Config.1 Failure Percentage","expression":"(m1Config1 / (m1Config1+m2Config1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Config.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Config1"}],["ASR","RemediationOutcome","ControlId","Config.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Config1"}],[{"label":"EC2.1 Failure Percentage","expression":"(m1EC21 / (m1EC21+m2EC21)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC21"}],["ASR","RemediationOutcome","ControlId","EC2.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC21"}],[{"label":"EC2.2 Failure Percentage","expression":"(m1EC22 / (m1EC22+m2EC22)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC22"}],["ASR","RemediationOutcome","ControlId","EC2.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC22"}],[{"label":"EC2.4 Failure Percentage","expression":"(m1EC24 / (m1EC24+m2EC24)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC24"}],["ASR","RemediationOutcome","ControlId","EC2.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC24"}],[{"label":"EC2.6 Failure Percentage","expression":"(m1EC26 / (m1EC26+m2EC26)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC26"}],["ASR","RemediationOutcome","ControlId","EC2.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC26"}],[{"label":"EC2.7 Failure Percentage","expression":"(m1EC27 / (m1EC27+m2EC27)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC27"}],["ASR","RemediationOutcome","ControlId","EC2.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC27"}],[{"label":"EC2.8 Failure Percentage","expression":"(m1EC28 / (m1EC28+m2EC28)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC28"}],["ASR","RemediationOutcome","ControlId","EC2.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC28"}],[{"label":"EC2.13 Failure Percentage","expression":"(m1EC213 / (m1EC213+m2EC213)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC213"}],["ASR","RemediationOutcome","ControlId","EC2.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC213"}],[{"label":"EC2.14 Failure Percentage","expression":"(m1EC214 / (m1EC214+m2EC214)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC214"}],["ASR","RemediationOutcome","ControlId","EC2.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC214"}],[{"label":"EC2.15 Failure Percentage","expression":"(m1EC215 / (m1EC215+m2EC215)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.15","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC215"}],["ASR","RemediationOutcome","ControlId","EC2.15","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC215"}],[{"label":"EC2.18 Failure Percentage","expression":"(m1EC218 / (m1EC218+m2EC218)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.18","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC218"}],["ASR","RemediationOutcome","ControlId","EC2.18","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC218"}],[{"label":"EC2.19 Failure Percentage","expression":"(m1EC219 / (m1EC219+m2EC219)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.19","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC219"}],["ASR","RemediationOutcome","ControlId","EC2.19","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC219"}],[{"label":"EC2.23 Failure Percentage","expression":"(m1EC223 / (m1EC223+m2EC223)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.23","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC223"}],["ASR","RemediationOutcome","ControlId","EC2.23","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC223"}],[{"label":"ECR.1 Failure Percentage","expression":"(m1ECR1 / (m1ECR1+m2ECR1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ECR.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ECR1"}],["ASR","RemediationOutcome","ControlId","ECR.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ECR1"}],[{"label":"IAM.3 Failure Percentage","expression":"(m1IAM3 / (m1IAM3+m2IAM3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM3"}],["ASR","RemediationOutcome","ControlId","IAM.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM3"}],[{"label":"IAM.7 Failure Percentage","expression":"(m1IAM7 / (m1IAM7+m2IAM7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM7"}],["ASR","RemediationOutcome","ControlId","IAM.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM7"}],[{"label":"IAM.8 Failure Percentage","expression":"(m1IAM8 / (m1IAM8+m2IAM8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM8"}],["ASR","RemediationOutcome","ControlId","IAM.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM8"}],[{"label":"IAM.11 Failure Percentage","expression":"(m1IAM11 / (m1IAM11+m2IAM11)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM11"}],["ASR","RemediationOutcome","ControlId","IAM.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM11"}],[{"label":"IAM.12 Failure Percentage","expression":"(m1IAM12 / (m1IAM12+m2IAM12)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.12","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM12"}],["ASR","RemediationOutcome","ControlId","IAM.12","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM12"}],[{"label":"IAM.13 Failure Percentage","expression":"(m1IAM13 / (m1IAM13+m2IAM13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM13"}],["ASR","RemediationOutcome","ControlId","IAM.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM13"}],[{"label":"IAM.14 Failure Percentage","expression":"(m1IAM14 / (m1IAM14+m2IAM14)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.14","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM14"}],["ASR","RemediationOutcome","ControlId","IAM.14","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM14"}],[{"label":"IAM.15 Failure Percentage","expression":"(m1IAM15 / (m1IAM15+m2IAM15)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.15","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM15"}],["ASR","RemediationOutcome","ControlId","IAM.15","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM15"}],[{"label":"IAM.16 Failure Percentage","expression":"(m1IAM16 / (m1IAM16+m2IAM16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM16"}],["ASR","RemediationOutcome","ControlId","IAM.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM16"}],[{"label":"IAM.17 Failure Percentage","expression":"(m1IAM17 / (m1IAM17+m2IAM17)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.17","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM17"}],["ASR","RemediationOutcome","ControlId","IAM.17","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM17"}],[{"label":"IAM.18 Failure Percentage","expression":"(m1IAM18 / (m1IAM18+m2IAM18)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.18","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM18"}],["ASR","RemediationOutcome","ControlId","IAM.18","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM18"}],[{"label":"IAM.22 Failure Percentage","expression":"(m1IAM22 / (m1IAM22+m2IAM22)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","IAM.22","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1IAM22"}],["ASR","RemediationOutcome","ControlId","IAM.22","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2IAM22"}],[{"label":"KMS.4 Failure Percentage","expression":"(m1KMS4 / (m1KMS4+m2KMS4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","KMS.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1KMS4"}],["ASR","RemediationOutcome","ControlId","KMS.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2KMS4"}],[{"label":"Lambda.1 Failure Percentage","expression":"(m1Lambda1 / (m1Lambda1+m2Lambda1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Lambda.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Lambda1"}],["ASR","RemediationOutcome","ControlId","Lambda.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Lambda1"}],[{"label":"RDS.1 Failure Percentage","expression":"(m1RDS1 / (m1RDS1+m2RDS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS1"}],["ASR","RemediationOutcome","ControlId","RDS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS1"}],[{"label":"RDS.2 Failure Percentage","expression":"(m1RDS2 / (m1RDS2+m2RDS2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS2"}],["ASR","RemediationOutcome","ControlId","RDS.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS2"}],[{"label":"RDS.4 Failure Percentage","expression":"(m1RDS4 / (m1RDS4+m2RDS4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS4"}],["ASR","RemediationOutcome","ControlId","RDS.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS4"}],[{"label":"RDS.5 Failure Percentage","expression":"(m1RDS5 / (m1RDS5+m2RDS5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS5"}],["ASR","RemediationOutcome","ControlId","RDS.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS5"}],[{"label":"RDS.6 Failure Percentage","expression":"(m1RDS6 / (m1RDS6+m2RDS6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS6"}],["ASR","RemediationOutcome","ControlId","RDS.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS6"}],[{"label":"RDS.7 Failure Percentage","expression":"(m1RDS7 / (m1RDS7+m2RDS7)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.7","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS7"}],["ASR","RemediationOutcome","ControlId","RDS.7","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS7"}],[{"label":"RDS.8 Failure Percentage","expression":"(m1RDS8 / (m1RDS8+m2RDS8)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS8"}],["ASR","RemediationOutcome","ControlId","RDS.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS8"}],[{"label":"RDS.13 Failure Percentage","expression":"(m1RDS13 / (m1RDS13+m2RDS13)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS13"}],["ASR","RemediationOutcome","ControlId","RDS.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS13"}],[{"label":"RDS.16 Failure Percentage","expression":"(m1RDS16 / (m1RDS16+m2RDS16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","RDS.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1RDS16"}],["ASR","RemediationOutcome","ControlId","RDS.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2RDS16"}],[{"label":"Redshift.1 Failure Percentage","expression":"(m1Redshift1 / (m1Redshift1+m2Redshift1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift1"}],["ASR","RemediationOutcome","ControlId","Redshift.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift1"}],[{"label":"Redshift.3 Failure Percentage","expression":"(m1Redshift3 / (m1Redshift3+m2Redshift3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift3"}],["ASR","RemediationOutcome","ControlId","Redshift.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift3"}],[{"label":"Redshift.4 Failure Percentage","expression":"(m1Redshift4 / (m1Redshift4+m2Redshift4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift4"}],["ASR","RemediationOutcome","ControlId","Redshift.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift4"}],[{"label":"Redshift.6 Failure Percentage","expression":"(m1Redshift6 / (m1Redshift6+m2Redshift6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Redshift.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Redshift6"}],["ASR","RemediationOutcome","ControlId","Redshift.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Redshift6"}],[{"label":"S3.1 Failure Percentage","expression":"(m1S31 / (m1S31+m2S31)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S31"}],["ASR","RemediationOutcome","ControlId","S3.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S31"}],[{"label":"S3.2 Failure Percentage","expression":"(m1S32 / (m1S32+m2S32)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S32"}],["ASR","RemediationOutcome","ControlId","S3.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S32"}],[{"label":"S3.3 Failure Percentage","expression":"(m1S33 / (m1S33+m2S33)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S33"}],["ASR","RemediationOutcome","ControlId","S3.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S33"}],[{"label":"S3.4 Failure Percentage","expression":"(m1S34 / (m1S34+m2S34)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S34"}],["ASR","RemediationOutcome","ControlId","S3.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S34"}],[{"label":"S3.5 Failure Percentage","expression":"(m1S35 / (m1S35+m2S35)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S35"}],["ASR","RemediationOutcome","ControlId","S3.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S35"}],[{"label":"S3.6 Failure Percentage","expression":"(m1S36 / (m1S36+m2S36)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S36"}],["ASR","RemediationOutcome","ControlId","S3.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S36"}],[{"label":"S3.8 Failure Percentage","expression":"(m1S38 / (m1S38+m2S38)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.8","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S38"}],["ASR","RemediationOutcome","ControlId","S3.8","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S38"}],[{"label":"S3.9 Failure Percentage","expression":"(m1S39 / (m1S39+m2S39)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.9","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S39"}],["ASR","RemediationOutcome","ControlId","S3.9","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S39"}],[{"label":"S3.11 Failure Percentage","expression":"(m1S311 / (m1S311+m2S311)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.11","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S311"}],["ASR","RemediationOutcome","ControlId","S3.11","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S311"}],[{"label":"S3.13 Failure Percentage","expression":"(m1S313 / (m1S313+m2S313)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","S3.13","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1S313"}],["ASR","RemediationOutcome","ControlId","S3.13","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2S313"}],[{"label":"SecretsManager.1 Failure Percentage","expression":"(m1SecretsManager1 / (m1SecretsManager1+m2SecretsManager1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager1"}],["ASR","RemediationOutcome","ControlId","SecretsManager.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager1"}],[{"label":"SecretsManager.3 Failure Percentage","expression":"(m1SecretsManager3 / (m1SecretsManager3+m2SecretsManager3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager3"}],["ASR","RemediationOutcome","ControlId","SecretsManager.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager3"}],[{"label":"SecretsManager.4 Failure Percentage","expression":"(m1SecretsManager4 / (m1SecretsManager4+m2SecretsManager4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SecretsManager.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SecretsManager4"}],["ASR","RemediationOutcome","ControlId","SecretsManager.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SecretsManager4"}],[{"label":"SNS.1 Failure Percentage","expression":"(m1SNS1 / (m1SNS1+m2SNS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SNS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SNS1"}],["ASR","RemediationOutcome","ControlId","SNS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SNS1"}],[{"label":"SNS.2 Failure Percentage","expression":"(m1SNS2 / (m1SNS2+m2SNS2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SNS.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SNS2"}],["ASR","RemediationOutcome","ControlId","SNS.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SNS2"}],[{"label":"SQS.1 Failure Percentage","expression":"(m1SQS1 / (m1SQS1+m2SQS1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SQS.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SQS1"}],["ASR","RemediationOutcome","ControlId","SQS.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SQS1"}],[{"label":"SSM.4 Failure Percentage","expression":"(m1SSM4 / (m1SSM4+m2SSM4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SSM.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SSM4"}],["ASR","RemediationOutcome","ControlId","SSM.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SSM4"}],[{"label":"GuardDuty.1 Failure Percentage","expression":"(m1GuardDuty1 / (m1GuardDuty1+m2GuardDuty1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty1"}],["ASR","RemediationOutcome","ControlId","GuardDuty.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty1"}],[{"label":"Athena.4 Failure Percentage","expression":"(m1Athena4 / (m1Athena4+m2Athena4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Athena.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Athena4"}],["ASR","RemediationOutcome","ControlId","Athena.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Athena4"}],[{"label":"APIGateway.1 Failure Percentage","expression":"(m1APIGateway1 / (m1APIGateway1+m2APIGateway1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","APIGateway.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1APIGateway1"}],["ASR","RemediationOutcome","ControlId","APIGateway.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2APIGateway1"}],[{"label":"APIGateway.5 Failure Percentage","expression":"(m1APIGateway5 / (m1APIGateway5+m2APIGateway5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","APIGateway.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1APIGateway5"}],["ASR","RemediationOutcome","ControlId","APIGateway.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2APIGateway5"}],[{"label":"AutoScaling.3 Failure Percentage","expression":"(m1AutoScaling3 / (m1AutoScaling3+m2AutoScaling3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","AutoScaling.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1AutoScaling3"}],["ASR","RemediationOutcome","ControlId","AutoScaling.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2AutoScaling3"}],[{"label":"Autoscaling.5 Failure Percentage","expression":"(m1Autoscaling5 / (m1Autoscaling5+m2Autoscaling5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Autoscaling.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Autoscaling5"}],["ASR","RemediationOutcome","ControlId","Autoscaling.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Autoscaling5"}],[{"label":"CloudWatch.16 Failure Percentage","expression":"(m1CloudWatch16 / (m1CloudWatch16+m2CloudWatch16)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","CloudWatch.16","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1CloudWatch16"}],["ASR","RemediationOutcome","ControlId","CloudWatch.16","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2CloudWatch16"}],[{"label":"EC2.10 Failure Percentage","expression":"(m1EC210 / (m1EC210+m2EC210)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","EC2.10","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1EC210"}],["ASR","RemediationOutcome","ControlId","EC2.10","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2EC210"}],[{"label":"SSM.1 Failure Percentage","expression":"(m1SSM1 / (m1SSM1+m2SSM1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","SSM.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1SSM1"}],["ASR","RemediationOutcome","ControlId","SSM.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2SSM1"}],[{"label":"GuardDuty.2 Failure Percentage","expression":"(m1GuardDuty2 / (m1GuardDuty2+m2GuardDuty2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty2"}],["ASR","RemediationOutcome","ControlId","GuardDuty.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty2"}],[{"label":"GuardDuty.4 Failure Percentage","expression":"(m1GuardDuty4 / (m1GuardDuty4+m2GuardDuty4)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","GuardDuty.4","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1GuardDuty4"}],["ASR","RemediationOutcome","ControlId","GuardDuty.4","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2GuardDuty4"}],[{"label":"Macie.1 Failure Percentage","expression":"(m1Macie1 / (m1Macie1+m2Macie1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","Macie.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1Macie1"}],["ASR","RemediationOutcome","ControlId","Macie.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2Macie1"}],[{"label":"DynamoDB.1 Failure Percentage","expression":"(m1DynamoDB1 / (m1DynamoDB1+m2DynamoDB1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB1"}],["ASR","RemediationOutcome","ControlId","DynamoDB.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB1"}],[{"label":"DynamoDB.5 Failure Percentage","expression":"(m1DynamoDB5 / (m1DynamoDB5+m2DynamoDB5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB5"}],["ASR","RemediationOutcome","ControlId","DynamoDB.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB5"}],[{"label":"DynamoDB.6 Failure Percentage","expression":"(m1DynamoDB6 / (m1DynamoDB6+m2DynamoDB6)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","DynamoDB.6","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1DynamoDB6"}],["ASR","RemediationOutcome","ControlId","DynamoDB.6","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2DynamoDB6"}],[{"label":"ElastiCache.1 Failure Percentage","expression":"(m1ElastiCache1 / (m1ElastiCache1+m2ElastiCache1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache1"}],["ASR","RemediationOutcome","ControlId","ElastiCache.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache1"}],[{"label":"ElastiCache.2 Failure Percentage","expression":"(m1ElastiCache2 / (m1ElastiCache2+m2ElastiCache2)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.2","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache2"}],["ASR","RemediationOutcome","ControlId","ElastiCache.2","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache2"}],[{"label":"ElastiCache.3 Failure Percentage","expression":"(m1ElastiCache3 / (m1ElastiCache3+m2ElastiCache3)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ElastiCache.3","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ElastiCache3"}],["ASR","RemediationOutcome","ControlId","ElastiCache.3","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ElastiCache3"}],[{"label":"ECS.5 Failure Percentage","expression":"(m1ECS5 / (m1ECS5+m2ECS5)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ECS.5","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ECS5"}],["ASR","RemediationOutcome","ControlId","ECS.5","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ECS5"}],[{"label":"ELB.1 Failure Percentage","expression":"(m1ELB1 / (m1ELB1+m2ELB1)) * 100","period":86400}],["ASR","RemediationOutcome","ControlId","ELB.1","Outcome","FAILED",{"period":86400,"visible":false,"id":"m1ELB1"}],["ASR","RemediationOutcome","ControlId","ELB.1","Outcome","SUCCESS",{"period":86400,"visible":false,"id":"m2ELB1"}]],"annotations":{"horizontal":[{"value":", { "Ref": "RemediationFailureAlarmThreshold", }, @@ -10474,6 +10564,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "AWS_PARTITION": { "Ref": "AWS::Partition", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "check_ssm_doc_state", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_TMN": "automated-security-response-on-aws", "SOLUTION_VERSION": "v1.0.0", @@ -10705,6 +10800,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "AWS_PARTITION": { "Ref": "AWS::Partition", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "exec_ssm_doc", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_TMN": "automated-security-response-on-aws", "SOLUTION_VERSION": "v1.0.0", @@ -10767,6 +10867,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "AWS_PARTITION": { "Ref": "AWS::Partition", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "get_approval_requirement", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_TMN": "automated-security-response-on-aws", "SOLUTION_VERSION": "v1.0.0", @@ -10830,6 +10935,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "AWS_PARTITION": { "Ref": "AWS::Partition", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "check_ssm_execution", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_TMN": "automated-security-response-on-aws", "SOLUTION_VERSION": "v1.0.0", @@ -11505,7 +11615,7 @@ exports[`Test if the Stack has all the resources. 1`] = ` "Fn::Join": [ "", [ - "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings","CustomActionName.$":"$.detail.actionName"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","Parameters":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType","CustomActionName.$":"$.CustomActionName"},"ItemsPath":"$.Findings","ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOT_NEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", + "{"StartAt":"Get Finding Data from Input","States":{"Get Finding Data from Input":{"Type":"Pass","Comment":"Extract top-level data needed for remediation","Parameters":{"EventType.$":"$.detail-type","Findings.$":"$.detail.findings","CustomActionName.$":"$.detail.actionName"},"Next":"Process Findings"},"Process Findings":{"Type":"Map","Comment":"Process all findings in CloudWatch Event","Next":"EOJ","ItemsPath":"$.Findings","ItemSelector":{"Finding.$":"$$.Map.Item.Value","EventType.$":"$.EventType","CustomActionName.$":"$.CustomActionName"},"ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Finding Workflow State NEW?","States":{"Finding Workflow State NEW?":{"Type":"Choice","Choices":[{"Or":[{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Custom Action"},{"And":[{"Variable":"$.Finding.Workflow.Status","StringEquals":"NEW"},{"Variable":"$.EventType","StringEquals":"Security Hub Findings - Imported"}]}],"Next":"Get Remediation Approval Requirement"}],"Default":"Finding Workflow State is not NEW"},"Finding Workflow State is not NEW":{"Type":"Pass","Parameters":{"Notification":{"Message.$":"States.Format('Finding Workflow State is not NEW ({}).', $.Finding.Workflow.Status)","State.$":"States.Format('NOT_NEW')"},"EventType.$":"$.EventType","Finding.$":"$.Finding"},"Next":"notify"},"notify":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Comment":"Send notifications","TimeoutSeconds":300,"HeartbeatSeconds":60,"Resource":"arn:", { "Ref": "AWS::Partition", }, @@ -11665,6 +11775,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "Description": "SO0111 ASR function that schedules remediations in member accounts", "Environment": { "Variables": { + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "schedule_remediation", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "RemediationWaitTime": "3", "SchedulingTableName": { "Ref": "SchedulingTable1EC09B43", @@ -11746,6 +11861,11 @@ exports[`Test if the Stack has all the resources. 1`] = ` "ENHANCED_METRICS": { "Ref": "EnableEnhancedCloudWatchMetrics", }, + "POWERTOOLS_LOGGER_LOG_EVENT": "false", + "POWERTOOLS_LOG_LEVEL": "INFO", + "POWERTOOLS_SERVICE_NAME": "send_notifications", + "POWERTOOLS_TRACER_CAPTURE_ERROR": "true", + "POWERTOOLS_TRACER_CAPTURE_RESPONSE": "true", "SOLUTION_ID": "SO0111", "SOLUTION_TMN": "automated-security-response-on-aws", "SOLUTION_VERSION": "v1.0.0", diff --git a/source/test/applyTag.test.ts b/source/test/applyTag.test.ts new file mode 100644 index 00000000..a2c7ec1b --- /dev/null +++ b/source/test/applyTag.test.ts @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { App, DefaultStackSynthesizer, Stack } from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources'; +import { Template } from 'aws-cdk-lib/assertions'; +import { removeEventSourceMappingTags } from '../lib/tags/applyTag'; + +describe('applyTag', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack', { + synthesizer: new DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), + stackName: 'TestStack', + }); + }); + + test('removeEventSourceMappingTags removes tags from EventSourceMapping', () => { + const testLambda = new lambda.Function(stack, 'TestLambda', { + runtime: lambda.Runtime.PYTHON_3_9, + handler: 'index.handler', + code: lambda.Code.fromInline('print("test")'), + }); + + const testQueue = new sqs.Queue(stack, 'TestQueue'); + + const eventSource = new lambdaEventSources.SqsEventSource(testQueue, { + batchSize: 1, + }); + + testLambda.addEventSource(eventSource); + + removeEventSourceMappingTags(testLambda); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::EventSourceMapping', { + BatchSize: 1, + }); + + const rawTemplate = template.toJSON(); + const eventSourceMappings = Object.entries(rawTemplate.Resources).filter( + ([_, resource]: [string, any]) => resource.Type === 'AWS::Lambda::EventSourceMapping', + ); + + expect(eventSourceMappings).toHaveLength(1); + + const [_, eventSourceMapping] = eventSourceMappings[0] as [string, any]; + expect(eventSourceMapping.Properties).not.toHaveProperty('Tags'); + + expect(eventSourceMapping.Properties).toHaveProperty('BatchSize', 1); + expect(eventSourceMapping.Properties).toHaveProperty('EventSourceArn'); + expect(eventSourceMapping.Properties).toHaveProperty('FunctionName'); + }); + + test('removeEventSourceMappingTags handles Lambda with no EventSourceMappings', () => { + const testLambda = new lambda.Function(stack, 'TestLambda', { + runtime: lambda.Runtime.PYTHON_3_9, + handler: 'index.handler', + code: lambda.Code.fromInline('print("test")'), + }); + + expect(() => { + removeEventSourceMappingTags(testLambda); + }).not.toThrow(); + + const template = Template.fromStack(stack); + + const rawTemplate = template.toJSON(); + const eventSourceMappings = Object.entries(rawTemplate.Resources).filter( + ([_, resource]: [string, any]) => resource.Type === 'AWS::Lambda::EventSourceMapping', + ); + + expect(eventSourceMappings).toHaveLength(0); + }); + + test('removeEventSourceMappingTags handles Lambda with multiple EventSourceMappings', () => { + const testLambda = new lambda.Function(stack, 'TestLambda', { + runtime: lambda.Runtime.PYTHON_3_9, + handler: 'index.handler', + code: lambda.Code.fromInline('print("test")'), + }); + + const testQueue1 = new sqs.Queue(stack, 'TestQueue1'); + const testQueue2 = new sqs.Queue(stack, 'TestQueue2'); + + const eventSource1 = new lambdaEventSources.SqsEventSource(testQueue1, { + batchSize: 1, + }); + const eventSource2 = new lambdaEventSources.SqsEventSource(testQueue2, { + batchSize: 2, + }); + + testLambda.addEventSource(eventSource1); + testLambda.addEventSource(eventSource2); + + removeEventSourceMappingTags(testLambda); + + const template = Template.fromStack(stack); + + const rawTemplate = template.toJSON(); + const eventSourceMappings = Object.entries(rawTemplate.Resources).filter( + ([_, resource]: [string, any]) => resource.Type === 'AWS::Lambda::EventSourceMapping', + ); + + expect(eventSourceMappings).toHaveLength(2); + + eventSourceMappings.forEach(([_, eventSourceMapping]: [string, any]) => { + expect(eventSourceMapping.Properties).not.toHaveProperty('Tags'); + }); + }); +});