diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ea2b77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.json +settings.py \ No newline at end of file diff --git a/get_folders.py b/get_folders.py new file mode 100644 index 0000000..05b533d --- /dev/null +++ b/get_folders.py @@ -0,0 +1,88 @@ +import subprocess +import json +from settings import ORGANIZATION_ID + +FOLDER_LIST_CMD = 'gcloud resource-manager folders list --format json' +FOLDER_IAM_CMD = 'gcloud resource-manager folders get-iam-policy --format json' + +def get_reverse_path(folder_id, path): + displayName = folders[folder_id]['displayName'] + parent = folders[folder_id]['parent'] + + parts = parent.split('/') + type = parts[0] + parent_id = parts[1] + if type == 'folders': + return get_reverse_path(folder_id=parent_id, + path=(path + '/' + displayName)) + else: + return path + '/' + displayName + return parent_id + + +def get_proper_path(reverse_path): + parts = reverse_path.split('/') + parts.reverse() + + path = '' + for part in parts: + if part: + path = path + '/' + part + + return path + + +def get_path(folder_id): + displayName = folders[folder_id]['displayName'] + parent = folders[folder_id]['parent'] + + parts = parent.split('/') + type = parts[0] + parent_id = parts[1] + if type != 'folders': + return '/' + displayName + + parent_path = folders[parent_id].get('path') + if parent_path: + return parent_path + '/' + displayName + + reverse_path = get_reverse_path(folder_id=folder_id, path='') + return get_proper_path(reverse_path=reverse_path) + + +def get_folders(folder_id, + type_flag='--folder'): + + list_cmd = FOLDER_LIST_CMD + ' ' + type_flag + '=' + str(folder_id) + folder_list = json.loads(subprocess.check_output(list_cmd, + shell=True, + stderr=subprocess.STDOUT)) + + for folder in folder_list: + name = folder['name'] + id = name.split('/')[1] + iam_cmd = FOLDER_IAM_CMD + ' ' + id + folder['perm'] = json.loads(subprocess.check_output(iam_cmd, + shell=True, + stderr=subprocess.STDOUT)) + folders[id] = folder + get_folders(folder_id=id) + + return folders + + +def add_paths(): + for folder_id in folders: + folder = folders[folder_id] + # reverse_path = get_reverse_path(folder_id=folder_id, path='') + folder['path'] = get_path(folder_id=folder_id) + + +folders = {} +folders = get_folders(folder_id=ORGANIZATION_ID, + type_flag='--organization') + +add_paths() + +with open('folders.json', 'w') as outfile: + json.dump(folders, outfile, ensure_ascii=False) diff --git a/get_projects.py b/get_projects.py new file mode 100644 index 0000000..cdbe24b --- /dev/null +++ b/get_projects.py @@ -0,0 +1,53 @@ +import subprocess +import json +from settings import APPS_SCRIPT_FOLDER_ID, EXCLUDED_PROJECTS + +IAM_POLICY_CMD = 'gcloud projects get-iam-policy --format json' + +def init_projects(): + projects = {} + project_list_cmd = 'gcloud projects list --format json' + + print('initializing projects') + proj_list = json.loads(subprocess.check_output(project_list_cmd, + shell=True, + stderr=subprocess.STDOUT)) + for proj in proj_list: + project_id = proj['projectId'] + if proj['parent']['id'] != APPS_SCRIPT_FOLDER_ID: + projects[project_id] = { + 'project': proj + } + return projects + + +def write_projects(projects): + with open('projects.json', 'w') as outfile: + json.dump(projects, outfile, ensure_ascii=False) + + +def read_projects(): + try: + with open('projects.json', 'r') as jsonfile: + return json.load(jsonfile) + except Exception: + projects = init_projects() + write_projects(projects=projects) + return projects + + +def get_iam_policy(project_id): + if project_id not in EXCLUDED_PROJECTS: + get_iam_policy_cmd = IAM_POLICY_CMD + ' ' + project_id + return json.loads(subprocess.check_output(get_iam_policy_cmd, + shell=True, + stderr=subprocess.STDOUT)) + + +projects = read_projects() +for project_id in projects: + proj = projects[project_id] + if not proj.get('iam_policy'): + print('reading iam_policy for ' + project_id) + proj['iam_policy'] = get_iam_policy(project_id=project_id) + write_projects(projects=projects) diff --git a/owner_report.py b/owner_report.py new file mode 100644 index 0000000..5394ed7 --- /dev/null +++ b/owner_report.py @@ -0,0 +1,78 @@ +import json +import time +import pandas as pd + +def get_entry(member, role, project): + parts = member.split(':') + type = parts[0] + email = parts[1] + + entry = { + 'project': project, + 'role': role, + 'type': type, + 'email': email + } + + if project['parent']['type'] == 'folder': + parent_id = project['parent']['id'] + folder = folders.get(parent_id) + entry['project']['parent']['folder'] = folder['displayName'] + entry['project']['parent']['path'] = folder['path'] + + return entry + + +projects_by_user = {} +entries = [] +folders = {} + +df = pd.DataFrame() + +with open('folders.json', 'r') as jsonfile: + folders = json.load(jsonfile) + +with open('projects.json', 'r') as jsonfile: + projects = json.load(jsonfile) + + for project_id in projects.keys(): + proj = projects[project_id] + + try: + for binding in proj['iam_policy']['bindings']: + for member in binding['members']: + entry = get_entry(member=member, + role=binding['role'], + project=proj['project']) + + + entry_df = pd.json_normalize(entry) + entries.append(entry_df) + + #local_part = entry['email'].split('@')[0].lower() + #if not projects_by_user.get(local_part): + # projects_by_user[local_part] = [] + + #projects_by_user[local_part].append(entry) + except: + pass + +#print(json.dumps(projects_by_user)) +df = df.append(other=entries) + +# get rid of the .'s in the column names created by json_normalize +df.columns = df.columns.str.replace(r".", "_") +df.columns = df.columns.str.replace("-", "_") + +# add the date of the audit so we can create a time series +df['audit_time'] = pd.Timestamp.now().isoformat() + +# convert all field values to string type +df = df.astype(str) + +# workaround for pandas v1.1.1, due to the fact that astype(str) will convert a np.nan to the literal string 'nan'.... +# so we'll just flip it back to a none type.... +df = df.replace(['nan'], [None]) + +#output to row delimited json +df.to_json(path_or_buf='owners_nldj.json',orient='records', lines=True, date_format='iso') \ No newline at end of file diff --git a/settings.default b/settings.default new file mode 100644 index 0000000..ee84cf2 --- /dev/null +++ b/settings.default @@ -0,0 +1,10 @@ +# the org id for the domain +# found at Google Cloud Platform -> [Select Domain] -> IAM & Admin -> Settings +ORGANIZATION_ID = '' + +# apps script project id. +# create a new app script project at script.google.com +APPS_SCRIPT_FOLDER_ID = '' + +# projects to exclude in the audit +EXCLUDED_PROJECTS = [] \ No newline at end of file