forked from awslabs/aws-securityhub-multiaccount-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
disablesecurityhub.py
executable file
·256 lines (209 loc) · 11.7 KB
/
disablesecurityhub.py
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
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env python
"""
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import boto3
import re
import argparse
import time
import utils
from collections import OrderedDict
from botocore.exceptions import ClientError
def get_master_members(sh_client, aws_region):
"""
Returns a list of current members of the SecurityHub master account
:param aws_region: AWS Region of the SecurityHub master account
:param detector_id: DetectorId of the SecurityHub master account in the AWS Region
:return: dict of AwsAccountId:RelationshipStatus
"""
member_dict = dict()
results = sh_client.list_members(
OnlyAssociated=False
)
for member in results['Members']:
member_dict.update({member['AccountId']: member['MemberStatus']})
while results.get("NextToken"):
results = sh_client.list_members(
OnlyAssociated=False,
NextToken=results['NextToken']
)
for member in results['Members']:
member_dict.update({member['AccountId']: member['MemberStatus']})
return member_dict
def assume_role(aws_account_number, role_name):
"""
Assumes the provided role in each account and returns a SecurityHub client
:param aws_account_number: AWS Account Number
:param role_name: Role to assume in target account
:param aws_region: AWS Region for the Client call, not required for IAM calls
:return: SecurityHub client in the specified AWS Account and Region
"""
# Beginning the assume role process for account
sts_client = boto3.client('sts')
# Get the current partition
partition = sts_client.get_caller_identity()['Arn'].split(":")[1]
response = sts_client.assume_role(
RoleArn='arn:{}:iam::{}:role/{}'.format(
partition,
aws_account_number,
role_name
),
RoleSessionName='EnableSecurityHub'
)
# Storing STS credentials
session = boto3.Session(
aws_access_key_id=response['Credentials']['AccessKeyId'],
aws_secret_access_key=response['Credentials']['SecretAccessKey'],
aws_session_token=response['Credentials']['SessionToken']
)
print("Assumed session for {}.".format(
aws_account_number
))
return session
if __name__ == '__main__':
# Setup command line arguments
parser = argparse.ArgumentParser(description='Disable and unlink AWS Accounts from central SecurityHub Account')
parser.add_argument('--master_account', type=str, required=True, help="AccountId for Central AWS Account")
parser.add_argument('input_file', type=argparse.FileType('r'), help='Path to CSV file containing the list of account IDs and Email addresses')
parser.add_argument('--assume_role', type=str, required=True, help="Role Name to assume in each account")
parser.add_argument('--delete_master', action='store_true', default=False, help="Disable SecurityHub in Master")
parser.add_argument('--enabled_regions', type=str, help="comma separated list of regions to remove SecurityHub. If not specified, all available regions disabled")
parser.add_argument('--disable_standards_only', type=str, required=False,help="comma separated list of standards ARNs to disable (ie. arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0 )")
args = parser.parse_args()
# Validate master accountId
if not re.match(r'[0-9]{12}',args.master_account):
raise ValueError("Master AccountId is not valid")
# Generate dict with account & email information
aws_account_dict = OrderedDict()
for acct in args.input_file.readlines():
split_line = acct.rstrip().split(",")
if len(split_line) < 2:
print("Unable to process line: {}".format(acct))
continue
if not re.match(r'[0-9]{12}',str(split_line[0])):
print("Invalid account number {}, skipping".format(split_line[0]))
continue
aws_account_dict[split_line[0]] = split_line[1]
# Getting SecurityHub regions
session = boto3.session.Session()
securityhub_regions = []
if args.disable_standards_only:
standards_arns = [str(item) for item in args.disable_standards_only.split(',')]
if args.enabled_regions:
securityhub_regions = [str(item) for item in args.enabled_regions.split(',')]
print("Disabling standards: {} in these regions: {}".format(args.disable_standards_only, securityhub_regions))
else:
securityhub_regions = session.get_available_regions('securityhub')
print("Disabling standards: {} in all available SecurityHub regions {}".format(args.disable_standards_only,securityhub_regions))
else:
if args.enabled_regions:
securityhub_regions = [str(item) for item in args.enabled_regions.split(',')]
print("Disabling members in these regions: {}".format(securityhub_regions))
else:
securityhub_regions = session.get_available_regions('securityhub')
print("Disabling members in all available SecurityHub regions {}".format(securityhub_regions))
master_session = assume_role(args.master_account, args.assume_role)
#master_session = boto3.Session()
master_clients = {}
members = {}
for aws_region in securityhub_regions:
master_clients[aws_region] = master_session.client('securityhub', region_name=aws_region)
members[aws_region] = get_master_members(master_clients[aws_region], aws_region)
# Processing accounts to be linked
failed_accounts = []
for account in aws_account_dict.keys():
try:
session = assume_role(account, args.assume_role)
for aws_region in securityhub_regions:
print('Beginning {account} in {region}'.format(
account=account,
region=aws_region
))
sh_client = session.client('securityhub', region_name=aws_region)
if args.disable_standards_only:
regional_standards_arns = [utils.get_standard_arn_for_region_and_resource(aws_region, standard) for standard in standards_arns]
for standard in regional_standards_arns:
try:
subscription_arn = 'arn:aws:securityhub:{}:{}:subscription/{}'.format(aws_region, account,standard.split(':')[-1].split('/',1)[1])
sh_client.batch_disable_standards(StandardsSubscriptionArns=[subscription_arn])
print("Finished disabling standard {} on account {} for region {}".format(standard,account, aws_region))
except ClientError as e:
print("Error disabling standards for account {}".format(account))
failed_accounts.append({ account : repr(e)})
else:
if account in members[aws_region]:
if sh_client.get_master_account().get('Master'):
try:
response = sh_client.disassociate_from_master_account()
except ClientError as e:
print("Error Processing Account {}".format(account))
failed_accounts.append({
account: repr(e)
})
master_clients[aws_region].disassociate_members(
AccountIds=[account]
)
time.sleep(2)
master_clients[aws_region].delete_members(
AccountIds=[account]
)
print('Removed Account {monitored} from member list in SecurityHub master account {master} for region {region}'.format(
monitored=account,
master=args.master_account,
region=aws_region
))
start_time = int(time.time())
while account in members[aws_region]:
if (int(time.time()) - start_time) > 300:
print("Membership did not show up for account {}, skipping".format(account))
failed_accounts.append({
account: "Membership did not show up for account {} in {}".format(
account,
aws_region
)
})
break
time.sleep(5)
members[aws_region] = get_master_members(master_clients[aws_region], aws_region)
else:
print('Account {monitored} is not a member of {master} in region {region}'.format(
monitored=account,
master=args.master_account,
region=aws_region
))
sh_client.disable_security_hub()
# Refresh the member dictionary
members[aws_region] = get_master_members(master_clients[aws_region], aws_region)
print('Finished {account} in {region}'.format(account=account, region=aws_region))
except ClientError as e:
print("Error Processing Account {}".format(account))
failed_accounts.append({
account: repr(e)
})
if args.delete_master and len(failed_accounts) == 0 and not args.disable_standards_only:
for aws_region in securityhub_regions:
master_clients[aws_region].disable_security_hub()
if args.delete_master and len(failed_accounts) == 0 and args.disable_standards_only:
for aws_region in securityhub_regions:
master_clients[aws_region].batch_disable_standards(StandardsSubscriptionArns = [ args.disable_standards_only])
if len(failed_accounts) > 0:
print("---------------------------------------------------------------")
print("Failed Accounts")
print("---------------------------------------------------------------")
for account in failed_accounts:
print("{}: \n\t{}".format(
account.keys()[0],
account[account.keys()[0]]
))
print("---------------------------------------------------------------")