in src/securityhub_enabler.py [0:0]
def lambda_handler(event, context):
LOGGER.info(f"REQUEST RECEIVED: {json.dumps(event, default=str)}")
partition = context.invoked_function_arn.split(":")[1]
admin_account_id = os.environ['sh_admin_account']
admin_session = assume_role(admin_account_id, os.environ['assume_role'])
# Regions to Deploy
if os.environ['region_filter'] == 'SecurityHub':
securityhub_regions = get_enabled_regions(
session, session.get_available_regions('securityhub'))
else:
securityhub_regions = get_ct_regions(session)
# Check for Custom Resource Call
if 'RequestType' in event and (
event['RequestType'] == "Delete" or
event['RequestType'] == "Create" or
event['RequestType'] == "Update"):
action = event['RequestType']
if action == "Create":
enable_admin(admin_session, securityhub_regions, partition)
if action == "Delete":
disable_admin(
admin_session,
os.environ['assume_role'],
securityhub_regions,
partition)
LOGGER.info(f"Sending Custom Resource Response")
response_data = {}
send(event, context, "SUCCESS", response_data)
if action == "Delete":
# Exit on delete so it doesn't re-enable existing accounts
raise SystemExit()
else:
action = 'Create'
LOGGER.info(f"Enabling SecurityHub in Regions: {securityhub_regions}")
aws_account_dict = dict()
# Checks if Function was called by SNS
if 'Records' in event:
message = event['Records'][0]['Sns']['Message']
json_message = json.loads(message)
LOGGER.info(f"SNS message: {json.dumps(json_message, default=str)}")
accountid = json_message['AccountId']
email = json_message['Email']
aws_account_dict.update({accountid: email})
action = json_message['Action']
# Checks if function triggered by Control Tower Lifecycle Event,
# testing in multiple steps to ensure invalid values
# short-circuit it instead of failing
elif ('detail' in event) and (
'eventName' in event['detail']) and (
event['detail']['eventName'] == 'CreateManagedAccount'):
service_detail = event['detail']['serviceEventDetails']
status = service_detail['createManagedAccountStatus']
LOGGER.info(f"Control Tower Event: CreateManagedAccount {status}")
accountid = status['account']['accountId']
email = session.client('organizations').describe_account(
AccountId=accountid)['Account']['Email']
aws_account_dict.update({accountid: email})
else:
# Not called by SNS or CloudFormation event, iterates through list of
# accounts and recursively calls the function itself via SNS. SNS is
# used to fan out the requests to avoid function timeout if too many
# accounts
aws_account_dict = get_account_list()
sns_client = session.client('sns', region_name=os.environ['AWS_REGION'])
for accountid, email in aws_account_dict.items():
sns_message = {
'AccountId': accountid,
'Email': email,
'Action': action
}
LOGGER.info(f"Publishing to configure Account {accountid}")
sns_client.publish(
TopicArn=os.environ['topic'], Message=json.dumps(sns_message))
return
# Ensure the Security Hub Admin is still enabled
enable_admin(admin_session, securityhub_regions, partition)
# Processing Accounts
LOGGER.info(f"Processing: {json.dumps(aws_account_dict)}")
for account in aws_account_dict.keys():
email_address = aws_account_dict[account]
if account == admin_account_id:
LOGGER.info(f"Account {account} cannot become a member of itself")
continue
LOGGER.debug(f"Working on SecurityHub on Account {account} in \
regions %{securityhub_regions}")
failed_invitations = []
member_session = assume_role(account, os.environ['assume_role'])
# Process Regions
for aws_region in securityhub_regions:
sh_member_client = member_session.client(
'securityhub',
endpoint_url=f"https://securityhub.{aws_region}.amazonaws.com",
region_name=aws_region
)
sh_admin_client = admin_session.client(
'securityhub',
endpoint_url=f"https://securityhub.{aws_region}.amazonaws.com",
region_name=aws_region
)
admin_members = get_admin_members(admin_session, aws_region)
LOGGER.info(f"Beginning {aws_region} in Account {account}")
if account in admin_members:
if admin_members[account] == 'Associated':
LOGGER.info(f"Account {account} is already associated "
f"with Admin Account {admin_account_id} in "
f"{aws_region}")
if action == 'Delete':
try:
sh_admin_client.disassociate_members(
AccountIds=[account])
except Exception as e:
continue
try:
sh_admin_client.delete_members(
AccountIds=[account])
except Exception as e:
continue
else:
LOGGER.warning(f"Account {account} exists, but not "
f"associated to Admin Account "
f"{admin_account_id} in {aws_region}")
LOGGER.info(f"Disassociating Account {account} from "
f"Admin Account {admin_account_id} in "
f"{aws_region}")
try:
sh_admin_client.disassociate_members(
AccountIds=[account])
except Exception as e:
continue
try:
sh_admin_client.delete_members(
AccountIds=[account])
except Exception as e:
continue
try:
sh_member_client.get_findings()
except Exception as e:
LOGGER.debug(str(e))
LOGGER.info(f"SecurityHub not currently Enabled on Account "
f"{account} in {aws_region}")
if action != 'Delete':
LOGGER.info(f"Enabled SecurityHub on Account {account} "
f"in {aws_region}")
sh_member_client.enable_security_hub()
else:
# Security Hub already enabled
if action != 'Delete':
LOGGER.info(f"SecurityHub already Enabled in Account "
f"{account} in {aws_region}")
else:
LOGGER.info(f"Disabled SecurityHub in Account "
f"{account} in {aws_region}")
try:
sh_member_client.disable_security_hub()
except Exception as e:
continue
if action != 'Delete':
process_security_standards(sh_member_client, partition,
aws_region, account)
LOGGER.info(f"Creating member for Account {account} and "
f"Email, {email_address} in {aws_region}")
member_response = sh_admin_client.create_members(
AccountDetails=[{
'AccountId': account,
'Email': email_address
}])
if len(member_response['UnprocessedAccounts']) > 0:
LOGGER.warning(f"Could not create member Account "
f"{account} in {aws_region}")
failed_invitations.append({
'AccountId': account, 'Region': aws_region
})
continue
LOGGER.info(f"Inviting Account {account} in {aws_region}")
sh_admin_client.invite_members(AccountIds=[account])
# go through each invitation (hopefully only 1)
# and pull the one matching the Security Admin Account ID
try:
paginator = sh_member_client.get_paginator(
'list_invitations')
invitation_iterator = paginator.paginate()
for invitation in invitation_iterator:
admin_invitation = next(
item for item in invitation['Invitations'] if
item["AccountId"] == admin_account_id)
LOGGER.info(f"Accepting invitation on Account {account} "
f"from Admin Account {admin_account_id} in "
f"{aws_region}")
sh_member_client.accept_administrator_invitation(
AdministratorId=admin_account_id,
InvitationId=admin_invitation['InvitationId'])
except Exception as e:
LOGGER.warning(f"Account {account} could not accept "
f"invitation from Admin Account "
f"{admin_account_id} in {aws_region}")
LOGGER.warning(e)
if len(failed_invitations) > 0:
failed_accounts = json.dumps(failed_invitations,
sort_keys=True, default=str)
LOGGER.warning(f"Error Processing the following Accounts: "
f"{failed_accounts}")