Skip to content

Instantly share code, notes, and snippets.

@adamcousins
Last active May 4, 2021 07:39
Show Gist options
  • Save adamcousins/59bafe50b20e2cb24b453bdfe3919ef4 to your computer and use it in GitHub Desktop.
Save adamcousins/59bafe50b20e2cb24b453bdfe3919ef4 to your computer and use it in GitHub Desktop.
AWSTemplateFormatVersion: '2010-09-09'
Description: "Security: Enable AWS SecurityHub in a centralised Account within an AWS Organisation."
Parameters:
AlertsTopicArn:
Description: Alert topic ARN
Type: String
LogRetention:
Type: String
Description: Log retention in number of days
Default: '7'
Conditions:
HasAlertsTopicArn: !Not [!Equals [!Ref AlertsTopicArn, '']]
Resources:
### Cloudformation Custom Resource ###
### AWS Security Hub - Centralised service with auto-enrol enabled ###
### Required Properties:
### ServiceToken: [Type: String]
### AdminAccountId: [Type: String]
### Attributes (Output):
### None
### Notes:
### This resource will enable auto-enrolment in an AWS Organisation
### This resource will also associate all pending AWS accounts in an AWS Organisation
### Example Usage ###
# EnableSecurityHubAutoEnrolment:
# Type: Custom::EnableSecurityHubAutoEnrolment
# Properties:
# ServiceToken: !GetAtt Function.Arn
# AdminAccountId: !Ref AWS::AccountId
### End Cloudformation Custom Resource ###
AlertsSnsTopicPolicy:
Condition: HasAlertsTopicArn
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Id: SnsTopicPolicy
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- securityhub.amazonaws.com
- events.amazonaws.com
Action: sns:Publish
Resource: !Ref AlertsTopicArn
Condition:
StringEquals:
AWS:SourceAccount: !Ref AWS::AccountId
Topics:
- !Ref AlertsTopicArn
SecurityHubEvent:
Condition: HasAlertsTopicArn
Type: AWS::Events::Rule
Properties:
Description: "AWS SecurityHub findings Event Rule"
EventPattern:
source:
- "aws.securityhub"
detail-type:
- "SecurityHub Finding"
State: "ENABLED"
Targets:
- Arn: !Ref AlertsTopicArn
Id: SecurityHubEvent
InputTransformer:
InputTemplate: |
"AWS SecurityHub from account <account> has discovered a <detail-type> at <at> in region <region>."
"Finding: <finding>"
"Reason: <title>."
"Description: <description>."
"More information can be found here : https://<region>.console.aws.amazon.com/securityhub/home?region=<region>#/findings"
InputPathsMap:
detail: "$.detail"
detail-type: "$.detail-type"
at: "$.time"
account: "$.detail.accountId"
region: "$.region"
finding: "$.detail.type"
title: "$.detail.title"
description: "$.detail.description"
id: "$.detail.id"
FunctionLogGroup:
Type: "AWS::Logs::LogGroup"
DeletionPolicy: Retain
Properties:
RetentionInDays: !Ref LogRetention
LogGroupName: !Sub "/aws/lambda/${Function}"
Function:
Type: AWS::Lambda::Function
Properties:
Description: Adds or Removes all accounts in the AWS Organisation to Enable AWS SecurityHub in the centralied Admin Account.
Handler: index.lambda_handler
Code:
ZipFile: |
import logging, boto3, os
from botocore.exceptions import ClientError
import cfnresponse
log = logging.getLogger()
log.setLevel(logging.INFO)
client = boto3.client('securityhub')
org_client = boto3.client('organizations')
region = os.environ['AWS_REGION']
def list_hub(admin_account_id):
log.info('finding AWS SecurityHub Hub Arn in AWS Account: ' + admin_account_id)
hub_arn = None
hub = client.describe_hub(HubArn="arn:aws:securityhub:" + region + ":" + admin_account_id + ":hub/default")['HubArn']
log.info('found ' + hub)
if hub:
hub_arn = hub
else:
log.info('can not find AWS SecurityHub Arn in AWS Account: ' + admin_account_id)
return hub_arn
def list_accounts(excluded_account_id, action):
paginator = org_client.get_paginator('list_accounts')
pages = paginator.paginate()
organization_accounts = []
for page in pages:
for obj in page['Accounts']:
current_account_id = (obj["Id"])
if excluded_account_id in current_account_id:
continue
accounts_map = {}
if action == 'Create':
accounts_map = {'AccountId': (obj["Id"]), 'Email': (obj["Email"])}
elif action == 'Delete':
accounts_map = (obj["Id"])
organization_accounts.append(accounts_map)
return organization_accounts
def lambda_handler(event, context):
log.info('REQUEST RECEIVED:\n %s', event)
admin_account_id = event['ResourceProperties']['AdminAccountId']
return_msg = {}
return_status = "FAILED"
hub_arn = list_hub(admin_account_id)
try:
if(event['RequestType'] == 'Create'):
organization_accounts = list_accounts(admin_account_id, event['RequestType'])
if hub_arn:
log.info('updating master account id: ' + admin_account_id + ' with hub arn ' + hub_arn + ' to auto enrol new aws accounts')
client.update_organization_configuration(AutoEnable=True)
log.info('enrolling all accounts currently available in the organization')
response = client.create_members(
AccountDetails=organization_accounts)
print(str(response))
return_msg = {"Message": "master account id: " + admin_account_id + " with hub arn " + hub_arn + " now will auto enrol new aws accounts"}
return_status = "SUCCESS"
else:
log.info('master account id: ' + admin_account_id + ' could not find any hubs')
return_msg = {"Message": "master account id: " + admin_account_id + " could not find any hubs"}
elif(event['RequestType'] == 'Update'):
log.info("Update account not supported")
return_msg = {"Message": "Update account not supported"}
return_status = "SUCCESS"
else:
organization_accounts = list_accounts(admin_account_id, event['RequestType'])
if hub_arn:
log.info('updating master account id: ' + admin_account_id + ' with hub arn ' + hub_arn + ' to no longer auto enrol new aws accounts')
client.update_organization_configuration(AutoEnable=False)
log.info('removing all accounts currently available in the organization')
response = client.disassociate_members(AccountIds=organization_accounts)
print(str(response))
return_msg = {"Message": "master account id: " + admin_account_id + " with hub arn " + hub_arn + " will no longer auto enrol new aws accounts"}
return_status = "SUCCESS"
else:
log.info('master account id: ' + admin_account_id + ' could not find any hubs')
return_msg = {"Message": "master account id: " + admin_account_id + " could not find any hubs"}
return_status = "SUCCESS"
except ClientError as e:
log.info(e.response['Error']['Message'])
return_msg = {"Message": "Exception Found: " + e.response['Error']['Message']}
cfnresponse.send(event, context, return_status, return_msg)
Runtime: python3.6
Role: !GetAtt FunctionRole.Arn
Timeout: 120
FunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Resource: "*"
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Resource: '*'
Effect: Allow
Action:
- organizations:ListAccounts
- securityhub:UpdateOrganizationConfiguration
- securityhub:DescribeHub
- securityhub:CreateMembers
- securityhub:ListMembers
- securityhub:DeleteMembers
- securityhub:DisassociateMembers
## Usage ###
EnableSecurityHubAutoEnrolment:
DependsOn: FunctionLogGroup
Type: Custom::EnableSecurityHubAutoEnrolment
Properties:
ServiceToken: !GetAtt Function.Arn
AdminAccountId: !Ref AWS::AccountId
Outputs:
FunctionArn:
Description: The Lambda Function Arn
Value: !GetAtt Function.Arn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment