Skip to main content

Data catalog & handling

This document is for privacy/compliance reviewers and data-governance stakeholders evaluating Find My Data. It catalogs every category of data the platform acquires from customer sources, derives from content, retains, and — equally important — what it never stores, with sensitivity, retention behavior, and access level for each. Every table name, column, constant, and env var below was verified against the code at release 26.7.15.0 (Bun 1.3.14 runtime, 196 passing tests). Companion documents: the threat model (trust boundaries and residual risks), ADR-0011 (retention and evidence profile), ADR-0003 (persistence model), and the permissions manifest (what the product asks Microsoft for).

How to read this catalog

Validation states. Find My Data never overstates its state. Each category below is marked with one of:
  • Live-validated (CDX) — exercised against the live Contoso CDX demo tenant, read-only (ADR-0013, docs/cdx-test-runbook.md §0).
  • Mock-validated, live gated — built and tested against the deterministic mock M365 fixture; the live path exists but is behind an explicit feature flag and has not been run against a real tenant.
  • Not built — recorded intent only; called out explicitly.
Storage locations. All structured data lives in one SQLite database (FMD_DB_PATH, default ./data/fmd.db) behind role interfaces (ADR-0003). Content-derived blobs live in the artifact store (FMD_ARTIFACT_DIR, default ./data/artifacts) at <FMD_ARTIFACT_DIR>/<tenantId>/<artifactId>, indexed by the artifacts table. Every tenant-scoped business table carries a NOT NULL tenant_id; the deliberate exception is the deployment-level scanners table (§7), which is infrastructure, not tenant data. Retention. Retention durations are not enforced in this build. Artifacts carry a retention_class column (default standard) and ADR-0011 records per-class TTLs as a follow-up, but no scheduled deletion sweep exists (threat model §4.3). Where a row below says “until superseded,” the schema keeps temporal history (is_current, valid_from/valid_to, supersession chains) rather than deleting.

1. Evidence access levels

Evidence rows carry an explicit access classification, and reads are gated by distinct capabilities so that “can see that an asset exists” never implies “can read content excerpts.” Enforcement facts (all tested):
  • The capability list is defined in packages/shared/src/capabilities.ts; platform_admin deliberately holds no asset.evidence.*, review.*, or analyst.* capabilities.
  • A caller with summary access but not excerpt access receives evidence with excerpts: null — never a lesser-redacted fallback (packages/server/src/review/service.ts).
  • Every evidence read is audited under category evidence_access, with distinct actions for reads that included excerpts (candidate_evidence_with_excerpts_read) vs. reads that did not.
  • Artifact-store reads are additionally purpose-checked: ObjectStore.get requires a declared purpose against the artifact’s content-use flags (use_flags_json, §3) and throws when the artifact does not permit it.

2. Data-handling profiles

tenants.data_handling_profile accepts three values (migration 0001). The exact profile notes shipped in packages/server/src/admin/settings.ts: Honest implementation state: only minimized_evidence (the default) drives actual pipeline behavior, and the current build deviates from its own profile note in one documented way: bounded extracted text (≤ 20,000 characters) is retained, because source-free re-scoring on detector releases requires a text representation. This deviation, its rationale, and its follow-ups (envelope encryption, per-class TTLs, a true drop-text option) are recorded in ADR-0011 and the threat model §4.3. The metadata_only and enhanced_evidence values are schema-ready configuration only — validated, audited, and cache-invalidating (a profile change bumps the tenant config_version, which participates in the evidence-bearing stage cache keys — content, features, inference — so the next scan/rescore re-derives those stages; tested in scan.test.ts and admin.test.ts), but they do not yet change what the pipeline persists. A reviewer should treat the platform’s actual retention posture as: minimized evidence, plus bounded extracted text as the ADR-0011 deviation.

3. Catalog: content-derived data (highest sensitivity)

Everything in this section is derived from customer file content. Per the handoff security model, derivative data is still sensitive — none of these tables is “safe” merely because it holds no full text. Validation state: extraction, fingerprints, keyphrases, detector hits, excerpts, and embeddings are live-validated (CDX) — 172/174 real files extracted, 172 embeddings indexed, re-score-without-refetch proven live. Azure OpenAI/Anthropic AI-provider paths are config-gated and unexercised; the deterministic mock is the only exercised provider.

4. Catalog: source observations (metadata acquired from connectors)

Validation state: the read-only Graph inventory/delta/permission/label-observation path is live-validated (CDX): 34 drives discovered, 174 assets via driveItem delta, deduplicated permission sets, labels honestly unknown under a read-only app. The mock connector remains the default and CI path (FMD_CONNECTOR_MODE=mock).

5. Catalog: messaging (Teams / Exchange)

