top of page
Search

Identifying Over-Privileged Identities Using Microsoft Graph

  • Writer: Shannon
    Shannon
  • 4 minutes ago
  • 6 min read

All code for this blog can be found here.


I keep hearing more and more interest by customers I work with in exploring Cloud Infrastructure Entitlement Management (CIEM) solutions. Their focus is usually on cloud security and governance, and a key question usually surfaces (mostly because CIEM usually costs them more money): What would the level of effort look like to identify over-privileged identities using cloud-native tooling such as Microsoft Graph?


This is not an unfair ask as enterprises grow into their overall cloud adoption. Organizations are increasingly concerned with who has access to what, and whether those privileges are appropriate. The truth is that over-privileged identities are one of the easiest ways for attackers to move through an environment. Once they find an account with elevated rights, the attacker can escalate privileges, disable controls, or gain persistence. Identity has become the new perimeter, which means governance is not optional.


Customers I work with are curious about the gap between dedicated CIEM platforms and what could be achieved with Microsoft’s own APIs and services. That is where the comparison gets interesting.


CIEM vs Native Tooling

CIEM platforms are built to provide comprehensive visibility across multi-cloud and hybrid environments. They aggregate entitlements, detect risky combinations of permissions, and often apply advanced analytics to identify toxic privilege chains. In environments with AWS, Azure, and Google Cloud running side by side, CIEM can save enormous amounts of manual effort.


However, many organizations want to start small and lean into what they already own. Microsoft Graph and Entra ID already contain the information you need to identify who is assigned to privileged roles. While native approaches will not deliver all of the context and cross-cloud insight that CIEM platforms provide, they do allow you to:


  • Discover identities with elevated privileges

  • Build custom reports or dashboards

  • Automate remediation steps with scripts or workflows


I've found this can be a powerful starting point. For many customers, the right path may be to first use Microsoft Graph to clean up their own house, then layer in CIEM tooling when they need broader visibility or advanced risk modeling (or even additional clouds as they expand multi-cloud adoption).


What Are Over-Privileged Identities?

Over-privileged identities are accounts, service principals, or applications that have more permissions than they need. They are common in every environment for several reasons:


  • Legacy projects that leave behind elevated accounts

  • Emergency access assignments that are never removed

  • Lack of controls around role assignment approvals

  • Growth in service principals and automation without governance


Some roles are inherently high risk. Common examples in Entra ID include:

  • Global Administrator

  • Privileged Role Administrator

  • Application Administrator


These roles provide sweeping powers, which makes them high-value targets for attackers. Even if the assignment was intentional, it should be regularly reviewed and challenged.


Why Microsoft Graph?

Microsoft Graph API provides a single endpoint to query Entra ID and other Microsoft 365 services. For identity governance, it allows administrators to:


  • Enumerate all roles in Entra ID

  • Identify who or what is assigned to those roles

  • Integrate this information into scripts, dashboards, or automation workflows


While CIEM tools give you broad and contextual analysis, Microsoft Graph provides a straightforward, native way to begin. If you need to answer questions like who are my global admins right now or which service principals hold privileged roles, I've found that Graph is the most direct route.


Getting Started with Microsoft Graph

Before you can query for privileged assignments, you need a few pieces in place.


Prerequisites

  • Entra ID Premium P1 or P2 for advanced governance

  • A Microsoft 365 tenant with administrative access

  • PowerShell, Python, or another scripting environment

  • Microsoft Graph PowerShell SDK or REST API access


Register an App in Entra ID (Used for the Python Script)

  1. Navigate to Azure Portal → Entra ID → App registrations

  2. Create a new registration (for example, Over-Privileged Identity Detector)

  3. Add the following API permissions:

  4. Generate a client secret and save it securely


Option 1: PowerShell Script


Install the Microsoft Graph SDK

Install-Module -Name Microsoft.Graph -Scope CurrentUser


Example PowerShell Script: Identifying High-Privilege Assignments

# Import Microsoft Graph module
Import-Module Microsoft.Graph

# Connect to Microsoft Graph with required scopes
Connect-MgGraph -Scopes "RoleManagement.Read.All", "Directory.Read.All"

# Function to fetch privileged roles
function Get-PrivilegedIdentities {
    Write-Host "Fetching privileged roles..." -ForegroundColor Yellow

    # Get all directory roles
    $roles = Get-MgDirectoryRole

    # Define high-privilege roles
    $highPrivilegeRoles = @(
        "Global Administrator",
        "Privileged Role Administrator",
        "Application Administrator"
    )

    foreach ($role in $roles) {
        if ($highPrivilegeRoles -contains $role.DisplayName) {
            Write-Host "`nRole: $($role.DisplayName)" -ForegroundColor Cyan
            $members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id

            foreach ($member in $members) {
                $memberDetails = Get-MgUser -UserId $member.Id -ErrorAction SilentlyContinue
                if ($memberDetails) {
                    Write-Host "User: $($memberDetails.DisplayName) ($($memberDetails.UserPrincipalName))" -ForegroundColor Green
                } else {
                    Write-Host "Service Principal: $($member.Id)" -ForegroundColor Magenta
                }
            }
        }
    }
}

