Last active
January 9, 2024 14:03
-
-
Save faisalfs10x/6951175a4857ae7ecb1412a1ea3d13f2 to your computer and use it in GitHub Desktop.
Retrieve user or group information from Microsoft 365 via Microsoft Graph API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
""" | |
# GitHub: https://github.com/faisalfs10x | |
# Usage | |
M365-msgraphenum.py -auth <token_file> -mode user | |
M365-msgraphenum.py -auth <token_file> -mode roles | |
M365-msgraphenum.py -auth <token_file> -mode group | |
M365-msgraphenum.py -auth <token_file> -mode groupmember -gid <group_id> | |
""" | |
import argparse | |
import requests | |
import json | |
import pandas as pd | |
from datetime import datetime | |
import jwt | |
import base64 | |
def read_bearer_token(auth_token_file): | |
with open(auth_token_file, 'r') as file: | |
return file.read().strip() | |
def get_headers(bearer_token): | |
return { | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0', | |
'Authorization': f'Bearer {bearer_token}', | |
} | |
def get_user_profile(headers): | |
profile_url = 'https://graph.microsoft.com/v1.0/me' | |
resp_profile = requests.get(profile_url, headers=headers) | |
display_name = resp_profile.json().get('displayName') | |
user_principal_name = resp_profile.json().get('userPrincipalName') | |
if display_name is not None and user_principal_name is not None: | |
print(f'\n[+] Successfully authenticated with {display_name} | {user_principal_name} [+]') | |
else: | |
print(f'\n[-] Failed to authenticate. Pls check the token file [-]\n') | |
exit() | |
return display_name, user_principal_name | |
def get_group_display_name(group_id, headers): | |
group_url = f'https://graph.microsoft.com/v1.0/groups/{group_id}' | |
response = requests.get(group_url, headers=headers) | |
return response.json().get("displayName") | |
def process_user_data(data): | |
users_data = [] | |
user_count = 0 | |
for user in data.get('value', []): | |
user_data = { | |
'id': user.get('id'), | |
'displayName': user.get('displayName'), | |
'businessPhones': user.get('businessPhones'), | |
'givenName': user.get('givenName'), | |
'jobTitle': user.get('jobTitle'), | |
'mail': user.get('mail'), | |
'mobilePhone': user.get('mobilePhone'), | |
'officeLocation': user.get('officeLocation'), | |
'preferredLanguage': user.get('preferredLanguage'), | |
'surname': user.get('surname'), | |
'userPrincipalName': user.get('userPrincipalName'), | |
} | |
users_data.append(user_data) | |
user_count += 1 | |
print(f'{user_count}. ID: {user_data["id"]} | Name: {user_data["displayName"]} | UPN: {user_data["userPrincipalName"]}') | |
return users_data, user_count | |
def process_group_data(data): | |
groups_data = [] | |
group_count = 0 | |
for group in data.get('value', []): | |
group_data = { | |
'id': group.get('id'), | |
'displayName': group.get('displayName'), | |
'description': group.get('description'), | |
'isAssignableToRole': group.get('isAssignableToRole'), | |
'mail': group.get('mail'), | |
'securityEnabled': group.get('securityEnabled'), | |
'onPremisesDomainName': group.get('onPremisesDomainName'), | |
'onPremisesSamAccountName': group.get('onPremisesSamAccountName'), | |
'onPremisesSecurityIdentifier': group.get('onPremisesSecurityIdentifier'), | |
'securityIdentifier': group.get('securityIdentifier'), | |
'onPremisesSyncEnabled': group.get('onPremisesSyncEnabled'), | |
'proxyAddresses': group.get('proxyAddresses'), | |
} | |
groups_data.append(group_data) | |
group_count += 1 | |
print(f'{group_count}. ID: {group_data["id"]} | Name: {group_data["displayName"]}') | |
return groups_data, group_count | |
def get_tenant_id(bearer_token): | |
# Decode the JWT token to obtain the Tenant ID | |
token_segments = bearer_token.split('.') | |
decoded_token = json.loads(base64.b64decode(token_segments[1] + '==' * (-len(token_segments[1]) % 4)).decode('utf-8')) | |
return decoded_token.get('tid') | |
def get_organization_name(headers): | |
organization_url = 'https://graph.microsoft.com/v1.0/organization' | |
response = requests.get(organization_url, headers=headers) | |
organization_data = response.json() | |
organization_name = organization_data['value'][0].get('displayName', None) | |
return organization_name | |
def retrieve_users(auth_token_file): | |
bearer_token = read_bearer_token(auth_token_file) | |
headers = get_headers(bearer_token) | |
mydisplay_name, myuserPrincipalName = get_user_profile(headers) | |
org_name = get_organization_name(headers) | |
current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") | |
#excel_file_path = f'userlist_{org_name}_{current_datetime}.xlsx' | |
users_data = [] | |
user_count = 0 | |
page_count = 2 | |
url = 'https://graph.microsoft.com/v1.0/users' | |
org_name = get_organization_name(headers) | |
tenant_id = get_tenant_id(bearer_token) | |
print(f'\n[+] Gather list of users in [{org_name}] with TenantID: [{tenant_id}] [+]\n') | |
while url: | |
response = requests.get(url, headers=headers) | |
data = response.json() | |
# Extract and process relevant data | |
user_data, count = process_user_data(data) | |
users_data.extend(user_data) | |
user_count += count | |
# Print to console when the next page is found | |
print(f'\n[X] Processing page {page_count} of [{org_name}] [X]') | |
page_count += 1 | |
# Check if there is the next page | |
url = data.get('@odata.nextLink') | |
# Create a DataFrame from the extracted data | |
df = pd.DataFrame(users_data) | |
# Export the DataFrame to Excel file | |
excel_file_path = f'userlist({user_count})_{org_name}_{current_datetime}.xlsx' | |
df.to_excel(excel_file_path, index=False) | |
print(f'\nData exported to {excel_file_path}') | |
print(f'Total number of users: {user_count}') | |
def retrieve_groups(auth_token_file): | |
bearer_token = read_bearer_token(auth_token_file) | |
headers = get_headers(bearer_token) | |
mydisplay_name, myuserPrincipalName = get_user_profile(headers) | |
org_name = get_organization_name(headers) | |
current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") | |
#excel_file_path = f'grouplist_{org_name}_{current_datetime}.xlsx' | |
groups_data = [] | |
group_count = 0 | |
page_count = 2 | |
url = 'https://graph.microsoft.com/v1.0/groups' | |
org_name = get_organization_name(headers) | |
tenant_id = get_tenant_id(bearer_token) | |
print(f'\n[+] Gather list of groups in [{org_name}] with TenantID: [{tenant_id}] [+]\n') | |
while url: | |
response = requests.get(url, headers=headers) | |
data = response.json() | |
# Extract and process relevant data | |
group_data, count = process_group_data(data) | |
groups_data.extend(group_data) | |
group_count += count | |
# Print to console when the next page is found | |
print(f'\n[X] Processing page {page_count} of [{org_name}] [X]') | |
page_count += 1 | |
# Check if there is the next page | |
url = data.get('@odata.nextLink') | |
# Create a DataFrame from the extracted data | |
df = pd.DataFrame(groups_data) | |
# Export the DataFrame to Excel file | |
excel_file_path = f'grouplist({group_count})_{org_name}_{current_datetime}.xlsx' | |
df.to_excel(excel_file_path, index=False) | |
print(f'\nData exported to {excel_file_path}') | |
print(f'Total number of groups: {group_count}') | |
def retrieve_directoryroles(auth_token_file): | |
bearer_token = read_bearer_token(auth_token_file) | |
headers = get_headers(bearer_token) | |
mydisplay_name, myuserPrincipalName = get_user_profile(headers) | |
org_name = get_organization_name(headers) | |
current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") | |
org_name = get_organization_name(headers) | |
tenant_id = get_tenant_id(bearer_token) | |
print(f'\n[+] Gather list of directory roles in [{org_name}] with TenantID: [{tenant_id}] [+]\n') | |
url = "https://graph.microsoft.com/v1.0/directoryRoles" | |
response = requests.get(url, headers=headers) | |
if response.status_code == 200: | |
data = response.json() | |
# Extract the values | |
roles = data.get("value", []) | |
# Create an empty list to store the data | |
all_data = [] | |
# Process each role and extract id, displayName and Description | |
for role_num, role in enumerate(roles, start=1): | |
role_id = role.get("id") | |
display_name = role.get("displayName") | |
description = role.get("description") | |
# Request to the members endpoint for each role | |
members_url = f'https://graph.microsoft.com/v1.0/directoryRoles/{role_id}/members' | |
members_response = requests.get(members_url, headers=headers) | |
if members_response.status_code == 200: | |
members_data = members_response.json() | |
# Extract member information | |
members = members_data.get("value", []) | |
# Append role and member information to the list | |
for member_num, member in enumerate(members, start=1): | |
member_info = {"Role ID": role_id, "Display Name": display_name, "Description" : description} | |
member_info.update(member) | |
all_data.append(member_info) | |
# Print member information to console | |
print(f"\n{role_num}-{member_num} - Role ID: {role_id}, Display Name: {display_name}, Member Information:") | |
for key, value in member.items(): | |
print(f" {key}: {value}") | |
print("-----------------------------------------------------") | |
else: | |
print(f"Error getting members for Role ID {role_id}: {members_response.status_code}") | |
else: | |
print(f"Error: {response.status_code}") | |
# Create a DataFrame from the list of data | |
df = pd.DataFrame(all_data) | |
# Print DataFrame to console | |
print("\nDataFrame:") | |
print(df) | |
# Save the DataFrame to an Excel file | |
excel_file_path = f'directoryrole_members_{org_name}_{current_datetime}.xlsx' | |
df.to_excel(excel_file_path, index=False) | |
print(f"\nData saved to {excel_file_path}") | |
def retrieve_users_in_group(group_id, auth_token_file): | |
bearer_token = read_bearer_token(auth_token_file) | |
headers = get_headers(bearer_token) | |
mydisplay_name, myuserPrincipalName = get_user_profile(headers) | |
users_data = [] | |
user_count = 0 | |
page_count = 2 | |
org_name = get_organization_name(headers) | |
tenant_id = get_tenant_id(bearer_token) | |
print(f'\n[+] Organization [{org_name}] with TenantID: [{tenant_id}] [+]') | |
group_display_name = get_group_display_name(group_id, headers) | |
current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") | |
invalid_chars = {'/', ':', '*', '?', '"', '<', '>', '|'} | |
sanitized_group_name = ''.join('_' if char in invalid_chars else char for char in group_display_name) | |
#excel_file_path = f'member_{sanitized_group_name}_group_{current_datetime}.xlsx' | |
print(f'\n[+] Gather list of users in group: {group_display_name} [+]') | |
endpoint_url = f'https://graph.microsoft.com/v1.0/groups/{group_id}/members?$count=true' | |
while endpoint_url: | |
response = requests.get(endpoint_url, headers=headers) | |
data = response.json() | |
# Extract and process relevant data | |
user_data, count = process_user_data(data) | |
users_data.extend(user_data) | |
user_count += count | |
# Print to console when the next page is found | |
print(f'\n[X] Processing page {page_count} of [{group_display_name}] [X]') | |
page_count += 1 | |
# Check if there is the next page | |
endpoint_url = data.get('@odata.nextLink') | |
# Create a DataFrame from the extracted data | |
df = pd.DataFrame(users_data) | |
# Add a title row with the group display name to the DataFrame | |
title_row = pd.DataFrame({'Title': [group_display_name]}) | |
df = pd.concat([title_row, df], ignore_index=False) | |
# Export the DataFrame to Excel file | |
excel_file_path = f'member({user_count})_{sanitized_group_name}_group_{current_datetime}.xlsx' | |
df.to_excel(excel_file_path, index=False) | |
print(f'\nData exported to {excel_file_path}') | |
print(f'Total number of users in group: {user_count}') | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description='Retrieve user or group information from Microsoft 365') | |
parser.add_argument('-auth', '--auth_token_file', type=str, required=True, help='File containing the Bearer token') | |
parser.add_argument('-mode', '--mode', type=str, choices=['user', 'group', 'groupmember', 'roles'], required=True, help='Operation mode') | |
parser.add_argument('-gid', '--group_id', type=str, help='ID of the group (for retrieving users in a group)') | |
args = parser.parse_args() | |
if args.mode == 'user': | |
retrieve_users(args.auth_token_file) | |
elif args.mode == 'group': | |
retrieve_groups(args.auth_token_file) | |
elif args.mode == 'groupmember' and args.group_id: | |
retrieve_users_in_group(args.group_id, args.auth_token_file) | |
elif args.mode == 'roles': | |
retrieve_directoryroles(args.auth_token_file) | |
else: | |
print("[-] Invalid mode or missing -gid [-]") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment