> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Threat model

# Find My Data — Preliminary Threat Model (Vertical Slice)

**Status:** Preliminary · 2026-07-14 (revised 2026-07-15 for the follow-on build: live read-only CDX Graph connector, rich-format extraction, change-notification receiver, Teams/Exchange connectors)
**Scope of this document:** the vertical slice and its follow-on work — a Bun + TypeScript modular monolith with SQLite behind role interfaces, a deterministic mock M365 connector as the primary/CI-exercised path (with the live Microsoft Graph connector additionally validated **read-only** against the Contoso CDX tenant, ADR-0013), dev identity auth with production lockout, capability-based authorization, hash-chained audit, connectors for SharePoint/OneDrive plus feature-gated Teams/Exchange, change-notification ingestion, and the minimized-evidence retention profile (ADR-0011).

This is a *preliminary* model for a single-tenant, locally-run slice. It describes what is actually implemented, honestly separates implemented / mocked / config-gated / future, and does not claim validation against any live Microsoft tenant or AI provider. It should be revised when the live Graph connector, Entra sign-in, or any multi-tenant control plane becomes real.

Companion sources: `find_my_data_handoff/FIND_MY_DATA_SECURITY_RBAC_AND_GOVERNANCE.md` (the target posture), `docs/adr/ADR-0001..0014` (the decisions), `STATUS.md` (what was exercised).

***

## 1. Scope and trust boundaries

### 1.1 Deployment shape in this slice

One server process (`packages/server/src/main.ts`) hosts the HTTP API (Hono) and an in-process durable worker loop over a SQLite-backed work queue (ADR-0002). The web UI is a separate Vite/React SPA (`packages/web/`) that talks to the API. Everything runs on one machine with one SQLite file and one artifact directory. There is no network segmentation, no separate identity provider, and no cloud service in the exercised path.

### 1.2 Boundaries

| #   | Boundary                                                | Trust posture in this slice                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Status                                                  |
| --- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| B1  | **Browser ↔ API**                                       | Untrusted client. Identity comes only from an HMAC-signed, HTTP-only session cookie (`packages/server/src/identity/sessions.ts`); client-supplied identity fields are never trusted (`packages/server/src/kernel/http.ts`). Mutating requests require a CSRF header.                                                                                                                                                                                                                                                                                                                                                                                                                        | Implemented                                             |
| B2  | **API ↔ worker**                                        | Same process and same trust zone in the slice; they share the SQLite queue. Worker can also run as a separate process against the same DB (ADR-0002) — still the same trust zone.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Implemented                                             |
| B3  | **Application ↔ SQLite**                                | The DB file (`data/fmd.db`) is fully trusted by the application. Anyone with filesystem access reads/writes everything, including the audit chain. No application-layer encryption.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Implemented; known limitation                           |
| B4  | **Application ↔ artifact store**                        | Filesystem directory `data/artifacts/<tenantId>/<artifactId>` behind `ObjectStore` (`packages/server/src/kernel/objectstore.ts`). Reads are purpose-checked in code; the files themselves are plain bytes on disk.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Implemented; purpose check is app-layer only            |
| B5  | **Scan pipeline ↔ connector (mock)**                    | The mock M365 connector (`packages/server/src/connectors/mock/`) is deterministic fixture data, but its *content* is treated as untrusted input: bounded fetch, MIME sniffing, size caps, injection fixtures.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Implemented (exercised path)                            |
| B6  | **Scan pipeline ↔ connector (live Graph)**              | The live connector is **implemented** (`packages/server/src/connectors/graph/connector.ts`) behind the versioned contract (`connectors/types.ts`) and was **validated read-only against the live Contoso CDX tenant** (ADR-0013): app-only client-credentials, `driveItem` delta, deduplicated permissions, honest label `unknown`. The registry (`connectors/registry.ts`) builds it only when `FMD_CONNECTOR_MODE=graph` **and** the tenant/client id + a **resolvable** client-secret reference are present; otherwise it raises an explicit `BlockedError` — never a silent mock fallback. Read path only; the write/remediation path uses a separate identity and stays feature-gated. | Implemented; read-only CDX-validated; write gated       |
| B7  | **Application ↔ AI gateway ↔ providers**                | All model calls go through `AiGateway` (`packages/server/src/aigateway/gateway.ts`). Default and only exercised provider is a deterministic mock. Azure OpenAI and Anthropic adapters exist and are config-gated; missing config is an explicit `BlockedError`, never a fallback to a differently governed provider. Untrusted content is delimited (`packages/server/src/aigateway/templates.ts`).                                                                                                                                                                                                                                                                                         | Mock exercised; live adapters config-gated, unexercised |
| B8  | **Future: Entra ID**                                    | The OIDC path exists only as a **501 seam** behind `FMD_AUTH_MODE=entra` (ADR-0004) — no OIDC/JWT validation code is implemented yet. Dev identity mode is the exercised path, with production lockout (see T12).                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Seam / not built                                        |
| B9  | **Future: Microsoft Graph + Purview**                   | Separate read vs. remediation app identities are modeled in config (`packages/server/src/kernel/config.ts`: `graph.clientId` vs. `graph.remediationClientId`). Purview label *write* runs only against the mock connector; live label write is additionally gated on `FMD_FEATURE_PURVIEW_LABEL_WRITE` and protected-API enablement, and drafts against a live connector without the flag become `blocked` with a reason (`packages/server/src/actions/service.ts`).                                                                                                                                                                                                                        | Mock exercised; live gated                              |
| B10 | **Future: Find My Data Control (vendor control plane)** | Does not exist in this slice. No telemetry leaves the machine. The handoff's control-plane requirements (§18) apply when it is built.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Future                                                  |

