'use client' import { useState, useCallback } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Loader } from '@/components/ui/loader' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' import { useNavigateToPanel } from '@/lib/navigation' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' interface AuthEvent { id: number type: string actor: string ip: string timestamp: number detail: string } interface AgentTrust { agentId: number name: string trustScore: number flagged: boolean lastEval: number } interface SecretAlert { id: number file: string line: number type: string preview: string detectedAt: number resolved: boolean } interface ToolAuditEntry { tool: string calls: number successes: number failures: number } interface RateLimitSignal { ip: string hits: number agent?: string lastHit: number } interface InjectionAttempt { id: number type: string source: string input: string blocked: boolean timestamp: number } interface TimelinePoint { timestamp: string authEvents: number injectionAttempts: number secretAlerts: number toolCalls: number } interface EvalScore { layer: string score: number maxScore: number } interface AgentEval { agentId: number name: string scores: EvalScore[] convergence: number driftDetected: boolean lastEvalAt: number } type CheckSeverity = 'critical' | 'high' | 'medium' | 'low' interface ScanCheck { id: string name: string status: 'pass' | 'fail' | 'warn' detail: string fix: string severity?: CheckSeverity } interface ScanCategory { score: number checks: ScanCheck[] } interface ScanData { score: number overall: string categories: Record } interface SecurityAuditData { posture: { score: number; level: string } scan?: ScanData authEvents: AuthEvent[] agentTrust: AgentTrust[] secretAlerts: SecretAlert[] toolAudit: ToolAuditEntry[] rateLimits: RateLimitSignal[] injectionAttempts: InjectionAttempt[] timeline: TimelinePoint[] } interface AgentEvalsData { agents: AgentEval[] overallConvergence: number driftAlerts: string[] } const SCAN_STATUS_ICON: Record = { pass: '+', fail: 'x', warn: '!' } const SCAN_STATUS_COLOR: Record = { pass: 'text-green-400', fail: 'text-red-400', warn: 'text-amber-400' } const SEVERITY_BADGE: Record = { critical: { label: 'C', className: 'bg-red-500/20 text-red-400' }, high: { label: 'H', className: 'bg-orange-500/20 text-orange-400' }, medium: { label: 'M', className: 'bg-amber-500/20 text-amber-400' }, low: { label: 'L', className: 'bg-blue-500/20 text-blue-300' }, } function ScanCategoryRow({ label, icon, category, failingCount }: { label: string; icon: string; category: ScanCategory; failingCount: number }) { const t = useTranslations('securityAudit') const [expanded, setExpanded] = useState(false) return (
{expanded && (
{[...category.checks].sort((a, b) => { if (a.status === 'pass' && b.status !== 'pass') return 1 if (a.status !== 'pass' && b.status === 'pass') return -1 const sev: Record = { critical: 0, high: 1, medium: 2, low: 3 } return (sev[a.severity ?? 'medium'] ?? 2) - (sev[b.severity ?? 'medium'] ?? 2) }).map(check => (
[{SCAN_STATUS_ICON[check.status]}]
{check.name} {check.severity && ( {SEVERITY_BADGE[check.severity].label} )}

{check.detail}

{check.fix && check.status !== 'pass' && (

{t('fixPrefix', { fix: check.fix })}

)}
))}
)}
) } export function SecurityAuditPanel() { const t = useTranslations('securityAudit') const { setSecurityPosture } = useMissionControl() const navigateToPanel = useNavigateToPanel() const [selectedTimeframe, setSelectedTimeframe] = useState<'hour' | 'day' | 'week' | 'month'>('day') const [data, setData] = useState(null) const [evalsData, setEvalsData] = useState(null) const [isLoading, setIsLoading] = useState(false) const fetchData = useCallback(async () => { setIsLoading(true) try { const [auditRes, evalsRes] = await Promise.all([ fetch(`/api/security-audit?timeframe=${selectedTimeframe}`), fetch(`/api/agents/evals?timeframe=${selectedTimeframe}`), ]) if (auditRes.ok) { const audit = await auditRes.json() // API returns authEvents as { loginFailures, tokenRotations, accessDenials, recentEvents } // but the panel expects authEvents to be an array of AuthEvent if (audit.authEvents && !Array.isArray(audit.authEvents)) { const events = audit.authEvents.recentEvents || [] audit.authEvents = events.map((e: any, i: number) => ({ id: i, type: (e.event_type || '').replace('auth.', ''), actor: e.agent_name || 'unknown', ip: e.ip_address || '', timestamp: e.created_at || 0, detail: e.detail || '', })) } // agentTrust: { agents: [...], flaggedCount } → AgentTrust[] if (audit.agentTrust && !Array.isArray(audit.agentTrust)) { const agents = audit.agentTrust.agents || [] const flaggedThreshold = 0.8 audit.agentTrust = agents.map((a: any, i: number) => ({ agentId: i, name: a.name, trustScore: a.score, flagged: a.score < flaggedThreshold, })) } // secretExposures → secretAlerts if (audit.secretExposures && !audit.secretAlerts) { const recent = audit.secretExposures.recent || [] audit.secretAlerts = recent.map((e: any, i: number) => ({ id: i, file: e.detail || '', line: 0, type: (e.event_type || '').replace('secret.', ''), preview: e.detail || '', detectedAt: e.created_at || 0, resolved: false, })) } if (!Array.isArray(audit.secretAlerts)) audit.secretAlerts = [] // mcpAudit → toolAudit if (audit.mcpAudit && !audit.toolAudit) { const topTools = audit.mcpAudit.topTools || [] audit.toolAudit = topTools.map((t: any) => ({ tool: t.name, calls: t.count, successes: t.count, failures: 0, })) } if (!Array.isArray(audit.toolAudit)) audit.toolAudit = [] // rateLimits: { totalHits, byIp } → RateLimitSignal[] if (audit.rateLimits && !Array.isArray(audit.rateLimits)) { const byIp = audit.rateLimits.byIp || [] audit.rateLimits = byIp.map((r: any) => ({ ip: r.ip, hits: r.count, lastHit: 0, })) } // injectionAttempts: { total, recent } → InjectionAttempt[] if (audit.injectionAttempts && !Array.isArray(audit.injectionAttempts)) { const recent = audit.injectionAttempts.recent || [] audit.injectionAttempts = recent.map((e: any, i: number) => ({ id: i, type: (e.event_type || '').replace('injection.', ''), source: e.agent_name || e.ip_address || 'unknown', input: e.detail || '', blocked: true, timestamp: e.created_at || 0, })) } // timeline: [{timestamp, eventCount, severity}] → [{timestamp, authEvents, ...}] if (Array.isArray(audit.timeline)) { audit.timeline = audit.timeline.map((t: any) => ({ timestamp: t.timestamp, authEvents: t.eventCount || 0, injectionAttempts: 0, secretAlerts: 0, toolCalls: 0, })) } setData(audit) if (audit.posture) { setSecurityPosture(audit.posture) } } if (evalsRes.ok) { const evals = await evalsRes.json() setEvalsData(evals) } } catch { // Silent failure — data will remain stale } finally { setIsLoading(false) } }, [selectedTimeframe, setSecurityPosture]) useSmartPoll(fetchData, 30_000) const postureColor = (score: number) => { if (score >= 80) return 'text-green-400' if (score >= 60) return 'text-yellow-400' if (score >= 40) return 'text-orange-400' return 'text-red-400' } const postureRingColor = (score: number) => { if (score >= 80) return 'stroke-green-500' if (score >= 60) return 'stroke-yellow-500' if (score >= 40) return 'stroke-orange-500' return 'stroke-red-500' } const postureBgColor = (level: string) => { switch (level) { case 'hardened': return 'bg-green-500/15 text-green-400' case 'secure': return 'bg-green-500/10 text-green-300' case 'needs-attention': return 'bg-yellow-500/15 text-yellow-400' case 'at-risk': return 'bg-red-500/15 text-red-400' default: return 'bg-muted text-muted-foreground' } } const trustBarColor = (score: number) => { if (score >= 0.8) return 'bg-green-500' if (score >= 0.5) return 'bg-yellow-500' return 'bg-red-500' } const formatTime = (ts: number) => new Date(ts * 1000).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) return (
{/* Header */}

{t('title')}

{t('subtitle')}

{isLoading && (
)}
{(['hour', 'day', 'week', 'month'] as const).map((tf) => ( ))}
{!data ? ( ) : (
{/* Posture Score Header */}
{/* Circular gauge */}
{data.posture.score}

{t('securityPosture')}

{data.posture.level}

{t('blendedScore')}

{/* Infrastructure Scan Categories */} {data.scan && (

{t('infrastructureScan')}

{data.scan.score}/100
{Object.entries(data.scan.categories).map(([key, cat]) => { const scanCategoryLabels: Record = { credentials: t('scanCredentials'), network: t('scanNetwork'), openclaw: t('scanOpenclaw'), runtime: t('scanRuntime'), os: t('scanOs') } const label = scanCategoryLabels[key] || key const icon = { credentials: 'K', network: 'N', openclaw: 'O', runtime: 'R', os: 'S' }[key] || key[0].toUpperCase() const failing = cat.checks.filter(c => c.status !== 'pass') return ( ) })}
)} {/* Auth Events + Agent Trust */}
{/* Auth Events */}

{t('authEvents')}

{data.authEvents.length === 0 ? (

{t('noAuthEvents')}

) : (
{data.authEvents.map(evt => ( ))}
{t('colType')} {t('colActor')} {t('colIP')} {t('colTime')}
{evt.type.replace(/_/g, ' ')} {evt.actor} {evt.ip} {formatTime(evt.timestamp)}
)}
{/* Agent Trust Scores */}

{t('agentTrustScores')}

{data.agentTrust.length === 0 ? (

{t('noAgentTrustData')}

) : (
{data.agentTrust.map(agent => (
{agent.name} {agent.flagged && ( {t('flagged')} )}
{(agent.trustScore * 100).toFixed(0)}%
))}
)}
{/* Secret Exposure Alerts */}

{t('secretExposureAlerts')}

{data.secretAlerts.length === 0 ? (
{t('noSecretsDetected')}
) : (
{data.secretAlerts.map(alert => ( ))}
{t('colType')} {t('colFile')} {t('colPreview')} {t('colStatus')} {t('colDetected')}
{alert.type} {alert.file}:{alert.line} {alert.preview} {alert.resolved ? t('statusResolved') : t('statusActive')} {formatTime(alert.detectedAt)}
)}
{/* MCP Tool Audit + Rate Limits */}
{/* MCP Tool Audit BarChart */}

{t('mcpToolAudit')}

{data.toolAudit.length === 0 ? (
{t('noToolUsageData')}
) : (
)}
{/* Rate Limit / Abuse Signals */}

{t('rateLimitAbuseSignals')}

{data.rateLimits.length === 0 ? (

{t('noRateLimitSignals')}

) : (
{data.rateLimits.map((rl, i) => (
{rl.ip} {rl.agent && ({rl.agent})}
100 ? 'text-red-400' : rl.hits > 50 ? 'text-yellow-400' : 'text-muted-foreground'}`}> {t('hits', { hits: rl.hits })} {formatTime(rl.lastHit)}
))}
)}
{/* Injection Attempts */}

{t('injectionAttempts')}

{data.injectionAttempts.length === 0 ? (
{t('noInjectionAttempts')}
) : (
{data.injectionAttempts.map(attempt => ( ))}
{t('colType')} {t('colSource')} {t('colInput')} {t('colStatus')} {t('colTime')}
{attempt.type} {attempt.source} {attempt.input} {attempt.blocked ? t('statusBlocked') : t('statusPassed')} {formatTime(attempt.timestamp)}
)}
{/* Timeline */}

{t('securityTimeline', { timeframe: selectedTimeframe })}

{data.timeline.length === 0 ? (
{t('noTimelineData')}
) : (
({ ...p, time: new Date(p.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), }))}>
)}
{/* Agent Eval Dashboard */} {evalsData && (

{t('agentEvalDashboard')}

{/* Convergence gauge + drift alerts */}
{evalsData.overallConvergence}

{t('overallConvergence')}

{t('crossAgentAlignment')}

{evalsData.driftAlerts.length > 0 && (
{evalsData.driftAlerts.map((alert, i) => (
{alert}
))}
)}
{/* Per-agent eval scores */} {evalsData.agents.length === 0 ? (

{t('noEvalData')}

) : (
{evalsData.agents.map(agent => (
{agent.name} {agent.driftDetected && ( {t('drift')} )}
{t('convergence')} {agent.convergence}%
{agent.scores.map(s => (
{s.layer}
= 0.8 ? 'bg-green-500' : (s.score / s.maxScore) >= 0.5 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${(s.score / s.maxScore) * 100}%` }} />
{s.score}/{s.maxScore}
))}
))}
)}
)}
)}
) }