Remediation Content Generation: Implementation Plan
Date: 2026-02-25
Status: Draft v2 (addresses review findings)
Depends on: None for backend (new file src/evidence/remediation.ts — no collision). Frontend phases (Steps 8-10) share EvidencePackViewer.tsx and FindingDetail.tsx with Gap 3 (scope drift UX) — remediation frontend should land FIRST, then scope drift adds to the same components.
Effort estimate: 1-2 days
Owner: TBD
Target: Week 1-2 (by Mar 7)
Problem Description
The evidence pack infrastructure defines a RemediationSection type with an actions: string[] array. The buildRemediation() function in src/evidence/sections.ts populates this from a static REMEDIATION_ACTIONS record — 3 generic, one-size-fits-all strings per FindingType.
The static actions are not context-aware. For example, orphaned_ownership always says "Assign a primary owner to this identity" regardless of whether the finding is about a workload named "HR Sync Flow" with 5 execution paths reaching restricted financial data, or a dormant service principal with no paths. The remediation text never mentions entity names, role names, resource names, sensitivity levels, or source systems.
Why This Matters
CISOs need actionable guidance, not boilerplate checklists. The W1 product definition explicitly states that W1 must deliver "evidence-backed artifacts that make the risk defensible and repeatable" and "language and structure that supports a CISO narrative."
Remediation that says "Assign a primary owner" without naming the entity, its source system, what it can reach, or why it matters is not CISO-ready. It fails the "evidence-grade" design constraint because it doesn't reference the evidence chain.
The pilot success criteria require findings to be "explainable with evidence." Generic remediation undermines credibility.
Scope boundary: W1 explicitly excludes "remediation workflows (ticketing, auto-fix, playbooks)." This plan improves advisory text within the existing evidence pack section — deterministic guidance from graph state, not automated enforcement.
Architectural Analysis
Where It Lives
Decision: New file src/evidence/remediation.ts, imported by sections.ts.
The EvidenceBuildContext already contains everything needed: FindingDoc (type, severity, explanation, evidence_refs), EntityDoc (type, display_name, source_system, execution_paths, relationships), owner entities, role entities, and temporal context.
Extracting to a separate file keeps sections.ts clean while the remediation logic grows (13 finding types, each with 3-5 context-aware actions).
Type Extension
Expand RemediationSection to support structured actions with priority and rationale:
export interface RemediationAction {
priority: "immediate" | "short_term" | "ongoing";
action: string;
rationale?: string;
}
export interface RemediationSection {
actions: string[]; // Backward-compatible plain text
structured_actions?: RemediationAction[]; // Prioritized with rationale
summary?: string; // One-sentence executive summary
}
The actions[] field stays for backward compatibility (markdown export, existing UI). The optional structured_actions enables the UI to render prioritized remediation with rationale.
Context Extraction Pattern
function extractContextVars(ctx: EvidenceBuildContext): ContextVars {
const entityName = ctx.entity.properties.display_name ?? ctx.entity.source_id;
const entityType = ctx.entity.entity_type;
const sourceSystem = ctx.entity.source_system;
const pathCount = (ctx.entity.execution_paths ?? []).length;
const roleCount = ctx.entity.relationships.filter(r => r.type === "HAS_ROLE").length;
const sensitivePaths = paths.filter(p =>
["confidential","restricted","high"].includes(p.sensitivity));
const domains = [...new Set(paths.map(p => p.business_domain))];
const ownerNames = ctx.ownerEntities.map(e => e.properties.display_name ?? e.source_id);
// ... etc
}
Dispatcher Pattern
const REMEDIATION_BUILDERS: Record<FindingType,
(ctx: EvidenceBuildContext, vars: ContextVars) => RemediationOutput> = {
orphaned_ownership: remediateOrphanedOwnership,
dormant_authority: remediateDormantAuthority,
scope_drift: remediateScopeDrift,
// ... all 13 types
};
export function buildContextAwareRemediation(ctx: EvidenceBuildContext): RemediationSection {
const vars = extractContextVars(ctx);
const builder = REMEDIATION_BUILDERS[ctx.finding.finding_type];
if (!builder) return { actions: [] };
const output = builder(ctx, vars);
return {
actions: output.structured_actions.map(a => a.action),
structured_actions: output.structured_actions,
summary: output.summary
};
}
Remediation Templates: All 13 Finding Types
Template variables shown in {braces} are populated from EvidenceBuildContext.
1. orphaned_ownership (Critical)
Summary: "{entity_name}" ({entity_type}) in {source_system} has {path_count} execution path(s) but no active owner — assign an accountable individual before reviewing access.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Assign an individual owner to {entity_type} "{entity_name}" in {source_system}. Contact the last known owner ({last_owner_name}, status: {last_owner_status}) or the {source_system} administrator. | Without an active owner, no one is accountable for {path_count} execution path(s) reaching {sensitivity_summary}. |
| immediate | Review whether "{entity_name}" should remain active. If no longer needed, disable it in {source_system}. | Orphaned entities with active execution paths are the highest-risk unmanaged surface. |
| short_term | Audit the {path_count} execution path(s) for least-privilege compliance. Current reach includes: {top_resources_summary}. | Execution paths may have been assigned under a previous owner's authorization. |
| ongoing | Establish an ownership review process to detect owner departures before they create orphaned entities. | This finding was detected because ownership decayed without governance catching it. |
2. ownership_degraded (High)
Summary: Primary owner(s) of "{entity_name}" are non-active. Governance has fallen back to {fallback_count} secondary/inherited owner(s): {fallback_names}.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Assign a new primary owner to "{entity_name}". The fallback owner(s) ({fallback_names}) may lack context for the {path_count} execution path(s). | Secondary/inherited owners are not direct accountability. |
| short_term | Verify that {fallback_names} can confirm the business justification for "{entity_name}" and its {role_count} role assignment(s). | Degraded ownership creates a gap between who authorized access and who is responsible. |
| ongoing | Configure ownership succession rules so primary owner departures trigger immediate reassignment. | This degradation was detected reactively; a proactive process would prevent exposure windows. |
3. dormant_authority (High)
Summary: "{entity_name}" holds {path_count} execution path(s) but has not executed in {days_dormant} days (threshold: 90 days). Standing authority should be revoked or justified.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Review whether "{entity_name}" is still required. If not, revoke its role assignments: {role_names_csv}. | {days_dormant} days of inactivity with {path_count} active path(s) is standing authority with no operational justification. |
| immediate | If still needed, rotate its credentials immediately. Dormant identities with standing access are prime targets for credential theft. | Stale credentials on dormant authority are often unreviewable. |
| short_term | Reduce execution paths to least-privilege. Current reach includes {sensitive_resource_count} sensitive resource(s) across {domain_count} data domain(s). | Even if reactivated, its current scope may exceed requirements. |
| ongoing | Implement dormancy detection to flag identities before they exceed the 90-day threshold. | Catching dormancy at 30-60 days allows orderly decommission. |
4. scope_drift (Medium)
Summary: "{entity_name}" has grown from {baseline_role_count} to {current_role_count} role(s) since first observation. {added_count} role(s) were added without documented justification.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Review the {added_count} role(s) added since baseline and confirm each has current business justification. | Role creep accumulates incrementally and often goes unnoticed until a material exposure is realized. |
| short_term | Remove any unjustified role assignments. Coordinate with the owner ({owner_name}) to confirm operational requirements. | Reducing scope to baseline eliminates unnecessary execution paths to {affected_resource_count} resource(s). |
| ongoing | Establish periodic access review for "{entity_name}" to detect scope expansion before it compounds. | Drift was detected by comparing current state to first-observed baseline. |
5. privilege_justification_gap (Medium)
Summary: "{entity_name}" has elevated access to {gap_count} sensitive resource(s) where granted permissions do not match observed activity.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Review the {gap_count} privilege gap(s): {gap_descriptions}. Determine whether granted permissions are still required. | Elevated access to {sensitivity_levels} data without matching activity suggests over-provisioning. |
| short_term | Reduce permissions to match observed usage. If write/admin access is granted but only read activity is observed, downgrade the role. | Least-privilege alignment reduces blast radius in the event of credential compromise. |
| short_term | For resources with zero observed activity, consider removing the execution path entirely. | Zero-activity paths to sensitive resources represent unnecessary exposure. |
| ongoing | Document business justification for elevated access that must be retained. | Documented justification closes the gap between granted authority and business need. |
6. unresolved_cross_system_auth (Medium)
Summary: "{entity_name}" ({identity_subtype}) has OAuth credentials that cannot be matched to a governed service principal in {source_system}. This is a shadow workload.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Investigate the OAuth application and determine which service principal it should be bound to. | Unresolved OAuth credentials represent ungoverned cross-system access. |
| immediate | If no legitimate binding exists, revoke the OAuth credentials and remove the integration. | Shadow OAuth apps authenticate to target systems without appearing in governance reviews. |
| short_term | For legitimate integrations, create the proper service principal registration and AUTHENTICATES_TO binding. | Governed bindings ensure cross-system auth is visible to governance tooling. |
| ongoing | Monitor for new OAuth registrations that lack corresponding service principal bindings. | Shadow integrations are often created by dev teams without security review. |
7. unproven_execution (High)
Summary: Workload "{entity_name}" has {path_count} execution path(s) but no execution evidence can be deterministically linked to it.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Verify whether "{entity_name}" is actively executing. Check {source_system} execution logs, trigger history, or runtime monitoring. | Without deterministic evidence, the workload's actual behavior cannot be assessed. |
| short_term | If executing, ensure logs produce records that can be deterministically linked to the workload or its RUNS_AS identity. | Execution evidence is required to distinguish active workloads from dormant ones. |
| short_term | If not executing, evaluate decommissioning to reduce standing authority. It currently reaches {affected_resource_count} resource(s). | Standing authority without proven execution is unnecessary exposure. |
| ongoing | Ensure execution evidence collection is configured for all autonomous workloads in {source_system}. | This reflects a monitoring gap, not necessarily a security incident. |
8. unknown_identity_binding (High)
Summary: Workload "{entity_name}" {binding_detail}. Without deterministic identity binding, its execution context is unknown.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Determine which identity "{entity_name}" executes as in {source_system}. Check runtime configuration, service account binding, or managed identity assignment. | Without identity binding, execution cannot be attributed and access scope cannot be assessed. |
| short_term | Establish a RUNS_AS relationship from the workload to a governed identity. This enables the platform to compute execution paths. | Identity binding is prerequisite for all downstream exposure assessment. |
| short_term | Until resolved, restrict the workload's execution paths in {source_system} to prevent unmonitored access. | Unbound workloads may execute with elevated privileges invisible to governance. |
9. reachable_sensitive_domain (Medium/High)
Summary: "{entity_name}" has {sensitive_path_count} execution path(s) reaching {sensitivity_levels} data across {domain_list}.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Review whether "{entity_name}" requires access to {sensitivity_levels} data in {domain_list}. Confirm with the owner ({owner_name_or_unknown}). | Access to {sensitivity_levels} data creates material exposure in the event of identity compromise. |
| short_term | If justified, document the business justification and attach to the entity's ownership record. | Documented justification ensures future reviewers understand why sensitive access was authorized. |
| short_term | If not justified, remove the role assignments that grant execution paths to sensitive resources. Currently: {via_roles_csv}. | Unnecessary sensitive access amplifies blast radius without operational benefit. |
| ongoing | Include sensitive domain access in periodic access reviews for "{entity_name}". | Structural exposure signals should be re-evaluated whenever access scope changes. |
10. llm_egress (High)
Summary: Workload "{entity_name}" has outbound data flow to an LLM endpoint. Data reaching this workload via {path_count} execution path(s) may be transmitted to the LLM.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Assess what data flows through "{entity_name}" to the LLM endpoint. Review execution paths for sensitive data domains: {domain_summary}. | Data sent to LLM endpoints may be retained for training, violating data residency or confidentiality requirements. |
| immediate | Verify that the LLM integration has been approved by security and privacy teams. Confirm endpoint and data handling agreement. | Unapproved LLM integrations represent shadow AI that bypasses organizational data governance. |
| short_term | Implement DLP controls on the egress path to prevent sensitive data (especially {max_sensitivity}) from reaching the LLM. | Without DLP, any data reachable by "{entity_name}" may be transmitted. |
| short_term | If approved, register in your AI governance inventory and subject to periodic review. | Registered integrations can be tracked for compliance with evolving AI regulations. |
11. external_egress (Medium)
Summary: Workload "{entity_name}" has outbound data flow to an external endpoint. Data reachable via {path_count} execution path(s) may leave the enterprise boundary.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Identify the external endpoint that "{entity_name}" transmits to. Verify it is a known, authorized integration partner. | External egress without governance creates data exfiltration risk. |
| short_term | Implement monitoring on the egress path. Ensure outbound transfers are logged and alertable. | Monitoring enables detection of anomalous data volumes or unexpected destination changes. |
| short_term | If authorized, document the data flow including what data is transmitted and business justification. | Documented egress paths can be reviewed during audits and compliance assessments. |
| ongoing | Include "{entity_name}" in periodic reviews of external integrations. | External partnerships change; endpoints may be decommissioned or acquired by untrusted parties. |
12. ownership_ambiguous (Medium)
Summary: "{entity_name}" is owned only by group/team owner(s): {owner_names}. No individual has ever been designated as accountable.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Designate an individual within {owner_names} as the primary accountable owner. | Group/team ownership creates diffusion of responsibility. |
| short_term | Verify the designated individual has context to manage "{entity_name}" — its {path_count} execution path(s) and {role_count} role assignment(s). | Accountability without context is nominal. |
| ongoing | Establish policy requiring individual ownership for entities with execution paths to sensitive resources. | Ambiguous ownership compounds over time as team membership changes. |
13. ownership_unknown (Medium)
Summary: "{entity_name}" has {path_count} execution path(s) but insufficient metadata to determine who owns it.
| Priority | Action | Rationale |
|---|---|---|
| immediate | Investigate {source_system} for ownership metadata not collected by the connector (sys_created_by, owner, managed_by). | Absence may reflect a connector gap rather than true unknown. |
| short_term | If ownership metadata exists, update the connector configuration. If not, assign an owner based on function and scope. | "{entity_name}" has {path_count} path(s) reaching {domain_count} domain(s) — someone must be accountable. |
| ongoing | Improve metadata collection across connectors to reduce ownership_unknown findings in future scans. | Unknown ownership is a data quality issue that degrades all downstream assessments. |
Demo Data Requirements
No changes to the seed script are required. Remediation content is generated at evidence-pack-build time from existing entity and finding data. Rerunning the seed or resyncing automatically produces context-aware remediation.
Key demo entities that produce the most compelling remediation:
| Entity | Finding Types | Why It's Compelling |
|---|---|---|
| wl-hr-sync | orphaned_ownership, reachable_sensitive_domain | References departed owner, restricted data domains |
| wl-audit-export | dormant_authority, external_egress | References 90+ day dormancy, external endpoint |
| wl-invoice-rule | scope_drift | References baseline vs current role count, specific added roles |
| wl-ai-assist | llm_egress | References LLM endpoint, data flowing through workload |
| wl-crm-sync | unknown_identity_binding | References missing RUNS_AS, restricted data |
Optional: Add display_name properties to role/resource entities in the seed if any are missing, so remediation text references human-readable names.
UI Changes
Phase 1: RemediationPanel Component
New file: ui/src/components/findings/RemediationPanel.tsx
- If
structured_actionspresent: render prioritized action cards with priority badge (red=immediate, amber=short_term, blue=ongoing), action text (bold), rationale (secondary). - If only
actionspresent: render as simple bulleted list. - If
summarypresent: render as highlighted header above actions. - Default to open (not collapsed) in EvidencePackViewer since remediation is the most actionable section.
Phase 2: Wire into EvidencePackViewer
File: ui/src/components/findings/EvidencePackViewer.tsx
Add dedicated remediation rendering path in SectionContent:
if (sectionKey === "remediation" && typeof data === "object") {
return <RemediationPanel remediation={data} />;
}
Currently this section renders via the generic KeyValueTable which outputs JSON.stringify(actions).
Phase 3: Promote Remediation on Finding Detail
File: ui/src/pages/FindingDetail.tsx
Add a standalone "Recommended Actions" card between the Explanation and Evidence Completeness sections. This card:
- Extracts
remediationfrom the evidence pack - Shows the
summaryand top 2immediateactions inline - Links to the full evidence pack remediation section for detail
This gives CISOs key remediation guidance without navigating into the evidence pack.
UI Type Updates
File: ui/src/api/api-types.ts
export interface RemediationAction {
priority: "immediate" | "short_term" | "ongoing";
action: string;
rationale?: string;
}
export interface RemediationSection {
actions: string[];
structured_actions?: RemediationAction[];
summary?: string;
}
Implementation Steps
| Step | File(s) | Description | Effort |
|---|---|---|---|
| 1 | src/domain/evidence-packs/types.ts | Add RemediationAction interface, expand RemediationSection | 10 min |
| 2 | src/evidence/remediation.ts (NEW) | Implement extractContextVars + 13 finding-type builders + dispatcher | 3-4 hr |
| 3 | src/evidence/sections.ts | Replace static buildRemediation with import from remediation.ts; remove REMEDIATION_ACTIONS | 10 min |
| 4 | src/evidence/markdown.ts | Enhance remediation section rendering (summary + structured actions by priority) | 30 min |
| 5 | test/evidence/remediation.test.ts (NEW) | Test each of 13 types: entity name appears, priorities correct, summary populated | 2 hr |
| 6 | test/evidence/sections.test.ts | Update buildRemediation test to verify context-aware content | 30 min |
| 7 | ui/src/api/api-types.ts | Add RemediationAction type, update evidence pack typing | 10 min |
| 8 | ui/src/components/findings/RemediationPanel.tsx (NEW) | Prioritized action cards with badges and rationale | 1-2 hr |
| 9 | ui/src/components/findings/EvidencePackViewer.tsx | Add dedicated remediation rendering path | 30 min |
| 10 | ui/src/pages/FindingDetail.tsx | Add "Recommended Actions" card above evidence pack | 1 hr |
| 11 | Verify | Run tests, typecheck, lint, reseed, verify in UI | 1 hr |
Steps 2 and 8 are the bulk of the work and can run in parallel (backend + frontend).
Risks
| Risk | Mitigation |
|---|---|
| Evidence pack integrity hash changes on every rebuild | Expected behavior. Pack chaining via previous_pack_id preserves history. Hash recalculation is by design. |
| Missing context data (no owners, no roles, etc.) | Each template function checks data presence and falls back to generic phrasing. extractContextVars handles all null/empty cases. |
| Long remediation text in markdown export | Limit each action to ~150 characters. Summary to one sentence. |
| Schema version bump | Not required — RemediationSection already includes actions: string[] and new fields are optional. |