Raw message bodies are never persisted. Both connectors persist the same bounded shape (packages/server/src/teams/service.ts, packages/server/src/exchange/service.ts):
  • Preview: whitespace-normalized body, passed through redactSensitivePreview, truncated to 200 chars (BODY_PREVIEW_MAX).
  • Redaction (packages/server/src/intelligence/redact.ts) masks, in order: US SSNs (123-45-6789 / space-separated) → [redacted-ssn]; card-like 13–16 digit runs (optionally space/dash grouped) → [redacted-card]; bare digit runs of 9+ → [redacted-number]. Detector signals are kept separately; only raw values are masked.
  • Body hash: sha256 of the full body (body_hash) for change detection — non-reversible.
  • Detector signals: detector_hits_json holds only {detectorKey, informationTypeName, count} (SafeDetectorHit) — no matched text.
Access: Teams/Exchange messages are not yet mapped to business-domain ownership, so their read views are admin/audit-scoped, deny-by-default (STATUS.md known limitation). Validation state: mock-validated, live gated. Both connectors are built and tested against fixtures (including a regression test that the preview redaction holds). Live paths require FMD_FEATURE_TEAMS_CONNECTOR (+ ChannelMessage.Read.All, a protected API) and FMD_FEATURE_EXCHANGE_CONNECTOR (+ Mail.Read scoped by application access policy) and have not been run against a real tenant.

6. Catalog: governance and human-decision records

7. Catalog: operational data

8. Catalog: audit

audit_events (migration 0004) is the platform’s tamper-evident record:
  • Append-only with a per-tenant SHA-256 hash chain (prev_hashhash, genesis-anchored); verifyAuditChain recomputes the chain and reports the first broken link (packages/server/src/kernel/audit.ts, tested).
  • Categories: auth, config, governance, scan, evidence_access, review, analyst, action, model, operations.
  • details_safe_json is restricted to structured references (ids, versions, outcomes, booleans) by the appendAudit contract and typed input — never excerpts, paths, prompts, or names. Honest nuance: unlike the logger (§10), this is a caller contract backed by tests, not a runtime key allowlist.
  • Read access requires the audit.read capability (held by auditor).
  • Limit (threat model T15): the chain detects tampering but does not prevent it against an attacker with direct DB write access; external anchoring is a documented follow-up.
Retention: indefinite, append-only. No audit export route exists yet.

9. What is never stored / never leaves the boundary

10. Log redaction policy

The logger (packages/server/src/kernel/log.ts) enforces redaction structurally, not best-effort:
  • Only field keys on the SAFE_KEYS allowlist are emitted (correlation/tenant/principal/work-unit ids, stage, status, outcome, error category, HTTP route/method/status, duration, counts, sizeBand, etc.). Unknown keys are dropped and counted (_droppedFields).
  • String values must match a conservative opaque-token pattern (^[A-Za-z0-9_.:/-]{1,120}$) or are replaced with [redacted] — so free-text content (excerpts, prompts, multi-word names or titles) cannot pass even under an allowlisted key. The pattern alone would admit a single-token value such as a URL or hex hash; the end-to-end scan test below is what verifies those never appear in default logs.
  • Sizes are logged as bands (lt10kgte10m), not exact values, via sizeBand.
  • A ring buffer of the last 500 already-sanitized entries backs the operations UI.
  • An end-to-end log-scan test asserts that default logs contain no names, paths, URLs, phrases, GUIDs, or hashes (packages/server/src/testing/acceptance.test.ts; unit tests in packages/server/src/kernel/log.test.ts; ADR-0011).
Known residual (threat model T10): HTTP error responses include AppError messages, and runtime crash output to stderr is outside the logger’s control.

11. Fleet telemetry (strict allowlist)

Defined in packages/server/src/controlplane/telemetry.ts (fleet-telemetry-v1), covering licensing + fleet health only. The Zod schema is .strict() — any field not on this list fails validation, so the payload fails closed rather than over-sharing: schemaVersion, deploymentId (a non-reversible sha256 of the internal tenant id, truncated to 24 hex chars — never the tenant name or id), productVersion, collectedAt, entitlementId, entitlementState, scannerCount, activeScannerCount, queueDepthBand (empty/low/moderate/high), deadLetterBand (none/low/elevated), scaleBand (small/medium/large/xlargeraw asset counts are never reported), telemetryEnabled. A SENSITIVE_FIELD_DENYLIST (name, path, excerpt, content, domain, informationtype, asset, email, principal, tenantid, secret, token, body, subject, count) backs a CI tripwire test so a future edit adding e.g. assetCount fails the build. scannerCount/activeScannerCount are the only deliberately allowed *count fields (operational, not content). Customer controls: control_plane_state.telemetry_enabled is opt-outable at any time, and the UI shows a transparency preview of the exact payload. Honest state: the payload builder and controls are built and tested; no receiving Control endpoint exists, so no telemetry leaves the machine in this release.

12. Retention summary and open items

Open retention items a reviewer should track: retention durations and sweeps (not built), envelope encryption of evidence artifacts (not built), a true text-dropping metadata_only profile (config-only today), and audit chain external anchoring (not built). Each is recorded in ADR-0011, the threat model, and the roadmap.