Skip to main content
Scope: Find My Data (release 26.7.16.14, revised 2026-07-17). Honesty rule: this document separates what is implemented and exercised (the deterministic mock M365 connector, plus the live Graph connector’s recorded read-only CDX validation), what is built but feature-/config-gated on the live path (live AI providers, Purview label read/write, change notifications, Teams, Exchange), and what is built and validated offline but awaiting a live-tenant round-trip (Entra OIDC sign-in — a full authorization-code + PKCE flow with RS256 ID-token validation, ADR-0035; the redirect-URI/consent round-trip is the operator step). See STATUS.md and ADR-0012 for the gating decision. Live validation: the live Graph connector has been validated read-only against a Microsoft CDX test tenant using application permissions Sites.Read.All + Files.Read.All (admin-consented on a dedicated read-only app registration). Sites.Selected (below) remains the least-privilege production recommendation; the broader Read.All pair was used only to stand up a working read-only pilot quickly. Reading assigned sensitivity labels was not attempted (needs the protected extractSensitivityLabels / InformationProtectionContent.Read.All), so the connector reports label state unknown — honestly, never guessing. See ADR-0013. All Microsoft Learn references were last checked on 2026-07-13. Re-verify API status, licensing, protected/metered requirements, and national-cloud support before any live deployment.

1. Identity model

1.1 Separate app registrations: read/scan vs. remediation

Find My Data’s live path uses (at minimum) two Entra ID app registrations with independent credentials: Why two identities:
  • Blast-radius containment. A compromise of the scanner — the component that touches untrusted content at volume — must not inherit any write capability against the customer tenant. The scan identity holds read-only permissions; only the remediation identity can mutate source state.
  • Independent rotation and revocation. A customer can disable remediation (revoke one credential) without losing inventory/scan continuity, and vice versa.
  • Auditable consent. Each registration carries a small, explainable permission set; the consent screens map 1:1 to product capabilities.
The reference architecture recommends further splitting notifications/ subscriptions into a third identity. Change notifications are built in this release: Graph /subscriptions on the read resource (no permission beyond the resource read the connector already holds), a public HTTPS notificationUrl (FMD_WEBHOOK_PUBLIC_URL) that Graph must reach, and a per-subscription clientState HMAC the receiver verifies before any side effect. Without the public URL the live path is honestly blocked; a dev simulator exercises the receiver + reconcile path. See packages/server/src/notifications/. Secrets are referenced by opaque IDs (*_SECRET_REF), never stored as values in configuration. The configuration layer (packages/server/src/kernel/config.ts) validates presence; the intended production store is Azure Key Vault or equivalent, with managed/workload identities preferred over client secrets.

1.2 Development-mode note

This build runs in dev identity mode by default (FMD_AUTH_MODE=dev, ADR-0004): sign-in is a persona picker over seeded fixture principals, every response carries x-fmd-auth-mode: dev, and the UI shows a persistent development banner. No Microsoft identity is involved on the exercised path. Two production protections are implemented and tested:
  • Production lockout: FMD_ENV=production with FMD_AUTH_MODE=dev is a hard startup failure (ConfigError), and dev-session cookies are rejected in production even if present.
  • Immutable identity mapping: external identities map to internal principals by (issuer, subject), never by email or display name.
Entra OIDC user sign-in is implemented as the production path (ADR-0035, 26.7.16.14): GET /api/auth/entra/login runs a real authorization-code flow with PKCE (S256), state, and nonce, and GET /api/auth/entra/callback validates the returned ID token — RS256 pinned (rejects alg:none/missing kid), signature checked before any claim against the tenant JWKS, then iss/aud/exp(+60 s skew)/nbf/nonce/sub/tid. Identity binds by the immutable (issuer, subject) pair (first login matches the token oid to a pre-provisioned principal), never by email; the delegated scopes are the standard openid profile email. There is still no silent fallback to dev — an unconfigured entra mode returns 501 not_configured and a dev-mode deployment returns 404. /api/ready reports entraSignIn as live only when the app registration, a resolvable client secret, and a redirect URI are all present; blocked when entra mode is selected but unconfigured; disabled in dev — never a misleading “live.” The whole flow is validated offline against a self-signing fake IdP (14 tests) and passed adversarial review with zero findings; the live tenant round-trip (redirect-URI registration, real consent) remains an operator step. See packages/server/src/identity/entra-oidc.ts and auth-routes.ts.

2. Microsoft Graph permission manifest (live path)

