Files
NeuroSploit/frontend/src/pages/AutoPentestPage.tsx
2026-02-11 10:52:07 -03:00

930 lines
42 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } 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
} from 'lucide-react'
import { agentApi, reportsApi } from '../services/api'
import type { AgentStatus, AgentFinding, AgentLog } from '../types'
const PHASES = [
{ key: 'parallel', label: 'Parallel Streams', icon: Layers, range: [0, 50] as const },
{ key: 'deep', label: 'Deep Analysis', icon: Brain, range: [50, 75] as const },
{ key: 'final', label: 'Finalization', icon: Shield, range: [75, 100] as const },
]
const STREAMS = [
{ key: 'recon', label: 'Recon', icon: Globe, color: 'blue', activeUntil: 25 },
{ key: 'junior', label: 'Junior AI', icon: Brain, color: 'purple', activeUntil: 35 },
{ key: 'tools', label: 'Tools', icon: Wrench, color: 'orange', activeUntil: 50 },
] as const
const STREAM_COLORS: Record<string, { bg: string; text: string; border: string; pulse: string }> = {
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' },
}
function phaseFromProgress(progress: number): number {
if (progress < 50) return 0
if (progress < 75) return 1
return 2
}
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 (
<div
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-xs font-medium transition-all duration-300 ${
active
? `${colors.bg} ${colors.text} ${colors.border}`
: done
? 'bg-dark-700/50 text-dark-400 border-dark-600'
: 'bg-dark-900 text-dark-500 border-dark-700'
}`}
>
{active && (
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full ${colors.pulse} opacity-75`} />
<span className={`relative inline-flex rounded-full h-2 w-2 ${colors.pulse}`} />
</span>
)}
{done && <CheckCircle2 className="w-3 h-3 text-green-500" />}
{!active && !done && <Icon className="w-3 h-3" />}
<span>{stream.label}</span>
</div>
)
}
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'
return ''
}
const SEVERITY_COLORS: Record<string, string> = {
critical: 'bg-red-500',
high: 'bg-orange-500',
medium: 'bg-yellow-500',
low: 'bg-blue-500',
info: 'bg-gray-500',
}
const SEVERITY_BORDER: Record<string, string> = {
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',
}
// Resolve a confidence score from the various possible sources on a finding.
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<string, number> = { 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 }
}
const CONFIDENCE_STYLES: Record<string, string> = {
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 SESSION_KEY = 'neurosploit_autopentest_session'
interface SavedSession {
agentId: string
target: string
startedAt: string
}
export default function AutoPentestPage() {
const navigate = useNavigate()
const [target, setTarget] = useState('')
const [multiTarget, setMultiTarget] = useState(false)
const [targets, setTargets] = useState('')
const [subdomainDiscovery, setSubdomainDiscovery] = useState(false)
const [showAuth, setShowAuth] = useState(false)
const [authType, setAuthType] = useState<string>('')
const [authValue, setAuthValue] = useState('')
const [showPrompt, setShowPrompt] = useState(false)
const [customPrompt, setCustomPrompt] = useState('')
// Running state
const [isRunning, setIsRunning] = useState(false)
const [agentId, setAgentId] = useState<string | null>(null)
const [status, setStatus] = useState<AgentStatus | null>(null)
const [error, setError] = useState<string | null>(null)
// Live findings & logs
const [showFindings, setShowFindings] = useState(true)
const [showLogs, setShowLogs] = useState(false)
const [logs, setLogs] = useState<AgentLog[]>([])
const [expandedFinding, setExpandedFinding] = useState<string | null>(null)
// Report generation
const [generatingReport, setGeneratingReport] = useState(false)
const [reportId, setReportId] = useState<string | null>(null)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const logsEndRef = useRef<HTMLDivElement>(null)
// Restore session on mount
useEffect(() => {
try {
const saved = localStorage.getItem(SESSION_KEY)
if (saved) {
const session: SavedSession = JSON.parse(saved)
setAgentId(session.agentId)
setTarget(session.target)
setIsRunning(true)
}
} catch {
// ignore parse errors
}
}, [])
// Save session when agentId changes
useEffect(() => {
if (agentId && isRunning) {
const session: SavedSession = {
agentId,
target: target || '',
startedAt: new Date().toISOString(),
}
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
}
}, [agentId, isRunning, target])
// Poll agent status + logs
useEffect(() => {
if (!agentId) return
const poll = async () => {
try {
const s = await agentApi.getStatus(agentId)
setStatus(s)
if (s.status === 'completed' || s.status === 'error' || s.status === 'stopped') {
setIsRunning(false)
if (pollRef.current) clearInterval(pollRef.current)
}
} catch {
// ignore polling errors
}
// Fetch recent logs
try {
const logData = await agentApi.getLogs(agentId, 200)
setLogs(logData.logs || [])
} catch {
// ignore
}
}
poll()
pollRef.current = setInterval(poll, 3000)
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [agentId])
// Auto-scroll logs
useEffect(() => {
if (showLogs && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [logs, showLogs])
const handleStart = async () => {
const primaryTarget = target.trim()
if (!primaryTarget) return
setError(null)
setIsRunning(true)
setStatus(null)
setLogs([])
setReportId(null)
try {
const targetList = multiTarget
? targets.split('\n').map(t => t.trim()).filter(Boolean)
: undefined
const resp = await agentApi.autoPentest(primaryTarget, {
subdomain_discovery: subdomainDiscovery,
targets: targetList,
auth_type: authType || undefined,
auth_value: authValue || undefined,
prompt: customPrompt.trim() || undefined,
})
setAgentId(resp.agent_id)
} catch (err: any) {
setError(err?.response?.data?.detail || err?.message || 'Failed to start pentest')
setIsRunning(false)
}
}
const handleStop = async () => {
if (!agentId) return
try {
await agentApi.stop(agentId)
setIsRunning(false)
} catch {
// ignore
}
}
const handleClearSession = () => {
localStorage.removeItem(SESSION_KEY)
setAgentId(null)
setStatus(null)
setLogs([])
setIsRunning(false)
setError(null)
setReportId(null)
setTarget('')
}
const handleGenerateAiReport = useCallback(async () => {
if (!status?.scan_id) return
setGeneratingReport(true)
try {
const report = await reportsApi.generateAiReport({
scan_id: status.scan_id,
title: `AI Pentest Report - ${target}`,
})
setReportId(report.id)
} catch (err: any) {
setError(err?.response?.data?.detail || 'Failed to generate AI report')
} finally {
setGeneratingReport(false)
}
}, [status?.scan_id, target])
const currentPhaseIdx = status ? phaseFromProgress(status.progress) : -1
const findings = status?.findings || []
const rejectedFindings = status?.rejected_findings || []
const allFindings = [...findings, ...rejectedFindings]
const [findingsFilter, setFindingsFilter] = useState<'confirmed' | 'rejected' | 'all'>('all')
const displayFindings = findingsFilter === 'confirmed' ? findings
: findingsFilter === 'rejected' ? rejectedFindings
: allFindings
const sevCounts = findings.reduce(
(acc, f) => {
acc[f.severity] = (acc[f.severity] || 0) + 1
return acc
},
{} as Record<string, number>
)
return (
<div className="min-h-screen flex flex-col items-center py-12 px-4">
{/* Header */}
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-500/20 rounded-2xl mb-4">
<Rocket className="w-8 h-8 text-green-400" />
</div>
<h1 className="text-3xl font-bold text-white mb-2">Auto Pentest</h1>
<p className="text-dark-400 max-w-md">
One-click comprehensive penetration test. 100 vulnerability types, AI-powered analysis, full report.
</p>
</div>
{/* Main Card - only show config when no session is active */}
{!agentId && (
<div className="w-full max-w-2xl bg-dark-800 border border-dark-700 rounded-2xl p-8">
{/* URL Input */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Target URL</label>
<input
type="url"
value={target}
onChange={e => setTarget(e.target.value)}
placeholder="https://example.com"
disabled={isRunning}
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 disabled:opacity-50"
/>
</div>
{/* Toggles */}
<div className="flex flex-wrap gap-4 mb-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={subdomainDiscovery}
onChange={e => setSubdomainDiscovery(e.target.checked)}
disabled={isRunning}
className="w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"
/>
<span className="text-sm text-dark-300">Subdomain Discovery</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={multiTarget}
onChange={e => setMultiTarget(e.target.checked)}
disabled={isRunning}
className="w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"
/>
<span className="text-sm text-dark-300">Multiple Targets</span>
</label>
</div>
{/* Multi-target textarea */}
{multiTarget && (
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Additional Targets (one per line)</label>
<textarea
value={targets}
onChange={e => setTargets(e.target.value)}
rows={4}
disabled={isRunning}
placeholder={"https://api.example.com\nhttps://admin.example.com"}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"
/>
</div>
)}
{/* Auth Section (collapsible) */}
<div className="mb-6">
<button
onClick={() => setShowAuth(!showAuth)}
disabled={isRunning}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50"
>
<Lock className="w-4 h-4" />
<span>Authentication (Optional)</span>
{showAuth ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showAuth && (
<div className="mt-3 space-y-3 pl-6">
<select
value={authType}
onChange={e => setAuthType(e.target.value)}
disabled={isRunning}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm focus:outline-none focus:border-green-500"
>
<option value="">No Authentication</option>
<option value="bearer">Bearer Token</option>
<option value="cookie">Cookie</option>
<option value="basic">Basic Auth (user:pass)</option>
<option value="header">Custom Header (Name:Value)</option>
</select>
{authType && (
<input
type="text"
value={authValue}
onChange={e => setAuthValue(e.target.value)}
disabled={isRunning}
placeholder={
authType === 'bearer' ? 'eyJhbGciOiJIUzI1NiIs...' :
authType === 'cookie' ? 'session=abc123; token=xyz' :
authType === 'basic' ? 'admin:password123' :
'X-API-Key:your-api-key'
}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500"
/>
)}
</div>
)}
</div>
{/* Custom AI Prompt (collapsible) */}
<div className="mb-6">
<button
onClick={() => setShowPrompt(!showPrompt)}
disabled={isRunning}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50"
>
<MessageSquare className="w-4 h-4" />
<span>Custom AI Prompt (Optional)</span>
{showPrompt ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showPrompt && (
<div className="mt-3 pl-6">
<textarea
value={customPrompt}
onChange={e => setCustomPrompt(e.target.value)}
rows={4}
disabled={isRunning}
placeholder={"Focus on authentication bypass and IDOR vulnerabilities.\nThe app uses JWT tokens in localStorage.\nAdmin panel at /admin requires role=admin cookie."}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"
/>
<p className="mt-1 text-xs text-dark-500">
Guide the AI agent with additional context, focus areas, or specific instructions.
</p>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-400" />
<span className="text-red-400 text-sm">{error}</span>
</div>
)}
{/* Start Button */}
<button
onClick={handleStart}
disabled={!target.trim()}
className="w-full py-4 bg-green-500 hover:bg-green-600 disabled:bg-dark-600 disabled:text-dark-400 text-white font-bold text-lg rounded-xl transition-colors flex items-center justify-center gap-3"
>
<Rocket className="w-6 h-6" />
START PENTEST
</button>
</div>
)}
{/* Active Session View */}
{agentId && (
<div className="w-full max-w-4xl">
{/* Session Header */}
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${isRunning ? 'bg-green-500 animate-pulse' : status?.status === 'completed' ? 'bg-green-500' : status?.status === 'error' ? 'bg-red-500' : 'bg-gray-500'}`} />
<h3 className="text-white font-semibold">
{isRunning ? 'Pentest Running' : status?.status === 'completed' ? 'Pentest Complete' : status?.status === 'error' ? 'Pentest Failed' : 'Pentest Stopped'}
</h3>
<span className="text-dark-400 text-sm">{target}</span>
</div>
<div className="flex items-center gap-2">
{isRunning && (
<button
onClick={handleStop}
className="px-4 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm transition-colors flex items-center gap-1.5"
>
<X className="w-4 h-4" /> Stop
</button>
)}
{!isRunning && (
<button
onClick={handleClearSession}
className="px-4 py-1.5 bg-dark-700 hover:bg-dark-600 text-dark-300 rounded-lg text-sm transition-colors flex items-center gap-1.5"
>
<Trash2 className="w-4 h-4" /> Clear Session
</button>
)}
</div>
</div>
{/* Progress Bar */}
{status && (
<>
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-dark-400">{status.phase || 'Initializing...'}</span>
<span className="text-dark-400">{status.progress}%</span>
</div>
<div className="w-full bg-dark-900 rounded-full h-2 mb-4">
<div
className="bg-green-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${status.progress}%` }}
/>
</div>
{/* Phase Indicators */}
<div className="grid grid-cols-3 gap-3">
{PHASES.map((phase, idx) => {
const Icon = phase.icon
const isActive = idx === currentPhaseIdx && isRunning
const isDone = idx < currentPhaseIdx || status.status === 'completed'
return (
<div
key={phase.key}
className={`rounded-xl p-3 border transition-all duration-300 ${
isActive ? 'bg-green-500/10 border-green-500/30' :
isDone ? 'bg-dark-700/50 border-dark-600' :
'bg-dark-900 border-dark-700'
}`}
>
<div className="flex items-center gap-2 mb-1">
{isActive ? (
<Loader2 className="w-4 h-4 animate-spin text-green-400 flex-shrink-0" />
) : isDone ? (
<CheckCircle2 className="w-4 h-4 text-green-500 flex-shrink-0" />
) : (
<Icon className={`w-4 h-4 flex-shrink-0 ${isActive ? 'text-green-400' : 'text-dark-500'}`} />
)}
<span className={`text-xs font-medium ${
isActive ? 'text-green-400' :
isDone ? 'text-dark-300' :
'text-dark-500'
}`}>
{phase.label}
</span>
<span className={`ml-auto text-[10px] ${
isActive ? 'text-green-500/60' :
isDone ? 'text-dark-500' :
'text-dark-600'
}`}>
{phase.range[0]}-{phase.range[1]}%
</span>
</div>
{/* Stream badges for the parallel phase */}
{idx === 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{STREAMS.map(stream => (
<StreamBadge
key={stream.key}
stream={stream}
progress={status.progress}
isRunning={isRunning}
/>
))}
</div>
)}
</div>
)
})}
</div>
</>
)}
</div>
{/* Severity Summary + Tabs */}
{findings.length > 0 && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<h3 className="text-white font-semibold">Findings ({findings.length})</h3>
<div className="flex gap-2">
{['critical', 'high', 'medium', 'low', 'info'].map(sev => {
const count = sevCounts[sev] || 0
if (count === 0) return null
return (
<span key={sev} className={`${SEVERITY_COLORS[sev]} text-white px-2 py-0.5 rounded-full text-xs font-bold`}>
{count}
</span>
)
})}
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => { setShowFindings(true); setShowLogs(false) }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showFindings ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<Bug className="w-3 h-3 inline mr-1" />Findings ({findings.length})
</button>
{rejectedFindings.length > 0 && (
<button
onClick={() => { setShowFindings(true); setShowLogs(false); setFindingsFilter(findingsFilter === 'rejected' ? 'all' : 'rejected') }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${findingsFilter === 'rejected' && showFindings ? 'bg-orange-500/30 text-orange-400' : 'bg-dark-700 text-orange-400/60 hover:text-orange-400'}`}
>
<AlertTriangle className="w-3 h-3 inline mr-1" />Rejected ({rejectedFindings.length})
</button>
)}
<button
onClick={() => { setShowLogs(true); setShowFindings(false) }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showLogs ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<ScrollText className="w-3 h-3 inline mr-1" />Activity Log
</button>
</div>
</div>
{/* Findings Filter Tabs */}
{showFindings && allFindings.length > 0 && rejectedFindings.length > 0 && (
<div className="flex gap-1 mb-2">
{(['all', 'confirmed', 'rejected'] as const).map(f => (
<button
key={f}
onClick={() => setFindingsFilter(f)}
className={`px-2 py-0.5 rounded-full text-xs transition-colors ${
findingsFilter === f ? 'bg-primary-500/20 text-primary-400' : 'text-dark-500 hover:text-dark-300'
}`}
>
{f === 'all' ? `All (${allFindings.length})` : f === 'confirmed' ? `Confirmed (${findings.length})` : `Rejected (${rejectedFindings.length})`}
</button>
))}
</div>
)}
{/* Findings List */}
{showFindings && (
<div className="space-y-2 max-h-[500px] overflow-y-auto pr-1">
{displayFindings.map((f: AgentFinding) => (
<div
key={f.id}
className={`border ${f.ai_status === 'rejected' ? 'border-orange-500/30 opacity-70' : (SEVERITY_BORDER[f.severity] || 'border-dark-600')} rounded-xl bg-dark-900 overflow-hidden`}
>
<button
onClick={() => setExpandedFinding(expandedFinding === f.id ? null : f.id)}
className="w-full flex items-center gap-3 p-3 text-left hover:bg-dark-800/50 transition-colors"
>
<span className={`${SEVERITY_COLORS[f.severity]} text-white px-2 py-0.5 rounded text-xs font-bold uppercase flex-shrink-0`}>
{f.severity}
</span>
<span className={`text-sm flex-1 truncate ${f.ai_status === 'rejected' ? 'text-dark-400' : 'text-white'}`}>{f.title}</span>
{f.ai_status === 'rejected' && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 flex-shrink-0">Rejected</span>
)}
{(() => {
const conf = getConfidenceDisplay(f)
if (!conf) return null
return (
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded-full border flex-shrink-0 ${CONFIDENCE_STYLES[conf.color]}`}>
{conf.score}/100
</span>
)
})()}
<span className="text-dark-500 text-xs flex-shrink-0">{f.vulnerability_type}</span>
{expandedFinding === f.id ? <ChevronUp className="w-4 h-4 text-dark-400" /> : <ChevronDown className="w-4 h-4 text-dark-400" />}
</button>
{expandedFinding === f.id && (
<div className="px-3 pb-3 space-y-2 border-t border-dark-700 pt-2">
<div className="grid grid-cols-2 gap-2 text-xs">
{f.affected_endpoint && (
<div>
<span className="text-dark-500">Endpoint: </span>
<span className="text-dark-300 break-all">{f.affected_endpoint}</span>
</div>
)}
{f.parameter && (
<div>
<span className="text-dark-500">Parameter: </span>
<span className="text-dark-300">{f.parameter}</span>
</div>
)}
{f.cwe_id && (
<div>
<span className="text-dark-500">CWE: </span>
<span className="text-dark-300">{f.cwe_id}</span>
</div>
)}
{f.cvss_score > 0 && (
<div>
<span className="text-dark-500">CVSS: </span>
<span className="text-dark-300">{f.cvss_score}</span>
</div>
)}
</div>
{f.description && (
<p className="text-dark-400 text-xs">{f.description.substring(0, 300)}{f.description.length > 300 ? '...' : ''}</p>
)}
{f.payload && (
<div className="bg-dark-800 rounded-lg p-2">
<span className="text-dark-500 text-xs">Payload: </span>
<code className="text-green-400 text-xs break-all">{f.payload.substring(0, 200)}</code>
</div>
)}
{f.evidence && (
<div className="bg-dark-800 rounded-lg p-2">
<span className="text-dark-500 text-xs">Evidence: </span>
<span className="text-dark-300 text-xs">{f.evidence.substring(0, 300)}</span>
</div>
)}
{f.poc_code && (
<div className="mt-2">
<p className="text-xs font-medium text-dark-400 mb-1">PoC Code</p>
<pre className="p-2 bg-dark-950 rounded text-xs text-green-400 overflow-x-auto max-h-[300px] overflow-y-auto whitespace-pre-wrap">{f.poc_code}</pre>
</div>
)}
{f.ai_status === 'rejected' && f.rejection_reason && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-2">
<span className="text-orange-400 text-xs font-medium">Rejection: </span>
<span className="text-orange-300/80 text-xs">{f.rejection_reason}</span>
</div>
)}
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs ${f.ai_status === 'rejected' ? 'text-orange-400' : f.ai_verified ? 'text-green-400' : 'text-dark-500'}`}>
{f.ai_status === 'rejected' ? 'AI Rejected' : f.ai_verified ? 'AI Verified' : 'Tool Detected'}
</span>
{(() => {
const conf = getConfidenceDisplay(f)
if (!conf) return null
return (
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded-full border ${CONFIDENCE_STYLES[conf.color]}`}>
Confidence: {conf.score}/100 ({conf.label})
</span>
)
})()}
</div>
{(() => {
const hasBreakdown = f.confidence_breakdown && Object.keys(f.confidence_breakdown).length > 0
const hasProof = !!f.proof_of_execution
const hasControls = !!f.negative_controls
if (!hasBreakdown && !hasProof && !hasControls) return null
return (
<div className="bg-dark-800 rounded-lg p-2 space-y-1">
{hasBreakdown && (
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5 text-xs text-dark-400">
{Object.entries(f.confidence_breakdown!).map(([key, val]) => (
<div key={key} className="flex justify-between">
<span className="capitalize">{key.replace(/_/g, ' ')}</span>
<span className={`font-mono font-medium ${
Number(val) > 0 ? 'text-green-400' : Number(val) < 0 ? 'text-red-400' : 'text-dark-500'
}`}>
{Number(val) > 0 ? '+' : ''}{val}
</span>
</div>
))}
</div>
)}
{hasProof && (
<p className="text-xs text-dark-400 flex items-start gap-1">
<span className="text-green-400">&#10003;</span>
<span>Proof: <span className="text-dark-300">{f.proof_of_execution}</span></span>
</p>
)}
{hasControls && (
<p className="text-xs text-dark-400 flex items-start gap-1">
<span className="text-blue-400">&#9632;</span>
<span>Controls: <span className="text-dark-300">{f.negative_controls}</span></span>
</p>
)}
</div>
)
})()}
</div>
)}
</div>
))}
</div>
)}
{/* Activity Log */}
{showLogs && (
<div className="bg-dark-900 rounded-xl p-3 max-h-[500px] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<p className="text-dark-500 text-center py-4">No logs yet...</p>
) : (
logs.map((log, i) => (
<div key={i} className="flex gap-2 py-0.5">
<span className="text-dark-600 flex-shrink-0">{log.time || ''}</span>
<span className={`flex-shrink-0 uppercase w-12 ${
log.level === 'error' ? 'text-red-400' :
log.level === 'warning' ? 'text-yellow-400' :
log.level === 'info' ? 'text-blue-400' :
'text-dark-500'
}`}>{log.level}</span>
<span className={logMessageColor(log.message) || (log.source === 'llm' ? 'text-purple-400' : 'text-dark-300')}>
{log.message}
</span>
</div>
))
)}
<div ref={logsEndRef} />
</div>
)}
</div>
)}
{/* No findings yet but running */}
{findings.length === 0 && isRunning && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-semibold">Activity</h3>
<button
onClick={() => setShowLogs(!showLogs)}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showLogs ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<ScrollText className="w-3 h-3 inline mr-1" />{showLogs ? 'Hide' : 'Show'} Logs
</button>
</div>
{showLogs ? (
<div className="bg-dark-900 rounded-xl p-3 max-h-[400px] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<div className="flex items-center justify-center py-4 text-dark-500">
<Loader2 className="w-4 h-4 animate-spin mr-2" /> Waiting for logs...
</div>
) : (
logs.map((log, i) => (
<div key={i} className="flex gap-2 py-0.5">
<span className="text-dark-600 flex-shrink-0">{log.time || ''}</span>
<span className={`flex-shrink-0 uppercase w-12 ${
log.level === 'error' ? 'text-red-400' :
log.level === 'warning' ? 'text-yellow-400' :
log.level === 'info' ? 'text-blue-400' :
'text-dark-500'
}`}>{log.level}</span>
<span className={logMessageColor(log.message) || (log.source === 'llm' ? 'text-purple-400' : 'text-dark-300')}>
{log.message}
</span>
</div>
))
)}
<div ref={logsEndRef} />
</div>
) : (
<div className="flex items-center justify-center py-6 text-dark-500">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
Scanning in progress... Findings will appear here as they are discovered.
</div>
)}
</div>
)}
{/* Completion Actions */}
{status?.status === 'completed' && (
<div className="bg-dark-800 border border-green-500/30 rounded-2xl p-6 mb-6">
<div className="flex items-center gap-3 mb-4">
<CheckCircle2 className="w-6 h-6 text-green-500" />
<h3 className="text-green-400 font-semibold text-lg">Pentest Complete</h3>
</div>
<p className="text-dark-400 mb-4">
Found {findings.length} vulnerabilities across {target}.
</p>
{/* Report Generation */}
{generatingReport && (
<div className="mb-4 p-4 bg-purple-500/10 border border-purple-500/20 rounded-xl flex items-center gap-3">
<Loader2 className="w-5 h-5 text-purple-400 animate-spin" />
<div>
<p className="text-purple-400 font-medium text-sm">Generating AI Report...</p>
<p className="text-dark-400 text-xs">The AI is analyzing findings and writing the executive summary.</p>
</div>
</div>
)}
<div className="flex flex-wrap gap-3">
<button
onClick={() => navigate(`/agent/${agentId}`)}
className="px-5 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<ExternalLink className="w-4 h-4" /> View Full Results
</button>
{!reportId ? (
<button
onClick={handleGenerateAiReport}
disabled={generatingReport || !status.scan_id}
className="px-5 py-2 bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<Sparkles className="w-4 h-4" /> Generate AI Report
</button>
) : (
<>
<a
href={reportsApi.getViewUrl(reportId)}
target="_blank"
rel="noopener noreferrer"
className="px-5 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<FileText className="w-4 h-4" /> View Report
</a>
<a
href={reportsApi.getDownloadZipUrl(reportId)}
className="px-5 py-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<Download className="w-4 h-4" /> Download ZIP
</a>
</>
)}
</div>
</div>
)}
{/* Error state */}
{status?.status === 'error' && (
<div className="bg-dark-800 border border-red-500/30 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-2">
<AlertTriangle className="w-6 h-6 text-red-400" />
<h3 className="text-red-400 font-semibold">Pentest Failed</h3>
</div>
<p className="text-dark-400">{status.error || 'An unexpected error occurred.'}</p>
</div>
)}
</div>
)}
</div>
)
}