├── requirements.txt ├── pictures ├── sample_created_files.png ├── sample_Permission_Set_Inline_Policy_JSON.png ├── sample_Account_Assignment_CSV_imported_to_excel.png └── sample_Permission_Set_Summary_CSV_imported_to_excel.png ├── readme.md ├── sso-permission-set-report.py └── sso-account-permission-assignment-report.py /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.16.7 -------------------------------------------------------------------------------- /pictures/sample_created_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onemorepereira/aws-sso-reporter/HEAD/pictures/sample_created_files.png -------------------------------------------------------------------------------- /pictures/sample_Permission_Set_Inline_Policy_JSON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onemorepereira/aws-sso-reporter/HEAD/pictures/sample_Permission_Set_Inline_Policy_JSON.png -------------------------------------------------------------------------------- /pictures/sample_Account_Assignment_CSV_imported_to_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onemorepereira/aws-sso-reporter/HEAD/pictures/sample_Account_Assignment_CSV_imported_to_excel.png -------------------------------------------------------------------------------- /pictures/sample_Permission_Set_Summary_CSV_imported_to_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onemorepereira/aws-sso-reporter/HEAD/pictures/sample_Permission_Set_Summary_CSV_imported_to_excel.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Why do I need this? 2 | Many customers with big AWS SSO environments struggle to create an overview over who has been given access to what. The GUI does not present any easy way of answering questions like: 3 | 4 | * What accounts does user John Doe have access to? 5 | * What permission sets are attached to him? 6 | * Which users and groups have access to our App-Prod-Account? 7 | 8 | This tools uses the AWS SSO API to list all users, accounts, permission sets etc. and dumps it into a CSV file for additional parsing or viewing. 9 | 10 | # AWS SSO Permission Set Assignment Reporter 11 | 12 | AWS SSO Permission Set Assignment Reporter consists of two script that exports information on the AWS SSO setup. The goal with the report is to be able to import the data into for example a spreadsheet and answer questions from the following perspectives: 13 | 14 | **1. For each AWS Account list:** 15 | 16 | * Which users are direct-assigned to the account, and using what Permission Set? 17 | * Which security groups are assigned to the account, and using what Permission Set? 18 | 19 | **2. For each User and Security Group list:** 20 | 21 | * Which account a user or security group has access to, and using what Permission Set? 22 | 23 | **3. For each Permission Set list:** 24 | 25 | * Which AWS accounts the permission set is assigned to, and with which users and security groups? 26 | 27 | **4. For each Permission Set list:** 28 | * What Managed policies are attached to it? 29 | * What Inline Policy is attached to it? 30 | 31 | ## Usage 32 | 33 | 1. Login to your AWS SSO Management Account with credentials to read from AWS Organizations and AWS SSO services using AWS CLI or AWS Access Keys 34 | 2. Set your region `aws configure set default.region eu-west-1` 35 | 3. Run the scripts: 36 | 4. `sso-account-permission-assignment-report.py` to create a CSV with all AWS Account assignments. `python3 sso-account-permission-assignment-report.py` 37 | 5. `sso-permission-set-report.py` to get a CSV summary file and JSON files of each permission set inline policy `python3 sso-permission-set-report.py` 38 | 6. The CSV files can be imported to for example a spreadsheet or a database for further analysis. 39 | 40 | ## Dependencies 41 | ### Python 3 42 | 43 | These scripts depends on Python 3. Please ensure Python 3 is installed and in your path, or use a virtual environment. 44 | 45 | To install the required libraries run the following command in the root folder: 46 | 47 | ```pip install -r requirements.txt``` 48 | 49 | ## Required permissions 50 | Ensure you have at least these permissions in the AWS SSO Management account. Even this list can probably be reduced further. 51 | 52 | "organizations:DescribeOrganization", 53 | "organizations:DescribeAccount", 54 | "organizations:ListAccounts", 55 | "sso:Describe*", 56 | "sso:Get*", 57 | "sso:List*", 58 | "identitystore:DescribeGroup", 59 | "identitystore:DescribeUser", 60 | "sso-directory:DescribeDirectory" 61 | 62 | ## Known limitations 63 | The script does not recursively look through Groups. Meaning that users inheriting access through a group is not directly listed in the CSV output 64 | 65 | ## Sample created report files 66 | ![Sample created report files](pictures/sample_created_files.png) 67 | 68 | ## Sample Account Assignment CSV output imported to Excel 69 | ![Sample Account Assignment CSV output imported to Excel](pictures/sample_Account_Assignment_CSV_imported_to_excel.png) 70 | 71 | ## Sample Permission Set Summary CSV imported to Excel 72 | ![Sample Permission Set Summary CSV imported to Excel](pictures/sample_Permission_Set_Summary_CSV_imported_to_excel.png) 73 | 74 | ## Sample Permission Set Inline Policy JSON file 75 | ![Sample Permission Set Inline Policy JSON file](pictures/sample_Permission_Set_Inline_Policy_JSON.png) 76 | -------------------------------------------------------------------------------- /sso-permission-set-report.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import csv 3 | import json 4 | import string 5 | import time 6 | import unicodedata 7 | 8 | from datetime import datetime 9 | 10 | """ 11 | list_existing_sso_instances 12 | 13 | Lists the SSO instances that the caller has access to. 14 | 15 | Parameters: 16 | -- None 17 | Returns: 18 | -- List[Dictionary]: sso_instance_list (a list of sso instances each described by a dictionary with keys 'instanceArn' and 'identityStore') 19 | """ 20 | def list_existing_sso_instances(): 21 | client = boto3.client('sso-admin') 22 | 23 | sso_instance_list = [] 24 | response = client.list_instances() 25 | for sso_instance in response['Instances']: 26 | # add only relevant keys to return 27 | sso_instance_list.append({'instanceArn': sso_instance["InstanceArn"], 'identityStore': sso_instance["IdentityStoreId"]}) 28 | 29 | return sso_instance_list 30 | 31 | """ 32 | list_permission_sets 33 | 34 | Lists the PermissionSet in an SSO instance. 35 | 36 | Parameters: 37 | -- String: ssoInstanceArn 38 | Returns: 39 | -- Dictionary: perm_set_dict (a dictionary with permission sets with key permission set name and value permission set arn) 40 | """ 41 | def list_permission_sets(ssoInstanceArn): 42 | client = boto3.client('sso-admin') 43 | 44 | perm_set_dict = {} 45 | response = client.list_permission_sets(InstanceArn=ssoInstanceArn, MaxResults=100) 46 | results = response["PermissionSets"] 47 | 48 | while "NextToken" in response: 49 | response = client.list_permission_sets(InstanceArn=ssoInstanceArn, NextToken=response["NextToken"]) 50 | results.extend(response["PermissionSets"]) 51 | 52 | for permission_set in results: 53 | # get the name of the permission set from the arn 54 | perm_description = client.describe_permission_set(InstanceArn=ssoInstanceArn,PermissionSetArn=permission_set) 55 | # key: permission set name, value: permission set arn 56 | perm_set_dict[perm_description["PermissionSet"]["Name"]] = permission_set 57 | 58 | return perm_set_dict 59 | 60 | """ 61 | create_report 62 | 63 | Creates a report of the assigned permissions on users for all accounts in an organization. 64 | 65 | Parameters: 66 | -- List[Dictionary]: sso_instance (a list of sso instances each described by a dictionary with keys 'instanceArn' and 'identityStore') 67 | -- Dictionary: permission_sets_list (a dictionary with permission sets with key permission set name and value permission set arn) 68 | Returns: 69 | -- None 70 | Output: 71 | -- CSV file: CSV with permissions set report 72 | -- Json files: Json files with inline policies attached to permission sets if any 73 | """ 74 | def create_report(sso_instance, permission_sets_list, break_after=None): 75 | client = boto3.client('sso-admin') 76 | 77 | # variables for displaying the progress of processed accounts 78 | length = str(len(permission_sets_list)) 79 | i = 1 80 | 81 | # loop through permission sets and write details to file 82 | filename = 'sso_report_Managed_Policies_per_Permission_Set_' + datetime.now().strftime("%Y-%m-%d_%H.%M.%S") + '.csv' 83 | filename = clean_filename(filename) 84 | with open(filename, 'w', newline='') as output_file: 85 | output_file.write("PermissionSet,ManagedPolicyName,ManagedPolicyARN\n") 86 | for permission_set in permission_sets_list.keys(): 87 | managed_policies = client.list_managed_policies_in_permission_set(InstanceArn=sso_instance['instanceArn'],PermissionSetArn=permission_sets_list[permission_set]) 88 | 89 | 90 | # create managed policy csv file if there are any managed policies attached to the permission set 91 | if(len(managed_policies['AttachedManagedPolicies']) > 0): 92 | for m_policy in managed_policies["AttachedManagedPolicies"]: 93 | output_file.write(permission_set + "," + m_policy["Name"] + "," + m_policy["Arn"] + "\n") 94 | 95 | 96 | # create inline policy json file if there is an inline policy attached to the permission set 97 | inline_policy = client.get_inline_policy_for_permission_set(InstanceArn=sso_instance['instanceArn'],PermissionSetArn=permission_sets_list[permission_set]) 98 | if(inline_policy["InlinePolicy"] != ''): 99 | json_filename = 'sso_report_InlinePolicy_for_' + permission_set + '_' + datetime.now().strftime("%Y-%m-%d_%H.%M.%S") + '.json' 100 | json_filename = clean_filename(json_filename) 101 | with open(json_filename, 'w', newline='') as json_output_file: 102 | json_object = json.loads(inline_policy["InlinePolicy"]) 103 | json_output_file.write(json.dumps(json_object, indent=2)) 104 | json_output_file.close() 105 | 106 | # display the progress of processed accounts 107 | print(str(i) + "/" + length + " permission sets done") 108 | i = i+1 109 | 110 | # debug code used for stopping after a certain amount of accounts for faster testing 111 | if break_after != None and i > break_after: 112 | break 113 | 114 | def print_time_taken(start, end): 115 | elapsed_time = end - start 116 | elapsed_time_string = str(int(elapsed_time/60)) + " minutes and " + str(int(elapsed_time%60)) + " seconds" 117 | print("The report took " + elapsed_time_string + " to generate.") 118 | 119 | def clean_filename(filename, replace=' ', char_limit=255): 120 | #allowed chars 121 | valid_filename_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) 122 | 123 | # replace spaces 124 | for r in replace: 125 | filename = filename.replace(r,'_') 126 | 127 | # keep only valid ascii chars 128 | cleaned_filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode() 129 | 130 | # keep only whitelisted chars 131 | cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) 132 | if len(cleaned_filename)>char_limit: 133 | print("Warning, filename truncated because it was over {}. Filenames may no longer be unique".format(char_limit)) 134 | return cleaned_filename[:char_limit] 135 | 136 | """ 137 | main 138 | 139 | Output: 140 | -- CSV file: CSV with SSO report. 141 | """ 142 | def main(): 143 | start = time.time() 144 | sso_instance = list_existing_sso_instances()[0] 145 | permission_sets_list = list_permission_sets(sso_instance['instanceArn']) 146 | create_report(sso_instance, permission_sets_list) 147 | 148 | # print the time it took to generate the report 149 | end = time.time() 150 | print_time_taken(start, end) 151 | 152 | main() 153 | -------------------------------------------------------------------------------- /sso-account-permission-assignment-report.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import csv 3 | import json 4 | import string 5 | import time 6 | import unicodedata 7 | 8 | from datetime import datetime 9 | 10 | """ 11 | list_accounts 12 | 13 | Lists all AWS accounts assigned to the user. 14 | 15 | Parameters: 16 | -- None 17 | Returns: 18 | -- List[Dictionary]: account_list (a list of accounts each described by a dictionary with keys 'name' and 'id') 19 | """ 20 | def list_accounts(): 21 | account_list = [] 22 | org = boto3.client('organizations') 23 | paginator = org.get_paginator('list_accounts') 24 | page_iterator = paginator.paginate() 25 | 26 | for page in page_iterator: 27 | for acct in page['Accounts']: 28 | # only add active accounts 29 | if acct['Status'] == 'ACTIVE': 30 | account_list.append({'name': acct['Name'], 'id': acct['Id']}) 31 | 32 | return account_list 33 | 34 | """ 35 | list_existing_sso_instances 36 | 37 | Lists the SSO instances that the caller has access to. 38 | 39 | Parameters: 40 | -- None 41 | Returns: 42 | -- List[Dictionary]: sso_instance_list (a list of sso instances each described by a dictionary with keys 'instanceArn' and 'identityStore') 43 | """ 44 | def list_existing_sso_instances(): 45 | client = boto3.client('sso-admin') 46 | 47 | sso_instance_list = [] 48 | response = client.list_instances() 49 | for sso_instance in response['Instances']: 50 | # add only relevant keys to return 51 | sso_instance_list.append({'instanceArn': sso_instance["InstanceArn"], 'identityStore': sso_instance["IdentityStoreId"]}) 52 | 53 | return sso_instance_list 54 | 55 | """ 56 | list_permission_sets 57 | 58 | Lists the PermissionSet in an SSO instance. 59 | 60 | Parameters: 61 | -- String: ssoInstanceArn 62 | Returns: 63 | -- Dictionary: perm_set_dict (a dictionary with permission sets with key permission set name and value permission set arn) 64 | """ 65 | def list_permission_sets(ssoInstanceArn): 66 | client = boto3.client('sso-admin') 67 | 68 | perm_set_dict = {} 69 | 70 | response = client.list_permission_sets(InstanceArn=ssoInstanceArn) 71 | 72 | results = response["PermissionSets"] 73 | while "NextToken" in response: 74 | response = client.list_permission_sets(InstanceArn=ssoInstanceArn, NextToken=response["NextToken"]) 75 | results.extend(response["PermissionSets"]) 76 | 77 | for permission_set in results: 78 | # get the name of the permission set from the arn 79 | perm_description = client.describe_permission_set(InstanceArn=ssoInstanceArn,PermissionSetArn=permission_set) 80 | # key: permission set name, value: permission set arn 81 | perm_set_dict[perm_description["PermissionSet"]["Name"]] = permission_set 82 | 83 | 84 | return perm_set_dict 85 | 86 | 87 | """ 88 | list_account_assignments 89 | 90 | Lists the assignee of the specified AWS account with the specified permission set. 91 | 92 | Parameters: 93 | -- String: ssoInstanceArn 94 | -- String: accountId 95 | -- String: permissionSetArn 96 | Returns: 97 | -- List[Dictionary]: account_assignments (a list of account assignments represented by dictionaries with the keys 'PrincipalType' and 'PrincipalId') 98 | """ 99 | def list_account_assignments(ssoInstanceArn, accountId, permissionSetArn): 100 | client = boto3.client('sso-admin') 101 | 102 | paginator = client.get_paginator("list_account_assignments") 103 | 104 | response_iterator = paginator.paginate( 105 | InstanceArn=ssoInstanceArn, 106 | AccountId=accountId, 107 | PermissionSetArn=permissionSetArn 108 | ) 109 | 110 | account_assignments = [] 111 | for response in response_iterator: 112 | for row in response['AccountAssignments']: 113 | # add only relevant keys to return 114 | account_assignments.append({'PrincipalType': row['PrincipalType'], 'PrincipalId': row['PrincipalId']}) 115 | 116 | return account_assignments 117 | 118 | """ 119 | describe_user 120 | 121 | Retrieves the user metadata and attributes from user id in an identity store to return a human friendly username. 122 | 123 | Parameters: 124 | -- String: userId 125 | -- String: identityStoreId 126 | Returns: 127 | -- String: username (a human friendly username for the user id) 128 | """ 129 | def describe_user(userId, identityStoreId): 130 | client = boto3.client('identitystore') 131 | try: 132 | response = client.describe_user( 133 | IdentityStoreId=identityStoreId, 134 | UserId=userId 135 | ) 136 | username = response['UserName'] 137 | 138 | return username 139 | except Exception as e: 140 | print("[WARN] User was deleted while the report was running: " + str(userId)) 141 | username = "USER-GROUP" 142 | return username 143 | 144 | """ 145 | describe_group 146 | 147 | Retrieves the group metadata and attributes from group id in an identity store to return a human friendly group name. 148 | 149 | Parameters: 150 | -- String: groupId 151 | -- String: identityStoreId 152 | Returns: 153 | -- String: groupname (a human friendly groupname for the group id) 154 | """ 155 | def describe_group(groupId, identityStoreId): 156 | client = boto3.client('identitystore') 157 | try: 158 | response = client.describe_group( 159 | IdentityStoreId=identityStoreId, 160 | GroupId=groupId 161 | ) 162 | groupname = response['DisplayName'] 163 | return groupname 164 | except Exception as e: 165 | print("[WARN] Group was deleted while the report was running: " + str(groupId)) 166 | groupname = "DELETED-GROUP" 167 | return groupname 168 | 169 | """ 170 | create_report 171 | 172 | Creates a report of the assigned permissions on users for all accounts in an organization. 173 | 174 | Parameters: 175 | -- List[Dictionary]: account_list (a list of accounts each described by a dictionary with keys 'name' and 'id') 176 | -- List[Dictionary]: sso_instance (a list of sso instances each described by a dictionary with keys 'instanceArn' and 'identityStore') 177 | -- Dictionary: permission_sets_list (a dictionary with permission sets with key permission set name and value permission set arn) 178 | Returns: 179 | -- List[Dictionary]: result (a list of dictionaries with keys 'AccountID', 'AccountName', 'PermissionSet', 'ObjectName', 'ObjectType') 180 | """ 181 | def create_report(account_list, sso_instance, permission_sets_list, break_after=None): 182 | result = [] 183 | 184 | # variables for displaying the progress of processed accounts 185 | length = str(len(account_list)) 186 | i = 1 187 | 188 | for account in account_list: 189 | for permission_set in permission_sets_list.keys(): 190 | # get all the users assigned to a permission set on the current account 191 | account_assignments = list_account_assignments(sso_instance['instanceArn'], account['id'], permission_sets_list[permission_set]) 192 | 193 | # add the users and additional information to the sso report result 194 | for account_assignment in account_assignments: 195 | account_assignments_dic = {} 196 | 197 | # add information for all the headers 198 | account_assignments_dic['AccountID'] = account['id'] 199 | account_assignments_dic['AccountName'] = account['name'] 200 | account_assignments_dic['PermissionSet'] = permission_set 201 | account_assignments_dic['ObjectType'] = account_assignment['PrincipalType'] 202 | 203 | # find human friendly name for user id if principal type is "USER" 204 | if account_assignments_dic['ObjectType'] == "USER": 205 | username = describe_user(account_assignment['PrincipalId'], sso_instance['identityStore']) 206 | account_assignments_dic['ObjectName'] = username 207 | # find human friendly name for group id if principal type is "GROUP" 208 | elif account_assignments_dic['ObjectType'] == "GROUP": 209 | groupname = describe_group(account_assignment['PrincipalId'], sso_instance['identityStore']) 210 | account_assignments_dic['ObjectName'] = groupname 211 | 212 | result.append(account_assignments_dic) 213 | 214 | # display the progress of processed accounts 215 | print(str(i) + "/" + length + " accounts done") 216 | i = i+1 217 | 218 | # debug code used for stopping after a certain amound of accounts for faster testing 219 | if break_after != None and i > break_after: 220 | break 221 | 222 | return result 223 | 224 | """ 225 | write_result_to_file 226 | 227 | Writes a list of dictionaries to a csv file. 228 | 229 | Parameters: 230 | -- String: result 231 | Returns: 232 | -- None 233 | Output: 234 | -- CSV file: CSV with SSO report. 235 | """ 236 | def write_result_to_file(result): 237 | filename = 'sso_report_Account_Assignments_' + datetime.now().strftime("%Y-%m-%d_%H.%M.%S") + '.csv' 238 | filename = clean_filename(filename) 239 | with open(filename, 'w', newline='') as csv_file: 240 | fieldnames = ['AccountID', 'AccountName', 'ObjectType', 'ObjectName', 'PermissionSet'] # The header/column names 241 | writer = csv.DictWriter(csv_file, fieldnames=fieldnames) 242 | 243 | writer.writeheader() 244 | for row in result: 245 | writer.writerow(row) 246 | 247 | def print_time_taken(start, end): 248 | elapsed_time = end - start 249 | elapsed_time_string = str(int(elapsed_time/60)) + " minutes and " + str(int(elapsed_time%60)) + " seconds" 250 | print("The report took " + elapsed_time_string + " to generate.") 251 | 252 | def clean_filename(filename, replace=' ', char_limit=255): 253 | #allowed chars 254 | valid_filename_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) 255 | 256 | # replace spaces 257 | for r in replace: 258 | filename = filename.replace(r,'_') 259 | 260 | # keep only valid ascii chars 261 | cleaned_filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode() 262 | 263 | # keep only whitelisted chars 264 | cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) 265 | if len(cleaned_filename)>char_limit: 266 | print("Warning, filename truncated because it was over {}. Filenames may no longer be unique".format(char_limit)) 267 | return cleaned_filename[:char_limit] 268 | 269 | """ 270 | main 271 | 272 | Output: 273 | -- CSV file: CSV with SSO report. 274 | """ 275 | def main(): 276 | start = time.time() 277 | account_list = list_accounts() 278 | sso_instance = list_existing_sso_instances()[0] 279 | permission_sets_list = list_permission_sets(sso_instance['instanceArn']) 280 | result = create_report(account_list, sso_instance, permission_sets_list) 281 | write_result_to_file(result) 282 | 283 | # print the time it took to generate the report 284 | end = time.time() 285 | print_time_taken(start, end) 286 | 287 | main() 288 | --------------------------------------------------------------------------------