G1 + G2 + G3 Implementation Plan
Date: 2026-03-01 Status: Draft v2 — addresses peer review findings Depends on: G4 (Shared Azure Modules) ✅, G5 (Function Key Authority Paths) ✅, Scan Safety ✅ Effort estimate: 5-8 days across 8 sessions Owner: TBD Target: Mar 4-11
Context
Three remaining W1 gaps need implementation before pilot:
- G1: Exposure Aggregation APIs — Backend APIs return path-centric counts but lack identity/workload aggregations, workload-grain exposures, and enriched detail panels
- G2: Remediation Content Generation — Evidence packs have static 3-string-per-type remediation; need context-aware structured actions with priority/rationale
- G3: Scope Drift UX — Rule fires correctly but finding detail doesn't show what roles drifted, when, or what resources they now reach
Sequencing constraint: G2 frontend must land before G3 frontend (both modify EvidencePackViewer.tsx and FindingDetail.tsx).
Detailed specs for each gap:
Session Plan (8 sessions, backend-first)
Session 1: G1 Backend
Files to modify:
| File | Change |
|---|---|
src/domain/posture/types.ts | Add optional active_autonomous_identities, workload_counts to PostureSnapshotDoc |
src/services/posture-service.ts | Add identity_counts (active_autonomous, dormant_authority) and workload_counts (autonomous, operator_assisted, human_triggered) computation; extend delta with new_autonomous_identities |
src/services/risk-cluster-service.ts | Add identity_count, workload_count, sensitive_domains, priority (P0/P1/P2) per cluster |
src/api/routes/exposures.ts | Rewrite list to workload-grain with EXP-{hash} IDs, identities[] array, enriched fields; add 5 detail panels (ownership_breakdown, workload_metadata, identity_bindings, execution_evidence_summary, path_summary) |
src/workers/handlers/evaluate-findings.ts | Update snapshot writer to persist new fields |
Key implementation details:
-
PostureSnapshotDoc (types.ts): Add optional fields so existing snapshots remain valid
active_autonomous_identities?: number;
workload_counts?: { autonomous: number; operator_assisted: number; human_triggered: number; }; -
PostureService (posture-service.ts):
identity_counts.active_autonomous: distinctidentity_idfrom active paths whereexecution_30d > 0identity_counts.dormant_authority: distinctidentity_idwhereexecution_30d === 0ANDfirst_seen_at < 90 daysworkload_counts: 3 calls tocountEntities(tenantId, { entityType: "workload", executionMode: "..." })- Delta: compare against prior snapshot's
active_autonomous_identities
-
RiskClusterService (risk-cluster-service.ts):
identity_count/workload_count: distinct from matched paths (already in memory)sensitive_domains: distinctdata_domainwhere sensitivity is confidential/restrictedpriority: P0 = critical + paths > 0, P1 = high + paths > 0, P2 = rest
-
Exposures route (exposures.ts):
- List: Group by
workload_id(notentity_id), one row per workload identities[]array from RUNS_AS relationshipsEXP-{sha256(tenant_id + workload_id).slice(0,8)}deterministic ID- Keep
workload_idin list response alongsideidfor the detail route - Detail route ID resolution (addresses review finding #1):
- Current detail route does
getEntity(req.params.id)— assumes entity ID - Must change: accept
EXP-{hash}OR rawworkload_idin:idparam - Resolution strategy: list response includes
workload_id; detail page fetches viauseExposureDetail(id)which returns the full exposure with entity data — remove the separateuseEntity(id)call from ExposureDetailPage - Backend detail route: if
idstarts withEXP-, query authority paths to find the workload entity, then proceed; otherwise fall back to direct entity lookup for backward compat - Detail response includes
workload_idso UI never needs to reverse the hash
- Current detail route does
- Detail: 5 new panels from existing StorageAdapter methods (
getEntitiesByIds,countExecutionEvidence,queryAuthorityPaths)
- List: Group by
-
Snapshot writer (evaluate-findings.ts): Persist
active_autonomous_identitiesandworkload_counts
Session 2: G2 Backend
Files to modify:
| File | Change |
|---|---|
src/domain/evidence-packs/types.ts | Add RemediationAction interface, expand RemediationSection with structured_actions? and summary? |
src/evidence/remediation.ts (NEW) | extractContextVars() + 13 finding-type builders + buildContextAwareRemediation() dispatcher |
src/evidence/sections.ts | Replace static buildRemediation() with import from remediation.ts; remove REMEDIATION_ACTIONS map |
src/evidence/markdown.ts | Render structured remediation (summary + actions by priority group) |
Key implementation details:
- RemediationAction type:
{ priority: "immediate" | "short_term" | "ongoing", action: string, rationale?: string } - RemediationSection extension: Keep
actions: string[]for backward compat, add optionalstructured_actionsandsummary - extractContextVars(): Pull entity name, type, source_system, path count, role count, sensitive paths, domains, owner names from
EvidenceBuildContext - 13 builder functions: Each returns context-aware actions referencing entity names, role names, resource names, sensitivity levels. Templates defined in G2 spec
- Dispatcher:
REMEDIATION_BUILDERSrecord keyed byFindingType, falls back to empty actions
Data availability assessment (addresses review finding #3):
Verified against actual EvidenceBuildContext — 11 of 13 finding types have 100% template variable coverage from existing pipeline data (entity name, type, source_system, execution_paths, roleEntities, ownerEntities, versionHistory are all populated by build-evidence-pack.ts).
2 types with partial degradation:
| Finding Type | Degraded Variable | What's Missing | Fallback |
|---|---|---|---|
privilege_justification_gap | {gap_descriptions} | Rule stores gap_resources (IDs only), not per-resource reason (no_activity vs action_mismatch) | Generic: "elevated access to {sensitivity_levels} data without matching activity" |
unknown_identity_binding | {binding_detail} | Rule stores resolved_count and runs_as_targets[] but not the reason category | Generic: "has no deterministic identity binding" |
Optional rule enrichments (can be deferred):
privilege_justification_gap: addgap_details: Array<{resource_id, reason: "no_activity" | "action_mismatch"}>to evidence_refsunknown_identity_binding: addbinding_status: "no_runs_as" | "unresolved_targets" | "ambiguous_targets"to evidence_refs
These are nice-to-have — the 2 degraded templates still produce useful context-aware text (entity name, path count, role names). Only the per-resource gap detail degrades to generic phrasing.
Session 3: G3 Backend
Review finding #5: Evidence pack
schema_version(currently "1.0", set inbuild-evidence-pack.tsline 10, included in integrity hash viaintegrity.ts) must be bumped when adding new sections. Bump to "1.1" in this session since it addsscope_drift_detailtoEvidencePackContent. G2's changes (expandingRemediationSectionwith optional fields) are backward-compatible within "1.0", but G3 adds a new top-level section key. The version bump happens once in Session 3 to cover both G2 and G3 structural changes.
Files to modify:
| File | Change |
|---|---|
src/domain/evidence-packs/types.ts | Add ScopeDriftRoleEntry, ScopeDriftDetailSection, add optional field to EvidencePackContent |
src/evidence/sections.ts | Add buildScopeDriftDetail(ctx) — version history walk, role resolution, per-role resource attribution, blast radius comparison |
src/evidence/build-evidence-pack.ts | Extend role ID collection to include evidence_refs.added_role_targets for scope_drift findings. Bump SCHEMA_VERSION from "1.0" to "1.1". |
src/api/routes/findings.ts | Add evidence_refs to GET /findings/:id response |
src/evidence/markdown.ts | Render scope_drift_detail section (timeline + blast radius) |
Key implementation details:
- ScopeDriftDetailSection: baseline/current role counts, added_roles with
first_seen_version/first_seen_at/grants_access_to, blast_radius before/after, new_resources - buildScopeDriftDetail: Return undefined if not scope_drift; extract
added_role_targetsfromevidence_refs; walk version history for first appearance; compute per-role resource access from execution_paths - build-evidence-pack.ts fix: When
finding_type === "scope_drift", also load role entities foradded_role_targetsIDs - findings.ts: Add
evidence_refs: finding.evidence_refsto response (additive, no breaking change) - Do NOT store role names in evidence_refs — would cause finding change detection churn on entity renames
Session 4: G2 Frontend
Files to modify:
| File | Change |
|---|---|
ui/src/api/api-types.ts | Add RemediationAction type, update RemediationSection type |
ui/src/components/findings/RemediationPanel.tsx (NEW) | Prioritized action cards: immediate (red badge), short_term (amber), ongoing (blue); summary header; fallback to plain list |
ui/src/components/findings/EvidencePackViewer.tsx | Add dedicated remediation rendering path using RemediationPanel instead of generic KeyValueTable |
ui/src/pages/FindingDetail.tsx | Add "Recommended Actions" card between Explanation and Evidence sections (summary + top 2 immediate actions) |
Session 5: G3 Frontend
Files to modify:
| File | Change |
|---|---|
ui/src/api/api-types.ts | Add ScopeDriftDetailSection type, add evidence_refs to DetailedFinding, add scope_drift_detail to EvidencePackContent |
ui/src/components/findings/ScopeDriftDetail.tsx (NEW) | Drift summary header (baseline vs current), drift timeline (vertical with role additions + resource arrows + sensitivity badges), blast radius comparison (before/after) |
ui/src/components/findings/EvidencePackViewer.tsx | Add scope_drift_detail to SECTION_ORDER, add rendering branch |
ui/src/pages/FindingDetail.tsx | Check finding_type === "scope_drift", render ScopeDriftDetail above evidence pack |
Reuse existing components: SeverityBadge pattern for sensitivity badges, DeltaIndicator for delta display
Session 6: G1 Frontend
Review finding #2: The exposure contract change from entity-grain to workload-grain affects more than just the detail page. The full impact chain is: list rows, inline expand, detail routing, hooks, and types.
Files to modify:
| File | Change |
|---|---|
ui/src/api/api-types.ts | Update PathPostureSummary (add identity_counts, workload_counts, extended delta), PathRiskCluster (add identity_count, workload_count, sensitive_domains, priority), rewrite ExposureSummary (workload-grain: id is now EXP-hash, add workload_id, workload_name, identities[], remove entity_id/entity_name or alias), rewrite ExposureDetail (add 5 panels, entity → workload) |
ui/src/hooks/use-exposures.ts | useExposureDetail(id) — no change needed (URL stays /api/v1/exposures/:id). But useExposures() list response shape changes. |
ui/src/pages/ExposuresPage.tsx | List rows: Update ExposureRowGroup — navigation link changes from exposure.entity_id to exposure.id (line 206). Row columns update: show workload_name instead of entity_name, show identities count badge. |
ui/src/components/ExposureRow.tsx | Update row rendering for workload-grain fields (workload_name, identity count, execution_mode) |
ui/src/components/ExposureInlineExpand.tsx | Update inline expand to show workload context instead of entity_id (line 22), show identities list |
ui/src/pages/ExposureDetailPage.tsx | Major refactor: Remove useEntity(id) call (line 58) — detail page should use useExposureDetail(id) as the sole data source. The exposure detail response now includes entity data, findings, paths, and 5 new panels. Remove separate useFindings call keyed on entity. Render ownership_breakdown, workload_metadata, identity_bindings panels. |
ui/src/pages/OverviewPage.tsx | Display identity_counts (active autonomous, dormant authority) and workload_counts in stat cards |
ui/src/pages/ClustersListPage.tsx | Display identity_count, workload_count, sensitive_domains tags, priority badge (P0=red, P1=amber, P2=blue); sort by priority |
Session 7: Seed Data
Review finding #4: The G3 source plan included specific seed work (intermediate sync for 3-point timeline) that was dropped in consolidation. Restored below.
File: scripts/seed-demo-w1.ts
G1 seed changes:
| Change | Purpose |
|---|---|
Change wl-sec-logger to execution_mode: "operator_assisted" | Populate all 3 workload_counts categories |
Add/modify one workload with execution_mode: "human_triggered" | Complete workload_counts coverage |
Extend posture snapshot with active_autonomous_identities and workload_counts | Enable delta computation on first load |
Verify all entities have display_name properties | Human-readable names in all panels |
G3 seed changes (from source scope-drift plan Phase 6):
| Change | Purpose |
|---|---|
Add intermediate sync1_5Payload between sync 1 and sync 2 | Creates 3-point drift timeline showing incremental role creep |
id-svc-finance gets 3 roles in sync 1.5 (baseline 2 + AP Write) | Shows AP Write added ~30 days ago, AR Write + Ledger Admin added recently |
| Use timestamp ~30 days ago for sync 1.5 | Creates realistic temporal gap between role additions |
| Add temporal marker: "AP Write role added by change request CR-4821" | Shows change provenance in timeline |
| Insert API call and poll between sync 1 and sync 2 | Third sync point processed by pipeline |
Expected 3-point timeline after seed:
[Nov 27] Baseline: Finance Read, Invoice View
[Jan 25] +1 role: AP Write → AP/AR Ledger (restricted)
[Feb 25] +2 roles: AR Write → AP/AR Ledger (restricted)
Ledger Admin → Financial Records API (restricted)
Session 8: Tests
Review finding #4 (continued): Restored G3 test scope from source plan Phase 7.
Files to create/modify:
| File | Description |
|---|---|
test/evidence/remediation.test.ts (NEW) | Test each of 13 types: entity name appears in action text, priorities correct, summary populated, fallback for missing context data |
test/evidence/sections.test.ts | Test buildScopeDriftDetail: version history walk produces correct first_seen_version/first_seen_at, returns undefined for non-scope_drift, per-role resource attribution matches execution_paths |
test/evaluator/rules/scope-drift.test.ts | Verify evidence_refs contains added_role_targets (IDs only). Explicitly assert no added_role_names key exists — prevents finding churn regression. |
| Tests for posture-service | identity_counts computation, workload_counts computation, delta against prior snapshot |
| Tests for risk-cluster-service | identity_count, workload_count, sensitive_domains, priority classification |
| Tests for exposures route | Workload-grain grouping, EXP-hash generation, identities array, detail panels, EXP-hash ID resolution |
Test patterns: Vitest, makeStorage(overrides) factory, makePath(overrides) fixtures
File Collision Map
| File | G1 | G2 | G3 | Resolution |
|---|---|---|---|---|
src/domain/evidence-packs/types.ts | — | Session 2 | Session 3 | G2 adds RemediationAction; G3 adds ScopeDriftDetailSection + bumps schema (no conflict) |
src/evidence/sections.ts | — | Session 2 | Session 3 | G2 replaces buildRemediation; G3 adds buildScopeDriftDetail (no conflict) |
src/evidence/build-evidence-pack.ts | — | — | Session 3 | G3 extends role ID collection + bumps SCHEMA_VERSION to "1.1" |
ui/src/api/api-types.ts | Session 6 | Session 4 | Session 5 | All additive, no conflicts |
ui/src/components/findings/EvidencePackViewer.tsx | — | Session 4 | Session 5 | G2 first (remediation path), G3 second (scope_drift path) |
ui/src/pages/FindingDetail.tsx | — | Session 4 | Session 5 | G2 first (Recommended Actions card), G3 second (ScopeDriftDetail) |
ui/src/pages/ExposureDetailPage.tsx | Session 6 | — | — | Major refactor: sole data source becomes useExposureDetail |
ui/src/pages/ExposuresPage.tsx | Session 6 | — | — | Navigation links, row rendering |
ui/src/hooks/use-exposures.ts | Session 6 | — | — | Response type update |
Verification
After each session:
cd sv0-platform && npx tsc --noEmit— type check passesnpx vitest run— all tests pass- After Session 7:
npx tsx scripts/seed-demo-w1.ts— seed completes - After Session 8: full test suite green
End-to-end:
- Overview page shows identity_counts and workload_counts
- Risk cluster cards show identity_count, workload_count, priority badge
- Exposures list shows workload-grain rows with identities array
- Exposure detail shows 5 enriched panels
- Finding detail shows context-aware remediation with priorities
- Scope drift finding shows drift timeline, blast radius comparison
Risks
| Risk | Mitigation |
|---|---|
| EXP-hash ID not resolvable by detail route | Detail route supports both EXP-{hash} prefix (query authority paths to find workload) and raw entity ID (backward compat). Detail response always includes workload_id so UI never reverses the hash. |
| G1 frontend scope larger than it appears | Exposure contract change touches 8 files: api-types.ts, use-exposures.ts, ExposuresPage.tsx, ExposureRow.tsx, ExposureInlineExpand.tsx, ExposureDetailPage.tsx, OverviewPage.tsx, ClustersListPage.tsx. ExposureDetailPage refactored to use useExposureDetail(id) as sole data source. |
| Remediation context degradation | 11/13 types have 100% data coverage. 2 types (privilege_justification_gap, unknown_identity_binding) degrade 1 variable each to generic phrasing. Optional rule enrichments can close these gaps later. |
| Schema version not bumped | Bump SCHEMA_VERSION from "1.0" to "1.1" in Session 3. Integrity hash includes schema_version, so old packs stay valid. EvidencePackViewer already skips undefined sections (data === undefined → return null). |
| Performance at scale | Authority path queries capped at 5000 (demo-scale: 20-50 paths). Add truncated flag. |
| Posture snapshot backward compat | Handle null/undefined for new fields in prior snapshots using priorSnapshot?.field ?? null pattern. |
| Historical roles not in ctx.roleEntities | Fix: extend role ID collection in build-evidence-pack.ts to include evidence_refs.added_role_targets for scope_drift findings. |
| Evidence_refs churn from display names | Do NOT store role names in evidence_refs. Name resolution happens in evidence pack builder only. |
| File collisions between G2/G3 | Managed via session sequencing — G2 frontend lands first, G3 adds to same files after. |