+ And various contributors (see git history)
+
+PyFiglet: An implementation of figlet written in Python
+
+The MIT License (MIT)
+
+Copyright © 2007-2018
+ Christopher Jones
+ And various contributors (see git history).
+
+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, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
+
+------
+
+** urllib3; version 1.24.3 -- https://pypi.python.org/pypi/urllib3/1.22
+Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
+
+https://github.com/shazow/urllib3/blob/master/CONTRIBUTORS.txt
+
+MIT License
+
+Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
+
+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, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
+
+------
+
+** six; version 1.12.0 -- https://pypi.python.org/pypi/six/1.11.0
+Copyright (c) 2010-2017 Benjamin Peterson
+
+Copyright (c) 2010-2017 Benjamin Peterson
+
+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,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
+
+------
+
+** terminaltables; version 3.1.0 -- https://github.com/Robpol86/terminaltables
+Copyright (c) 2017 Robpol86
+
+MIT License
+
+Copyright (c) 2017 Robpol86
+
+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, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
+
+------
+
+** ProgressBar; version 1.5 -- https://github.com/kimmobrunfeldt/progressbar.js
+The MIT License (MIT)
+
+Copyright (c) 2015 Kimmo Brunfeldt
+
+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, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+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.
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Kimmo Brunfeldt
+
+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, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+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.
\ No newline at end of file
diff --git a/bin/iamctl b/bin/iamctl
new file mode 100755
index 0000000..b827428
--- /dev/null
+++ b/bin/iamctl
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# A copy of the License is located at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+import iamctl.iamctl
+import sys
+
+
+def main():
+ return iamctl.iamctl.main()
+
+
+if __name__ == '__main__':
+ sys.exit(main())
\ No newline at end of file
diff --git a/iamctl/__init__.py b/iamctl/__init__.py
new file mode 100755
index 0000000..a1fe6b0
--- /dev/null
+++ b/iamctl/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# A copy of the License is located at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+from colorama import init
+
+init()
\ No newline at end of file
diff --git a/iamctl/__main__.py b/iamctl/__main__.py
new file mode 100755
index 0000000..8616148
--- /dev/null
+++ b/iamctl/__main__.py
@@ -0,0 +1,20 @@
+# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# A copy of the License is located at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+import sys
+
+from iamctl.iamctl import main
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/iamctl/conf/logging.conf b/iamctl/conf/logging.conf
new file mode 100755
index 0000000..46a9f85
--- /dev/null
+++ b/iamctl/conf/logging.conf
@@ -0,0 +1,34 @@
+[loggers]
+keys=root,harvester,differ
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleFormatter
+
+[logger_root]
+level=ERROR
+handlers=consoleHandler
+
+[logger_harvester]
+level=ERROR
+handlers=consoleHandler
+qualname=harvester
+propagate=0
+
+[logger_differ]
+level=ERROR
+handlers=consoleHandler
+qualname=differ
+propagate=0
+
+[handler_consoleHandler]
+class=StreamHandler
+level=ERROR
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleFormatter]
+format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
+datefmt=
\ No newline at end of file
diff --git a/iamctl/differ.py b/iamctl/differ.py
new file mode 100755
index 0000000..18962da
--- /dev/null
+++ b/iamctl/differ.py
@@ -0,0 +1,280 @@
+# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# A copy of the License is located at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+import boto3
+import json
+from botocore.exceptions import ClientError
+import re
+import logging
+import logging.config
+import csv
+import sys
+import os
+import argparse
+import time
+from datetime import datetime
+from progress.bar import ChargingBar, Bar
+from pyfiglet import Figlet
+from colorama import init,Fore, Back, Style
+from terminaltables import SingleTable
+from os.path import expanduser
+from os import path
+
+class Differ:
+ def __init__(self, extract_file_name_1, extract_file_name_2, account_1_tag, account_2_tag, output_directory):
+
+ self.output_directory=output_directory
+ self.logger = logging.getLogger(__name__)
+ self.extract_file_name_1 = extract_file_name_1
+ self.extract_file_name_2 = extract_file_name_2
+ self.extract_file_handler_1 = None
+ self.extract_file_handler_2 = None
+ self.account_1_tag = account_1_tag
+ self.account_2_tag = account_2_tag
+ self.account_1_raw = None
+ self.account_2_raw = None
+ self.equivalency_list_dict = None
+ self.account_1_to_account_2_csv_out = None
+ self.account_2_to_account_1_csv_out = None
+ self.read_equivalency_dict()
+ self.read_extract_files()
+
+ def read_equivalency_dict(self):
+ with open('equivalency_list.json') as f:
+ self.equivalency_list_dict = json.load(f)
+
+ def read_extract_files(self):
+ # Read the 2 extract files.
+ with open(self.extract_file_name_1) as f:
+ self.extract_file_handler_1 = f
+ next(csv.reader(f))
+ self.account_1_raw = [tuple(line) for line in csv.reader(f)]
+
+ with open(self.extract_file_name_2) as f:
+ self.extract_file_handler_2 = f
+ next(csv.reader(f))
+ self.account_2_raw = [tuple(line) for line in csv.reader(f)]
+
+ # Get values after matching equivalency list
+ def sanitize_value_with_equivalency(self, value):
+ for key, valuelist in self.equivalency_list_dict.items():
+ for eachvalue in valuelist:
+ value = value.replace(eachvalue, key)
+ return value
+
+ # Get list after matching equivalency list.
+ def get_sanitized_list_with_equivalency(self, tuples, tag):
+ output_list = []
+ bar = ChargingBar('Sanitizing IAM items from '+tag, max=len(tuples),suffix='%(index)d/%(max)d')
+ for each_tuple in tuples:
+ output_tuple = []
+ for each_item in each_tuple:
+ output_tuple.append(self.sanitize_value_with_equivalency(each_item))
+ output_list.append(tuple(output_tuple))
+ bar.next()
+ bar.finish()
+ return output_list
+
+ def write_to_csv(self,tuples, header, filename):
+ filehandler = open(self.output_directory+ "/" + filename, "wt", newline='')
+ csv_out = csv.writer(filehandler)
+ csv_out.writerow(header)
+ for each_tuple in tuples:
+ csv_out.writerow(each_tuple)
+ filename = filehandler.name
+ filehandler.close()
+ return filename
+
+ def generate_diff_and_summary(self):
+
+ summary = []
+ summary.append(['Metric', self.account_1_tag, self.account_2_tag])
+
+ sanitized_account_1_list = self.get_sanitized_list_with_equivalency(self.account_1_raw, self.account_1_tag)
+ sanitized_account_2_list = self.get_sanitized_list_with_equivalency(self.account_2_raw, self.account_2_tag)
+
+ print(Style.BRIGHT)
+ print(Fore.BLUE + "Summary report in text format:")
+ print(Style.RESET_ALL)
+
+ print("Number of items in %s: %d" %(self.account_1_tag, len(self.account_1_raw)))
+ print("Number of items in %s: %d" %(self.account_2_tag, len(self.account_2_raw)))
+
+ summary.append(['Harvested Items', len(self.account_1_raw), len(self.account_2_raw)])
+
+ print("Number of items in %s after sanitizing: %d" % (self.account_1_tag, len(sanitized_account_1_list)))
+ print("Number of items in %s after sanitizing: %d" % (self.account_2_tag, len(sanitized_account_2_list)))
+
+ summary.append(['Sanitized Items', len(sanitized_account_1_list), len(sanitized_account_2_list)])
+
+ account_1_roles = set([(item[0], item[1]) for item in sanitized_account_1_list])
+ print("Number of roles in %s: %d" %(self.account_1_tag, len(account_1_roles)))
+ headerrow =('rolename', 'path')
+ filename = self.account_1_tag + "_roles.csv"
+ self.write_to_csv(account_1_roles, headerrow, filename)
+
+ account_1_service_linked_roles = set([(item[0],) for item in sanitized_account_1_list if (item[1].startswith('/aws-service-role/'))])
+ print("Number of service linked roles in %s: %d" %(self.account_1_tag, len(account_1_service_linked_roles)))
+ headerrow =('rolename',)
+ filename = self.account_1_tag + "_service_linked_roles.csv"
+ self.write_to_csv(account_1_service_linked_roles, headerrow, filename)
+
+ account_1_non_service_linked_roles = set([(item[0],) for item in sanitized_account_1_list if not item[1].startswith('/aws-service-role/')])
+ print("Number of Non-service linked roles in %s: %d" %(self.account_1_tag, len(account_1_non_service_linked_roles)))
+ headerrow =('rolename',)
+ filename = self.account_1_tag + "_non_service_linked_roles.csv"
+ self.write_to_csv(account_1_non_service_linked_roles, headerrow, filename)
+
+ account_2_roles = set([(item[0],item[1]) for item in sanitized_account_2_list])
+ print("Number of roles in %s: %d" %(self.account_2_tag, len(account_2_roles)))
+ headerrow =('rolename', 'path')
+ filename = self.account_2_tag + "_roles.csv"
+ self.write_to_csv(account_2_roles, headerrow, filename)
+
+ account_2_service_linked_roles = set([(item[0],) for item in sanitized_account_2_list if (item[1].startswith('/aws-service-role/'))])
+ print("Number of service linked roles in %s: %d" %(self.account_2_tag, len(account_2_service_linked_roles)))
+ headerrow =('rolename',)
+ filename = self.account_2_tag + "_service_linked_roles.csv"
+ self.write_to_csv(account_2_service_linked_roles, headerrow, filename)
+
+ account_2_non_service_linked_roles = set([(item[0],) for item in sanitized_account_2_list if not item[1].startswith('/aws-service-role/')])
+ print("Number of Non-service linked roles in %s: %d" %(self.account_2_tag, len(account_2_non_service_linked_roles)))
+ headerrow =('rolename',)
+ filename = self.account_2_tag + "_non_service_linked_roles.csv"
+ self.write_to_csv(account_2_non_service_linked_roles, headerrow, filename)
+
+ summary.append(['Roles',len(account_1_roles), len(account_2_roles)])
+ summary.append(['Service Linked Roles', len(account_1_service_linked_roles), len(account_2_service_linked_roles)])
+ summary.append(['Non-Service Linked Roles', len(account_1_non_service_linked_roles), len(account_2_non_service_linked_roles)])
+
+ # Getting list of common roles between 2 accounts using bitwise operator.
+ # If roles are common will return a 1.
+ common_role_list = set(account_1_roles) & set(account_2_roles)
+ print("Number of common roles: %d" %( len(common_role_list)))
+ headerrow =('rolename', 'path')
+ filename = "common_roles.csv"
+ self.write_to_csv(common_role_list, headerrow, filename)
+
+ common_service_linked_role_list = [(item[0],) for item in common_role_list if item[1].startswith('/aws-service-role/')]
+ print("Number of common roles that are service linked: %d" %(len(common_service_linked_role_list)))
+ headerrow =('rolename',)
+ filename = "common_service_linked_roles.csv"
+ self.write_to_csv(common_service_linked_role_list, headerrow, filename)
+
+ common_non_service_linked_role_list = [(item[0],) for item in common_role_list if not item[1].startswith('/aws-service-role/')]
+ print("Number of common roles that are Non-service linked: %d" %(len(common_non_service_linked_role_list)))
+ headerrow =('rolename',)
+ filename = "common_non_service_linked_roles.csv"
+ self.write_to_csv(common_non_service_linked_role_list, headerrow, filename)
+
+ summary.append(['Common Roles', len(common_role_list), len(common_role_list)])
+ summary.append(['Common Service Linked Roles', len(common_service_linked_role_list), len(common_service_linked_role_list)])
+ summary.append(['Common Non-Service Linked Roles', len(common_non_service_linked_role_list), len(common_non_service_linked_role_list)])
+
+ # Get roles that are in first but not in second.
+ account_1_diff_account_2 = set(account_1_roles) - set(account_2_roles)
+
+
+ #difference in items will not translate to roles, for e.g. you could have a role in account-a that has action item that is not in account-b
+ account_1_diff_account_2_roles = set([(item[0],item[1]) for item in account_1_diff_account_2])
+ print("Number of roles from %s not in %s: %d" %(self.account_1_tag,self.account_2_tag, len(account_1_diff_account_2_roles)))
+ headerrow =('rolename', 'path')
+ filename = "roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag + ".csv"
+ self.write_to_csv(account_1_diff_account_2_roles, headerrow, filename)
+
+ account_1_diff_account_2_service_linked_roles= [tup for tup in account_1_diff_account_2_roles if tup[1].startswith('/aws-service-role/')]
+ print("Number of service linked roles from %s not in %s: %d" %(self.account_1_tag,self.account_2_tag, len(account_1_diff_account_2_service_linked_roles)))
+ headerrow =('rolename', 'path')
+ filename = "service_linked_roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag+" .csv"
+ self.write_to_csv(account_1_diff_account_2_service_linked_roles, headerrow, filename)
+
+ account_1_diff_account_2_non_service_linked_roles= [tup for tup in account_1_diff_account_2_roles if not tup[1].startswith('/aws-service-role/')]
+ print("Number of non-service linked roles from %s not in %s: %d" %(self.account_1_tag, self.account_2_tag, len(account_1_diff_account_2_non_service_linked_roles)))
+ headerrow =('rolename', 'path')
+ filename = "non_service_linked_roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag + ".csv"
+ self.write_to_csv(account_1_diff_account_2_non_service_linked_roles, headerrow, filename)
+
+ # Get roles that are in second but not in first.
+
+ account_2_diff_account_1 = set(account_2_roles) - set(account_1_roles)
+ account_2_diff_account_1_roles = list(set([(item[0],item[1]) for item in account_2_diff_account_1]))
+ print("Number of roles from %s not in %s: %d" %(self.account_2_tag,self.account_1_tag, len(account_2_diff_account_1_roles)))
+ headerrow =('rolename', 'path')
+ filename = "roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_2_tag + ".csv"
+ self.write_to_csv(account_2_diff_account_1_roles, headerrow, filename)
+
+ account_2_diff_account_1_service_linked_roles= [tup for tup in account_2_diff_account_1_roles if tup[1].startswith('/aws-service-role/')]
+ print("Number of service linked roles from %s not in %s: %d" %(self.account_2_tag,self.account_1_tag, len(account_2_diff_account_1_service_linked_roles)))
+ headerrow =('rolename', 'path')
+ filename = "service_linked_roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_1_tag + ".csv"
+ self.write_to_csv(account_2_diff_account_1_service_linked_roles, headerrow, filename)
+
+
+ account_2_diff_account_1_non_service_linked_roles= [tup for tup in account_2_diff_account_1_roles if not tup[1].startswith('/aws-service-role/')]
+ print("Number of non-service linked roles from %s not in %s: %d" %(self.account_2_tag, self.account_1_tag, len(account_2_diff_account_1_non_service_linked_roles)))
+ headerrow =('rolename', 'path')
+ filename = "non_service_linked_roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_1_tag + ".csv"
+ self.write_to_csv(account_2_diff_account_1_non_service_linked_roles, headerrow, filename)
+
+ summary.append(['Unique Roles', len(account_1_diff_account_2_roles), len(account_2_diff_account_1_roles)])
+ summary.append(['Unique Service Linked Roles', len(account_1_diff_account_2_service_linked_roles), len(account_2_diff_account_1_service_linked_roles)])
+ summary.append(['Unique Non-Service Linked Roles', len(account_1_diff_account_2_non_service_linked_roles), len(account_2_diff_account_1_non_service_linked_roles)])
+
+ account_1_diff_items_account_2 = set(sanitized_account_1_list).difference(set(sanitized_account_2_list))
+ account_2_diff_items_account_1 = set(sanitized_account_2_list).difference(set(sanitized_account_1_list))
+
+
+ true_diff_account_1_with_common = [tup for tup in account_1_diff_items_account_2 if (tup[0] in [item[0] for item in common_role_list])]
+ print("There are %d items that are in different in %s among common roles between %s,%s" %(len(true_diff_account_1_with_common),self.account_1_tag, self.account_1_tag,self.account_2_tag))
+ headerrow =('rolename', 'path','trust', 'policyname', 'effect', 'service', 'action', 'arn')
+ filename = self.account_1_tag + "_to_" + self.account_2_tag + "_common_role_difference_items.csv"
+ self.write_to_csv(true_diff_account_1_with_common, headerrow, filename)
+
+ true_diff_role_account_1_with_common = set([(item[0],) for item in true_diff_account_1_with_common])
+ print("There are %d common roles in %s that have differences with %s "%(len(true_diff_role_account_1_with_common), self.account_1_tag, self.account_2_tag))
+ headerrow =('rolename',)
+ filename = "common_roles_in_" + self.account_1_tag + "_with_differences" + ".csv"
+ self.write_to_csv(true_diff_role_account_1_with_common, headerrow, filename)
+
+ # base_diff_account_2_with_common = [tup for tup in account_2 if (tup[0] in common_role_list)]
+ # print("Number of items in " + account2tag + " tied to common roles: " + str(len(base_diff_account_2_with_common)))
+ true_diff_account_2_with_common = [tup for tup in account_2_diff_items_account_1 if (tup[0] in [item[0] for item in common_role_list])]
+ print("There are %d items that are in different in %s among common roles between %s,%s" %(len(true_diff_account_2_with_common), self.account_2_tag, self.account_1_tag, self.account_2_tag))
+ headerrow =('rolename', 'path','trust', 'policyname', 'effect', 'service', 'action', 'arn')
+ filename = self.account_2_tag + "_to_" + self.account_1_tag + "_common_role_difference_items.csv"
+ self.write_to_csv(true_diff_account_2_with_common, headerrow, filename)
+
+ true_diff_role_account_2_with_common = set([(item[0],) for item in true_diff_account_2_with_common])
+ print("There are %d common roles in %s that have differences with %s "%(len(true_diff_role_account_2_with_common), self.account_2_tag, self.account_1_tag))
+ headerrow = ('rolename',)
+ filename = "common_roles_in_" + self.account_2_tag + "_with_differences" + ".csv"
+ self.write_to_csv(true_diff_role_account_2_with_common, headerrow, filename)
+
+ summary.append(['Common Roles with Differences', len(true_diff_role_account_1_with_common), len(true_diff_role_account_2_with_common)])
+ summary.append(['Differences among Common Roles', len(true_diff_account_1_with_common), len(true_diff_account_2_with_common)])
+
+ print(Style.BRIGHT)
+ print(Fore.YELLOW +"Summary report in tabular format:")
+ print(Style.RESET_ALL)
+
+ table = SingleTable(summary)
+ table.title = "Summary Report"
+ table.inner_heading_row_border = True
+ table.inner_row_border = True
+ table.justify_columns[1] = 'right'
+ table.justify_columns[2] = 'right'
+ print(table.table)
+
+ print(Style.BRIGHT)
+ print(Fore.GREEN + "Detailed reports are available at this location:\n%s" %(self.output_directory))
+ print(Style.RESET_ALL)
diff --git a/iamctl/harvester.py b/iamctl/harvester.py
new file mode 100755
index 0000000..ce1375b
--- /dev/null
+++ b/iamctl/harvester.py
@@ -0,0 +1,339 @@
+# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# A copy of the License is located at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+import boto3
+import json
+from botocore.exceptions import ClientError
+import re
+import fnmatch
+import logging
+import logging.config
+import csv
+import sys
+import os
+import argparse
+import time
+from datetime import datetime
+from progress.bar import ChargingBar, Bar
+from pyfiglet import Figlet
+from colorama import init,Fore, Back, Style
+from terminaltables import SingleTable
+from os.path import expanduser
+from os import path
+
+class Harvester:
+
+ def close_file_handler(self):
+ self.extract_file.close()
+
+ def read_iam_file(self):
+ with open('iam.json') as json_file:
+ return json.load(json_file)
+
+ def return_service_iam_actions(self,service_prefix):
+ for p in self.iam_reference['serviceMap']:
+ if (self.iam_reference['serviceMap'][p]['StringPrefix'] == service_prefix):
+ return self.iam_reference['serviceMap'][p]['Actions']
+
+ def return_service_arns(self):
+ arns=[]
+ for p in self.iam_reference['serviceMap']:
+ if ('ARNRegex' in self.iam_reference['serviceMap'][p]):
+ arns.append({'ARNRegex':self.iam_reference['serviceMap'][p]['ARNRegex'], 'StringPrefix':self.iam_reference['serviceMap'][p]['StringPrefix']})
+ return arns
+
+ def match_action_regex(self, match_action, service_prefix):
+ matches = []
+ actions = self.return_service_iam_actions(service_prefix)
+ for action in actions or []:
+ if fnmatch.fnmatch(action, match_action):
+ matches.append(action)
+ return matches
+
+
+ def match_resource_regex(self, match_resource):
+ matches = []
+ arns = self.return_service_arns()
+ for arn in arns or []:
+ arn_regex = re.compile(arn['ARNRegex'])
+ if arn_regex.match(match_resource):
+ matches.append(arn)
+ return matches
+
+
+ def get_iam_roles(self):
+ paginator = self.client.get_paginator('list_roles')
+ response_iterator = paginator.paginate(
+ PaginationConfig = {
+ 'PageSize': 1000,
+ 'StartingToken': None})
+
+ roles = response_iterator.build_full_result()
+ self.logger.info("Number of roles: %d",len(roles['Roles']))
+ return roles['Roles']
+
+
+ def get_role_inline_policies(self, role_name):
+ return self.client.list_role_policies(
+ RoleName = role_name
+ )
+
+
+ def get_role_attached_policies(self, role_name):
+
+ return self.client.list_attached_role_policies(
+ RoleName = role_name
+ )
+
+ def get_policy(self, policy_arn):
+ return self.client.get_policy(PolicyArn = policy_arn)
+
+ def get_policy_version(self,policy_arn, version_id):
+ return self.client.get_policy_version(PolicyArn = policy_arn, VersionId = version_id)
+
+ def get_role_policy(self, rolename, inline_policy_name):
+ return self.client.get_role_policy(RoleName = rolename, PolicyName = inline_policy_name)
+
+ def get_role(self, role_name):
+ return self.client.get_role(RoleName = role_name)
+
+ def parse_statement_action(self,action_tag ,statement_action):
+ actions = []
+
+ if(statement_action == "*"):
+ self.logger.info("All Actions against all Services")
+ actions.append({'service':'*' , action_tag:'*'})
+ else:
+ self.logger.debug("Statement Action: " + statement_action)
+ self.logger.debug(action_tag+": " + statement_action.encode("utf-8").decode().split(':')[1])
+ self.logger.debug("service: " + statement_action.encode("utf-8").decode().split(':')[0])
+ action_matches = self.match_action_regex(statement_action.encode("utf-8").decode().split(':')[1], statement_action.encode("utf-8").decode().split(':')[0])
+ for action in action_matches or []:
+ actions.append({'service' : statement_action.encode("utf-8").decode().split(':')[0], action_tag:action})
+ self.logger.info("Statement Action: " + statement_action.encode("utf-8").decode().split(':')[0]+" : " + action )
+ return actions
+
+ def parse_statement_resource(self,resource_tag, statement_resource):
+ resources = []
+ if(statement_resource == "*"):
+ self.logger.info("All resources for all Services")
+ resources.append({'service' : '*' , resource_tag : '*'})
+ else:
+ resource_matches = self.match_resource_regex(statement_resource)
+ for resource in resource_matches:
+ resources.append({'service' : resource['StringPrefix'], resource_tag : statement_resource})
+ self.logger.info("Statement Resource: " + resource['StringPrefix'] + " : " + statement_resource)
+
+ return resources
+
+ def mux(self,action_tag,actions,resource_tag,resources):
+ #actions structure is: service, action
+ #resources sturcture is: service, arn
+ #muxedup structure is: service, action, arn
+ self.logger.debug("I am muxed up and I received this actions:")
+ self.logger.debug(str(actions))
+ self.logger.debug("I am muxed up and I received this resources:")
+ self.logger.debug(str(resources))
+ muxedup=[]
+ for action in actions:
+ for resource in resources:
+ if ((action['service'] == resource['service']) or (action['service'] == "*") or (resource['service'] == "*")):
+ muxedup.append({'service': action['service'], 'action' : action[action_tag], 'arn' : resource[resource_tag]})
+
+ return muxedup
+
+
+ def parse_policy(self,policy_document):
+ # instantiate empty policy array and policy statement array
+ policy_statement_array = []
+ parsed_policy = []
+
+ # determining if there is a single statement or an array of statements in the policy document
+ # and appending those statement(s) to policy_statement_array
+ #
+ if not isinstance(policy_document['Statement'], list):
+ policy_statement_array.append(policy_document['Statement'])
+ else:
+ policy_statement_array = policy_document['Statement']
+
+ # code that parses each policy statement into its components
+ # and calls parse_statement_action for action/notaction, parse_statement_resource for resource/notresource block
+ for policy_statement in policy_statement_array:
+ self.logger.info("Statement Effect: "+policy_statement['Effect'])
+ actions = []
+ statement_has_action = 'Action'
+ # Checking if statement has action or notaction block
+ if policy_statement.get('Action',False):
+ statement_has_action = 'Action'
+ else:
+ statement_has_action = 'NotAction'
+ # checking if Action is single item or a list
+ if not isinstance(policy_statement[statement_has_action], list):
+ actions=actions + self.parse_statement_action(statement_has_action, policy_statement[statement_has_action])
+ else:
+ for statement_action in policy_statement[statement_has_action]:
+ actions = actions+self.parse_statement_action(statement_has_action, statement_action)
+
+
+
+ resources=[]
+ statement_has_resource = 'Resource'
+ # Checking if statment has resource or notresource block
+ if policy_statement.get('Resource',False):
+ statement_has_resource = 'Resource'
+ else:
+ statement_has_resource = 'NotResource'
+ self.logger.debug("Statement Resource: "+str(policy_statement[statement_has_resource]))
+ if not isinstance(policy_statement[statement_has_resource], list):
+ resources=resources+self.parse_statement_resource(statement_has_resource, policy_statement[statement_has_resource])
+ else:
+ for statement_resource in policy_statement[statement_has_resource]:
+ resources = resources + self.parse_statement_resource(statement_has_resource, statement_resource)
+
+ muxed_up=self.mux(statement_has_action,actions,statement_has_resource,resources)
+ self.logger.debug("Going to print Muxed up results for: ")
+ self.logger.debug(str(muxed_up))
+ parsed_policy.append({'effect' : policy_statement['Effect'], 'action_resources' : muxed_up })
+ return parsed_policy
+
+ def write_out_exhaust(self, role):
+
+ #self.logger.info("here is the exhaust",str(exhaust))
+ #exhaust: data: List