### 1.3 What "single-tenant SQLite slice" means for the model

* Every row carries `tenant_id` and repositories refuse unscoped queries (ADR-0003), but only **one** tenant is ever seeded and exercised. Cross-tenant isolation is enforced logically, not physically, and has not been tested under adversarial multi-tenant load.
* The OS user running the process is effectively root of the whole system: DB, artifacts, audit chain, and session secret (env var) are all reachable. Threats below that reduce to "attacker has local filesystem access" are out of scope for the slice and in scope for the Azure deployment design.
* There is no TLS termination in the slice; transport security is a deployment concern.

***

## 2. Assets to protect

Mapping of the handoff SECURITY §2 asset list to what actually exists in this build. Per the handoff: derivative data (fingerprints, paths, questions) is still sensitive — none of these tables is "safe" merely because it holds no full text.

| Asset (handoff §2)                                                                   | Where it lives in this slice                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Source content and temporary extracted representations                               | Not persisted as originals. Bounded extracted text (≤ 20,000 chars) is retained as a purpose-flagged artifact in `data/artifacts/` + `artifacts` table (see §4 for the honest caveat). Raw bytes are processed in memory only.                                                       |
| Evidence excerpts, keyphrases, hashes, fingerprints, classifications, findings       | `evidence`, `assertions`, `feature_artifacts` (keyphrases, redacted excerpts), `fingerprints`, `fingerprint_bands`, `classifications`, `risk_assessments`, `findings`, `clusters`/`cluster_members` (migrations `0002`–`0003`). No embeddings exist yet.                             |
| File names, paths, URLs, labels, metadata, permissions, activity, identities, groups | `physical_assets`, `asset_versions`, `asset_locations`, `source_containers`, `asset_label_obs`, `asset_permission_obs`, `permission_sets`/`permission_grants`, `activity_aggregates`, `principals`, `group_memberships`.                                                             |
| Policy and governance documents                                                      | `policy_documents`, `policy_sections` (with citations).                                                                                                                                                                                                                              |
| Taxonomy, regulatory mappings, risk configuration, owner assignments                 | `business_domains`, `information_types`, `info_type_categories`, `info_type_elements`, `sensitive_categories`, `data_elements`, `governance_mappings`, `regulatory_obligations`, `classification_label_mappings`, `sensitivity_labels`, `ownership_assignments`, `role_assignments`. |
| Review feedback, datasets, model artifacts, prompts, AI outputs                      | `review_candidates`, `review_decisions`, `dataset_candidates`, `detector_releases`, `ensemble_releases`, `generation_runs` (task, provider, model, template version, **input hash only** — no prompt bodies persisted).                                                              |
| Credentials (Graph, AI provider, signing)                                            | No secrets are stored in the DB or repo. Config holds *references* (`*Ref` fields resolve to env var names, `packages/server/src/kernel/config.ts`). The session secret and any provider keys live in the process environment.                                                       |
| Connector cursors, source locators, remediation capabilities                         | `source_cursors` (stored plaintext in the slice; ADR-0008 documents opaque+encrypted persistence for live delta links), `source_scopes`, `connector_instances`.                                                                                                                      |
| Analyst questions, plans, answers                                                    | `analyst_queries` (question, plan, result references, planner, status).                                                                                                                                                                                                              |
| Audit records and operational telemetry                                              | `audit_events` (per-tenant hash chain) and the sanitized in-memory log ring buffer (`Logger.recent`, `packages/server/src/kernel/log.ts`).                                                                                                                                           |
| Sessions                                                                             | `sessions` table (session id, CSRF token, expiry, revocation).                                                                                                                                                                                                                       |
| Licensing/fleet-control identity, update/signing infrastructure                      | Not present in this slice (future control plane).                                                                                                                                                                                                                                    |

