Appearance
Threat Model
This section covers realistic threats to the platform and the controls that address them, grounded in the actual implementation. It is not an exhaustive security audit — it documents the threats the design explicitly defends against.
Threat landscape
The system's attack surface:
- Browser sessions (agents) — the largest user population, clearance 1–5.
- The bot (BotGhost) — holds the API key, an integration credential.
- Public/integration entry points — OAuth endpoints, generators, the liveness route, ER:LC lookup.
- External data — report content, evidence URLs, Discord profiles, ER:LC player data.
- The database — Supabase (PostgREST + Realtime), grants, RLS, functions.
- Infrastructure — Render (API), Cloudflare (Pages/Workers), secrets.
Unauthorized agent privilege escalation
Scenario: an agent raises their own rank/clearance, or gains admin access.
Controls:
rpc_admin_agent_updateblocks self-modification (SELF_PROMOTION_BLOCKED) — enforced in the API and the database.- Clearance changes require Director rank (live row, not clearance level) and can never target the requester.
- Rank changes above Head Investigator require Director rank; all rank changes are subject to the hierarchy guard (only at/below your own rank).
- The admin allowlist gates System Administration on top of clearance; access management is Director-only and audited.
- The bot cannot set
agent_rankthrough any path (034); its onboarding writes are whitelisted and audited under the system identity. - A demoted Director loses Director capabilities immediately (rank read live per request).
Residual: a Director is by definition trusted with rank/clearance powers; compromise of a Director account is covered by credential protection and the audit trail (every change is attributable).
Unauthorized report modification
Scenario: an agent edits, deletes, or re-flags a case they have no right to touch.
Controls:
- Assigned-agent gate on every per-report mutation (exact identifier match; supervisors bypass by rank).
- Bot-only patchable fields (
is_supervisor,notes,reason) rejected for sessions — a clearance-2 agent cannot de-escalate a supervisor case. - Supervisor reports are redacted (restricted placeholders, zeroed counts) for sub-Senior agents at every read surface.
- Deletion requires clearance 5 and a reason, and writes the forensic audit in the deletion transaction.
- Status transitions are enum-guarded at the API and re-checked under the row lock in
rpc_report_action.
Audit tampering
Scenario: an attacker rewrites or deletes audit/event records, or performs a mutation without a record.
Controls:
- Append-only grants + triggers on
events,admin_audit_log,admin_health_history; no role (including service_role) can UPDATE/DELETE. - RLS deny-all on the streams.
- Destructive operations write their audit row in the same transaction.
- Actor identity is server-derived (see Audit integrity).
Race conditions
Scenario: two agents (or the bot and an agent) perform the same action concurrently; the loser's action corrupts state or double-applies.
Controls:
- Report actions:
SELECT … FOR UPDATEon the report row + status re-check; loser gets 409. - Queue pickups:
FOR UPDATE SKIP LOCKED; two bot ticks cannot claim the same action. - Queue completion/requeue: locked + status guard; second completion → 409.
- Punishment revocation: locked + terminal-status guard.
- Training: idempotent client actions via the
(session_id, client_id)unique index; the API is the sole training writer. - Claim:
ALREADY_CLAIMEDguard.
Malicious URLs
Scenario: a stored javascript:/data: URL executes in an agent's browser (stored XSS).
Controls:
- Server-side http(s)-only validation on evidence writes.
- Client-side
safeHttpUrl/safeImageUrlre-validation at every sink (evidence, lightbox, profile images, navigation). - No raw HTML embedding of server content.
Stored XSS
Scenario: script content embedded in report/note/evidence text executes.
Controls:
- React escapes text interpolation; no
dangerouslySetInnerHTMLfor server content. - URL sinks are scheme-validated (the vector XSS travels through).
- Identifier scrubbing replaces known user IDs with display names before rendering, reducing the injection payload surface.
Malformed input
Scenario: oversized bodies, megabyte keys, filter-injection strings, or invalid enum values consume resources or bypass checks.
Controls:
- 1 MiB body cap (413).
- Report-ID bounds + character whitelist.
- Search-length caps; PostgREST
or=metacharacter stripping on admin search. - Enum/whitelist validation at the API and inside the RPCs.
- Pagination bounds on every list endpoint.
Unauthorized database access
Scenario: the browser (or any external party) reads or writes data through PostgREST/Realtime directly, bypassing the API.
Controls:
- Browser receives only the anon key; RLS deny-all + revoked grants on every production table make it useless for data access.
- RPC EXECUTE is service_role-only (016 lockdown).
- Realtime tokens are short-lived, identity-scoped, and authorized by RLS membership policies.
- The service-role key exists only on the API host, never in the browser bundle or build environment.
Accidental destructive operations
Scenario: a legitimate operator deletes a case or changes a record by mistake (or a bug triggers it).
Controls:
- Reasons required for every destructive/admin mutation.
- Destructive RPCs capture pre-images; deletion rolls back if the audit write fails.
- Punishments cannot be deleted at all (no DELETE grant, trigger block).
- Confirmation flows in the frontend for high-impact actions (
AdminConfirmDialog, reason modals). - Report deletion is clearance-5 only.
Leaked credentials
Scenario: the API key, session secret, service-role key, or Discord client secret leaks (logs, bundle, repo, misconfigured env).
Controls:
- Secrets are never logged (error paths log messages, not credentials).
- The browser bundle contains only
VITE_DPS_API_BASE_URLand the public anon key. - Environment-bound sessions and keys: a demo credential cannot authenticate against production and vice versa.
- Repository hygiene: secret-bearing files (
db.url,.env*) are ignored and never committed; this documentation contains no secret values. - The API key's blast radius is bounded: it cannot touch training, punishment, or admin routes.
Residual: leaked credentials require rotation at the platform level; the fail-fast boot (missing production config) and environment isolation limit blast radius but cannot undo an already-exfiltrated secret.
Non-threats (deliberate design decisions)
- ER:LC relay tokens live only on the API host and the relay machine; routes never see them.
- View tracking endpoints are no-ops, so there is no view-flood vector against the audit log.
- The demo environment is intentionally weaker (allowlisted real IDs map to fictional agents) and is fully isolated from production data and credentials.