diff --git a/.specs/security-agent.md b/.specs/security-agent.md index 6d60007efb..d049021551 100644 --- a/.specs/security-agent.md +++ b/.specs/security-agent.md @@ -2,22 +2,23 @@ ## Role of This Document -This spec defines the business rules and outcome guarantees for Security Agent Auto Remediation and Security Agent Notifications. It is the source of truth for what users should be able to rely on when Security Agent creates or manages remediation work and sends New-finding, SLA Warning, or SLA Breach Notifications. +This spec defines the business rules and outcome guarantees for Security Agent Auto Remediation, Security Agent Notifications, and Security Agent Audit Reports. It is the source of truth for what users should be able to rely on when Security Agent creates or manages remediation work, sends New-finding, SLA Warning, or SLA Breach Notifications, and reports recorded Security Finding activity. This document deliberately does not specify database tables, queue design, worker names, router names, UI layout, email markup, or prompt implementation details. Those belong in plans and code. ## Status -Draft -- created 2026-06-09; notification rules added 2026-06-11. +Draft -- created 2026-06-09; notification rules added 2026-06-11; audit report rules added 2026-06-12. ## Scope -This spec covers two Security Agent capabilities: +This spec covers three Security Agent capabilities: - Auto Remediation; -- New-finding, SLA Warning, and SLA Breach Notifications. +- New-finding, SLA Warning, and SLA Breach Notifications; +- Security Agent Audit Reports. -It does not backfill the complete Security Agent product spec. Existing Security Agent behavior such as finding sync, Auto Analysis, Auto Dismiss, dashboard statistics, SLA calculation, and Dependabot writeback is included only where it affects Auto Remediation or notification outcomes. +It does not backfill the complete Security Agent product spec. Existing Security Agent behavior such as finding sync, Auto Analysis, Auto Dismiss, dashboard statistics, SLA calculation, and Dependabot writeback is included only where it affects Auto Remediation, notification outcomes, or audit report evidence. ## Conventions @@ -41,6 +42,8 @@ BDD-style scenarios use "Given", "When", and "Then" to describe user-visible beh - **SLA Breach Notification**: A Security Agent Notification admitted when an eligible open finding reaches or passes its persisted SLA deadline. - **Notification Recipient**: The personal owner or a current organization owner authorized to receive a notification for a finding. - **Email Delivery**: An attempt to render and send one Security Agent Notification through the email provider. +- **Security Finding Activity Event**: An immutable owner-scoped record of one material user, system-policy, or source-driven action or outcome that changes or explains a Security Finding. +- **Security Agent Audit Report**: An owner-scoped, period-bounded audit view of Security Finding Activity Events grouped by Security Finding. ## Auto Remediation configuration @@ -425,6 +428,134 @@ Given Auto Remediation starts a remediation after analysis When the remediation appears in history Then Security Agent SHOULD identify it as policy-driven automatic remediation. +## Security Agent Audit Reports + +### Report purpose and evidence basis + +Security Agent Audit Reports MUST report Security Finding activity recorded by Kilo. They MUST NOT claim that legacy history is complete, prove repository scan coverage, reconstruct activity Kilo did not record, or calculate authoritative historical SLA compliance. + +The report evidence basis MUST be Security Finding Activity Events. A Security Finding Activity Event MUST belong to exactly one Security Agent owner and one Security Finding, including after that finding is deleted. + +Security Agent MAY include supplemental legacy audit records when they can be mapped to a Security Finding without ambiguity. Legacy supplemental activity MUST be labeled as potentially incomplete. Ambiguous legacy records MUST NOT be guessed into a Security Finding group. + +Every report MUST display the reliable event-coverage start. Baseline events for existing Security Findings, if produced, MUST use actual capture time and MUST NOT be backdated or presented as original creation events. + +### Reportable activity + +Security Agent Audit Reports MUST include material Security Finding activity when recorded during the selected period: + +- finding imported into Kilo; +- severity changed; +- status changed, including reopened and fixed; +- finding manually dismissed, automatically dismissed, or superseded; +- terminal analysis completed or failed when the outcome explains a disposition or remediation decision; +- remediation requested; +- remediation ended with PR opened, failed, blocked, cancelled, or no changes needed; +- finding deleted. + +Security Agent Audit Reports MUST NOT include reads, page views, unchanged sync observations, queue claims, heartbeats, retries with no new finding-level outcome, analysis admission or start, stale cleanup, recipient-level notification transitions, repository scan-coverage evidence, configuration timelines, or report-generation events inside the report itself. + +### Periods and ordering + +Reports MUST use UTC calendar-day boundaries. The default period SHOULD end on the current UTC calendar day and include the preceding 89 calendar days. + +Report ranges MUST be valid, non-future, non-reversed, and no longer than 90 inclusive calendar days. Period inclusion MUST use when Kilo recorded or applied the event. External source timestamps MAY be shown as supporting evidence but MUST NOT determine report inclusion. + +A report MUST include a Security Finding when at least one reportable Security Finding Activity Event falls inside the selected period. The report MUST group events by Security Finding. Events inside each Security Finding group MUST be chronological. Security Finding groups MUST be deterministically ordered by first in-period event, repository, title, and Security Finding ID. + +The interactive report MUST let viewers filter Security Finding groups by severity, recorded state, and repository. Filters MUST retain the complete in-period timeline for every matching Security Finding group. + +Repository filter options MUST come from repository names recorded in report evidence, not current Security Agent repository selection or current repository accessibility. Repository matching MUST use the exact recorded full name. Renamed or transferred repositories MAY appear as separate options when report evidence contains both names. Security Findings without recorded repository identity MUST remain visible when all repositories are selected. + +### Authorization and availability + +Personal Security Agent Audit Reports MUST be available only to the owning user. + +Organization Security Agent Audit Reports MUST be available to organization owners, billing managers, and Kilo platform admins. Ordinary organization members and non-members MUST NOT access organization reports solely because they can access other organization surfaces. + +Report access MUST NOT have a separate plan, active-subscription, enabled Security Agent, or active GitHub integration entitlement. Authorized viewers MUST retain read-only historical access after Security Agent or the GitHub integration is disabled. + +Every platform-admin report generation MUST be audited after successful report assembly. Ordinary customer access MAY rely on existing operational request logs in v1. + +### Report content + +Successful reports MUST include every matching reportable Security Finding Activity Event recorded through the displayed cutoff. Timeout, over-budget, or query failure MUST return no report content and MUST NOT return a partial report. + +Each Security Finding group SHOULD show stable Security Finding and source identity, repository, title, severity, status, safe advisory metadata, first detected time, canonical Security Finding ID when recorded, and deletion status when applicable. + +Human actions MUST show an event-time display name and stable typed actor reference. Automated actions MUST show explicit system attribution. Actor email and notification recipient identity MUST NOT be report evidence. + +Internal Kilo admin actors MUST be masked for non-admin viewers. Deleting an actor's Kilo account MUST anonymize dedicated identity fields in organization-owned Security Finding Activity Events while preserving stable non-PII attribution and event evidence. + +Security Agent Audit Reports MAY show recorded SLA evidence for a Security Finding when trustworthy event or snapshot data exists: + +- persisted SLA deadline; +- recorded terminal timestamp; +- whether terminal timestamp was before or at/after recorded deadline; +- whether an open finding was before or at/after recorded deadline at report cutoff; +- `unknown` when legacy or missing history prevents trustworthy classification. + +Security Agent Audit Reports MUST NOT publish aggregate SLA compliance percentages. They MUST NOT classify ignored or superseded findings as compliant controls. They MUST NOT change SLA enable, disable, severity, warning, breach, reopen, or deadline behavior. + +### Privacy and redaction + +Security Finding Activity Event snapshots and metadata MUST contain only structured, sanitized evidence needed for the report. They MUST NOT contain actor identity, notification recipient identity, prompts, raw analysis markdown, transcripts, assistant messages, full execution logs, provider responses, raw source payloads, credentials, tokens, auth headers, cookies, webhook secrets, or unredacted raw errors. + +External links in reports MUST be validated and rendered safely. Source-controlled text MUST be rendered as escaped text. + +### Scenario: UTC report period + +Given an authorized owner requests a same-day UTC report +When Security Agent assembles the report +Then events recorded at or after `00:00:00.000Z` on that day and before `00:00:00.000Z` on the next day MUST be eligible. +And events outside that range MUST NOT be included. + +### Scenario: Invalid report period + +Given an authorized owner requests a future, reversed, or longer-than-90-day range +When Security Agent validates the request +Then Security Agent MUST reject the request before scanning report events. + +### Scenario: Complete query failure + +Given a report scan has loaded some matching events +When a later page fails, times out, or exceeds the tested budget +Then Security Agent MUST discard accumulated data. +And Security Agent MUST return a complete-query failure state rather than a partial report. + +### Scenario: Deleted finding remains reportable + +Given a Security Finding has been deleted +And a deletion Security Finding Activity Event with a final compact snapshot was recorded +When an authorized owner requests a period containing that event +Then the report MUST show the deleted Security Finding from immutable event evidence. +And the report MUST NOT rely on joining through the mutable Security Finding row. + +### Scenario: Organization report permissions + +Given an organization has Security Finding Activity Events +When an organization owner, billing manager, or Kilo platform admin requests its report +Then Security Agent MUST allow the report. + +Given an ordinary organization member or non-member requests the report +When Security Agent checks authorization +Then Security Agent MUST reject access before loading counts or report data. + +### Scenario: Legacy coverage wording + +Given a report period overlaps activity before reliable event coverage began +When Security Agent renders the report +Then the report MUST state that it contains activity recorded by Kilo. +And it MUST label supplemental legacy activity as potentially incomplete. + +### Scenario: Repository report filter + +Given a report contains Security Finding groups with recorded repository identity +When an authorized viewer selects one repository +Then the report MUST show only groups whose recorded repository full name matches that selection. +And every event in each matching group's in-period timeline MUST remain visible. +And current Security Agent repository selection or accessibility MUST NOT remove recorded repository options from the report. + ## Security Agent Notifications ### Delivery and policy ownership @@ -635,4 +766,11 @@ The following are intentionally outside the guaranteed v1 behavior: - Settings-time or launch-time repository write-permission preflight by Security Agent. - Notification channels other than email. - Per-member organization notification overrides. -- Backfilling complete specifications for Security Agent features unrelated to Auto Remediation or Security Agent Notifications. +- Historical SLA compliance metrics. +- Repository scan-coverage appendices. +- Configuration policy appendices. +- Exhaustive notification delivery history in Audit Reports. +- Report ranges longer than 90 days. +- Server-side stored report artifacts or server-generated PDFs. +- Server-side report result caching. +- Backfilling complete specifications for Security Agent features unrelated to Auto Remediation, Security Agent Notifications, or Security Agent Audit Reports. diff --git a/CONTEXT.md b/CONTEXT.md index d3ecee3387..a103550273 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -29,6 +29,8 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr | **SLA Breach Notification** | Security Agent Notification admitted when eligible finding reaches or passes persisted SLA deadline | Referring to at-or-after-deadline event | Overdue alert, breach reminder | | **Notification Recipient** | User authorized to receive one Security Agent Notification: personal owner or current organization owner | Referring to per-user event identity and authorization | Subscriber, watcher, all organization members | | **Email Delivery** | Attempt to render and send one Security Agent Notification through Mailgun | Referring to provider side effect, retry, or acceptance | Notification event | +| **Security Finding Activity Event** | Immutable record of one material user, system-policy, or source-driven action or outcome that changes or explains a Security Finding | Referring to evidence included in a Security Agent Audit Report | Page view, unchanged sync observation, queue claim, heartbeat | +| **Security Agent Audit Report** | Owner-scoped, period-bounded audit view of Security Finding Activity Events grouped by Security Finding | Referring to the interactive audit report | Generic audit-log export, activity dump | ## Relationships @@ -36,9 +38,26 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - A **Security Finding** can create at most one **Security Agent Notification** of each kind per **Notification Recipient**. - A **New-finding Notification** depends on first insertion into Kilo, not source alert creation time. - An **SLA Warning Notification** and **SLA Breach Notification** use persisted `sla_due_at`; warning does not suppress later breach. +- A Security Agent Audit Report may show a persisted SLA deadline and recorded outcome when trustworthy evidence exists. V1 does not redefine live SLA behavior or calculate authoritative historical SLA compliance. - A **Notification Recipient** for an organization finding is a current organization member with role `owner`. - An **Email Delivery** realizes a durable **Security Agent Notification** and may be retried without creating new event identity. - A **Security Remediation** belongs to one **Security Finding** and can have one or more **Security Remediation Attempts**. +- A **Security Finding Activity Event** belongs to one Security Agent owner and one Security Finding, including after that finding is deleted. +- A **Security Finding Activity Event** falls into a report period based on when Kilo recorded or applied it. External source timestamps are supporting evidence and do not determine report inclusion. +- A **Security Agent Audit Report** groups every matching reportable **Security Finding Activity Event** recorded by Kilo in the selected period. +- V1 reports persisted SLA evidence only when it can do so from trustworthy recorded data. It does not calculate historical SLA compliance percentages or introduce new SLA lifecycle semantics. +- A personal **Security Agent Audit Report** is available only to its owning user. An organization report is available to organization owners, billing managers, and audited Kilo platform admins, not ordinary members. +- Security Agent Audit Report access has no separate plan or active-subscription gate; authorized owners retain read-only historical access after cancellation or disablement. +- A **Security Agent Audit Report** includes owner history from current, deselected, unavailable, and deleted repository scope. Current Security Agent repository selection does not limit historical evidence; an explicit report repository filter may narrow displayed Security Finding groups by exact recorded repository full name. +- Human activity in a **Security Agent Audit Report** uses an event-time display name and stable typed actor reference; automated activity uses explicit system attribution. Actor and notification recipient emails are not report evidence. +- Deleting an actor's Kilo account anonymizes their dedicated identity fields in organization-owned Security Finding Activity Events while preserving stable non-PII attribution and event evidence. Identity-bearing values do not belong in event snapshots or arbitrary metadata. +- Superseded Security Findings remain separate report groups and show their canonical Security Finding ID when recorded; canonical remediation evidence is not copied into superseded groups. +- Each v1 report range is capped at 90 inclusive calendar days. +- A report displays its reliable event-coverage start and labels supplemental legacy activity as potentially incomplete. +- Disabling Security Agent or its integration does not hide authorized historical Security Agent Audit Reports. +- `security_audit_log` is the canonical ledger for Security Finding Activity Events; finding events are distinguished by stable finding identity. +- A reportable local Security Finding state transition and its Security Finding Activity Event are atomic. External side effects use a durable request event and terminal outcome event without keeping database transactions open across network calls. +- Security Agent Audit Reports include structured, sanitized analysis and remediation outcomes, not prompts, raw analysis markdown, transcripts, assistant messages, full execution logs, or recipient-level notification history. ## Agent Rules @@ -50,6 +69,8 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - Keep notification eligibility and outbox transitions in **Security Sync**. Keep rendering and Mailgun access in **Security Agent Email Delivery**. - Keep notification config parsing and pure eligibility semantics in **Shared Security Notification Policy** so web and Worker cannot drift. - Do not call organization members or billing managers **Notification Recipients** unless they also hold current organization `owner` role. +- Treat "all activity" in a **Security Agent Audit Report** as all material actions and outcomes recorded by Kilo, not every internal processing step or an attestation that legacy history is exhaustive. Exclude reads, unchanged sync observations, queue claims, heartbeats, and retries with no new finding-level outcome. +- A rollout baseline event records current state at actual capture time for an existing Security Finding; it is not a synthetic creation event and must not be backdated. ## Ambiguities @@ -73,3 +94,4 @@ Kilo Code Cloud hosts Kilo Code agents, integrations, and automation. This contr - `.specs/security-agent.md` defines Security Agent Auto Remediation and notification guarantees. - `.plans/security-agent-notifications.md` records notification implementation and rollout design. +- `.plans/security-agent-audit-report.md` records Security Agent Audit Report implementation and evidence design. diff --git a/apps/web/package.json b/apps/web/package.json index a89e29b9af..2d4801cb06 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -51,9 +51,9 @@ "@kilocode/event-service": "workspace:*", "@kilocode/kilo-chat": "workspace:*", "@kilocode/kilo-chat-hooks": "workspace:*", - "@kilocode/mcp-gateway": "workspace:*", "@kilocode/kiloclaw-instance-tiers": "workspace:*", "@kilocode/kiloclaw-secret-catalog": "workspace:*", + "@kilocode/mcp-gateway": "workspace:*", "@kilocode/organization-entitlement": "workspace:*", "@kilocode/worker-utils": "workspace:*", "@linear/sdk": "76.0.0", @@ -151,6 +151,7 @@ "posthog-node": "5.10.4", "react": "19.2.6", "react-countup": "6.5.3", + "react-day-picker": "10.0.1", "react-dom": "19.2.6", "react-markdown": "10.1.0", "react-turnstile": "1.1.5", diff --git a/apps/web/src/app/(app)/organizations/[id]/security-agent/audit-report/page.tsx b/apps/web/src/app/(app)/organizations/[id]/security-agent/audit-report/page.tsx new file mode 100644 index 0000000000..4384ebd19d --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/security-agent/audit-report/page.tsx @@ -0,0 +1,16 @@ +import { Suspense } from 'react'; +import { SecurityAuditReportPage } from '@/components/security-agent/SecurityAuditReportPage'; + +export default function OrgAuditReportPage() { + return ( + + Loading audit report... + + } + > + + + ); +} diff --git a/apps/web/src/app/(app)/organizations/[id]/security-agent/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/security-agent/layout.tsx index 6385077120..f068dd0d0a 100644 --- a/apps/web/src/app/(app)/organizations/[id]/security-agent/layout.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/security-agent/layout.tsx @@ -1,11 +1,10 @@ -import { PageContainer } from '@/components/layouts/PageContainer'; import { OrganizationByPageLayout } from '@/components/organizations/OrganizationByPageLayout'; import { SecurityAgentLayout } from '@/components/security-agent/SecurityAgentLayout'; import { SecurityAgentProvider } from '@/components/security-agent/SecurityAgentContext'; export const metadata = { title: 'Security Agent | Kilo Code', - description: 'Monitor and manage Dependabot security alerts', + description: 'Monitor and manage Security Findings synced from Dependabot', }; type LayoutProps = { @@ -17,12 +16,11 @@ export default async function OrgSecurityAgentLayout({ params, children }: Layou return ( ( - - - {children} - - + + {children} + )} /> ); diff --git a/apps/web/src/app/(app)/organizations/[id]/security-agent/page.tsx b/apps/web/src/app/(app)/organizations/[id]/security-agent/page.tsx index 537010dd34..9974b42d9d 100644 --- a/apps/web/src/app/(app)/organizations/[id]/security-agent/page.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/security-agent/page.tsx @@ -7,10 +7,13 @@ import { useSecurityAgent } from '@/components/security-agent/SecurityAgentConte import { SecurityDashboard } from '@/components/security-agent/SecurityDashboard'; export default function OrgSecurityAgentDashboardPage() { - const { hasIntegration, isEnabled, isLoadingConfig, organizationId } = useSecurityAgent(); + const { hasIntegration, isEnabled, isLoadingConfig, isLoadingPermission, organizationId } = + useSecurityAgent(); const router = useRouter(); - const shouldRedirectToConfig = hasIntegration && isEnabled === false && !!organizationId; + const shouldRedirectToConfig = + !!organizationId && + ((!isLoadingPermission && !hasIntegration) || (hasIntegration && isEnabled === false)); useEffect(() => { if (shouldRedirectToConfig) { @@ -26,7 +29,7 @@ export default function OrgSecurityAgentDashboardPage() { ); } - if (hasIntegration && isLoadingConfig) { + if (isLoadingPermission || (hasIntegration && isLoadingConfig)) { return (
+ } + > + + + ); +} diff --git a/apps/web/src/app/(app)/security-agent/layout.tsx b/apps/web/src/app/(app)/security-agent/layout.tsx index 0ecdea21c3..1e8336b454 100644 --- a/apps/web/src/app/(app)/security-agent/layout.tsx +++ b/apps/web/src/app/(app)/security-agent/layout.tsx @@ -1,18 +1,15 @@ -import { PageContainer } from '@/components/layouts/PageContainer'; import { SecurityAgentLayout } from '@/components/security-agent/SecurityAgentLayout'; import { SecurityAgentProvider } from '@/components/security-agent/SecurityAgentContext'; export const metadata = { title: 'Security Agent | Kilo Code', - description: 'Monitor and manage Dependabot security alerts', + description: 'Monitor and manage Security Findings synced from Dependabot', }; export default function SecurityAgentRootLayout({ children }: { children: React.ReactNode }) { return ( - - - {children} - - + + {children} + ); } diff --git a/apps/web/src/app/(app)/security-agent/page.tsx b/apps/web/src/app/(app)/security-agent/page.tsx index 3c5033dcf8..6fddb31b3f 100644 --- a/apps/web/src/app/(app)/security-agent/page.tsx +++ b/apps/web/src/app/(app)/security-agent/page.tsx @@ -7,15 +7,16 @@ import { useSecurityAgent } from '@/components/security-agent/SecurityAgentConte import { SecurityDashboard } from '@/components/security-agent/SecurityDashboard'; export default function SecurityAgentDashboardPage() { - const { hasIntegration, isEnabled, isLoadingConfig } = useSecurityAgent(); + const { hasIntegration, isEnabled, isLoadingConfig, isLoadingPermission } = useSecurityAgent(); const router = useRouter(); // Redirect per truth table: - // No integration -> show dashboard with install CTA (handled by SecurityDashboard) + // No integration -> redirect to settings with install CTA // Installed + disabled -> redirect to config // Installed + enabled -> show dashboard // isEnabled is undefined while config is loading — wait before deciding - const shouldRedirectToConfig = hasIntegration && isEnabled === false; + const shouldRedirectToConfig = + (!isLoadingPermission && !hasIntegration) || (hasIntegration && isEnabled === false); useEffect(() => { if (shouldRedirectToConfig) { @@ -31,7 +32,7 @@ export default function SecurityAgentDashboardPage() { ); } - if (hasIntegration && isLoadingConfig) { + if (isLoadingPermission || (hasIntegration && isLoadingConfig)) { return (
diff --git a/apps/web/src/components/security-agent/FindingDetailDialog.tsx b/apps/web/src/components/security-agent/FindingDetailDialog.tsx index 66ef9a0d9a..7ea8adb7a1 100644 --- a/apps/web/src/components/security-agent/FindingDetailDialog.tsx +++ b/apps/web/src/components/security-agent/FindingDetailDialog.tsx @@ -1,781 +1,2946 @@ 'use client'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { Dialog, + DialogClose, DialogContent, DialogDescription, - DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { SeverityBadge } from './SeverityBadge'; -import { FindingStatusBadge } from './FindingStatusBadge'; -import { ExploitabilityBadge } from './ExploitabilityBadge'; -import { MarkdownProse } from './MarkdownProse'; -import { format, formatDistanceToNow, isPast } from 'date-fns'; +import { cn } from '@/lib/utils'; +import { useTRPC } from '@/lib/trpc/utils'; +import { useQuery } from '@tanstack/react-query'; +import type { SecurityFinding } from '@kilocode/db/schema'; +import { formatDistanceToNow, isPast } from 'date-fns'; import { + Ban, + Brain, + Check, + CheckCircle2, + CircleHelp, + Clock3, ExternalLink, + FileCheck2, + FileCode2, + FileWarning, + GitBranch, + GitMerge, + GitPullRequest, + Info, + Loader2, Package, - Clock, - CheckCircle2, + RefreshCw, + Search, + ShieldAlert, + ShieldCheck, + Sparkles, + Square, + TriangleAlert, + UserRound, + Wrench, + X, XCircle, - Brain, - Loader2, - Zap, - GitPullRequest, - RotateCw, - AlertCircle, + type LucideIcon, } from 'lucide-react'; -import type { SecurityFinding } from '@kilocode/db/schema'; -import { useTRPC } from '@/lib/trpc/utils'; -import { useQuery } from '@tanstack/react-query'; import Link from 'next/link'; +import { MarkdownProse } from './MarkdownProse'; import { useSecurityAgent } from './SecurityAgentContext'; -import { securityAgentCommandAdmissionCopy } from './security-agent-command-copy'; -import { manualAnalysisAdmissionCopy } from './manual-analysis-admission-copy'; -import type { SecurityFindingWithRemediation } from './SecurityFindingRow'; -import { getRemediationUnavailableCopy } from './remediation-unavailable-copy'; +import { + isAwaitingManualAnalysisAdmission, + manualAnalysisAdmissionCopy, + manualAnalysisCapacityFullCopy, +} from './manual-analysis-admission-copy'; +import { + getRemediationUnavailableCopy, + isCodebaseAnalysisRequiredReason, +} from './remediation-unavailable-copy'; +import type { SecurityFindingWithRemediation } from '@/lib/security-agent/db/security-remediation'; -type Severity = 'critical' | 'high' | 'medium' | 'low'; type FindingAnalysis = SecurityFinding['analysis']; -type StartAnalysis = (options?: { forceSandbox?: boolean; retrySandboxOnly?: boolean }) => void; - -const ANALYSIS_POLL_INTERVAL_MS = 3000; -const statusPanelClassName = 'rounded-lg border border-border bg-muted/40 p-3'; -const linkClassName = - 'text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none'; +type FindingTab = 'details' | 'analysis' | 'remediation'; +type Tone = 'success' | 'warning' | 'destructive' | 'neutral'; +type StartAnalysisOptions = { + forceSandbox?: boolean; + retrySandboxOnly?: boolean; + restartActive?: boolean; +}; +type StartAnalysis = (options?: StartAnalysisOptions) => void; +type StartFindingAnalysis = (findingId: string, options?: StartAnalysisOptions) => void; type RemediationAttempt = { id: string; status: string; origin: string; attemptNumber: number; + requestedByUserId: string | null; remediationModelSlug: string; branchName: string; prUrl: string | null; + prNumber: number | null; prDraft: boolean | null; failureCode: string | null; blockedReason: string | null; lastErrorRedacted: string | null; + validationEvidence: Record[] | null; riskNotes: string | null; draftReason: string | null; + cancellationRequestedAt: string | null; queuedAt: string; launchedAt: string | null; completedAt: string | null; updatedAt: string; }; -function isSeverity(value: string): value is Severity { - return ['critical', 'high', 'medium', 'low'].includes(value); -} +type StatusValue = { + value: string; + tone: Tone; +}; + +type DetailFact = { + label: string; + value: string; + mono?: boolean; +}; + +type SummaryItem = { + label: string; + value: string; + detail: string; + icon: LucideIcon; + tone: Tone; +}; + +type ProgressStep = { + title: string; + detail: string; + state: 'done' | 'running' | 'waiting' | 'attention' | 'pending' | 'error'; +}; + +type AnalysisAction = + | 'none' + | 'start-analysis' + | 'retry-analysis' + | 'start-remediation' + | 'view-remediation' + | 'dismiss' + | 'source' + | 'cloud-agent'; + +type AnalysisPresentation = { + hero: { + title: string; + description: string; + icon: LucideIcon; + tone: Tone; + spinning?: boolean; + }; + summary: SummaryItem[]; + context: string; + action: { + label: 'Next step' | 'Current status'; + title: string; + description: string; + buttonLabel?: string; + buttonIcon?: LucideIcon; + kind: AnalysisAction; + primary?: boolean; + }; + disclosureTitle: string; +}; + +type RemediationPresentation = { + hero: { + title: string; + description: string; + icon: LucideIcon; + tone: Tone; + spinning?: boolean; + }; + summary: SummaryItem[]; + context: string; + action: { + label: 'Next step' | 'Attempt controls' | 'Current status'; + title: string; + description: string; + }; + disclosureTitle: string; + steps: ProgressStep[]; +}; + +const ANALYSIS_POLL_INTERVAL_MS = 3000; +const SUPERSEDED_PREFIX = 'superseded:'; +const utcDateFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', +}); +const utcTimeFormatter = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'UTC', +}); + +const toneStyles: Record< + Tone, + { status: string; icon: string; text: string; step: string; border: string } +> = { + success: { + status: 'border-status-success-border bg-status-success-surface text-status-success', + icon: 'bg-status-success-surface text-status-success-icon ring-status-success-border', + text: 'text-status-success', + step: 'bg-status-success-surface text-status-success ring-status-success-border', + border: 'border-status-success-border bg-status-success-surface', + }, + warning: { + status: 'border-status-warning-border bg-status-warning-surface text-status-warning', + icon: 'bg-status-warning-surface text-status-warning-icon ring-status-warning-border', + text: 'text-status-warning', + step: 'bg-status-warning-surface text-status-warning ring-status-warning-border', + border: 'border-status-warning-border bg-status-warning-surface', + }, + destructive: { + status: + 'border-status-destructive-border bg-status-destructive-surface text-status-destructive', + icon: 'bg-status-destructive-surface text-status-destructive-icon ring-status-destructive-border', + text: 'text-status-destructive', + step: 'bg-status-destructive-surface text-status-destructive ring-status-destructive-border', + border: 'border-status-destructive-border bg-status-destructive-surface', + }, + neutral: { + status: 'border-status-neutral-border bg-status-neutral-surface text-status-neutral', + icon: 'bg-status-neutral-surface text-status-neutral-icon ring-status-neutral-border', + text: 'text-status-neutral', + step: 'bg-status-neutral-surface text-status-neutral-icon ring-status-neutral-border', + border: 'border-status-neutral-border bg-surface-inset', + }, +}; + +const dismissalReasonLabels: Record = { + fix_started: 'a fix has already started', + no_bandwidth: 'no bandwidth is available', + tolerable_risk: 'the risk is tolerable', + inaccurate: 'the finding is inaccurate', + not_used: 'vulnerable code is not used', +}; function LoadingSpinner({ className = 'size-4' }: { className?: string }) { return (