-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathhandle_event.py
More file actions
executable file
·243 lines (212 loc) · 11.9 KB
/
handle_event.py
File metadata and controls
executable file
·243 lines (212 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import re
import os
import boto3
import importlib
import json
import sys
import traceback
from botocore.exceptions import ClientError
MININAL_TAG_LENGTH = 2
MININAL_ACTION_LENGTH = 1
permissions_link = 'https://github.com/dome9/cloud-bots/blob/master/template.yml'
relaunch_stack = 'https://github.com/dome9/cloud-bots#update-cloudbots'
account_mode = os.getenv('ACCOUNT_MODE', '')
cross_account_role_name = os.getenv('CROSS_ACCOUNT_ROLE_NAME', '')
def get_aws_region_code(region):
ec2 = boto3.client('ec2')
ssm_client = boto3.client('ssm', region_name='us-east-1')
regions = ec2.describe_regions()
region_names = [region['RegionName'] for region in regions['Regions']]
if region in region_names:
return region
else:
for region_id in region_names:
ssm_name = '/aws/service/global-infrastructure/regions/%s/longName' % region_id
ssm_response = ssm_client.get_parameter(Name=ssm_name)
if region.lower() in ssm_response['Parameter']['Value'].lower():
return region_id
if region.lower() == 'global':
return 'us-east-1'
raise Exception('not valid region:'+region)
def get_data_from_message(message):
data = {}
if 'rule' in message:
data['rule_name'] = message['rule'].get('name')
if 'complianceTags' in message['rule']:
# All of the remediation values are coming in on the compliance tags and they're pipe delimited
data['compliance_tags'] = message['rule']['complianceTags'].split('|')
if 'status' in message:
data['status'] = message['status']
entity = message.get('entity')
if entity:
data['entity_id'] = entity.get('id')
data['entity_name'] = entity.get('name')
data['region'] = entity.get('region')
# Some events come through with 'null' as the region. If so, default to us-east-1
if not data.get('region'):
data['region'] = 'us-east-1'
else:
data['region'] = data['region'].replace('_', '-')
if 'remediationActions' in message:
data['remediationActions'] = message['remediationActions']
data['region_code'] = get_aws_region_code(data['region'])
return data
def get_bots_from_finding(compliance_tags, remediation_actions):
bots = []
policy = None
# Check if any of the tags have AUTO: in them. If there's nothing to do at all, skip it.
if compliance_tags is not None:
auto_pattern = re.compile('AUTO:')
for tag in compliance_tags:
tag = tag.strip() # Sometimes the tags come through with trailing or leading spaces.
# Check the tag to see if we have AUTO: in it
if auto_pattern.match(tag):
tag_pattern = tuple(tag.split(' '))
# The format is AUTO: bot_name param1 param2
if len(tag_pattern) < MININAL_TAG_LENGTH:
continue
tag, bot, *params = tag_pattern
bots.append([bot, params])
if remediation_actions is not None:
for action in remediation_actions:
try:
remAction = json.loads(action)
if 'SuggestedRole' in remAction:
policy = 'SuggestedPolicy:%s' % remAction['SuggestedRole']
except ValueError as e:
action_pattern = tuple(action.split(' '))
# The format is bot_name param1 param2
if len(action_pattern) < MININAL_ACTION_LENGTH:
continue
bot, *params = action_pattern
if policy:
params.append(policy)
bots.append((bot, params))
return bots
def handle_event(message, output_message):
post_to_sns = True
message_data = get_data_from_message(message)
role_arn = None
# evaluate the event and tags and decide is there's something to do with them.
if message_data.get('status') == 'Passed':
print(f'''{__file__} - Rule: {message_data.get('rule_name')} passed''')
return False
compliance_tags = message_data.get('compliance_tags')
remediation_actions = message_data.get('remediationActions')
output_message['Rules violations found'] = []
bots = get_bots_from_finding(compliance_tags, remediation_actions)
if not bots or not len(bots):
print(f'''{__file__} - Rule: {message_data.get('rule_name')} Doesnt have any bots to run. Skipping.''')
return False
for bot_to_run in bots:
bot_msg = ''
bot_data = {}
bot_data['Rule'] = message_data.get('rule_name')
bot_data['ID'] = message_data.get('entity_id')
bot_data['Name'] = message_data.get('entity_name')
bot, params = bot_to_run
bot_data['Remediation'] = bot
print(f'''{__file__} - Bot name to execute: {bot}''')
try:
bot_module = importlib.import_module(''.join(['bots.', bot]), package=None)
except Exception as e:
exception_type, exception_object, exception_traceback = sys.exc_info()
bot_msg = f'{__file__} - Error in function {bot}. function didnt execute. Error: {e}. For more details please see the CloudWatch logs. \n'
bot_data['Execution status'] = 'failed'
print(
f'{__file__} Details: {" ".join(traceback.format_exception(exception_type, exception_object, exception_traceback))} \n')
bot_data['Bot message'] = bot_msg
output_message['Rules violations found'].append(bot_data.copy())
continue
# Get the session info here. No point in waisting cycles running it up top if we aren't going to run an bot anyways:
try: # get the accountID
sts = boto3.client('sts')
lambda_account_id = sts.get_caller_identity()['Account']
except ClientError as e:
print(f'{__file__} Unexpected STS error - {e}')
# return False
event_account_id = output_message['Account id']
# Account mode will be set in the lambda variables. We'll default to single mode
if lambda_account_id != event_account_id: # The remediation needs to be done outside of this account
if account_mode == 'multi': # multi or single account mode?
# If it's not the same account, try to assume role to the new one
role_arn = ''.join(['arn:aws:iam::', event_account_id, ':role/'])
# This allows users to set their own role name if they have a different naming convention they have to follow
role_arn = ''.join([role_arn, cross_account_role_name]) if cross_account_role_name else ''.join(
[role_arn, 'Dome9CloudBots'])
bot_data[
'Compliance failure was found for an account outside of the one the function is running in. Trying to assume_role to target account'] = event_account_id
try:
credentials_for_event = globals()['all_session_credentials'][event_account_id]
except (NameError, KeyError):
# If we can't find the credentials, try to generate new ones
global all_session_credentials
all_session_credentials = {}
# create an STS client object that represents a live connection to the STS service
sts_client = boto3.client('sts')
# Call the assume_role method of the STSConnection object and pass the role ARN and a role session name.
try:
assumedRoleObject = sts_client.assume_role(
RoleArn=role_arn,
RoleSessionName='CloudBotsAutoRemedation'
)
# From the response that contains the assumed role, get the temporary credentials that can be used to make subsequent API calls
all_session_credentials[event_account_id] = assumedRoleObject['Credentials']
credentials_for_event = all_session_credentials[event_account_id]
except ClientError as e:
error = e.response['Error']['Code']
bot_data['Execution status'] = 'failed'
print(f'{__file__} - Error - {e}')
if error == 'AccessDenied':
bot_data[
'Access Denied'] = 'Tried and failed to assume a role in the target account. Please verify that the cross account role is createad.'
else:
bot_data['Unexpected error'] = e
continue
boto_session = boto3.Session(
region_name=message_data.get('region_code'),
aws_access_key_id=credentials_for_event['AccessKeyId'],
aws_secret_access_key=credentials_for_event['SecretAccessKey'],
aws_session_token=credentials_for_event['SessionToken']
)
else:
# In single account mode, we don't want to try to run bots outside of this account therefore error , the lambda will exit with error
bot_data[
'Error'] = f'This finding was found in account id {event_account_id}. The Lambda function is running in account id: {lambda_account_id}. Remediations need to be ran from the account there is the issue in.'
output_message['Rules violations found'].append(bot_data.copy())
continue
else: # Boto will default to default session if we don't need assume_role credentials
boto_session = boto3.Session(region_name=message_data.get('region_code'))
try: ## Run the bot
# Find and add Log.ic event time to the entity
try:
message['entity']['eventTime'] = next(json.loads(element['value'])['alertWindowStartTime']
for element in message['additionalFields']
if element.get('name') == 'logic_data' and
'alertWindowStartTime' in element.get('value'))
except:
print(f'{__file__} - Warning - Adding Log.ic event time to entity failed')
# Add CloudAccount ID to entity argument
entity = message['entity']
entity['cloud_account_id'] = output_message['Account id']
# Add executer arn, add assumed role arn to params in case they will be used by the bot.
if 'function_arn' in message:
params.append('exec_function_arn=%s' % message['function_arn'])
if role_arn is not None:
params.append('assumed_role_arn=%s' % role_arn)
bot_msg = bot_module.run_action(boto_session, message['rule'], entity, params)
bot_data['Execution status'] = 'passed'
except ClientError as e:
bot_msg = f"Unexpected client error: {e} \n"
if 'AccessDenied' in e.response['Error']['Code']:
bot_msg += f"Make sure your dome9CloudBots-RemediationFunctionRole is updated with the relevant permissions. The permissions can be found here: {permissions_link}. You can update them manually or relaunch the CFT stack as described here: {relaunch_stack} \n"
except Exception as e:
exception_type, exception_object, exception_traceback = sys.exc_info()
bot_msg = f'Error while executing function {bot}. Error: {e} \nFor more details please see the CloudWatch logs. \n'
bot_data['Execution status'] = 'failed'
print(
f'{__file__} Details: {" ".join(traceback.format_exception(exception_type, exception_object, exception_traceback))} \n')
bot_data['Bot message'] = bot_msg
output_message['Rules violations found'].append(bot_data.copy())
# After the remediation functions finish, send the notification out.
return post_to_sns