feat(reports): identity_access_review evidence report type, end to end (#5818 W06) - #6034
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…anaged registry (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d list (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ic (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…restricted refusal (#5784 W06) Also adds loadDomainFreshness to m365Sync/summary.ts — the per-domain last_complete_snapshot_at reader W03 specifies, with the identical name and shape so the two waves converge rather than fork. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Pdf arm (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…f-serve generate (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… end (#5784 W06) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n-in risk are unmeasured, not zero (#5784 W06) Three findings from /pr-review-toolkit:review-pr on #6034, all in the one family this report type exists to prevent — an absence of MEASUREMENT rendered as a measurement of ZERO. 1. remoteAccess had no measured-gate, unlike every other section. An org with no devices, or whose devices have never reported `active_vpns` (the column is NULL until the collector runs), produced `{}` — which the renderer prints as "None observed at last check-in". It now returns null with a data-gap line, and a partially-reporting fleet discloses the silent devices instead of folding them into the total. 2. The PDF asserted "sign-in risk requires an Entra ID P2 licence" whenever byRiskLevel was null — but that is null for two reasons, and only one is a licensing statement. A quiet, fully-licensed tenant was being told on a customer-facing document that they lack a subscription they have. The generator now records `coverage.riskUnmeasured` (events held AND every risk value hidden) and the renderer prints a neutral line otherwise. 3. Four branches had no test at all and would have survived deletion: the identity-inventory unmeasured arm, the caMeasured never-synced-vs-empty distinction, the 500-row admin-detail cap and its withheld disclosure, and inactive-client filtering. Each now has a regression test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deploying breeze with
|
| Latest commit: |
f0c276d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://cd68ade2.breeze-9te.pages.dev |
| Branch Preview URL: | https://feature-5812-service-plan-ev-w01e.breeze-9te.pages.dev |
|
Review run: Findings: 3 raised → all 3 addressed in
The reviewers independently confirmed the six named design invariants hold, each backed by a discriminating test: restricted authority reads nothing ( Tests: Not verified locally: the new Status: review-clean, awaiting maintainer merge. Do not merge until |
|
Final unit-suite numbers (re-run at reduced concurrency after the review fixes in
The five Everything this PR actually changed is green, including the 8 new regression tests added for the review findings. |
…y_access_review (W06) registrations Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByUsCYMsa2c4EDG797LTqf
|
Merged |
W06 —
identity_access_review: the sign-in review artifactCloses #5818 (wave W06 of #5812 / #5784).
Turns W05's accumulated
m365_signin_events— plus the identity, conditional-accessand remote-access data the M365 sync already holds — into a service-plan evidence
artifact, end to end: enum label → generator → PDF renderer → portal → web wiring →
eight locales → integration proof.
Plan:
docs/superpowers/plans/billing/2026-09-14-service-plan-evidence-reports-w06-identity-access-review.mdSpec:
docs/superpowers/specs/billing/2026-09-14-service-plan-evidence-reports-spec.md§3.5.2–3.5.3What the artifact claims
Interactive sign-ins only, for the window it actually covers. Non-interactive,
service-principal and managed-identity sign-ins are not persisted by W05 and the
artifact never implies otherwise — the PDF's own title block says "Interactive
sign-ins for <period>", and a unit test asserts the string "all sign-ins" never
appears in the rendered bytes.
The three limits it prints, every time
AuditLog.Read.Allgrants thepermission; the licence grants the data. W05 persists Microsoft's
unlicensedoutcome as a complete, zero-row success, so every sign-in figure goes NULL
and the artifact renders a data-gap page, never an empty table. If that case
ever reads
0, the feature is shipping a lie — integration Case 3 asserts NULL.hiddensentinel without P2. They aredropped by
isRiskFieldMeasuredand the section renders unmeasured, never"no risk detected".
sync only, so the first monthly report after enabling W05 is partial and says so
(
coverage.coveredFrom/coveredTo+ one printed sentence fromsigninCoverageLine). Freshness islast_complete_snapshot_at, neverlast_success_at— a partial run succeeds without enumerating the tenant.mfa_registered: NULLmeans UNKNOWN, counted intomfaUnknown/adminsMfaUnknownand never rendered as "not registered".Restricted scope is a refusal, not a filter (OD-8 = A)
M365 identity data has no site dimension, so a site-restricted technician gets
the
zeroSafeReportempty-but-shaped result with an explanatory data-gap line —and nothing is read at all. Serving an org-wide identity view to a site-limited
account would be a scope escalation. This differs from W02–W04 on purpose; the
config schema deliberately has no
siteskey and the options form deliberatelyhas no site selector. Integration Case 2 asserts no identity row and no admin
UPN or IP survives into the result.
Remote access is client presence, not policy
devices.active_vpnsrecords which overlay client was running at last check-in; thecollector carries no rules, peers or keys (
agent/internal/collectors/vpn.go). Thesection is titled Remote-access client presence and its caveat string says so.
A PDF test asserts the phrase "VPN policy review" never appears.
PII and the delivery gate
This artifact carries user principal names, IP addresses and cities. It reaches a
customer only through W01's OD-12 delivery gate (integration Case 6: invisible in
the portal list and
renderRunPdfrefuses, until the occurrence is delivered), thePDF is never persisted (
report_runs.resultis jsonb only), and the type isdeliberately out of
PORTAL_REPORT_TYPESand both portal-user allowlists inreportGenerationService.ts(OD-10 = A) — a customer can read a delivered artifactbut can never generate one on demand.
No consent change
Every Graph read behind this is already granted by
customer-graph-readmanifestv3:
AuditLog.Read.All,Policy.Read.All,User.Read.All,RoleManagement.Read.Directory. Identity Protection risky users/events stay out —adding them would be a manifest v4 bump forcing every existing customer to
re-consent.
The silent failure this PR closes
buildReportPdf's finalelsefalls through torenderGenericReport, which printsthe rows as a plain table and drops the entire designed summary. On a
PII-bearing identity artifact that is not merely an ugly PDF: the caveats that keep
it honest disappear while the sign-in rows remain. No unit test catches it unless
one is written for the arm specifically, so this wave ships both a unit test
(
reportPdf.identityAccess.test.ts, spying on the renderer) and a server-sideintegration assertion on the rendered bytes.
Shared helper added for W03 as well
loadDomainFreshness(orgId, domains)is added toapps/api/src/services/m365Sync/summary.tswith exactly the name and shape W03's plan specifies (
{ asOf, lastStatus, truncated, sources, unlicensed }, total record), because W03 (#5815, PR #6007) hadnot landed it on
mainwhen this wave ran.loadSyncSummaryis left untouched, sothe two waves converge rather than fork.
Rollout
M365_TENANT_SYNC_ENABLEDmust be on or this wave is inert (the generator saysso explicitly rather than reporting zeros). A value in
/opt/breeze/.envisnecessary but not sufficient: compose interpolation only happens for variables
listed in the service's
environment:block, so it must also be mapped in theapiservice'senvironment:block of/opt/breeze/docker-compose.yml. Confirmper region.
a customer. Graph retains sign-in logs ~30 days and Breeze accumulates forward
from first sync, so an org whose earliest event is two days old gets a two-day
report — correctly labelled, but not what a customer promised a monthly review
expects. Check per region:
this PR.)
pnpm --filter @breeze/api reports:reprovision-portal-definitions(dry run, then
--apply), or orgs that enabled portal reports earlier will lack thenew definition.
Migration
apps/api/migrations/2026-10-17-110700-report-type-identity-access-review.sql—one
ALTER TYPE report_type ADD VALUE IF NOT EXISTS 'identity_access_review',alone in the file (a label added by
ALTER TYPEcannot be used until itstransaction commits). DDL only, writes no rows, so no
breeze.scopeelection isrequired and it is not added to
migrationRlsScope.test.ts's frozen baseline.No new table, no new column, no RLS change — the cascade/export-policy registries are
untouched.
Localization caveat
The eight locale files localize the web UI only; the PDF renderer is English.
🤖 Generated with Claude Code