npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
Overview
Microsoft Entra Privileged Identity Management (PIM) provides time-based and approval-based role activation to mitigate risks from excessive, unnecessary, or misused access to critical resources. PIM replaces permanent (standing) privilege assignments with eligible assignments that require users to explicitly activate their role before use, with configurable duration, MFA enforcement, approval workflows, and justification requirements. This is a core component of Zero Trust identity governance in Microsoft environments.
When to Use
- When deploying or configuring implementing azure ad privileged identity management capabilities in your environment
- When establishing security controls aligned to compliance requirements
- When building or improving security architecture for this domain
- When conducting security assessments that require this implementation
Prerequisites
- Microsoft Entra ID P2 or Microsoft Entra ID Governance license
- Global Administrator or Privileged Role Administrator role
- Azure subscription for Azure resource role management
- MFA configured for all privileged users
- Microsoft Authenticator or FIDO2 key for admin accounts
Core Concepts
Assignment Types
| Type | Behavior | Use Case |
|---|---|---|
| Eligible | User must activate the role before use; expires after configured duration | Day-to-day admin work |
| Active | Role is always active; no activation needed | Service accounts, break-glass accounts |
| Time-Bound | Either type with explicit start/end dates | Temporary project access, contractor access |
PIM Activation Flow
User with Eligible Assignment
│
├── Opens PIM portal → My Roles
│
├── Clicks "Activate" on the desired role
│
├── Provides justification and optional ticket number
│
├── Completes MFA challenge (if required)
│
├── [If approval required] → Notification sent to approvers
│ │
│ ├── Approver reviews and approves/denies
│ └── User notified of decision
│
├── Role activated for configured duration (e.g., 8 hours)
│
└── Role automatically deactivated when duration expiresSupported Resource Types
- Microsoft Entra Roles: Global Admin, Exchange Admin, Security Admin, etc.
- Azure Resource Roles: Owner, Contributor, User Access Administrator on subscriptions/resource groups
- PIM for Groups: Manage membership in privileged security groups
Workflow
Step 1: Plan Role Assignments
Audit current permanent role assignments and determine which should be converted to eligible:
| Current Role | Permanent Holders | Action |
|---|---|---|
| Global Administrator | 2-3 admins | Convert to eligible, keep 1 break-glass active |
| Exchange Administrator | IT team | Convert all to eligible |
| Security Administrator | SOC team | Convert to eligible |
| User Administrator | Help desk | Convert to eligible |
| Application Administrator | DevOps | Convert to eligible |
Best practice: Maintain no more than 2 permanent Global Administrators (break-glass accounts).
Step 2: Configure Role Settings
For each Entra directory role, configure PIM settings:
Via Microsoft Entra Admin Center:
- Navigate to Identity Governance > Privileged Identity Management > Microsoft Entra roles
- Select "Settings" and choose the role to configure
- Configure the following:
Activation Settings:
- Maximum activation duration: 8 hours (recommended; max 72 hours)
- Require MFA on activation: Enabled
- Require justification: Enabled
- Require ticket information: Enabled (for change management integration)
- Require approval: Enabled for Global Admin, Security Admin
Assignment Settings:
- Allow permanent eligible assignment: No (set expiry)
- Expire eligible assignments after: 6 months (requires re-certification)
- Allow permanent active assignment: Only for break-glass accounts
- Require MFA on active assignment: Enabled
- Require justification on active assignment: Enabled
Notification Settings:
- Send email when members are assigned eligible: Role assigners, admins
- Send email when members activate: Admins, security team
- Send email when eligible members activate roles: Role assignees
Step 3: Configure via Microsoft Graph API
import requests
# Acquire token for Microsoft Graph
def get_graph_token(tenant_id, client_id, client_secret):
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default"
}
response = requests.post(url, data=data)
return response.json()["access_token"]
# Create eligible role assignment
def create_eligible_assignment(token, role_definition_id, principal_id,
directory_scope="/", duration_hours=8):
url = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleRequests"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
body = {
"action": "adminAssign",
"justification": "PIM eligible assignment",
"roleDefinitionId": role_definition_id,
"directoryScopeId": directory_scope,
"principalId": principal_id,
"scheduleInfo": {
"startDateTime": "2025-01-01T00:00:00Z",
"expiration": {
"type": "afterDuration",
"duration": "P180D" # 180-day eligible window
}
}
}
response = requests.post(url, headers=headers, json=body)
return response.json()
# Activate a role (user self-service)
def activate_role(token, role_definition_id, principal_id, justification,
duration_hours=8):
url = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
body = {
"action": "selfActivate",
"principalId": principal_id,
"roleDefinitionId": role_definition_id,
"directoryScopeId": "/",
"justification": justification,
"scheduleInfo": {
"startDateTime": None, # Now
"expiration": {
"type": "afterDuration",
"duration": f"PT{duration_hours}H"
}
}
}
response = requests.post(url, headers=headers, json=body)
return response.json()Step 4: Configure Access Reviews
Set up recurring access reviews to verify eligible assignments remain appropriate:
- Navigate to Identity Governance > Access Reviews > New Access Review
- Configure:
- Review scope: Privileged Identity Management role assignments
- Roles: Select all critical roles (Global Admin, Security Admin, etc.)
- Reviewers: Managers or self-review with justification
- Frequency: Quarterly for critical roles, semi-annually for others
- Auto-apply results: Remove access for non-responsive reviews
- Duration: 14 days for reviewers to respond
Step 5: Configure Alerts
Enable PIM security alerts:
| Alert | Trigger | Action |
|---|---|---|
| Too many global admins | > 5 Global Admins | Review and reduce |
| Roles being assigned outside PIM | Direct role assignment | Investigate and convert to PIM |
| Roles not requiring MFA | Activation without MFA | Enable MFA requirement |
| Stale eligible assignments | Not activated in 90 days | Review and potentially remove |
| Potential stale service accounts | Active assignments not used | Investigate and decommission |
Validation Checklist
- All permanent privileged role assignments converted to eligible (except break-glass)
- Break-glass accounts configured as active with monitoring alerts
- MFA required for all role activations
- Approval workflow configured for Global Administrator and Security Administrator
- Maximum activation duration set to 8 hours or less for critical roles
- Eligible assignments expire after 6 months (requires re-certification)
- Justification and ticket information required for activations
- Email notifications configured for role assignments and activations
- Access reviews scheduled quarterly for all privileged roles
- PIM alerts enabled and reviewed weekly
- Audit logs forwarded to SIEM for monitoring
References
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md1.9 KB
API Reference: Azure AD PIM Audit Agent
Dependencies
| Library | Version | Purpose |
|---|---|---|
| requests | >=2.28 | HTTP client for Microsoft Graph API |
CLI Usage
python scripts/agent.py \
--tenant-id YOUR_TENANT_ID \
--client-id YOUR_CLIENT_ID \
--client-secret YOUR_SECRET \
--output-dir /reports/ \
--output pim_report.jsonFunctions
PIMClient(tenant_id, client_id, client_secret)
Authenticates via OAuth2 client credentials flow to Microsoft Graph API.
list_role_definitions() -> list
GET /roleManagement/directory/roleDefinitions - Available directory roles.
list_eligible_assignments() -> list
GET /roleManagement/directory/roleEligibilityScheduleInstances - PIM eligible roles.
list_active_assignments() -> list
GET /roleManagement/directory/roleAssignmentScheduleInstances - Active assignments.
list_role_settings() -> list
GET /policies/roleManagementPolicyAssignments - PIM policy configurations.
audit_permanent_assignments(active, eligible) -> list
Identifies permanent role assignments not managed via PIM eligible workflow.
compute_pim_coverage(active, eligible) -> dict
Calculates percentage of assignments managed through PIM.
Microsoft Graph Endpoints
| Endpoint | Purpose |
|---|---|
POST /oauth2/v2.0/token |
Client credentials auth |
GET /roleManagement/directory/roleDefinitions |
Role catalog |
GET /roleManagement/directory/roleEligibilityScheduleInstances |
Eligible assignments |
GET /roleManagement/directory/roleAssignmentScheduleInstances |
Active assignments |
Output Schema
{
"coverage": {"active_assignments": 15, "eligible_assignments": 42, "pim_coverage_pct": 73.7},
"permanent_assignments": [{"role": "Global Administrator", "recommendation": "Convert to eligible"}],
"recommendations": ["Convert 5 permanent assignments to PIM-eligible"]
}standards.md2.0 KB
Azure AD PIM - Standards Reference
Microsoft Entra ID Licensing
| Feature | Required License |
|---|---|
| PIM for Entra Roles | Entra ID P2 or Entra ID Governance |
| PIM for Azure Resources | Entra ID P2 or Entra ID Governance |
| PIM for Groups | Entra ID P2 or Entra ID Governance |
| Access Reviews | Entra ID P2 or Entra ID Governance |
| Conditional Access | Entra ID P1 (minimum) |
Critical Entra Directory Roles
| Role | Risk Level | Recommended PIM Setting |
|---|---|---|
| Global Administrator | Critical | Eligible, approval required, max 1hr activation |
| Privileged Role Administrator | Critical | Eligible, approval required |
| Security Administrator | High | Eligible, MFA required |
| Exchange Administrator | High | Eligible, MFA required |
| SharePoint Administrator | High | Eligible, MFA required |
| User Administrator | Medium | Eligible, MFA required |
| Application Administrator | High | Eligible, MFA required |
| Cloud Application Administrator | High | Eligible, MFA required |
| Intune Administrator | Medium | Eligible, justification required |
| Compliance Administrator | Medium | Eligible, justification required |
Compliance Framework Mapping
NIST SP 800-53 Rev 5
- AC-2(4): Automated Audit Actions (PIM audit logs)
- AC-2(5): Inactivity Logout (time-bound activations)
- AC-6(1): Authorize Access to Security Functions
- AC-6(2): Non-Privileged Access for Non-Security Functions
- AC-6(5): Privileged Accounts (eligible vs. active)
CIS Microsoft 365 Foundations Benchmark
- 1.1.1: Ensure MFA is enabled for all users in admin roles
- 1.1.3: Ensure that between two and four Global Admins are designated
- 1.1.6: Ensure Administrative accounts are separate and cloud-only
- 1.3.1: Ensure PIM is used to manage roles
SOC 2 Trust Service Criteria
- CC6.1: Logical and physical access controls
- CC6.2: Prior to issuing credentials, registration and authorization
- CC6.3: Authorize, modify, or remove access timely
workflows.md3.8 KB
Azure AD PIM - Workflows
PIM Deployment Workflow
Phase 1: DISCOVERY
├── Export all permanent role assignments via Microsoft Graph
├── Identify users with multiple admin roles
├── Flag accounts without MFA enabled
└── Document break-glass account strategy
Phase 2: PLANNING
├── Define activation settings per role (duration, MFA, approval)
├── Identify approvers for each critical role
├── Create communication plan for affected admins
└── Schedule pilot group for initial rollout
Phase 3: CONFIGURATION
├── Configure PIM role settings (activation, assignment, notification)
├── Convert permanent assignments to eligible (except break-glass)
├── Configure conditional access policies for admin activation
└── Enable audit logging and SIEM integration
Phase 4: TESTING
├── Test role activation with pilot users
├── Test approval workflow end-to-end
├── Test MFA enforcement during activation
├── Test auto-deactivation after duration expires
└── Validate audit logs capture all PIM events
Phase 5: ROLLOUT
├── Convert remaining permanent assignments to eligible
├── Notify all affected users with activation instructions
├── Monitor for activation failures and help desk tickets
└── Configure access reviews on quarterly scheduleRole Activation Workflow
Admin needs to perform privileged task
│
├── Navigate to PIM portal (Entra Admin Center > PIM > My Roles)
│
├── Click "Activate" on the needed role
│
├── Select activation duration (up to configured max)
│
├── Enter justification and optional ticket number
│
├── Complete MFA challenge
│
├── [If approval required]
│ ├── Request submitted to approvers
│ ├── Approvers receive email notification
│ ├── Approver reviews justification and approves/denies
│ └── Admin receives approval notification
│
├── Role becomes active
│
├── Admin performs required task
│
└── Role automatically deactivates when duration expires
(or admin manually deactivates early)Access Review Workflow
Quarterly Access Review Triggered
│
├── PIM sends review notifications to designated reviewers
│
├── For each eligible assignment:
│ ├── Reviewer checks: Is this role still needed?
│ ├── Reviewer checks: When was role last activated?
│ ├── Decision: Approve (maintain), Deny (remove), or Don't know
│ └── Provide justification for decision
│
├── Review period expires (14 days default)
│
├── Auto-apply results:
│ ├── Approved assignments maintained
│ ├── Denied assignments removed
│ └── No-response: configurable (remove or maintain)
│
└── Review summary report generated for complianceBreak-Glass Account Workflow
Normal Operations:
└── Break-glass accounts exist as ACTIVE Global Admin
├── Stored in secure physical safe (password printout)
├── Excluded from conditional access policies
├── Monitored by Azure Monitor alert rule
└── Monthly verification: confirm no unauthorized sign-ins
Emergency Use:
├── Primary admin methods unavailable (MFA outage, PIM issue)
├── Retrieve break-glass credentials from safe
├── Sign in and resolve the emergency
├── Document all actions taken
├── Reset break-glass credentials after use
└── Review and document in incident logScripts 2
agent.py5.9 KB
#!/usr/bin/env python3
"""Azure AD Privileged Identity Management agent using Microsoft Graph API via requests."""
import argparse
import json
import logging
import os
import sys
from datetime import datetime
from typing import List
try:
import requests
except ImportError:
sys.exit("requests required: pip install requests")
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
class PIMClient:
"""Client for Microsoft Entra PIM via Graph API."""
def __init__(self, tenant_id: str, client_id: str, client_secret: str):
self.tenant_id = tenant_id
self.token = self._acquire_token(client_id, client_secret)
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
def _acquire_token(self, client_id: str, client_secret: str) -> str:
resp = requests.post(
f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token",
data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default",
}, timeout=15)
resp.raise_for_status()
return resp.json()["access_token"]
def list_role_definitions(self) -> List[dict]:
"""List available directory role definitions."""
resp = self.session.get(f"{GRAPH_BASE}/roleManagement/directory/roleDefinitions", timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def list_eligible_assignments(self) -> List[dict]:
"""List PIM eligible role assignments."""
resp = self.session.get(
f"{GRAPH_BASE}/roleManagement/directory/roleEligibilityScheduleInstances", timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def list_active_assignments(self) -> List[dict]:
"""List currently active (activated) role assignments."""
resp = self.session.get(
f"{GRAPH_BASE}/roleManagement/directory/roleAssignmentScheduleInstances", timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def list_role_settings(self) -> List[dict]:
"""List PIM role management policy assignments."""
resp = self.session.get(
f"{GRAPH_BASE}/policies/roleManagementPolicyAssignments?"
"$filter=scopeId eq '/' and scopeType eq 'DirectoryRole'", timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def get_activation_requests(self, top: int = 50) -> List[dict]:
"""List recent role activation requests."""
resp = self.session.get(
f"{GRAPH_BASE}/roleManagement/directory/roleEligibilityScheduleRequests?"
f"$top={top}&$orderby=createdDateTime desc", timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def audit_permanent_assignments(active: List[dict], eligible: List[dict]) -> List[dict]:
"""Identify permanent (non-PIM) role assignments that should be converted to eligible."""
eligible_ids = {a.get("principalId") for a in eligible}
permanent = []
for a in active:
if a.get("assignmentType") == "Assigned" and a.get("principalId") not in eligible_ids:
permanent.append({
"principal_id": a.get("principalId", ""),
"role": a.get("roleDefinition", {}).get("displayName", ""),
"start": a.get("startDateTime", ""),
"recommendation": "Convert to eligible assignment with JIT activation",
})
return permanent
def compute_pim_coverage(active: List[dict], eligible: List[dict]) -> dict:
"""Calculate PIM coverage metrics."""
total = len(active)
eligible_count = len(eligible)
pim_pct = (eligible_count / (total + eligible_count) * 100) if (total + eligible_count) else 0
return {
"active_assignments": total,
"eligible_assignments": eligible_count,
"pim_coverage_pct": round(pim_pct, 1),
}
def generate_report(client: PIMClient) -> dict:
"""Generate PIM compliance report."""
roles = client.list_role_definitions()
eligible = client.list_eligible_assignments()
active = client.list_active_assignments()
permanent = audit_permanent_assignments(active, eligible)
coverage = compute_pim_coverage(active, eligible)
report = {
"analysis_date": datetime.utcnow().isoformat(),
"role_definitions": len(roles),
"coverage": coverage,
"permanent_assignments": permanent,
"permanent_count": len(permanent),
"recommendations": [],
}
if permanent:
report["recommendations"].append(
f"Convert {len(permanent)} permanent assignments to PIM-eligible")
if coverage["pim_coverage_pct"] < 80:
report["recommendations"].append("Increase PIM coverage above 80%")
return report
def main():
parser = argparse.ArgumentParser(description="Azure AD PIM Audit Agent")
parser.add_argument("--tenant-id", required=True, help="Azure AD tenant ID")
parser.add_argument("--client-id", required=True, help="App registration client ID")
parser.add_argument("--client-secret", required=True, help="App registration secret")
parser.add_argument("--output-dir", default=".")
parser.add_argument("--output", default="pim_report.json")
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
client = PIMClient(args.tenant_id, args.client_id, args.client_secret)
report = generate_report(client)
out_path = os.path.join(args.output_dir, args.output)
with open(out_path, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", out_path)
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()
process.py8.3 KB
#!/usr/bin/env python3
"""
Azure AD PIM Audit and Configuration Tool
Audits Entra ID role assignments, identifies permanent privileged assignments
that should be converted to PIM eligible, and monitors PIM activation events.
Requirements:
pip install msal requests pandas
"""
import json
import sys
from datetime import datetime, timezone
try:
import requests
import msal
except ImportError:
print("[ERROR] Required: pip install msal requests")
sys.exit(1)
class EntraPIMAuditor:
"""Audit and manage Microsoft Entra PIM configuration."""
def __init__(self, tenant_id, client_id, client_secret):
self.tenant_id = tenant_id
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self._authenticate()
def _authenticate(self):
"""Acquire Microsoft Graph access token using client credentials."""
app = msal.ConfidentialClientApplication(
self.client_id,
authority=f"https://login.microsoftonline.com/{self.tenant_id}",
client_credential=self.client_secret,
)
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
if "access_token" in result:
self.token = result["access_token"]
print("[OK] Authenticated to Microsoft Graph")
else:
raise Exception(f"Auth failed: {result.get('error_description')}")
def _graph_get(self, endpoint):
"""Make authenticated GET request to Microsoft Graph."""
headers = {"Authorization": f"Bearer {self.token}"}
url = f"https://graph.microsoft.com/v1.0{endpoint}"
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
def get_directory_roles(self):
"""List all Entra directory role definitions."""
result = self._graph_get("/roleManagement/directory/roleDefinitions")
roles = {}
for role in result.get("value", []):
roles[role["id"]] = {
"displayName": role["displayName"],
"description": role.get("description", ""),
"isBuiltIn": role.get("isBuiltIn", False),
}
return roles
def get_active_assignments(self):
"""List all permanently active role assignments (non-PIM)."""
result = self._graph_get(
"/roleManagement/directory/roleAssignments?$expand=principal"
)
assignments = []
for item in result.get("value", []):
assignments.append({
"roleDefinitionId": item["roleDefinitionId"],
"principalId": item["principalId"],
"directoryScopeId": item.get("directoryScopeId", "/"),
"principalDisplayName": item.get("principal", {}).get("displayName", ""),
"principalType": item.get("principal", {}).get("@odata.type", ""),
})
return assignments
def get_eligible_assignments(self):
"""List all PIM eligible role assignments."""
result = self._graph_get(
"/roleManagement/directory/roleEligibilityScheduleInstances"
)
eligible = []
for item in result.get("value", []):
eligible.append({
"roleDefinitionId": item["roleDefinitionId"],
"principalId": item["principalId"],
"directoryScopeId": item.get("directoryScopeId", "/"),
"startDateTime": item.get("startDateTime"),
"endDateTime": item.get("endDateTime"),
})
return eligible
def get_pim_activation_history(self, days=30):
"""Retrieve PIM role activation audit events."""
result = self._graph_get(
f"/auditLogs/directoryAudits?"
f"$filter=category eq 'RoleManagement' and "
f"activityDisplayName eq 'Add member to role completed (PIM activation)'"
f"&$top=100"
)
activations = []
for event in result.get("value", []):
activations.append({
"activityDateTime": event.get("activityDateTime"),
"activityDisplayName": event.get("activityDisplayName"),
"initiatedBy": event.get("initiatedBy", {}).get("user", {}).get("displayName", ""),
"targetResources": [
t.get("displayName", "") for t in event.get("targetResources", [])
],
"result": event.get("result"),
})
return activations
def identify_permanent_admins(self):
"""Find permanently active admin assignments that should be PIM eligible."""
roles = self.get_directory_roles()
active = self.get_active_assignments()
eligible = self.get_eligible_assignments()
critical_roles = {
rid: info for rid, info in roles.items()
if info["displayName"] in [
"Global Administrator",
"Privileged Role Administrator",
"Security Administrator",
"Exchange Administrator",
"SharePoint Administrator",
"User Administrator",
"Application Administrator",
"Cloud Application Administrator",
"Intune Administrator",
"Compliance Administrator",
]
}
findings = []
eligible_principals = {
(e["roleDefinitionId"], e["principalId"]) for e in eligible
}
for assignment in active:
role_id = assignment["roleDefinitionId"]
if role_id in critical_roles:
is_also_eligible = (role_id, assignment["principalId"]) in eligible_principals
findings.append({
"role": critical_roles[role_id]["displayName"],
"principal": assignment["principalDisplayName"],
"principalType": assignment["principalType"],
"hasEligibleAssignment": is_also_eligible,
"recommendation": "Convert to PIM eligible" if not is_also_eligible else "Review - has both active and eligible",
})
return findings
def generate_audit_report(self):
"""Generate comprehensive PIM audit report."""
roles = self.get_directory_roles()
active = self.get_active_assignments()
eligible = self.get_eligible_assignments()
permanent_findings = self.identify_permanent_admins()
report = {
"report_title": "Microsoft Entra PIM Audit Report",
"tenant_id": self.tenant_id,
"generated_at": datetime.now(timezone.utc).isoformat(),
"summary": {
"total_directory_roles": len(roles),
"total_active_assignments": len(active),
"total_eligible_assignments": len(eligible),
"permanent_admin_findings": len(permanent_findings),
},
"findings": permanent_findings,
"recommendations": [],
}
if len(permanent_findings) > 0:
report["recommendations"].append({
"priority": "Critical",
"finding": f"{len(permanent_findings)} permanent privileged role assignments found",
"action": "Convert to PIM eligible assignments with MFA and approval requirements"
})
active_global_admins = sum(
1 for f in permanent_findings
if f["role"] == "Global Administrator"
)
if active_global_admins > 2:
report["recommendations"].append({
"priority": "High",
"finding": f"{active_global_admins} permanent Global Administrators (should be max 2 break-glass)",
"action": "Reduce to 2 break-glass accounts, convert rest to PIM eligible"
})
return report
if __name__ == "__main__":
print("=" * 60)
print("Microsoft Entra PIM Audit Tool")
print("=" * 60)
print()
print("Usage:")
print(" auditor = EntraPIMAuditor(tenant_id, client_id, client_secret)")
print(" report = auditor.generate_audit_report()")
print(" print(json.dumps(report, indent=2))")
print()
print("Required Microsoft Graph permissions:")
print(" - RoleManagement.Read.All")
print(" - AuditLog.Read.All")
print(" - Directory.Read.All")