Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand All @@ -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
Expand All @@ -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 ]
Expand All @@ -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 ]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "automated_security_response_on_aws"
version = "2.3.0"
version = "2.3.1"

[tool.setuptools]
package-dir = {"" = "source"}
Expand Down
2 changes: 1 addition & 1 deletion solution-manifest.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

50 changes: 24 additions & 26 deletions source/Orchestrator/check_ssm_doc_state.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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":
Expand All @@ -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"})

Expand All @@ -70,15 +68,15 @@ 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(
{
"status": "ACCESSDENIED",
"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 = {
Expand All @@ -88,33 +86,33 @@ 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(
{
"status": "CLIENTERROR",
"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"]
Expand Down Expand Up @@ -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"])

Expand Down Expand Up @@ -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)
Expand All @@ -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]
35 changes: 16 additions & 19 deletions source/Orchestrator/check_ssm_execution.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
"""
Expand All @@ -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


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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"]

Expand All @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -298,4 +295,4 @@ def lambda_handler(event, _):
}
)

return answer.json()
return answer.json() # type: ignore[no-any-return]
Loading