# Execute the function
Get-PrivilegedIdentities

Example Output

Fetching privileged roles...

Role: Global Administrator
User: Shannon Eldridge-Kuehn ([email protected])

How the Script Works

  • Authentication: Connects to Microsoft Graph with the required scopes

  • Fetching Roles: Retrieves all directory roles from Entra ID

  • Filtering: Focuses on Global Administrator, Privileged Role Administrator, and Application Administrator

  • Listing Members: Outputs the users and service principals assigned to those roles


This approach gives you immediate visibility into the identities that matter most.


Option 2: Python Script

For engineers and security teams who prefer Python, the script below provides the same functionality. It prompts for Tenant ID, Client ID, and Client Secret at runtime, then retrieves high-privilege assignments. It also exports the results to a CSV file for reporting.


Install Dependencies

pip install msal requests

Example Script

import requests
import msal
import csv
from datetime import datetime
import getpass

TENANT_ID = input("Enter your Tenant ID: ").strip()
CLIENT_ID = input("Enter your Client ID: ").strip()
CLIENT_SECRET = getpass.getpass("Enter your Client Secret: ").strip()

AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPE = ["https://graph.microsoft.com/.default"]
GRAPH_API_ENDPOINT = "https://graph.microsoft.com/v1.0"

HIGH_PRIVILEGE_ROLES = [
    "Global Administrator",
    "Privileged Role Administrator",
    "Application Administrator"
]

def get_access_token():
    app = msal.ConfidentialClientApplication(
        CLIENT_ID,
        authority=AUTHORITY,
        client_credential=CLIENT_SECRET
    )
    result = app.acquire_token_silent(SCOPE, account=None)
    if not result:
        result = app.acquire_token_for_client(scopes=SCOPE)
    if "access_token" in result:
        return result["access_token"]
    else:
        raise Exception("Failed to obtain access token.")

def get_directory_roles(access_token):
    headers = {"Authorization": f"Bearer {access_token}"}
    url = f"{GRAPH_API_ENDPOINT}/directoryRoles"
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json().get("value", [])

def get_role_members(access_token, role_id):
    headers = {"Authorization": f"Bearer {access_token}"}
    url = f"{GRAPH_API_ENDPOINT}/directoryRoles/{role_id}/members"
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json().get("value", [])

def main():
    print("Fetching privileged identities from Entra ID...")
    token = get_access_token()
    roles = get_directory_roles(token)
    results = []

    for role in roles:
        if role.get("displayName") in HIGH_PRIVILEGE_ROLES:
            print(f"\nRole: {role['displayName']}")
            members = get_role_members(token, role["id"])
            if not members:
                print("  No members found.")
            for member in members:
                member_type = member.get("@odata.type", "")
                if "user" in member_type.lower():
                    entry = {
                        "Role": role["displayName"],
                        "Type": "User",
                        "Name": member.get("displayName"),
                        "Identifier": member.get("userPrincipalName")
                    }
                elif "serviceprincipal" in member_type.lower():
                    entry = {
                        "Role": role["displayName"],
                        "Type": "Service Principal",
                        "Name": member.get("displayName"),
                        "Identifier": member.get("appId")
                    }
                else:
                    entry = {
                        "Role": role["displayName"],
                        "Type": "Other",
                        "Name": str(member),
                        "Identifier": ""
                    }
                results.append(entry)
                print(f"  {entry['Type']}: {entry['Name']} ({entry['Identifier']})")

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"over_privileged_identities_{timestamp}.csv"
    with open(filename, mode="w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["Role", "Type", "Name", "Identifier"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(results)

    print(f"\nResults exported to {filename}")

if __name__ == "__main__":
    main()

Example Output

Enter your Tenant ID: 11111111-2222-3333-4444-555555555555
Enter your Client ID: abcdef12-3456-7890-abcd-ef1234567890
Enter your Client Secret: ************

Fetching privileged identities from Entra ID...

Role: Global Administrator
User: Shannon Eldridge-Kuehn ([email protected])

Results exported to over_privileged_identities_20250926_143215.csv

Next Steps

Once you have the list of privileged identities, you should:


  1. Review necessity - Validate whether the assignment is still required.

  2. Remove unnecessary privileges - Apply the principle of least privilege.

  3. Implement Privileged Identity Management (PIM). Use just-in-time role activation and approval workflows.

  4. Automate reporting - Schedule the script or build dashboards to continuously monitor role assignments.


If you later bring in a CIEM platform, these steps will make your environment cleaner and easier to manage.


Conclusion

Over-privileged identities are a persistent risk. CIEM platforms can give you a broad, multi-cloud view and advanced analytics, but you do not need to wait until you buy a tool to start addressing the problem. Microsoft Graph API provides native, scriptable access to the data you need.


By combining a simple script with a governance process, you can begin identifying, reviewing, and reducing risk today. Think of Microsoft Graph as the first step on the journey, with CIEM as the next layer that adds scale, visibility, and intelligence across your cloud platforms, unifying your identity ecosystem in a comprehensive way!

© 2020 Shannon B. Kuehn

  • LinkedIn
  • Twitter
bottom of page