***

## 3. Threat actors and scenarios

Conventions: **Implemented** = enforced in code in this build, with automated tests where noted. **Residual** = what remains true even with the mitigation. **Planned** = documented follow-up, not built.

| #   | Scenario                                                                                                | Implemented mitigations (file references)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Residual risk                                                                                                                                                                                                                                                                                                                                                                     | Planned mitigations                                                                                                                                                                                                                                                                                        |
| --- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T1  | **Malicious or compromised domain owner reads another domain's data** (evidence, review queue, Analyst) | Capability grants for `domain_owner` are domain-scoped, resolved against active `ownership_assignments` at evaluation time (`packages/shared/src/capabilities.ts`, `packages/server/src/kernel/authz.ts`: `hasCapability`, `domainScopeFilter`). Review queue filters to owned domains and re-checks per-domain capability (`packages/server/src/review/service.ts`). Analyst injects tenant + domain predicates server-side; the plan cannot set or widen them, and scope violations reject fail-closed (`packages/server/src/analyst/executor.ts`). Tested in `review.test.ts`, `analyst.test.ts`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Enforcement is application-layer over one shared DB; a code path that forgets the filter has full access to all rows. Domain scoping covers the exercised routes; every new route must repeat the pattern.                                                                                                                                                                        | Repository-level mandatory scope predicates for all new modules; periodic authz-matrix tests as routes are added; row-level security when the system of record moves to PostgreSQL.                                                                                                                        |
| T2  | **Overprivileged platform administrator reads content/evidence**                                        | Deliberate capability exclusion: `platform_admin` holds operations, connector, and scan-job capabilities but **no** `asset.evidence.*`, `review.*`, or `analyst.*` capabilities (`packages/shared/src/capabilities.ts` §"Deliberate exclusions"; enforced by `requireCapability` in each route). Excerpt reads additionally require `asset.evidence.excerpt.read` in the owning domain (`packages/server/src/review/service.ts`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | A platform admin with shell access to the host bypasses everything (reads SQLite and `data/artifacts/` directly). Role assignment itself is seeded; there is no admin-grants-admin governance flow yet.                                                                                                                                                                           | Host-level separation in the Azure design (managed identity, Key Vault, storage ACLs); audited break-glass procedure; role-assignment change workflow with audit + second person.                                                                                                                          |
| T3  | **IDOR — enumerate or fetch other objects by changing IDs**                                             | Object loads are keyed by `(tenant_id, id)` and then capability-checked against the object's own domain; cross-domain probes return **404, not 403**, so existence does not leak (`packages/server/src/review/service.ts`: "Cross-domain probing gets 404"). Analyst re-resolves display values to internal IDs within the caller's scope (`packages/server/src/analyst/executor.ts`). Artifact reads require the artifact's purpose flag (`packages/server/src/kernel/objectstore.ts`: `get`). IDs are random with prefixes (`packages/server/src/kernel/ids.ts`), not sequential. Tested in `review.test.ts` (cross-domain candidate fetch).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Pattern coverage is per-route; unexercised future routes are the risk. Findings/actions listings rely on the same convention.                                                                                                                                                                                                                                                     | A shared "load-scoped-object" helper as more modules appear; fuzzing the API for ID probing as part of the acceptance suite.                                                                                                                                                                               |
| T4  | **Session forgery, fixation, or CSRF**                                                                  | Session cookie value is `sessionId.HMAC-SHA256(secret, sessionId)` verified with a constant-time compare (`packages/server/src/identity/sessions.ts`: `parseSessionCookie`). Cookies are HTTP-only, SameSite=Lax, `secure` in production (`packages/server/src/kernel/http.ts`: `issueSessionCookie`). Sessions expire after 8 h and support revocation. All POST/PUT/PATCH/DELETE require the per-session CSRF token in the `x-fmd-csrf` header (`packages/server/src/kernel/http.ts`). Production refuses the default session secret at boot (`packages/server/src/kernel/config.ts`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | HMAC key is a single env-provided secret with no rotation story. No step-up/recent-auth for sensitive actions. SameSite=Lax plus header token is solid for the slice but untested against a real cross-site deployment topology.                                                                                                                                                  | Entra OIDC as the production path (ADR-0004) with short-lived tokens and Conditional Access; secret rotation via Key Vault; step-up policy for approval/execution routes.                                                                                                                                  |
| T5  | **Prompt injection via scanned content, policy documents, or Analyst questions**                        | Scanned text is data by construction: every prompt template wraps input in labeled `<untrusted-data>` delimiters with an explicit non-instruction rule (`packages/server/src/aigateway/templates.ts`: `INJECTION_GUARD`, `renderUntrusted`). Authorization and tenant/domain filtering live entirely outside the model: any plan — AI or deterministic — is treated as untrusted, Zod-validated against the allowlisted semantic schema, and scope-injected server-side (`packages/server/src/analyst/executor.ts`, `packages/server/src/analyst/schema.ts`). The model has no tools that mutate or fetch arbitrarily; action drafting is a separate explicitly invoked path (ADR-0009). Injection tests: hostile question and hostile fixture document (`packages/server/src/analyst/analyst.test.ts` §"scope enforcement and injection resistance").                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Injection resistance is proven only against the deterministic mock provider and fixture attacks; a live LLM may fail in ways the mock cannot exhibit. The guard instruction itself is not a security boundary — the *executor validation* is, and that holds regardless.                                                                                                          | Re-run the injection suite against gated live providers before enabling them; add exfiltration-style fixtures (markdown links, encoded payloads); redaction of prompt inputs per provider policy (ADR-0005) when live egress is enabled.                                                                   |
| T6  | **Parser bombs, oversized or malformed content (DoS via extraction)**                                   | Limits enforced *before* parsing: the connector fetch is bounded to 5 MB (`packages/server/src/scan/pipeline.ts`: `CONTENT_MAX_BYTES`), and the extractor independently rejects any input over 25 MB and caps output at 20,000 chars (`packages/server/src/intelligence/extract.ts`: `DEFAULT_LIMITS` — `maxInputBytes` / `maxOutputChars`). Declared MIME is not trusted — magic-byte + NUL sniffing rejects disguised binaries (`extract.ts`: `looksBinary`, `EXTRACTION_IMPL_VERSION`). The parser set is now the text family plus **DOCX/XLSX/PPTX, HTML, RTF, and best-effort born-digital PDF**; OOXML decompression runs through a **bounded ZIP reader** (`intelligence/zip.ts`: `maxEntries` 5,000 + `inflateRawSync` `maxOutputLength` expansion cap), so a decompression-bomb surface now exists but is bounded. Scanned/image PDFs and unknown formats return an honest `unsupported`/`failed`, never a silent skip. Format and oversized-input tests in `intelligence/extract.test.ts`; the hard-negative fixture is exercised in `intelligence/intelligence.test.ts`.                                                                                                                                                                                                                                      | Extraction runs **in-process** on the fetched bytes — no sandbox, no separate worker — sharing the API/worker process with source-connector reachability. The rich-format parsers (OOXML ZIP, PDF operators, RTF) are a real DoS/exploit surface: bounded, but not isolated. No OCR/image parsing exists (scanned PDFs are honest `unsupported`).                                 | Move extraction into sandboxed workers (no credentials, no egress, read-only FS + bounded scratch) per handoff §10 now that rich-format parsers ship; keep tightening archive depth/entry/expansion-ratio limits; parser version already tracked per artifact (`EXTRACTION_IMPL_VERSION`, `algo_version`). |
| T7  | **Action replay / duplicate execution (repeated source writes)**                                        | Three layers: (1) executor no-ops if the action is already `succeeded` (`packages/server/src/actions/service.ts`: `executeApprovedAction`); (2) every attempt carries the action's idempotency key (`(tenantId, idempotencyKey)`, `action_attempts` table); (3) the connector itself suppresses duplicates (`packages/server/src/connectors/mock/connector.ts`: `executeLabelAction` returns `duplicate_suppressed` for a seen key). Queue delivery is at-least-once with leases + heartbeats; consumers are idempotent by contract (`packages/server/src/scan/queue.ts`). Duplicate-delivery test in `actions.test.ts` (ADR-0010).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | Source-side dedup is proven against the mock connector's in-memory key set; the live Graph adapter's idempotency behavior is unvalidated. Key set in mock is process-lifetime, which is fine for fixtures but not a durability statement.                                                                                                                                         | Validate idempotency semantics against a real tenant per `docs/cdx-test-runbook.md`; persist executor dedup state with the same durability as the action ledger (already in SQLite).                                                                                                                       |
| T8  | **Self-approval / separation-of-duties bypass**                                                         | The requester can never satisfy their own approval: checked in `approveAction` with an explicit audit event `approval_self_dealing_denied` on violation (`packages/server/src/actions/service.ts`). Approval requires the `action.approve` capability, which `domain_owner` does not hold; `remediation_approver` holds approve/cancel but not draft/submit (`packages/shared/src/capabilities.ts`). Tested in `actions.test.ts`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | SoD is per-action, not per-collusion: two cooperating principals defeat it, as anywhere. Approval policy is a boolean (`approval_required`), not yet type/volume/classification-sensitive.                                                                                                                                                                                        | Policy-based approval matrix (action type, volume, label downgrade) per handoff §15.1; multi-approver thresholds for bulk actions.                                                                                                                                                                         |
| T9  | **Source drift — executing an approved action against changed state**                                   | Before execution, the connector re-reads the live item version and compares it to the approved snapshot; any mismatch transitions the action to `drifted` with an audit event and requires explicit re-preview — never silent convergence (`packages/server/src/actions/service.ts`: drift check, `repreviewDriftedAction`). Post-execution verification re-reads the source and stores an `action_verifications` row; executor success text alone never marks success (ADR-0010). Tested in `actions.test.ts`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Drift detection is version-equality only; a same-version metadata change (where the source does not bump versions) would pass. TOCTOU window between the drift check and the write exists in principle.                                                                                                                                                                           | Validate eTag/cTag semantics against live Graph; conditional writes (If-Match) where the API supports them.                                                                                                                                                                                                |
| T10 | **Secrets or evidence leaking into logs, errors, or diagnostics**                                       | Redaction is structural, not best-effort: the logger emits only allowlisted field keys, and string values must match a conservative opaque-token pattern or are replaced with `[redacted]`; unknown keys are dropped and counted (`packages/server/src/kernel/log.ts`: `SAFE_KEYS`, `SAFE_VALUE`). Sizes are logged as bands (`sizeBand`). Audit `details_safe_json` is restricted to structured references by contract (`packages/server/src/kernel/audit.ts`). Dead-letter diagnostics are truncated safe strings (`packages/server/src/scan/queue.ts`). Unhandled errors log only the error class name (`packages/server/src/kernel/http.ts`: `onError`). No secrets in the DB — env references only (`config.ts`). Log-scan test per ADR-0011 (`kernel/log.test.ts`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | HTTP error *responses* include exception messages for `AppError` subclasses; a carelessly worded message could leak a path or name to the client (not to logs). Crash output from the runtime itself (stack traces to stderr) is not governed by the logger.                                                                                                                      | Error-message hygiene review as routes grow; structured error catalog; support-bundle allowlist schema when support tooling exists (handoff §9).                                                                                                                                                           |
| T11 | **Cross-tenant leakage (queries, cache, index, artifacts)**                                             | Every table carries non-null `tenant_id`; repository helpers refuse unscoped queries (ADR-0003). Stage cache, fingerprints, search FTS rows, graph nodes/edges, artifacts, and audit are all tenant-keyed (`packages/server/migrations/0001–0005`, `packages/server/src/kernel/objectstore.ts` path layout `artifacts/<tenantId>/…`). Audit hash chain is per-tenant (`packages/server/src/kernel/audit.ts`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Honest limitation: only one tenant is ever seeded, so isolation is enforced-by-convention but **not adversarially exercised**. SQLite gives no physical separation; FTS and graph projections share files.                                                                                                                                                                        | Multi-tenant isolation tests before any second tenant exists; physical per-tenant separation decisions (DB-per-tenant vs. RLS) in the Azure design; cache keys already carry tenant — keep that invariant under review.                                                                                    |
| T12 | **Dev identity mode reaching production**                                                               | Hard startup failure: `FMD_ENV=production` + `FMD_AUTH_MODE=dev` refuses to boot (`packages/server/src/kernel/config.ts`: `buildConfig`, tested in `config.test.ts`). Defense in depth: production rejects dev-mode *sessions* even if a cookie exists (`packages/server/src/kernel/http.ts`: `devSessionInProd`). Dev persona endpoints 404 outside development (`packages/server/src/identity/auth-routes.ts`). Every response carries `x-fmd-auth-mode`; the UI shows a persistent dev banner (ADR-0004). Default session secret is refused in production (`config.ts`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | The lockout hinges on `FMD_ENV` being set correctly by the deployment; a misconfigured "production" host left as `development` gets no protection. The Entra sign-in path is an **unimplemented 501 seam**, so there is no built production authentication yet — it must be implemented before any production deployment.                                                         | Fail-closed deployment templates that pin `FMD_ENV=production`; build + live-verify the Entra OIDC flow (issuer/audience/nonce) on the seam before any production claim.                                                                                                                                   |
| T13 | **Supply-chain compromise (packages, parsers, models, images)**                                         | Deliberately tiny runtime dependency surface: `hono` and `zod` are the server's only third-party runtime deps (`packages/server/package.json`); crypto, SQLite, and the test runner are Bun built-ins. `bun.lock` is committed. No model artifacts, containers, or auto-update channels exist in the slice. Fixture data is synthetic (fictional "Meridian Grove Holdings"); no real tenant data or credentials in the repo (verified by convention and `.gitignore`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Dependency versions use `^` ranges — the lockfile pins them, but there is no vulnerability scanning, SBOM, artifact signing, or CI provenance. Bun itself is trusted implicitly.                                                                                                                                                                                                  | Dependency/secret scanning in CI, SBOM generation, pinned exact versions for release builds, signed release artifacts and scanner images per handoff §19.                                                                                                                                                  |
| T14 | **Compromised connector or AI provider credential**                                                     | Exposure is minimized structurally: config stores env-var *references*, never secret values (`config.ts` `*_REF` indirection), and selecting a live provider/connector without a **resolvable** secret is an explicit `BlockedError` (`packages/server/src/aigateway/gateway.ts`: `buildAiProvider`; `packages/server/src/connectors/registry.ts`). The one live credential used to date is the **read-only** CDX app secret, kept out of source control (gitignored `.env`) and never logged (ADR-0013). Read vs. remediation Graph identities are separate config slots so a scanner credential never inherits write capability (`config.ts`, handoff §7.1).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | A leaked env var is a full credential; the current build has no rotation, no managed identity, and the process environment is readable by the host user. Provider retention/region policies are recorded in ADR-0005 but unenforced.                                                                                                                                              | Key Vault references + managed identities in the Azure deployment (`infra/main.bicep`); per-identity consent manifests (`docs/permissions-manifest.md`); revocation and rotation runbook; delete the `FMD-CDX-Reader` app registration to invalidate its read secret when CDX testing is done.             |
| T15 | **Audit tampering / repudiation**                                                                       | Append-only `audit_events` with a per-tenant SHA-256 hash chain; `verifyAuditChain` recomputes and reports the first broken link (`packages/server/src/kernel/audit.ts`). All review, Analyst, action, and authz-denied flows append audit events (`http.ts` `onError`, `actions/service.ts`, `analyst/executor.ts`). Tested in `audit.test.ts`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | The chain detects tampering but does not prevent it: an attacker with DB write access can rewrite the entire chain from genesis. There is no external anchor, no periodic checkpoint export, and no restricted-export control yet.                                                                                                                                                | Periodic chain-head anchoring to an external write-once store (or customer SIEM) in the Azure design; audit export with access control (`audit.read` exists; export path is future).                                                                                                                       |
| T16 | **Forged change notifications / cursor tampering / source-observation spoofing**                        | A change-notification receiver now exists (`packages/server/src/notifications/routes.ts`: `POST /api/webhooks/graph/notifications` and `/lifecycle`). It is **unauthenticated** (Graph calls it), so trust flows entirely from a **per-subscription `clientState` HMAC** — `HMAC(sessionSecret, subscriptionId)`, stored only as a `sha256`, verified in **constant time before any side effect** on both the notification and lifecycle paths (`notifications/subscriptions.ts`). A notification is treated as a **hint**: the pipeline reconciles by re-reading the delta feed, never trusting the notification body; a `clientState` mismatch is metered but **not persisted** (no write amplification); the Graph validation-token handshake is echoed. Cursors are persisted server-side (`source_cursors`) and never accepted from clients; the mock connector exercises `410 resync` and duplicate delivery (`scan.test.ts`, ADR-0008). Observations record acquisition context and completeness — "no broad share observed" is never presented as "none exists" (`pipeline.ts`: `upsertPermissionSet`). Live subscriptions additionally require a public webhook URL (honestly `blocked` otherwise). Tested in `notifications.test.ts` (incl. clientState mismatch and missing-clientState lifecycle rejection). | `clientState` is derived from `FMD_SESSION_SECRET`; a compromise of that secret would let an attacker forge valid notifications — so it must be protected (Key Vault) with the same care as any signing key, and a mismatch only forces a re-read (no content is accepted from the body regardless). Persisted delta links/cursors are still stored in plaintext in the slice DB. | Use a dedicated subscription-signing secret distinct from the session secret; rotate it via Key Vault; encrypt persisted delta links (ADR-0008).                                                                                                                                                           |

***

## 4. Data-retention profile (default: minimized evidence, ADR-0011)

The single seeded tenant runs the `minimized_evidence` profile (`tenants.data_handling_profile`, migration `0001`; `metadata_only` and `enhanced_evidence` are schema-ready values only — neither is implemented beyond configuration validation).

### 4.1 Persisted by default

* Stable source IDs, source/content versions, change types, safe locator fields (`physical_assets`, `asset_versions`, `asset_locations`).
* Label GUID/state observations and catalog references (`asset_label_obs`, `sensitivity_labels`).
* Normalized permission summaries with completeness state — digests, broad categories, principal counts — not unbounded per-user expansion (`permission_sets`, `permission_grants`).
* Activity aggregates (`activity_aggregates`).
* Exact hashes (SHA-256 over raw bytes and normalized text) and SimHash64 similarity fingerprints with band index (`fingerprints`, `fingerprint_bands`).
* Keyphrases, language, detector hits, and **bounded redacted excerpts** (`feature_artifacts`, `evidence`).
* Assertions with confidence, provenance kind, evidence links, and model/template versions; review decisions; findings; actions; audit (`assertions`, `review_decisions`, `findings`, `action_*`, `audit_events`).
* AI generation runs record task, provider, model, template version, and an input **hash** — not prompt bodies (`generation_runs`).

### 4.2 Not persisted by default

* Complete source originals (never written to disk).
* Plaintext credentials or tokens (config stores env-var references only).
* Unrestricted permission expansion per user.
* Raw prompts/responses for debugging.

### 4.3 Honest caveats against ADR-0011

1. **Bounded extracted text is retained.** The pipeline stores extracted text up to the 20,000-character extraction cap as an `extracted_text_bounded` artifact (`packages/server/src/scan/pipeline.ts` content stage; `packages/server/src/intelligence/extract.ts` limits). For any file under the cap, this is effectively the complete extracted text of that file. It is purpose-flagged and purpose-checked, but reviewers of this model should treat "complete extracted text is not persisted" as holding only for content larger than the cap.
2. **Scratch directory is described, not exercised.** ADR-0011 describes a per-work-unit scratch area under `data/scratch/` with cleanup on completion and startup recovery. The current slice processes content — including the rich-format parsers (OOXML via the bounded ZIP reader, PDF, RTF, HTML) — **entirely in memory** on the fetched bytes (bounded to 5 MB per item by `CONTENT_MAX_BYTES`) and never writes raw source bytes to disk, so no scratch directory is created or cleaned. The scratch + sandbox mechanism remains the hardening path to **isolate** that in-process rich-format extraction (see T6); it is not yet implemented.
3. **Retention sweeps are not implemented.** Artifacts carry a `retention_class` column, but no scheduled deletion job exists in this build. Retention is a recorded intent, not an enforced behavior.

### 4.4 Purpose flags (content-use flags)

Every artifact row carries `use_flags_json` from the set `classification_inference, owner_review, search, analyst, evaluation, training, troubleshooting` (`packages/server/src/kernel/objectstore.ts`). **Absence means not permitted**: `ObjectStore.get` requires a declared purpose and throws when the artifact's flags do not include it. Extracted-text artifacts are written with `classification_inference` and `owner_review` only — they are not readable for search, Analyst retrieval, evaluation, training, or troubleshooting purposes. No artifact in this build carries `training` or `evaluation` flags.

### 4.5 Excerpt bounds and redaction

Owner-review excerpts are bounded to **≤ 240 characters each, ≤ 3 per asset version** (`packages/server/src/scan/pipeline.ts`: `EXCERPT_MAX_CHARS`, `EXCERPT_MAX_COUNT`) and pass through digit masking for SSN-like patterns, currency amounts, and dates before storage (`redactExcerpt`). Excerpt *reads* additionally require the domain-scoped `asset.evidence.excerpt.read` capability (`packages/server/src/review/service.ts`); callers without it receive summaries with `excerpts: null`. The redaction patterns are fixture-tuned (US SSN format, `$` amounts) — they are a demonstration of the mechanism, not a production-grade PII redactor.

***

## 5. Explicitly deferred items (handoff SECURITY §21)

These are flagged, not silently assumed. None of them is resolved by this slice.

| Deferred decision                                       | Slice posture                                                                                                                                                                                                         |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Exact Azure service and network topology                | Undecided. ADR-0003 documents per-role store evolution targets (PostgreSQL, Blob, AI Search, Service Bus); no topology chosen.                                                                                        |
| Customer-managed keys and private endpoints             | Not designed. Slice relies on OS/disk encryption only; envelope encryption for evidence blobs is a documented follow-up (ADR-0011).                                                                                   |
| Evidence and audit retention defaults                   | Bounds implemented (excerpt caps, minimized profile); retention *durations* undecided and sweeps unimplemented (§4.3).                                                                                                |
| Support-access model                                    | Does not exist; no support bundles, no impersonation, no remote access path.                                                                                                                                          |
| Protected/encrypted document handling                   | Untouched; the parsers (text family, DOCX/XLSX/PPTX, HTML, RTF, born-digital PDF — see T6) do not decrypt, and protected/encrypted or otherwise unparseable content is honestly `unsupported`.                        |
| Regulatory certification roadmap                        | Out of scope for the slice.                                                                                                                                                                                           |
| Data export controls and aggregate privacy thresholds   | Analyst has row limits and scope injection but no export controls or differencing/aggregation-privacy defenses (handoff §16). `analyst.export` capability exists in the namespace but no export route is implemented. |
| Scanner update distribution/signing service             | No scanner fleet exists; single co-located worker.                                                                                                                                                                    |
| Disaster recovery targets                               | None; single SQLite file, no backup policy defined.                                                                                                                                                                   |
| Future opted-in telemetry beyond licensing/fleet health | No telemetry of any kind leaves the machine in this slice.                                                                                                                                                            |

Additional deferrals specific to this build, recorded for honesty:

* **Live Graph connector validation** — the read-only path was validated against the Contoso CDX tenant (ADR-0013; run record in `docs/cdx-test-runbook.md` §0). Change-sequence deltas, label read/write, Teams/Exchange, and write actions remain unvalidated live (`STATUS.md`).
* **Live AI provider governance** — Azure OpenAI/Anthropic adapters compiled but unexercised; provider retention/region policy enforcement (ADR-0005 §provider policy) activates only when a live provider is configured.
* **Entra OIDC** — no OIDC code-flow or token-validation code exists yet; the Entra path is a 501 seam to implement against (ADR-0004, B8).
* **Purview protected API enablement** — live label write requires Microsoft protected/metered API approval; the slice reports `blocked` honestly rather than simulating success (`packages/server/src/actions/service.ts`, `docs/permissions-manifest.md`).
