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
2222from crhelper import CfnResource
2323
2424if 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
4041SERVICE_NAME = "securityhub.amazonaws.com"
4142SLEEP_SECONDS = 60
4243PRE_DISABLE_SLEEP_SECONDS = 30
43- SSM_PARAMETER_PREFIX = os . environ . get ( "SSM_PARAMETER_PREFIX" , " /sra/securityhub-org")
44+ SSM_PARAMETER_PREFIX = " /sra/securityhub-org"
4445PARAMETER_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
135131def 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
226227def 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
340341def 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
353357def 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
373381def 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
383393def set_configuration_ssm_parameter (params : dict ) -> None :
0 commit comments