Skip to content

Commit 9d0fa7e

Browse files
committed
single region support, added standards check, minor code updates
1 parent f69124a commit 9d0fa7e

File tree

9 files changed

+386
-236
lines changed

9 files changed

+386
-236
lines changed

aws_sra_examples/solutions/config/config_management_account/templates/sra-config-management-account-main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Metadata:
5454
pFrequency:
5555
default: Frequency
5656
pHomeRegion:
57-
Description: Control Tower Home Region
57+
default: Control Tower Home Region
5858
pIncludeGlobalResourceTypes:
5959
default: Include Global Resource Types
6060
pKmsKeyArn:

aws_sra_examples/solutions/securityhub/securityhub_org/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-
1414

1515
## Introduction
1616

17-
The Security Hub Organization solution will enable AWS Security Hub by delegating administration to the `Audit account` within the `management account` and assuming an IAM role within the `Audit account` to configure Security Hub for all the existing
18-
and future AWS Organization accounts.
17+
The Security Hub Organization solution will automate enabling AWS Security Hub by delegating administration to an account (e.g. Audit or Security Tooling) and configuring Security Hub for all the existing and future AWS Organization accounts.
1918

2019
**Key solution features:**
2120

@@ -99,7 +98,7 @@ populated from the `SecurityAccountId` parameter within the `AWSControlTowerBP-B
9998

10099
#### 2.3 Security Hub (Home Region)<!-- omit in toc -->
101100

102-
- A region aggregator is configured within the `Home region` to aggregate findings from the configured regions.
101+
- A region aggregator is configured within the `Home region` to aggregate findings from the configured regions, if more than one region is configured.
103102
- A parameter is provided to aggregate all configured Security Hub regions including any future regions.
104103

105104
#### 2.4 Security Hub (Regions)<!-- omit in toc -->

aws_sra_examples/solutions/securityhub/securityhub_org/lambda/src/app.py

Lines changed: 69 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""This script performs operations to enable, configure, and disable SecurityHub.
22
3-
Version: 1.0
3+
Version: 1.1
44
55
'securityhub_org' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples
66
@@ -22,6 +22,7 @@
2222
from crhelper import CfnResource
2323

2424
if TYPE_CHECKING:
25+
from aws_lambda_typing.context import Context
2526
from aws_lambda_typing.events import CloudFormationCustomResourceEvent
2627
from mypy_boto3_organizations import OrganizationsClient
2728
from mypy_boto3_sns import SNSClient
@@ -40,7 +41,7 @@
4041
SERVICE_NAME = "securityhub.amazonaws.com"
4142
SLEEP_SECONDS = 60
4243
PRE_DISABLE_SLEEP_SECONDS = 30
43-
SSM_PARAMETER_PREFIX = os.environ.get("SSM_PARAMETER_PREFIX", "/sra/securityhub-org")
44+
SSM_PARAMETER_PREFIX = "/sra/securityhub-org"
4445
PARAMETER_LIST = [
4546
"AWS_PARTITION",
4647
"CIS_VERSION",
@@ -100,7 +101,7 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> None:
100101
@helper.create
101102
@helper.update
102103
@helper.delete
103-
def process_cloudformation_event(event: CloudFormationCustomResourceEvent, context: Any) -> str:
104+
def process_cloudformation_event(event: CloudFormationCustomResourceEvent, context: Context) -> str:
104105
"""Process Event from AWS CloudFormation.
105106
106107
Args:
@@ -110,26 +111,21 @@ def process_cloudformation_event(event: CloudFormationCustomResourceEvent, conte
110111
Returns:
111112
AWS CloudFormation physical resource id
112113
"""
113-
request_type = event["RequestType"]
114-
LOGGER.info(f"{request_type} Event")
115-
LOGGER.debug(f"Lambda Context: {context}")
114+
event_info = {"Event": event}
115+
LOGGER.info(event_info)
116116

117117
params = get_validated_parameters(event)
118118
set_configuration_ssm_parameter(params)
119119

120-
if params["action"] in "Add, Update":
120+
if params["action"] in ["Add", "Update"]:
121121
process_add_update_event(params)
122122
else:
123-
regions = common.get_enabled_regions(params.get("ENABLED_REGIONS", ""), (params.get("CONTROL_TOWER_REGIONS_ONLY", "false")).lower() in "true")
123+
regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"] == "true")
124124
LOGGER.info("...Disable Security Hub")
125125
securityhub.disable_organization_admin_account(regions)
126126
securityhub.disable_securityhub(params["DELEGATED_ADMIN_ACCOUNT_ID"], params["CONFIGURATION_ROLE_NAME"], regions)
127127

128-
return (
129-
f"sra-securityhub-org-{params['DELEGATED_ADMIN_ACCOUNT_ID']}-{params['DISABLE_SECURITY_HUB']}-{params['CIS_VERSION']}-"
130-
+ f"{params['ENABLE_CIS_STANDARD']}-{params['ENABLE_PCI_STANDARD']}-{params['ENABLE_SECURITY_BEST_PRACTICES_STANDARD']}-"
131-
+ f"{params['PCI_VERSION']}-{params['REGION_LINKING_MODE']}-{params['SECURITY_BEST_PRACTICES_VERSION']}"
132-
)
128+
return f"sra-securityhub-org-{params['DELEGATED_ADMIN_ACCOUNT_ID']}"
133129

134130

135131
def process_add_update_event(params: dict) -> str:
@@ -142,9 +138,9 @@ def process_add_update_event(params: dict) -> str:
142138
Status
143139
"""
144140
accounts = common.get_all_organization_accounts(params["DELEGATED_ADMIN_ACCOUNT_ID"])
145-
regions = common.get_enabled_regions(params.get("ENABLED_REGIONS", ""), (params.get("CONTROL_TOWER_REGIONS_ONLY", "false")).lower() in "true")
141+
regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"] == "true")
146142

147-
if (params.get("DISABLE_SECURITY_HUB", "false")).lower() in "true" and params["action"] == "Update":
143+
if params["DISABLE_SECURITY_HUB"] == "true" and params["action"] == "Update":
148144
LOGGER.info("...Disable Security Hub")
149145
securityhub.disable_organization_admin_account(regions)
150146
securityhub.disable_securityhub(params["DELEGATED_ADMIN_ACCOUNT_ID"], params["CONFIGURATION_ROLE_NAME"], regions)
@@ -156,21 +152,24 @@ def process_add_update_event(params: dict) -> str:
156152
return "DISABLE_COMPLETE"
157153
else:
158154
LOGGER.info("...Enable or Update Security Hub")
159-
common.create_service_linked_role(
160-
"AWSServiceRoleForSecurityHub",
161-
"securityhub.amazonaws.com",
162-
"A service-linked role required for AWS Security Hub to access your resources.",
163-
)
164-
securityhub.enable_securityhub(
165-
params["DELEGATED_ADMIN_ACCOUNT_ID"],
166-
params["CONFIGURATION_ROLE_NAME"],
155+
# Configure Security Hub Delegated Admin and Organizations
156+
securityhub.configure_delegated_admin_securityhub(
167157
accounts,
168158
regions,
169-
get_standards_dictionary(params),
170-
params["AWS_PARTITION"],
159+
params["DELEGATED_ADMIN_ACCOUNT_ID"],
160+
params["CONFIGURATION_ROLE_NAME"],
171161
params["REGION_LINKING_MODE"],
172162
params["HOME_REGION"],
173163
)
164+
# Configure Security Hub in the Delegated Admin Account
165+
securityhub.enable_account_securityhub(
166+
params["DELEGATED_ADMIN_ACCOUNT_ID"],
167+
regions,
168+
params["CONFIGURATION_ROLE_NAME"],
169+
params["AWS_PARTITION"],
170+
get_standards_dictionary(params),
171+
)
172+
174173
account_ids = common.get_account_ids(accounts)
175174
if params["action"] == "Add":
176175
LOGGER.info(f"Waiting {SLEEP_SECONDS} seconds before configuring member accounts.")
@@ -193,9 +192,9 @@ def get_standards_dictionary(params: dict) -> dict:
193192
"CISVersion": params["CIS_VERSION"],
194193
"PCIVersion": params["PCI_VERSION"],
195194
"StandardsToEnable": {
196-
"cis": (params.get("ENABLE_CIS_STANDARD", "false")).lower() in "true",
197-
"pci": (params.get("ENABLE_PCI_STANDARD", "false")).lower() in "true",
198-
"sbp": (params.get("ENABLE_SECURITY_BEST_PRACTICES_STANDARD", "false")).lower() in "true",
195+
"cis": params["ENABLE_CIS_STANDARD"] == "true",
196+
"pci": params["ENABLE_PCI_STANDARD"] == "true",
197+
"sbp": params["ENABLE_SECURITY_BEST_PRACTICES_STANDARD"] == "true",
199198
},
200199
}
201200

@@ -219,8 +218,10 @@ def create_sns_messages(account_ids: list, regions: list, configuration_role: st
219218
"Action": action,
220219
}
221220
LOGGER.info(f"Publishing SNS message for {action} in {account_id}.")
222-
LOGGER.info(f"{json.dumps(sns_message)}")
223-
management_sns_client.publish(TopicArn=sns_topic_arn, Message=json.dumps(sns_message))
221+
LOGGER.info({"SNSMessage": sns_message})
222+
response = management_sns_client.publish(TopicArn=sns_topic_arn, Message=json.dumps(sns_message))
223+
api_call_details = {"Account": "management", "API_Call": "sns:Publish", "API_Response": response}
224+
LOGGER.info(api_call_details)
224225

225226

226227
def process_sns_records(records: list) -> None:
@@ -232,14 +233,12 @@ def process_sns_records(records: list) -> None:
232233
params = get_configuration_ssm_parameters()
233234

234235
for record in records:
235-
sns_info = record["Sns"]
236-
LOGGER.info(f"SNS INFO: {sns_info}")
237-
message = json.loads(sns_info["Message"])
236+
LOGGER.info(record["Sns"])
237+
message = json.loads(record["Sns"]["Message"])
238238

239239
if message["Action"] == "configure":
240-
LOGGER.info("Configuring SecurityHub")
241-
securityhub.configure_member_account(
242-
message["AccountId"], params["CONFIGURATION_ROLE_NAME"], message["Regions"], get_standards_dictionary(params), params["AWS_PARTITION"]
240+
securityhub.enable_account_securityhub(
241+
message["AccountId"], message["Regions"], params["CONFIGURATION_ROLE_NAME"], params["AWS_PARTITION"], get_standards_dictionary(params)
243242
)
244243
elif message["Action"] == "disable":
245244
LOGGER.info("Disabling SecurityHub")
@@ -256,9 +255,9 @@ def process_lifecycle_event(event: Dict[str, Any]) -> str:
256255
string with account ID
257256
"""
258257
params = get_configuration_ssm_parameters()
259-
LOGGER.info(f"Parameters: {params}")
258+
LOGGER.info({"Parameters": params})
260259

261-
regions = common.get_enabled_regions(params.get("ENABLED_REGIONS", ""), (params.get("CONTROL_TOWER_REGIONS_ONLY", "false")).lower() in "true")
260+
regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"] == "true")
262261
account_id = event["detail"]["serviceEventDetails"]["createManagedAccountStatus"]["account"]["accountId"]
263262

264263
LOGGER.info(f"Configuring SecurityHub in {account_id}")
@@ -284,7 +283,7 @@ def parameter_pattern_validator(parameter_name: str, parameter_value: str, patte
284283
raise ValueError(f"'{parameter_name}' parameter with value of '{parameter_value}' does not follow the allowed pattern: {pattern}.")
285284

286285

287-
def get_validated_parameters(event: CloudFormationCustomResourceEvent) -> dict: # noqa: CCR001 (cognitive complexity)
286+
def get_validated_parameters(event: CloudFormationCustomResourceEvent) -> dict:
288287
"""Validate AWS CloudFormation parameters.
289288
290289
Args:
@@ -296,28 +295,30 @@ def get_validated_parameters(event: CloudFormationCustomResourceEvent) -> dict:
296295
params = event["ResourceProperties"].copy()
297296
actions = {"Create": "Add", "Update": "Update", "Delete": "Remove"}
298297
params["action"] = actions[event["RequestType"]]
298+
true_false_pattern = r"^true|false$"
299+
version_pattern = r"^[0-9.]+$"
299300

300301
parameter_pattern_validator("AWS_PARTITION", params.get("AWS_PARTITION", ""), pattern=r"^(aws[a-zA-Z-]*)?$")
302+
parameter_pattern_validator("CIS_VERSION", params.get("CIS_VERSION", ""), pattern=version_pattern)
301303
parameter_pattern_validator("CONFIGURATION_ROLE_NAME", params.get("CONFIGURATION_ROLE_NAME", ""), pattern=r"^[\w+=,.@-]{1,64}$")
302-
parameter_pattern_validator("CONTROL_TOWER_REGIONS_ONLY", params.get("CONTROL_TOWER_REGIONS_ONLY", ""), pattern=r"^true|false$")
304+
parameter_pattern_validator("CONTROL_TOWER_REGIONS_ONLY", params.get("CONTROL_TOWER_REGIONS_ONLY", ""), pattern=true_false_pattern)
303305
parameter_pattern_validator("DELEGATED_ADMIN_ACCOUNT_ID", params.get("DELEGATED_ADMIN_ACCOUNT_ID", ""), pattern=r"^\d{12}$")
304-
parameter_pattern_validator("DISABLE_SECURITY_HUB", params.get("DISABLE_SECURITY_HUB", ""), pattern=r"^true|false$")
305-
parameter_pattern_validator("ENABLE_CIS_STANDARD", params.get("ENABLE_CIS_STANDARD", ""), pattern=r"^true|false$")
306-
parameter_pattern_validator("ENABLE_PCI_STANDARD", params.get("ENABLE_PCI_STANDARD", ""), pattern=r"^true|false$")
306+
parameter_pattern_validator("DISABLE_SECURITY_HUB", params.get("DISABLE_SECURITY_HUB", ""), pattern=true_false_pattern)
307+
parameter_pattern_validator("ENABLE_CIS_STANDARD", params.get("ENABLE_CIS_STANDARD", ""), pattern=true_false_pattern)
308+
parameter_pattern_validator("ENABLE_PCI_STANDARD", params.get("ENABLE_PCI_STANDARD", ""), pattern=true_false_pattern)
307309
parameter_pattern_validator(
308-
"ENABLE_SECURITY_BEST_PRACTICES_STANDARD", params.get("ENABLE_SECURITY_BEST_PRACTICES_STANDARD", ""), pattern=r"^true|false$"
310+
"ENABLE_SECURITY_BEST_PRACTICES_STANDARD", params.get("ENABLE_SECURITY_BEST_PRACTICES_STANDARD", ""), pattern=true_false_pattern
309311
)
310312
parameter_pattern_validator("ENABLED_REGIONS", params.get("ENABLED_REGIONS", ""), pattern=r"^$|[a-z0-9-, ]+$")
311313
parameter_pattern_validator("HOME_REGION", params.get("HOME_REGION", ""), pattern=r"^(?!(.*--))(?!(.*-$))[a-z0-9]([a-z0-9-]){0,62}$")
314+
parameter_pattern_validator("PCI_VERSION", params.get("PCI_VERSION", ""), pattern=version_pattern)
315+
parameter_pattern_validator("REGION_LINKING_MODE", params.get("REGION_LINKING_MODE", ""), pattern=r"^ALL_REGIONS|SPECIFIED_REGIONS$")
312316
parameter_pattern_validator(
313317
"SNS_TOPIC_ARN",
314318
params.get("SNS_TOPIC_ARN", ""),
315319
pattern=r"^arn:(aws[a-zA-Z-]*){1}:sns:[a-z0-9-]+:\d{12}:[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$",
316320
)
317-
parameter_pattern_validator("CIS_VERSION", params.get("CIS_VERSION", ""), pattern=r"^[0-9.]+$")
318-
parameter_pattern_validator("PCI_VERSION", params.get("PCI_VERSION", ""), pattern=r"^[0-9.]+$")
319-
parameter_pattern_validator("REGION_LINKING_MODE", params.get("REGION_LINKING_MODE", ""), pattern=r"^ALL_REGIONS|SPECIFIED_REGIONS$")
320-
parameter_pattern_validator("SECURITY_BEST_PRACTICES_VERSION", params.get("SECURITY_BEST_PRACTICES_VERSION", ""), pattern=r"^[0-9.]+$")
321+
parameter_pattern_validator("SECURITY_BEST_PRACTICES_VERSION", params.get("SECURITY_BEST_PRACTICES_VERSION", ""), pattern=version_pattern)
321322

322323
return params
323324

@@ -334,7 +335,7 @@ def deregister_delegated_administrator(delegated_admin_account_id: str, service_
334335

335336
ORG_CLIENT.deregister_delegated_administrator(AccountId=delegated_admin_account_id, ServicePrincipal=service_principal)
336337
except ORG_CLIENT.exceptions.AccountNotRegisteredException as error:
337-
LOGGER.debug(f"Account is not a registered delegated administrator: {error}")
338+
LOGGER.info(f"Account ({delegated_admin_account_id}) is not a registered delegated administrator: {error}")
338339

339340

340341
def get_ssm_parameter_value(ssm_client: SSMClient, name: str) -> str:
@@ -347,7 +348,10 @@ def get_ssm_parameter_value(ssm_client: SSMClient, name: str) -> str:
347348
Returns:
348349
Value string
349350
"""
350-
return ssm_client.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]
351+
response = ssm_client.get_parameter(Name=name, WithDecryption=True)
352+
api_call_details = {"API_Call": "ssm:GetParameter", "API_Response": response}
353+
LOGGER.info(api_call_details)
354+
return response["Parameter"]["Value"]
351355

352356

353357
def put_ssm_parameter(ssm_client: SSMClient, name: str, description: str, value: str) -> None:
@@ -359,15 +363,19 @@ def put_ssm_parameter(ssm_client: SSMClient, name: str, description: str, value:
359363
description: Parameter description
360364
value: Parameter value
361365
"""
362-
ssm_client.put_parameter(
363-
Name=name,
364-
Description=description,
365-
Value=value,
366-
Type="SecureString",
367-
Overwrite=True,
368-
Tier="Standard",
369-
DataType="text",
366+
response = (
367+
ssm_client.put_parameter(
368+
Name=name,
369+
Description=description,
370+
Value=value,
371+
Type="SecureString",
372+
Overwrite=True,
373+
Tier="Standard",
374+
DataType="text",
375+
),
370376
)
377+
api_call_details = {"API_Call": "ssm:PutParameter", "API_Response": response}
378+
LOGGER.info(api_call_details)
371379

372380

373381
def delete_ssm_parameter(ssm_client: SSMClient, name: str) -> None:
@@ -377,7 +385,9 @@ def delete_ssm_parameter(ssm_client: SSMClient, name: str) -> None:
377385
ssm_client: SSM Boto3 Client
378386
name: Parameter Name
379387
"""
380-
ssm_client.delete_parameter(Name=name)
388+
response = ssm_client.delete_parameter(Name=name)
389+
api_call_details = {"API_Call": "ssm:DeleteParameter", "API_Response": response}
390+
LOGGER.info(api_call_details)
381391

382392

383393
def set_configuration_ssm_parameter(params: dict) -> None:

0 commit comments

Comments
 (0)