npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
Overview
Illicit consent grant attacks trick users into granting excessive permissions to malicious OAuth applications in Azure AD / Microsoft Entra ID. This skill uses the Microsoft Graph API to enumerate OAuth2 permission grants, analyze application permissions for overly broad scopes, review directory audit logs for consent events, and flag high-risk applications based on publisher verification status and permission scope.
When to Use
- When investigating security incidents that require detecting suspicious oauth application consent
- When building detection rules or threat hunting queries for this domain
- When SOC analysts need structured procedures for this analysis type
- When validating security monitoring coverage for related attack techniques
Prerequisites
- Azure AD / Entra ID tenant with Global Reader or Security Reader role
- Microsoft Graph API access with
Application.Read.All,AuditLog.Read.All,Directory.Read.All - Python 3.9+ with
msal,requests - App registration with client secret or certificate for authentication
Steps
- Authenticate to Microsoft Graph using MSAL client credentials flow
- Enumerate all OAuth2 permission grants via
/oauth2PermissionGrants - List service principals and their assigned application permissions
- Query directory audit logs for
Consent to applicationevents - Flag applications with high-risk scopes (Mail.Read, Files.ReadWrite.All, etc.)
- Check publisher verification status for each application
- Generate risk report with remediation recommendations
Expected Output
- JSON report listing all OAuth apps with granted permissions, risk scores, unverified publishers, and suspicious consent patterns
- Audit trail of consent grant events with user and IP details
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md2.7 KB
API Reference — Detecting Suspicious OAuth Application Consent
Libraries Used
- msal: Microsoft Authentication Library — client credentials flow for Microsoft Graph
- requests: HTTP client for Graph API calls with pagination
CLI Interface
python agent.py --tenant-id TENANT --client-id CLIENT --client-secret SECRET grants
python agent.py --tenant-id TENANT --client-id CLIENT --client-secret SECRET apps
python agent.py --tenant-id TENANT --client-id CLIENT --client-secret SECRET audit-logs
python agent.py --tenant-id TENANT --client-id CLIENT --client-secret SECRET full --days 30Core Functions
get_access_token(tenant_id, client_id, client_secret) — MSAL client credentials auth
Uses ConfidentialClientApplication.acquire_token_for_client() with scope
https://graph.microsoft.com/.default to obtain a bearer token.
graph_get(token, endpoint, params) — Paginated Graph API GET
Handles @odata.nextLink pagination. Returns aggregated value array.
enumerate_oauth_grants(token) — List delegated permission grants
Calls GET /oauth2PermissionGrants. Parses scope strings, flags high-risk scopes.
list_service_principals(token) — Enumerate apps with verification status
Calls GET /servicePrincipals?$top=999. Checks verifiedPublisher field.
query_consent_audit_logs(token, days) — Consent event audit trail
Calls GET /auditLogs/directoryAudits filtered by activityDisplayName eq 'Consent to application'.
Returns user principal, IP, target app, and result.
analyze_risk(grants, service_principals) — Risk scoring engine
Scoring: +15 per high-risk scope, +25 for unverified publisher, +20 for AllPrincipals consent. Levels: CRITICAL >= 70, HIGH >= 50, MEDIUM >= 25, LOW < 25.
full_audit(token, days) — Comprehensive consent audit
Microsoft Graph Endpoints
| Endpoint | Method | Required Permission |
|---|---|---|
/oauth2PermissionGrants |
GET | Directory.Read.All |
/servicePrincipals |
GET | Application.Read.All |
/auditLogs/directoryAudits |
GET | AuditLog.Read.All |
High-Risk OAuth Scopes
Mail.Read, Mail.ReadWrite, Mail.Send, Files.ReadWrite.All, Files.Read.All, User.ReadWrite.All, Directory.ReadWrite.All, Sites.ReadWrite.All, Contacts.ReadWrite, MailboxSettings.ReadWrite, People.Read.All, Calendars.ReadWrite, Notes.ReadWrite.All
Risk Scoring
| Factor | Points |
|---|---|
| Each high-risk scope | +15 |
| Unverified publisher | +25 |
| Admin consent (AllPrincipals) | +20 |
Dependencies
msal>= 1.24.0requests>= 2.28.0- Azure AD app registration with
Application.Read.All,AuditLog.Read.All,Directory.Read.All
Scripts 1
agent.py7.9 KB
#!/usr/bin/env python3
"""Agent for detecting suspicious OAuth application consent grants in Azure AD / Entra ID."""
import json
import argparse
from datetime import datetime, timedelta
try:
import msal
except ImportError:
msal = None
try:
import requests
except ImportError:
requests = None
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
HIGH_RISK_SCOPES = [
"Mail.Read", "Mail.ReadWrite", "Mail.Send",
"Files.ReadWrite.All", "Files.Read.All",
"User.ReadWrite.All", "Directory.ReadWrite.All",
"Sites.ReadWrite.All", "Contacts.ReadWrite",
"MailboxSettings.ReadWrite", "People.Read.All",
"Calendars.ReadWrite", "Notes.ReadWrite.All",
]
def get_access_token(tenant_id, client_id, client_secret):
"""Authenticate via MSAL client credentials flow and return access token."""
if not msal:
return None
authority = f"https://login.microsoftonline.com/{tenant_id}"
app = msal.ConfidentialClientApplication(
client_id, authority=authority, client_credential=client_secret
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" in result:
return result["access_token"]
raise RuntimeError(f"Auth failed: {result.get('error_description', result.get('error'))}")
def graph_get(token, endpoint, params=None):
"""Make authenticated GET request to Microsoft Graph API."""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
url = f"{GRAPH_BASE}{endpoint}"
all_items = []
while url:
resp = requests.get(url, headers=headers, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()
all_items.extend(data.get("value", []))
url = data.get("@odata.nextLink")
params = None
return all_items
def enumerate_oauth_grants(token):
"""List all delegated OAuth2 permission grants in the tenant."""
grants = graph_get(token, "/oauth2PermissionGrants")
results = []
for g in grants:
scope_list = g.get("scope", "").split()
risky = [s for s in scope_list if s in HIGH_RISK_SCOPES]
results.append({
"id": g.get("id"),
"clientId": g.get("clientId"),
"consentType": g.get("consentType"),
"principalId": g.get("principalId"),
"resourceId": g.get("resourceId"),
"scopes": scope_list,
"high_risk_scopes": risky,
"risk_score": len(risky) * 15,
})
return results
def list_service_principals(token):
"""List service principals with their app roles and permissions."""
sps = graph_get(token, "/servicePrincipals", params={"$top": "999"})
results = []
for sp in sps:
app_roles = sp.get("appRoles", [])
verified = sp.get("verifiedPublisher", {})
results.append({
"id": sp.get("id"),
"appId": sp.get("appId"),
"displayName": sp.get("displayName"),
"publisherName": sp.get("publisherName"),
"verifiedPublisher": verified.get("displayName") if verified else None,
"isVerified": bool(verified.get("verifiedPublisherId")),
"appRoleCount": len(app_roles),
"accountEnabled": sp.get("accountEnabled"),
"signInAudience": sp.get("signInAudience"),
})
return results
def query_consent_audit_logs(token, days=30):
"""Query directory audit logs for consent grant events."""
since = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
filter_str = (
f"activityDisplayName eq 'Consent to application' "
f"and activityDateTime ge {since}"
)
logs = graph_get(token, "/auditLogs/directoryAudits", params={"$filter": filter_str})
events = []
for log in logs:
initiated = log.get("initiatedBy", {}).get("user", {})
targets = log.get("targetResources", [])
events.append({
"activityDateTime": log.get("activityDateTime"),
"activityDisplayName": log.get("activityDisplayName"),
"result": log.get("result"),
"initiatedByUser": initiated.get("userPrincipalName"),
"initiatedByIp": initiated.get("ipAddress"),
"targetApp": targets[0].get("displayName") if targets else None,
"targetAppId": targets[0].get("id") if targets else None,
"additionalDetails": log.get("additionalDetails"),
})
return events
def analyze_risk(grants, service_principals):
"""Correlate grants with service principals to produce risk assessment."""
sp_map = {sp["id"]: sp for sp in service_principals}
findings = []
for grant in grants:
sp = sp_map.get(grant["clientId"], {})
risk = grant["risk_score"]
if not sp.get("isVerified"):
risk += 25
if grant.get("consentType") == "AllPrincipals":
risk += 20
risk = min(risk, 100)
level = "CRITICAL" if risk >= 70 else "HIGH" if risk >= 50 else "MEDIUM" if risk >= 25 else "LOW"
findings.append({
"appDisplayName": sp.get("displayName", "Unknown"),
"appId": sp.get("appId"),
"publisherVerified": sp.get("isVerified", False),
"consentType": grant.get("consentType"),
"highRiskScopes": grant.get("high_risk_scopes"),
"riskScore": risk,
"riskLevel": level,
"recommendation": "Revoke consent and investigate" if risk >= 50
else "Review scopes and publisher" if risk >= 25
else "Monitor",
})
findings.sort(key=lambda x: x["riskScore"], reverse=True)
return findings
def full_audit(token, days=30):
"""Run comprehensive OAuth consent audit."""
grants = enumerate_oauth_grants(token)
sps = list_service_principals(token)
audit_events = query_consent_audit_logs(token, days)
risk_findings = analyze_risk(grants, sps)
critical = sum(1 for f in risk_findings if f["riskLevel"] == "CRITICAL")
high = sum(1 for f in risk_findings if f["riskLevel"] == "HIGH")
unverified = sum(1 for sp in sps if not sp.get("isVerified"))
return {
"audit_type": "OAuth Application Consent Audit",
"timestamp": datetime.utcnow().isoformat(),
"summary": {
"total_grants": len(grants),
"total_service_principals": len(sps),
"consent_events_last_n_days": len(audit_events),
"critical_findings": critical,
"high_findings": high,
"unverified_publishers": unverified,
},
"risk_findings": risk_findings[:25],
"recent_consent_events": audit_events[:20],
}
def main():
parser = argparse.ArgumentParser(description="OAuth Application Consent 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 client secret")
sub = parser.add_subparsers(dest="command")
sub.add_parser("grants", help="Enumerate OAuth2 permission grants")
sub.add_parser("apps", help="List service principals")
sub.add_parser("audit-logs", help="Query consent audit logs")
p_full = sub.add_parser("full", help="Full OAuth consent audit")
p_full.add_argument("--days", type=int, default=30, help="Audit log lookback days")
args = parser.parse_args()
token = get_access_token(args.tenant_id, args.client_id, args.client_secret)
if args.command == "grants":
result = enumerate_oauth_grants(token)
elif args.command == "apps":
result = list_service_principals(token)
elif args.command == "audit-logs":
result = query_consent_audit_logs(token)
elif args.command == "full":
result = full_audit(token, args.days)
else:
parser.print_help()
return
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()