The read/scan permissions below (Sites.Read.All + Files.Read.All) were consented and exercised read-only against a live Microsoft CDX test tenant (ADR-0013). Everything else remains unconsented and gated. The table lists every permission, why it is needed, and its least-privilege alternative. Type key: A = application permission, D = delegated permission. Status key: mock = capability exercised via the deterministic mock connector; gated = code path exists behind configuration, not validated live; future = designed, not built.

2.0 Azure Storage data-plane RBAC (not Graph)

The Azure Storage connector authenticates with the same Entra app registration (or the FMD_AZURE_* override identity) against the storage data plane (https://storage.azure.com audience) — these are Azure RBAC role assignments, not Graph permissions, and you grant them per storage account or per container/share, which is the least-privilege mechanism for storage: The connector never enumerates the storage account: only the exact account/name entries in the selection lists are probed and listed, the selection entry is persisted per scope, and — like OneDrive — it is re-checked at scan time, so removing an entry (or disabling the feature) stops that container/share from being scanned even though its scope row persists. Container public-access level is observed from the data plane and surfaces anonymous exposure; management-plane RBAC role assignments are not enumerated in this build, and permission observations are recorded with completeness partial (blob) / not_observable (files) accordingly.

2.0b SMB / on-prem file shares (no Graph, no app credential)

The SMB connector reads operator-mounted shares through the deployment host’s filesystem — there is no protocol client and no application credential: The mount’s account is the read identity: scope it to read-only on the shares you list, and rotate/revoke it with normal AD tooling — Find My Data never sees or stores its password. The walker never follows symlinks, never reads outside the listed roots, and each root is re-checked at scan time (de-selecting a share stops its scans immediately). NTFS/share ACLs are not observable through a mount, so permission observations are recorded as not_observable — never guessed; native ACL mapping (a Windows scanner host reading security descriptors) is a designed, gated follow-up (ADR-0031).

2.0c SQL databases (Azure SQL / SQL Server; a DB login, not Graph)

The SQL connector authenticates with a database login the operator provides (via the FMD_SQL_CONNECTIONS_REF connection map) and the operator-installed mssql driver — no Graph permission and no app secret held by Find My Data: The connector is strictly read-only (never DDL/DML), queries only the databases in FMD_SQL_DATABASES (re-checked at scan time), bracket-quotes every catalog identifier and binds every value as a parameter (no SQL injection surface), and bounds the sampled data: ≤5 values × ≤64 chars per column over ≤100 columns, sampling only string-typed columns (numeric/date values are never emitted), with the row count reported only as a magnitude band — the exact count never becomes evidence. Grant the login the least privilege that lets it read the catalog and SELECT the tables you want profiled; a read-only login scoped to specific schemas is the recommended posture. SQL’s native sensitivity classifications are not mapped to Purview labels in v1 (unsupported); reading them is a gated follow-up (ADR-0032).

2.0d Google Drive (a service account, not Graph)

The Google Drive connector authenticates with a Google service account (JWT-bearer, read-only drive.readonly scope) the operator provides via FMD_GDRIVE_SA_KEY_REF, optionally impersonating a user through domain-wide delegation (FMD_GDRIVE_SUBJECT) — no Microsoft Graph permission involved: The connector is read-only, only ever enumerates the drives you list (re-checked at scan time), and the service-account private key is read by reference and never logged. Drive ACLs are fully observable, so sharing is mapped honestly (a publicly-shared file surfaces as anonymous exposure) with completeness complete. Native Google docs are exported to text for classification. Google Drive labels (Label API) are not mapped in v1 (unsupported).

2.0e Box (a Box app, not Graph)

The Box connector authenticates with a Box app using the Client Credentials Grant (server-to-server; no user in the loop) the operator provides via FMD_BOX_CLIENT_ID + FMD_BOX_CLIENT_SECRET_REF + FMD_BOX_SUBJECT_ID — no Microsoft/Google permission involved: The connector is read-only, only ever walks the folders you list (re-checked at scan time), and the client secret is read by reference and never logged. Box collaborations and shared links are observable, so sharing is mapped (an open shared link surfaces as anonymous exposure) with completeness partial (inherited folder collaborations are not fully expanded per file — the connector does not over-claim). Box classifications (metadata) are not mapped to Purview labels in v1 (unsupported).

2.1 Files.Read.All vs. Sites.Selected

Files.Read.All grants read on every drive in the tenant; it is simple but maximally broad. Sites.Selected grants nothing by itself — it requires both Entra admin consent and an explicit per-site grant (via the sites/{id}/permissions API or admin tooling) before any site is readable. See Selected permissions in OneDrive and SharePoint. Find My Data’s position:
  • Default the read/scan identity to Sites.Selected and surface, per scope, the difference between “connector is consented” and “target is actually accessible.” The connector contract’s discoverScopes() exists precisely to validate reachable scopes rather than assume them.
  • Fall back to Files.Read.All only where selected models cannot cover a required target (historically OneDrive personal drives; Microsoft has been extending selected-permission models — Files.SelectedOperations.Selected and related — so re-verify coverage at implementation time and document any endpoint that forces the broader grant).
  • The mock connector models both SharePoint drives and a OneDrive scope so the scope-granting UX and per-scope accessibility states are exercised without a tenant.
  • Implemented (ADR-0036, 26.7.16.14): setting FMD_SP_SITES (comma-separated site path identifiers, host:/sites/Name) switches discovery to resolve only the listed sites (GET /sites/{id} per entry) — the tenant-wide search=* enumeration is never issued, so least privilege holds by construction. Each discovered drive is tagged with its selection entry and re-checked at scan time, so removing a site from FMD_SP_SITES stops its persisted drive being scanned (and drains it on re-register). Identifiers are charset- and dot-segment-validated (a ../… traversal that would escape /sites/ is rejected), and a FMD_SP_SITES that is present but reduces to no valid entries fails closed at boot rather than silently reverting to the tenant-wide pilot. A granted site the app cannot yet reach (403/404) is skipped rather than failing the whole discovery. Empty FMD_SP_SITES keeps the Sites.Read.All broad-read pilot for backward compatibility. Three adversarial-review defects (path-traversal, config fail-open, discovery-only gate) were fixed with regression tests.

2.2 driveItem delta

Initial inventory and incremental change tracking both use driveItem delta. Contract expectations implemented in this release (against the mock connector):
  • @odata.nextLink / @odata.deltaLink are persisted as opaque cursors, exactly as returned, never parsed (connectors/types.ts; ADR-0008).
  • 410 Gone → full resynchronization is a first-class result (ChangesResult.kind = "resync_required"), tested via the mock connector’s simulated 410.
  • Deletes, renames, and duplicate delivery are handled and tested via scripted delta sequences (scan.test.ts).
  • The eTag/cTag distinction (any-change vs. content-change version) drives the stage-cache invalidation matrix, so a rename or permission change does not re-fetch content.

2.3 Permission observation caveats

driveItem permissions returns direct and inherited grants, but the detail returned depends on the caller’s access. Find My Data therefore treats permission observations as evidence with explicit completeness, not ground truth:
  • Every permission observation carries a completeness value — complete | partial | not_observable (packages/shared/src/domain-enums.ts) — and the acquiring authorization context is recorded on the observation envelope.
  • “No broad share observed” is never presented as “no broad share exists.” Downstream exposure/risk scoring consumes the completeness state.
  • Broad audiences (anonymous links, organization links, tenant-wide groups) are stored as categories, not expanded into per-user edges.

2.4 extractSensitivityLabels and assignSensitivityLabel

ObservationextractSensitivityLabels refreshes label metadata from supported file content and has explicit failure modes (notably encrypted content). The label observation result records the method used (metadata | extract_api | fixture) and the label state including unsupported and inaccessible; the exercised path is fixture. RemediationassignSensitivityLabel is a protected, metered API. Practical consequences, all reflected in the implementation:
  • Enablement process: access must be requested from Microsoft (protected API request/approval flow) before the tenant will honor calls; usage is metered (billed). In this build the capability is behind FMD_FEATURE_PURVIEW_LABEL_WRITE (default false) and /api/ready reports purviewLabelWrite: blocked until enabled.
  • Justification requirements: label downgrade or removal requires a justification. The action state machine (ADR-0010) enforces justification plus approval by policy default for downgrade/removal, independent of the API requirement.
  • Application-only limitations: some protected (encrypted) files cannot be relabeled in application-only mode. The connector’s previewLabelAction returns supported: false with limitations, and validation transitions the action to a blocked state with the reason (actions/service.ts).
  • Asynchrony and verification: the live operation is long-running; the state machine polls and then re-reads source state to verify — executor success text alone never marks an action succeeded.
  • Marking caveat: Office-at-rest labeling does not imply desktop-client visual markings (headers/footers) were rendered; verification records label state, not rendering.
In this build the full draft → validate → approve → execute → verify state machine is real and tested in mock mode only; live execution additionally requires the remediation app identity and protected-API enablement.

2.5 Label catalog listing (beta caveat)

The Graph label-list operation (security/informationProtection sensitivityLabels) is documented under /beta, which Microsoft states is not supported for production applications. Per ADR-0008, the catalog is therefore an isolated adapter (LabelCatalogSource) with three paths:
  1. Fixture import — default and exercised in this build.
  2. Customer-run Get-Label (Security & Compliance PowerShell) CSV export/import — the recommended production path until a v1.0 API exists.
  3. Future supported API — slot reserved.
Label identity everywhere is (msTenantId, labelGuid). Names and hierarchy are versioned display attributes; label names are never hard-coded and never used as identity.

3. Throttling and Retry-After handling expectations

Per Microsoft Graph throttling guidance, every live adapter must implement:
  • Honor Retry-After on 429/503 exactly; exponential backoff with jitter when the header is absent.
  • Retry classification: the durable job model (ADR-0007) already distinguishes throttled from transient, auth, unsupported, corrupt, policy_denied, and permanent; throttled work re-queues with the provider-specified delay and does not burn retry budget the way permanent failures do.
  • Per-provider concurrency budgets and tenant/source fairness so one large scope cannot starve others; backpressure propagates upstream so acquisition never overwhelms extraction or persistence.
  • Prefer delta over polling — the pipeline is delta-driven by design, which is the primary throttle-avoidance strategy.
  • Request correlation — correlation IDs are part of the canonical observation envelope for diagnosing throttling with Microsoft support.
Status: the queue mechanics (leases, heartbeat, backoff, dead-letter, retry classes, pause/resume) are implemented and tested against the mock connector, which can script throttling responses. Live throttling behavior has not yet been observed. Microsoft recommends Graph Data Connect for bulk extraction; it is explicitly out of scope for this release.

4. Change notifications

Microsoft Graph change notifications are built in this release (section 1.1; /api/ready reports changeNotifications as mock in mock mode, and disabled/blocked/live in graph mode depending on FMD_FEATURE_CHANGE_NOTIFICATIONS, resolvable Graph credentials, and FMD_WEBHOOK_PUBLIC_URL — see section 5). Design position:
  • Delta remains authoritative. Notifications are wake-up hints only; the pipeline processes changes exclusively from the delta feed. This means a deployment loses freshness, not correctness, when notifications are unavailable — every scope can still be reconciled by delta scans regardless.
  • Subscription lifecycle is real operational surface: subscriptions expire and must be renewed ahead of expiry; lifecycle events must be consumed; duplicates must be deduplicated; silent/stale subscriptions and widening cursor lag need alarms; delivery latency varies by resource and can exceed the 15-minute processing target, which is why the internal SLO is defined from receipt or delta observation, not source event time.
  • Deployment prerequisite: a publicly reachable, validated webhook endpoint (or supported Azure messaging delivery), which a local development deployment does not have.
Live notifications require no additional Graph permissions beyond the resource permissions above, but should run under a distinct identity per section 1.1.

5. Capability status (mirrors /api/ready)

GET /api/ready (packages/server/src/routes/health.ts) reports one honest mode per capability: mock, live, blocked (configured/required but prerequisites unmet — never silently faked), or disabled (not configured / not built). Current build state and exact prerequisites to advance each: The readiness payload never reports live on a feature flag alone: live additionally requires the relevant credentials to resolve (the *_REF env var names a variable that actually holds a value); an enabled capability whose credentials do not resolve reports blocked (routes/health.ts, tested in routes/health.test.ts). Even so, do not set feature flags ahead of their consent prerequisites: Graph rejects unconsented calls at run time (403), which surfaces as failed work units and probe failures — never silent success.

Internal authorization (for contrast)

Independent of Microsoft permissions, all product APIs are governed by the product’s own capability-based authorization (kernel/authz.ts): role grants are (capability, scope) tuples where scope is tenant-wide or domain-scoped (e.g., an HR domain owner holds asset.read only within owned domains), evaluated per request against the tenant context, with enforcement tested. Remediation additionally enforces separation of duties (requester cannot approve, ADR-0010). Every privileged operation lands in the per-tenant hash-chained audit log. Data handling follows the minimized-evidence retention profile (ADR-0011): bounded redacted excerpts, purpose-flagged artifacts, and default-deny log redaction.