import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { Rocket, Shield, ChevronDown, ChevronUp, Loader2, AlertTriangle, CheckCircle2, Globe, Lock, Bug, MessageSquare, FileText, ScrollText, X, ExternalLink, Download, Sparkles, Trash2, Brain, Wrench, Layers, Clock, Search, Activity, Terminal, Crosshair } from 'lucide-react' import { PieChart, Pie, Cell, Tooltip as RechartsTooltip, ResponsiveContainer } from 'recharts' import { agentApi, reportsApi, promptsApi, cliAgentApi } from '../services/api' import type { AgentStatus, AgentFinding, AgentLog, Prompt, ToolExecution, ContainerStatus } from '../types' import VulnAgentGrid from '../components/VulnAgentGrid' // ─── Constants ──────────────────────────────────────────────────────────────── const PHASES = [ { key: 'recon', label: 'Reconnaissance', icon: Globe, range: [0, 20] as const }, { key: 'agents', label: 'Agent Grid (108 agents)', icon: Layers, range: [20, 85] as const }, { key: 'final', label: 'Finalization', icon: Shield, range: [85, 100] as const }, ] const STREAMS = [ { key: 'recon', label: 'Recon', icon: Globe, color: 'blue', activeUntil: 20 }, { key: 'agents', label: 'Agent Grid', icon: Brain, color: 'purple', activeUntil: 85 }, { key: 'final', label: 'Report', icon: Wrench, color: 'orange', activeUntil: 100 }, ] as const const STREAM_COLORS: Record = { blue: { bg: 'bg-blue-500/20', text: 'text-blue-400', border: 'border-blue-500/40', pulse: 'bg-blue-400' }, purple: { bg: 'bg-purple-500/20', text: 'text-purple-400', border: 'border-purple-500/40', pulse: 'bg-purple-400' }, orange: { bg: 'bg-orange-500/20', text: 'text-orange-400', border: 'border-orange-500/40', pulse: 'bg-orange-400' }, } const SEVERITY_COLORS: Record = { critical: 'bg-red-500', high: 'bg-orange-500', medium: 'bg-yellow-500', low: 'bg-blue-500', info: 'bg-gray-500', } const SEVERITY_BORDER: Record = { critical: 'border-red-500/40', high: 'border-orange-500/40', medium: 'border-yellow-500/40', low: 'border-blue-500/40', info: 'border-gray-500/40', } const SEVERITY_CHART_COLORS: Record = { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#3b82f6', info: '#6b7280', } const CONFIDENCE_STYLES: Record = { green: 'bg-green-500/15 text-green-400 border-green-500/30', yellow: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30', red: 'bg-red-500/15 text-red-400 border-red-500/30', } const LOG_FILTERS = [ { key: 'all', label: 'All', color: '' }, { key: 'recon', label: 'Recon', color: 'text-blue-400' }, { key: 'agents', label: 'Agents', color: 'text-green-400' }, { key: 'judge', label: 'Validation', color: 'text-amber-300' }, { key: 'final', label: 'Final', color: 'text-cyan-400' }, { key: 'error', label: 'Errors', color: 'text-red-400' }, ] const SESSIONS_KEY = 'neurosploit_autopentest_sessions' const LEGACY_SESSION_KEY = 'neurosploit_autopentest_session' const POLL_INTERVAL = 1500 const POLL_INTERVAL_ERROR = 5000 const TOAST_DURATION = 5000 const MAX_TOASTS = 5 // ─── Types ──────────────────────────────────────────────────────────────────── interface SavedSession { agentId: string target: string startedAt: string status: 'running' | 'completed' | 'error' | 'stopped' } interface Toast { id: string message: string severity: string timestamp: number } // ─── Utility Functions ──────────────────────────────────────────────────────── function phaseFromProgress(progress: number): number { if (progress < 20) return 0 if (progress < 85) return 1 return 2 } function formatElapsed(totalSeconds: number): string { const h = Math.floor(totalSeconds / 3600) const m = Math.floor((totalSeconds % 3600) / 60) const s = totalSeconds % 60 return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } function logMessageColor(message: string): string { if (message.startsWith('[STREAM 1]')) return 'text-blue-400' if (message.startsWith('[STREAM 2]')) return 'text-purple-400' if (message.startsWith('[STREAM 3]')) return 'text-orange-400' if (message.startsWith('[TOOL]')) return 'text-orange-300' if (message.startsWith('[DEEP]')) return 'text-cyan-400' if (message.startsWith('[FINAL]')) return 'text-green-400' if (message.startsWith('[CONTAINER]')) return 'text-cyan-300' if (message.startsWith('[CLI-AGENT]')) return 'text-pink-400' if (message.startsWith('[PHASE]')) return 'text-yellow-400' if (message.startsWith('[PHASE FAIL]')) return 'text-red-400' if (message.startsWith('[BANNER]')) return 'text-teal-400' if (message.startsWith('[WAF]')) return 'text-amber-400' if (message.startsWith('[PLAYBOOK]')) return 'text-indigo-400' if (message.startsWith('[SITE ANALYZER]')) return 'text-emerald-400' if (message.startsWith('[MD-AGENTS]')) return 'text-cyan-300' if (message.startsWith('[AGENT GRID]')) return 'text-green-400' if (message.startsWith('[PHASE 1]')) return 'text-blue-300' if (message.startsWith('[PHASE 2]')) return 'text-purple-300' if (message.startsWith('[PHASE 3]')) return 'text-yellow-300' if (message.startsWith('[RECON]')) return 'text-blue-400' if (message.startsWith('[CVE]')) return 'text-red-300' if (message.startsWith('[CHAIN]')) return 'text-orange-300' if (message.startsWith('[JUDGE]')) return 'text-amber-300' if (message.includes('Starting (real HTTP)')) return 'text-green-300' return '' } function matchLogFilter(log: AgentLog, filter: string): boolean { if (filter === 'all') return true if (filter === 'stream1') return log.message.startsWith('[STREAM 1]') if (filter === 'stream2') return log.message.startsWith('[STREAM 2]') if (filter === 'stream3') return log.message.startsWith('[STREAM 3]') if (filter === 'deep') return log.message.startsWith('[DEEP]') if (filter === 'container') return log.message.startsWith('[CONTAINER]') if (filter === 'cli_agent') return log.message.startsWith('[CLI-AGENT]') if (filter === 'error') return log.level === 'error' || log.level === 'warning' return true } function getConfidenceDisplay(finding: { confidence_score?: number; confidence?: string }): { score: number; color: string; label: string } | null { let score: number | null = null if (typeof finding.confidence_score === 'number') { score = finding.confidence_score } else if (finding.confidence) { const parsed = Number(finding.confidence) if (!isNaN(parsed)) score = parsed else { const map: Record = { high: 90, medium: 60, low: 30 } score = map[finding.confidence.toLowerCase()] ?? null } } if (score === null) return null const color = score >= 90 ? 'green' : score >= 60 ? 'yellow' : 'red' const label = score >= 90 ? 'Confirmed' : score >= 60 ? 'Likely' : 'Low' return { score, color, label } } // ─── Sub-Components ─────────────────────────────────────────────────────────── function StreamBadge({ stream, progress, isRunning }: { stream: typeof STREAMS[number]; progress: number; isRunning: boolean }) { const active = isRunning && progress < stream.activeUntil const done = progress >= stream.activeUntil const colors = STREAM_COLORS[stream.color] const Icon = stream.icon return (
{active && ( )} {done && } {!active && !done && } {stream.label}
) } function LiveStatsDashboard({ status, elapsedSeconds, toolExecutions }: { status: AgentStatus; elapsedSeconds: number; toolExecutions: ToolExecution[] }) { return (
Elapsed
{formatElapsed(elapsedSeconds)}
Findings
{status.findings_count} {(status.rejected_findings_count ?? 0) > 0 && ( +{status.rejected_findings_count} rej )}
Tools Run
{toolExecutions.length}
Progress
{status.progress}% {status.phase || 'Init'}
) } function ToolExecutionRow({ exec, expanded, onToggle }: { exec: ToolExecution; expanded: boolean; onToggle: () => void }) { const hasExpandable = !!(exec.stdout_preview || exec.stderr_preview || exec.reason) return (
{expanded && (
{exec.reason && (

Reason: {exec.reason}

)} {exec.stdout_preview && (

stdout

{exec.stdout_preview}
)} {exec.stderr_preview && (

stderr

{exec.stderr_preview}
)} {exec.container_name && (

Container: {exec.container_name}

)}
)}
) } function SeverityMiniChart({ sevCounts }: { sevCounts: Record }) { const data = ['critical', 'high', 'medium', 'low', 'info'] .filter(s => (sevCounts[s] || 0) > 0) .map(s => ({ name: s, value: sevCounts[s] || 0 })) if (data.length === 0) return null return (
{data.map(entry => ( ))} [`${value}`, name.charAt(0).toUpperCase() + name.slice(1)]} />
) } function LogViewer({ logs, logFilter, setLogFilter, logSearch, setLogSearch, logsEndRef }: { logs: AgentLog[]; logFilter: string; setLogFilter: (f: string) => void logSearch: string; setLogSearch: (s: string) => void; logsEndRef: React.RefObject }) { const filteredLogs = useMemo(() => logs.filter(log => { if (!matchLogFilter(log, logFilter)) return false if (logSearch && !log.message.toLowerCase().includes(logSearch.toLowerCase())) return false return true }), [logs, logFilter, logSearch] ) return (
{LOG_FILTERS.map(f => ( ))}
setLogSearch(e.target.value)} placeholder="Search..." className="pl-6 pr-2 py-1 bg-dark-800 border border-dark-700 rounded text-xs text-white placeholder-dark-500 focus:outline-none focus:border-dark-500 w-32 sm:w-40" />
{filteredLogs.length}/{logs.length}
{filteredLogs.length === 0 ? (

{logs.length === 0 ? 'Waiting for logs...' : 'No logs match filter'}

) : ( filteredLogs.map((log, i) => (
{log.time?.slice(11, 19) || ''} {log.level} {log.message}
)) )}
) } function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) { if (toasts.length === 0) return null return (
{toasts.map(toast => (
{toast.severity === 'completed' ? ( ) : toast.severity === 'error' || toast.severity === 'critical' ? ( ) : ( )} {toast.message}
))}
) } // ─── Main Component ─────────────────────────────────────────────────────────── export default function AutoPentestPage() { const navigate = useNavigate() // Form state const [target, setTarget] = useState('') const [multiTarget, setMultiTarget] = useState(false) const [targets, setTargets] = useState('') const [subdomainDiscovery, setSubdomainDiscovery] = useState(false) const [enableKaliSandbox, setEnableKaliSandbox] = useState(false) const [showAuth, setShowAuth] = useState(false) const [authType, setAuthType] = useState('') const [authValue, setAuthValue] = useState('') const [showPrompt, setShowPrompt] = useState(false) const [customPrompt, setCustomPrompt] = useState('') const [savedPrompts, setSavedPrompts] = useState([]) const [selectedPromptIds, setSelectedPromptIds] = useState([]) const [showSavedPrompts, setShowSavedPrompts] = useState(false) // Model selection const [availableModels, setAvailableModels] = useState>([]) const [selectedProvider, setSelectedProvider] = useState('anthropic') const [selectedModel, setSelectedModel] = useState('claude-sonnet-4-20250514') // MD Agent selection const [availableMdAgents, setAvailableMdAgents] = useState>([]) const [selectedMdAgents, setSelectedMdAgents] = useState([]) const [showAgentSelector, setShowAgentSelector] = useState(false) // CLI Agent mode const [testMode, setTestMode] = useState<'auto_pentest' | 'cli_agent' | 'full_llm_pentest'>('auto_pentest') const [cliProviders, setCliProviders] = useState>([]) const [cliEnabled, setCliEnabled] = useState(false) const [selectedCliProvider, setSelectedCliProvider] = useState('') const [methodologies, setMethodologies] = useState>([]) const [selectedMethodology, setSelectedMethodology] = useState('') const [enableCliPhase, setEnableCliPhase] = useState(false) // Checkbox in auto_pentest mode // Learning stats (TP/FP per vuln type) const [learningStats, setLearningStats] = useState>({}) // History const [showHistory, setShowHistory] = useState(false) const [history, setHistory] = useState>([]) const [historyLoading, setHistoryLoading] = useState(false) // Triple-check const [tripleCheckScanId, setTripleCheckScanId] = useState(null) const [tripleCheckProvider, setTripleCheckProvider] = useState('') const [tripleCheckModel, setTripleCheckModel] = useState('') // Multi-session state const [sessions, setSessions] = useState([]) const [activeSessionIdx, setActiveSessionIdx] = useState(-1) const [maxConcurrent, setMaxConcurrent] = useState(5) const [statusCache, setStatusCache] = useState>({}) const [error, setError] = useState(null) // Derived from active session const activeSession = activeSessionIdx >= 0 && activeSessionIdx < sessions.length ? sessions[activeSessionIdx] : null const agentId = activeSession?.agentId ?? null const isRunning = activeSession?.status === 'running' const status = agentId ? statusCache[agentId] ?? null : null // Live stats const [elapsedSeconds, setElapsedSeconds] = useState(0) // UI state const [activeTab, setActiveTab] = useState<'findings' | 'logs' | 'agents'>('findings') const [expandedFinding, setExpandedFinding] = useState(null) const [expandedTool, setExpandedTool] = useState(null) const [findingsFilter, setFindingsFilter] = useState<'confirmed' | 'rejected' | 'all'>('all') const [logFilter, setLogFilter] = useState('all') const [logSearch, setLogSearch] = useState('') // Toast notifications const [toasts, setToasts] = useState([]) // Finding animations const [newFindingIds, setNewFindingIds] = useState>(new Set()) // Connection state const [connectionLost, setConnectionLost] = useState(false) // Logs & Report const [logs, setLogs] = useState([]) const [generatingReport, setGeneratingReport] = useState(false) const [reportId, setReportId] = useState(null) // Refs const pollRef = useRef | null>(null) const logsEndRef = useRef(null) const failCountRef = useRef>({}) const seenFindingIdsRef = useRef>(new Set()) const prevPhaseRef = useRef(null) const prevStatusRef = useRef(null) const newFindingTimerRef = useRef | null>(null) // ─── Toast Helper ───────────────────────────────────────────────────────── const addToast = useCallback((message: string, severity: string = 'info') => { const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}` setToasts(prev => [...prev.slice(-(MAX_TOASTS - 1)), { id, message, severity, timestamp: Date.now() }]) setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), TOAST_DURATION) }, []) const dismissToast = useCallback((id: string) => { setToasts(prev => prev.filter(t => t.id !== id)) }, []) // ─── Mount: restore sessions + load prompts + models ─────────────────────── useEffect(() => { // Migrate legacy single-session key try { const legacy = localStorage.getItem(LEGACY_SESSION_KEY) if (legacy) { const old = JSON.parse(legacy) const migrated: SavedSession = { agentId: old.agentId, target: old.target, startedAt: old.startedAt, status: 'running', } setSessions([migrated]) setActiveSessionIdx(0) localStorage.removeItem(LEGACY_SESSION_KEY) localStorage.setItem(SESSIONS_KEY, JSON.stringify([migrated])) } } catch { /* ignore */ } // Restore multi-session array try { const saved = localStorage.getItem(SESSIONS_KEY) if (saved) { const parsed: SavedSession[] = JSON.parse(saved) setSessions(parsed) const runningIdx = parsed.findIndex(s => s.status === 'running') setActiveSessionIdx(runningIdx >= 0 ? runningIdx : parsed.length > 0 ? 0 : -1) } } catch { /* ignore */ } // Fetch backend limits agentApi.listActive().then(data => { setMaxConcurrent(data.max_concurrent) }).catch(() => {}) promptsApi.list().then(p => setSavedPrompts(p || [])).catch(() => {}) // Fetch available LLM providers/models fetch('/api/v1/providers/available-models') .then(r => r.json()) .then(data => setAvailableModels(data.models || [])) .catch(() => {}) // Fetch available MD agents fetch('/api/v1/agent/md-agents') .then(r => r.json()) .then(data => setAvailableMdAgents(data.agents || [])) .catch(() => {}) // Fetch learning stats (TP/FP counts per vuln type) fetch('/api/v1/scans/vulnerabilities/learning/stats') .then(r => r.json()) .then(data => { if (data.vuln_types) { const stats: Record = {} for (const [vt, info] of Object.entries(data.vuln_types as Record)) { stats[vt] = { tp: info.true_positives || 0, fp: info.false_positives || 0 } } setLearningStats(stats) } }) .catch(() => {}) // Fetch CLI agent providers and methodologies cliAgentApi.getProviders() .then(data => { setCliProviders(data.providers || []) setCliEnabled(data.enabled || false) const connected = (data.providers || []).find((p: any) => p.connected) if (connected) setSelectedCliProvider(connected.id) }) .catch(() => {}) cliAgentApi.getMethodologies() .then(data => { setMethodologies(data.methodologies || []) const def = (data.methodologies || []).find((m: any) => m.is_default) if (def) setSelectedMethodology(def.path) }) .catch(() => {}) // Restore statusCache for ALL saved sessions on mount try { const saved = localStorage.getItem(SESSIONS_KEY) if (saved) { const parsed: SavedSession[] = JSON.parse(saved) for (const sess of parsed) { agentApi.getStatus(sess.agentId) .then(s => { setStatusCache(prev => ({ ...prev, [sess.agentId]: s })) if (s.status !== sess.status && (s.status === 'completed' || s.status === 'error' || s.status === 'stopped')) { setSessions(prev => prev.map(p => p.agentId === sess.agentId ? { ...p, status: s.status as SavedSession['status'] } : p )) } }) .catch(() => {}) } } } catch { /* ignore */ } }, []) // Persist sessions to localStorage useEffect(() => { if (sessions.length > 0) { localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)) } else { localStorage.removeItem(SESSIONS_KEY) } }, [sessions]) // ─── Elapsed Time Ticker ────────────────────────────────────────────────── useEffect(() => { if (!status?.started_at) return const startTime = new Date(status.started_at).getTime() if (isRunning) { // Live ticker while scan is active const tick = () => setElapsedSeconds(Math.floor((Date.now() - startTime) / 1000)) tick() const id = setInterval(tick, 1000) return () => clearInterval(id) } else { // Completed/stopped/error — compute final duration from timestamps const endTime = status.completed_at ? new Date(status.completed_at).getTime() : Date.now() setElapsedSeconds(Math.max(0, Math.floor((endTime - startTime) / 1000))) } }, [isRunning, status?.started_at, status?.completed_at]) // ─── Polling — ALL running sessions + active session logs ───────────────── useEffect(() => { const runningSessions = sessions.filter(s => s.status === 'running') const needsPoll = runningSessions.length > 0 || agentId if (!needsPoll) return const poll = async () => { // Poll all running sessions for (const sess of runningSessions) { try { const s = await agentApi.getStatus(sess.agentId) setStatusCache(prev => ({ ...prev, [sess.agentId]: s })) failCountRef.current[sess.agentId] = 0 if (s.status === 'completed' || s.status === 'error' || s.status === 'stopped') { setSessions(prev => prev.map(p => p.agentId === sess.agentId ? { ...p, status: s.status as SavedSession['status'] } : p )) // Toast for completion of active session if (sess.agentId === agentId) { if (s.status === 'completed') addToast(`Pentest complete! ${s.findings_count} findings`, 'completed') else if (s.status === 'error') addToast('Pentest failed', 'error') else if (s.status === 'stopped') addToast('Pentest stopped', 'info') } } } catch (err: any) { const httpStatus = err?.response?.status const fails = (failCountRef.current[sess.agentId] || 0) + 1 failCountRef.current[sess.agentId] = fails if (httpStatus === 404 || fails >= 3) { setSessions(prev => prev.map(p => p.agentId === sess.agentId ? { ...p, status: 'stopped' } : p )) } } } // Fetch status + logs for active session if (agentId) { try { const s = await agentApi.getStatus(agentId) setStatusCache(prev => ({ ...prev, [agentId]: s })) // Connection restored if (connectionLost) setConnectionLost(false) // Phase change detection if (prevPhaseRef.current && s.phase && s.phase !== prevPhaseRef.current) { addToast(`Phase: ${s.phase}`, 'info') } prevPhaseRef.current = s.phase || null // Status transition detection if (prevStatusRef.current === 'running' && (s.status === 'completed' || s.status === 'error' || s.status === 'stopped')) { // Handled above in the running sessions loop } prevStatusRef.current = s.status // New finding detection const currentIds = new Set((s.findings || []).map((f: AgentFinding) => f.id)) if (seenFindingIdsRef.current.size > 0) { const newIds = [...currentIds].filter(id => !seenFindingIdsRef.current.has(id)) if (newIds.length > 0) { newIds.forEach(id => { const f = s.findings?.find((x: AgentFinding) => x.id === id) if (f) addToast(`${f.severity.toUpperCase()}: ${f.title}`, f.severity) }) setNewFindingIds(new Set(newIds)) if (newFindingTimerRef.current) clearTimeout(newFindingTimerRef.current) newFindingTimerRef.current = setTimeout(() => setNewFindingIds(new Set()), 3000) } } seenFindingIdsRef.current = currentIds } catch { const fails = (failCountRef.current[agentId] || 0) + 1 failCountRef.current[agentId] = fails if (fails >= 3) setConnectionLost(true) } try { const logData = await agentApi.getLogs(agentId, 300) setLogs(logData.logs || []) } catch { /* ignore */ } } } poll() const interval = connectionLost ? POLL_INTERVAL_ERROR : POLL_INTERVAL pollRef.current = setInterval(poll, interval) return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [sessions, agentId, connectionLost, addToast]) // Auto-scroll logs disabled — user controls scroll position // ─── History ────────────────────────────────────────────────────────────── const fetchHistory = useCallback(async () => { setHistoryLoading(true) try { const data = await agentApi.getHistory(1, 50) setHistory(data.history || []) } catch { /* ignore */ } setHistoryLoading(false) }, []) // ─── Triple-Check ───────────────────────────────────────────────────────── const handleTripleCheck = async (scanId: string) => { try { const resp = await agentApi.tripleCheck(scanId, tripleCheckProvider || undefined, tripleCheckModel || undefined) const newSession: SavedSession = { agentId: resp.agent_id, target: `Triple-Check: ${scanId.slice(0, 8)}`, startedAt: new Date().toISOString(), status: 'running', } const updated = [...sessions, newSession] setSessions(updated) setActiveSessionIdx(updated.length - 1) localStorage.setItem(SESSIONS_KEY, JSON.stringify(updated)) setTripleCheckScanId(null) addToast('Triple-check started', 'info') } catch (e: any) { setError(e.response?.data?.detail || 'Triple-check failed') } } // ─── Start / Stop / Clear ────────────────────────────────────────────────── const handleStart = async () => { const primaryTarget = target.trim() if (!primaryTarget) return const runningCount = sessions.filter(s => s.status === 'running').length if (runningCount >= maxConcurrent) { setError(`Maximum ${maxConcurrent} concurrent scans reached. Stop one first.`) return } setError(null) setLogs([]) setReportId(null) seenFindingIdsRef.current = new Set() prevPhaseRef.current = null prevStatusRef.current = null try { const targetList = multiTarget ? targets.split('\n').map(t => t.trim()).filter(Boolean) : undefined const isCliMode = testMode === 'cli_agent' const resp = await agentApi.autoPentest(primaryTarget, { mode: testMode, subdomain_discovery: subdomainDiscovery, targets: targetList, auth_type: authType || undefined, auth_value: authValue || undefined, prompt: customPrompt.trim() || undefined, enable_kali_sandbox: isCliMode ? true : (enableKaliSandbox || undefined), custom_prompt_ids: selectedPromptIds.length > 0 ? selectedPromptIds : undefined, preferred_provider: selectedProvider || undefined, preferred_model: selectedModel || undefined, enable_cli_agent: isCliMode || enableCliPhase || undefined, cli_agent_provider: (isCliMode || enableCliPhase) ? (selectedCliProvider || undefined) : undefined, methodology_file: (isCliMode || enableCliPhase) ? (selectedMethodology || undefined) : undefined, selected_md_agents: selectedMdAgents.length > 0 ? selectedMdAgents : undefined, }) const newSession: SavedSession = { agentId: resp.agent_id, target: primaryTarget, startedAt: new Date().toISOString(), status: 'running', } setSessions(prev => { const updated = [...prev, newSession] setActiveSessionIdx(updated.length - 1) return updated }) setTarget('') addToast('Auto pentest started', 'info') } catch (err: any) { if (err?.response?.status === 429) { setError(err.response.data.detail) } else { setError(err?.response?.data?.detail || err?.message || 'Failed to start pentest') } } } const handleStop = async () => { if (!agentId) return try { await agentApi.stop(agentId) setSessions(prev => prev.map(s => s.agentId === agentId ? { ...s, status: 'stopped' as const } : s )) } catch { /* ignore */ } } const handleClearSession = (agentIdToClear?: string) => { const idToRemove = agentIdToClear || agentId if (idToRemove) { setSessions(prev => prev.filter(s => s.agentId !== idToRemove)) if (activeSession?.agentId === idToRemove) { setActiveSessionIdx(-1) } } else { setSessions([]) setActiveSessionIdx(-1) } setLogs([]) setError(null) setReportId(null) setElapsedSeconds(0) setNewFindingIds(new Set()) setConnectionLost(false) seenFindingIdsRef.current = new Set() prevPhaseRef.current = null prevStatusRef.current = null } // ─── AI Report ───────────────────────────────────────────────────────────── const handleGenerateAiReport = useCallback(async (reportProvider?: string, reportModel?: string) => { if (!status?.scan_id) return setGeneratingReport(true) try { const report = await reportsApi.generateAiReport({ scan_id: status.scan_id, title: `AI Pentest Report - ${activeSession?.target || target}`, preferred_provider: reportProvider || selectedProvider || undefined, preferred_model: reportModel || selectedModel || undefined, }) setReportId(report.id) addToast('AI Report generated', 'completed') } catch (err: any) { setError(err?.response?.data?.detail || 'Failed to generate AI report') } finally { setGeneratingReport(false) } }, [status?.scan_id, activeSession?.target, target, selectedProvider, selectedModel, addToast]) // ─── Derived State ────────────────────────────────────────────────────────── const currentPhaseIdx = status ? phaseFromProgress(status.progress) : -1 const findings = status?.findings || [] const rejectedFindings = status?.rejected_findings || [] const allFindings = useMemo(() => [...findings, ...rejectedFindings], [findings, rejectedFindings]) const displayFindings = findingsFilter === 'confirmed' ? findings : findingsFilter === 'rejected' ? rejectedFindings : allFindings const sevCounts = useMemo(() => findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] || 0) + 1; return acc }, {} as Record), [findings] ) const toolExecutions: ToolExecution[] = status?.tool_executions || [] const containerStatus: ContainerStatus | undefined = status?.container_status // ─── Render ───────────────────────────────────────────────────────────────── return (
{/* Inline keyframes */} {/* Toast Notifications */} {/* Connection Lost Banner */} {connectionLost && (
Connection issues — retrying...
)} {/* Header */}

