From f13893b63f4dc96bc1be1cba1d85dae4e4d376cd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 11 May 2026 10:33:26 +0100 Subject: [PATCH] Add Create rule from this button on Patrol findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last of the seven contextual entries from the captured Pulse Intelligence rubric. "Remember as expected" handles one instance; "Create rule from this" promotes the pattern: any future finding on the same {resource, category} pair auto- dismisses inside the backend's existing FindingsStore.isSuppressedInternal / MatchesSuppressionRule machinery rather than surfacing as a new finding. The backend endpoint already exists (POST /api/ai/patrol/suppressions → HandleAddSuppressionRule → FindingsStore.AddSuppressionRule). The button + inline confirm panel is the missing surface: - frontend-modern/src/api/patrol.ts gains createSuppressionRuleFromFinding(input) that POSTs to the existing endpoint with the finding's resource + category + operator-supplied reason. - FindingsPanel adds a Create rule button at the end of the lifecycle action row, plus an inline confirmation that surfaces the rule scope (resource + category), requires a reason, and explains the future-auto-dismiss commitment. Submission goes through aiIntelligenceStore.loadDashboardData so the local view reflects the audit trail of record. - Mirrors the visual pattern of the existing dismiss-confirmation panel but uses neutral surface styling because this isn't a dismissal — it's a permanent commitment, distinct from Remember as expected which dismisses just this instance. No backend changes; the rule machinery and the API endpoint are unchanged. This is the surface piece. Type-check clean. --- frontend-modern/src/api/patrol.ts | 34 ++++++ .../src/components/AI/FindingsPanel.tsx | 112 ++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index 6b51ddd8c..314d2229c 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -308,6 +308,40 @@ export async function dismissFinding( }); } +/** + * Create a permanent suppression rule from a finding. + * + * Unlike dismissFinding (which acts on a single instance), this records a + * durable pattern: future findings on the same {resource, category} + * pair will be auto-dismissed by the backend's + * isSuppressedInternal/MatchesSuppressionRule machinery. Used by the + * per-finding "Create rule from this" button so an operator who has + * been silencing the same noisy pattern repeatedly can promote that + * judgment into a remembered rule rather than dismissing every recurrence. + * + * The description is required by the backend — it's the operator's + * stated reason, surfaced when the rule is later listed or audited. + * Pass an empty resourceId or category to broaden the rule's scope + * (e.g. all categories on this resource, or this category on any + * resource). + */ +export async function createSuppressionRuleFromFinding(input: { + resourceId: string; + resourceName: string; + category: string; + description: string; +}): Promise<{ success: boolean; message: string; rule: { id: string } }> { + return apiFetchJSON('/api/ai/patrol/suppressions', { + method: 'POST', + body: JSON.stringify({ + resource_id: input.resourceId, + resource_name: input.resourceName, + category: input.category, + description: input.description, + }), + }); +} + /** * Set or update a user note on a finding * Notes provide context that Patrol sees on future runs. diff --git a/frontend-modern/src/components/AI/FindingsPanel.tsx b/frontend-modern/src/components/AI/FindingsPanel.tsx index cc034703a..0c76d76e0 100644 --- a/frontend-modern/src/components/AI/FindingsPanel.tsx +++ b/frontend-modern/src/components/AI/FindingsPanel.tsx @@ -30,6 +30,7 @@ import { import { useResources } from '@/hooks/useResources'; import { InvestigationSection, ApprovalSection } from '@/components/patrol'; import { AIAPI, type ApprovalRequest, type RemediationPlan } from '@/api/ai'; +import { createSuppressionRuleFromFinding } from '@/api/patrol'; import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol'; import { buildResolvedResourceSurfaceLinks } from '@/routing/resourceLinks'; import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation'; @@ -635,6 +636,59 @@ export const FindingsPanel: Component = (props) => { } }; + // Create rule from this is the promotion path: take the operator's + // implicit pattern (silencing the same {resource, category} pair + // repeatedly) and turn it into a durable suppression rule the backend + // remembers. After creation, future findings matching the rule + // auto-dismiss inside FindingsStore.isSuppressedInternal rather than + // re-surfacing every Patrol run. The button confirms the scope and + // requires a reason so the rule has audit context. + const [creatingRuleForId, setCreatingRuleForId] = createSignal(null); + const [createRuleDescription, setCreateRuleDescription] = createSignal(''); + + const handleStartCreateRule = (finding: UnifiedFinding, e: Event) => { + e.stopPropagation(); + setCreatingRuleForId(finding.id); + setCreateRuleDescription(finding.userNote || ''); + setExpandedId(finding.id); + }; + + const handleCancelCreateRule = (e: Event) => { + e.stopPropagation(); + setCreatingRuleForId(null); + }; + + const handleConfirmCreateRule = async (finding: UnifiedFinding, e: Event) => { + e.stopPropagation(); + const description = createRuleDescription().trim(); + if (!description) { + notificationStore.error('A reason for the rule is required'); + return; + } + setActionLoading(finding.id); + try { + await createSuppressionRuleFromFinding({ + resourceId: finding.resourceId, + resourceName: finding.resourceName, + category: finding.category || '', + description, + }); + notificationStore.success( + `Rule created: future ${finding.category || 'matching'} findings on ${finding.resourceName} will auto-dismiss`, + ); + setCreatingRuleForId(null); + // Refresh so the operator sees the finding update (typically the + // rule takes effect on next Patrol cycle, but the local view + // should reflect the audit-trail-of-record). + void aiIntelligenceStore.loadDashboardData(); + } catch (err) { + console.error('Failed to create suppression rule:', err); + notificationStore.error('Failed to create suppression rule'); + } finally { + setActionLoading(null); + } + }; + // Copy a Markdown summary of the finding to the clipboard so the operator // can paste it into a chat, ticket, or incident channel. The shape mirrors // the seven-question schema (title + impact + recommendation + trust @@ -1400,10 +1454,68 @@ export const FindingsPanel: Component = (props) => { > Dismiss: Later + + {/* Inline create-rule confirmation. Confirms scope (resource + + category) and requires a reason so the persisted rule has + audit context. Mirrors the dismiss-confirmation panel + visually but uses neutral surface styling — this isn't a + dismissal, it's a permanent commitment. */} + +
+
+ + Create suppression rule for{' '} + {finding.resourceName} + + {' '}({finding.category}) + + +
+

+ Future findings matching this resource and category will be + auto-dismissed by Patrol without surfacing as new findings. + You can list or remove rules later from the suppressions + management surface. +

+