Skip to main content

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:

FileChange
src/domain/posture/types.tsAdd optional active_autonomous_identities, workload_counts to PostureSnapshotDoc
src/services/posture-service.tsAdd 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.tsAdd identity_count, workload_count, sensitive_domains, priority (P0/P1/P2) per cluster
src/api/routes/exposures.tsRewrite 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.tsUpdate snapshot writer to persist new fields

Key implementation details:

  1. 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; };
  2. PostureService (posture-service.ts):

    • identity_counts.active_autonomous: distinct identity_id from active paths where execution_30d > 0
    • identity_counts.dormant_authority: distinct identity_id where execution_30d === 0 AND first_seen_at < 90 days
    • workload_counts: 3 calls to countEntities(tenantId, { entityType: "workload", executionMode: "..." })
    • Delta: compare against prior snapshot's active_autonomous_identities
  3. RiskClusterService (risk-cluster-service.ts):

    • identity_count/workload_count: distinct from matched paths (already in memory)
    • sensitive_domains: distinct data_domain where sensitivity is confidential/restricted
    • priority: P0 = critical + paths > 0, P1 = high + paths > 0, P2 = rest
  4. Exposures route (exposures.ts):

    • List: Group by workload_id (not entity_id), one row per workload
    • identities[] array from RUNS_AS relationships
    • EXP-{sha256(tenant_id + workload_id).slice(0,8)} deterministic ID
    • Keep workload_id in list response alongside id for 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 raw workload_id in :id param
      • Resolution strategy: list response includes workload_id; detail page fetches via useExposureDetail(id) which returns the full exposure with entity data — remove the separate useEntity(id) call from ExposureDetailPage
      • Backend detail route: if id starts with EXP-, query authority paths to find the workload entity, then proceed; otherwise fall back to direct entity lookup for backward compat
      • Detail response includes workload_id so UI never needs to reverse the hash
    • Detail: 5 new panels from existing StorageAdapter methods (getEntitiesByIds, countExecutionEvidence, queryAuthorityPaths)
  5. Snapshot writer (evaluate-findings.ts): Persist active_autonomous_identities and workload_counts


Session 2: G2 Backend

Files to modify:

FileChange
src/domain/evidence-packs/types.tsAdd RemediationAction interface, expand RemediationSection with structured_actions? and summary?
src/evidence/remediation.ts (NEW)extractContextVars() + 13 finding-type builders + buildContextAwareRemediation() dispatcher
src/evidence/sections.tsReplace static buildRemediation() with import from remediation.ts; remove REMEDIATION_ACTIONS map
src/evidence/markdown.tsRender structured remediation (summary + actions by priority group)

Key implementation details:

  1. RemediationAction type: { priority: "immediate" | "short_term" | "ongoing", action: string, rationale?: string }
  2. RemediationSection extension: Keep actions: string[] for backward compat, add optional structured_actions and summary
  3. extractContextVars(): Pull entity name, type, source_system, path count, role count, sensitive paths, domains, owner names from EvidenceBuildContext
  4. 13 builder functions: Each returns context-aware actions referencing entity names, role names, resource names, sensitivity levels. Templates defined in G2 spec
  5. Dispatcher: REMEDIATION_BUILDERS record keyed by FindingType, 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 TypeDegraded VariableWhat's MissingFallback
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 categoryGeneric: "has no deterministic identity binding"

Optional rule enrichments (can be deferred):

  • privilege_justification_gap: add gap_details: Array<{resource_id, reason: "no_activity" | "action_mismatch"}> to evidence_refs
  • unknown_identity_binding: add binding_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 in build-evidence-pack.ts line 10, included in integrity hash via integrity.ts) must be bumped when adding new sections. Bump to "1.1" in this session since it adds scope_drift_detail to EvidencePackContent. G2's changes (expanding RemediationSection with 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:

FileChange
src/domain/evidence-packs/types.tsAdd ScopeDriftRoleEntry, ScopeDriftDetailSection, add optional field to EvidencePackContent
src/evidence/sections.tsAdd buildScopeDriftDetail(ctx) — version history walk, role resolution, per-role resource attribution, blast radius comparison
src/evidence/build-evidence-pack.tsExtend 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.tsAdd evidence_refs to GET /findings/:id response
src/evidence/markdown.tsRender scope_drift_detail section (timeline + blast radius)