Auto Pentest

One-click comprehensive penetration test. 100 vulnerability types, AI-powered analysis, full report.

{/* History Panel */} {showHistory && (

Past Pentest Runs

{historyLoading ? (
) : history.length === 0 ? (

No completed scans yet

) : (
{history.map((h: any) => (
{h.status} {h.target}
{h.findings_count} findings {h.critical_count > 0 && {h.critical_count}C} {h.high_count > 0 && {h.high_count}H} {h.medium_count > 0 && {h.medium_count}M} {h.endpoints_count} endpoints {h.duration_seconds && {Math.round(h.duration_seconds / 60)}min} {new Date(h.created_at).toLocaleDateString()}
))}
)}
)} {/* Triple-Check Modal */} {tripleCheckScanId && (
setTripleCheckScanId(null)}>
e.stopPropagation()} style={{ animation: 'fadeSlideIn 0.3s ease-out' }}>

Triple-Check Findings

Re-validate all findings from this scan using a different AI model.

)} {/* Multi-session tabs */} {sessions.length > 0 && (
{sessions.map((sess, idx) => { const isActive = idx === activeSessionIdx const sessStatus = statusCache[sess.agentId] const progress = sessStatus?.progress ?? 0 return ( ) })} {sessions.filter(s => s.status === 'running').length}/{maxConcurrent} slots
)} {/* ═══ START FORM ═══ */} {!agentId && (
{/* URL Input */}
setTarget(e.target.value)} placeholder="https://example.com" className="w-full px-4 py-4 bg-dark-900 border border-dark-600 rounded-xl text-white text-lg placeholder-dark-500 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500 transition-colors" />
{/* Test Mode Toggle */}
{/* CLI Agent Options (shown in CLI mode) */} {testMode === 'cli_agent' && (

CLI Agent Configuration

{!cliEnabled && (

Set ENABLE_CLI_AGENT=true in .env to enable CLI Agent mode

)}
)} {/* Toggles */}
{testMode === 'auto_pentest' && cliProviders.some(p => p.connected) && ( )}
{/* CLI Agent Phase Options (shown when checkbox enabled in auto_pentest mode) */} {testMode === 'auto_pentest' && enableCliPhase && (
)} {/* MD Agent Selection */} {availableMdAgents.length > 0 && (
{showAgentSelector && (

Select which AI agents run after recon. Empty = all {availableMdAgents.length} offensive agents.

{availableMdAgents.map(agent => { const isSelected = selectedMdAgents.includes(agent.name) const catColor = agent.category === 'offensive' ? 'cyan' : agent.category === 'analysis' ? 'yellow' : agent.category === 'defensive' ? 'blue' : 'gray' const agentKey = agent.name.replace(/\.md$/, '').replace(/_/g, '_') const ls = learningStats[agentKey] return ( ) })}
{selectedMdAgents.length > 0 && ( )}
)}
)} {/* LLM Provider / Model Selection */}
{/* Multi-target textarea */} {multiTarget && (