Key implementation details:

  1. ScopeDriftDetailSection: baseline/current role counts, added_roles with first_seen_version/first_seen_at/grants_access_to, blast_radius before/after, new_resources
  2. buildScopeDriftDetail: Return undefined if not scope_drift; extract added_role_targets from evidence_refs; walk version history for first appearance; compute per-role resource access from execution_paths
  3. build-evidence-pack.ts fix: When finding_type === "scope_drift", also load role entities for added_role_targets IDs
  4. findings.ts: Add evidence_refs: finding.evidence_refs to response (additive, no breaking change)
  5. Do NOT store role names in evidence_refs — would cause finding change detection churn on entity renames

Session 4: G2 Frontend

Files to modify:

FileChange
ui/src/api/api-types.tsAdd 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.tsxAdd dedicated remediation rendering path using RemediationPanel instead of generic KeyValueTable
ui/src/pages/FindingDetail.tsxAdd "Recommended Actions" card between Explanation and Evidence sections (summary + top 2 immediate actions)

Session 5: G3 Frontend

Files to modify:

FileChange
ui/src/api/api-types.tsAdd 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.tsxAdd scope_drift_detail to SECTION_ORDER, add rendering branch
ui/src/pages/FindingDetail.tsxCheck 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:

FileChange
ui/src/api/api-types.tsUpdate 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.tsuseExposureDetail(id) — no change needed (URL stays /api/v1/exposures/:id). But useExposures() list response shape changes.
ui/src/pages/ExposuresPage.tsxList 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.tsxUpdate row rendering for workload-grain fields (workload_name, identity count, execution_mode)
ui/src/components/ExposureInlineExpand.tsxUpdate inline expand to show workload context instead of entity_id (line 22), show identities list
ui/src/pages/ExposureDetailPage.tsxMajor 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.tsxDisplay identity_counts (active autonomous, dormant authority) and workload_counts in stat cards
ui/src/pages/ClustersListPage.tsxDisplay 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:

ChangePurpose
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_countsEnable delta computation on first load
Verify all entities have display_name propertiesHuman-readable names in all panels

G3 seed changes (from source scope-drift plan Phase 6):

ChangePurpose
Add intermediate sync1_5Payload between sync 1 and sync 2Creates 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.5Creates 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 2Third 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:

FileDescription
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.tsTest 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.tsVerify evidence_refs contains added_role_targets (IDs only). Explicitly assert no added_role_names key exists — prevents finding churn regression.
Tests for posture-serviceidentity_counts computation, workload_counts computation, delta against prior snapshot
Tests for risk-cluster-serviceidentity_count, workload_count, sensitive_domains, priority classification
Tests for exposures routeWorkload-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

FileG1G2G3Resolution
src/domain/evidence-packs/types.tsSession 2Session 3G2 adds RemediationAction; G3 adds ScopeDriftDetailSection + bumps schema (no conflict)
src/evidence/sections.tsSession 2Session 3G2 replaces buildRemediation; G3 adds buildScopeDriftDetail (no conflict)
src/evidence/build-evidence-pack.tsSession 3G3 extends role ID collection + bumps SCHEMA_VERSION to "1.1"
ui/src/api/api-types.tsSession 6Session 4Session 5All additive, no conflicts
ui/src/components/findings/EvidencePackViewer.tsxSession 4Session 5G2 first (remediation path), G3 second (scope_drift path)
ui/src/pages/FindingDetail.tsxSession 4Session 5G2 first (Recommended Actions card), G3 second (ScopeDriftDetail)
ui/src/pages/ExposureDetailPage.tsxSession 6Major refactor: sole data source becomes useExposureDetail
ui/src/pages/ExposuresPage.tsxSession 6Navigation links, row rendering
ui/src/hooks/use-exposures.tsSession 6Response type update

Verification

After each session:

  1. cd sv0-platform && npx tsc --noEmit — type check passes
  2. npx vitest run — all tests pass
  3. After Session 7: npx tsx scripts/seed-demo-w1.ts — seed completes
  4. 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

RiskMitigation
EXP-hash ID not resolvable by detail routeDetail 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 appearsExposure 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 degradation11/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 bumpedBump 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 scaleAuthority path queries capped at 5000 (demo-scale: 20-50 paths). Add truncated flag.
Posture snapshot backward compatHandle null/undefined for new fields in prior snapshots using priorSnapshot?.field ?? null pattern.
Historical roles not in ctx.roleEntitiesFix: 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 namesDo NOT store role names in evidence_refs. Name resolution happens in evidence pack builder only.
File collisions between G2/G3Managed via session sequencing — G2 frontend lands first, G3 adds to same files after.