mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-02-12 14:02:45 +00:00
821 lines
32 KiB
TypeScript
821 lines
32 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
import {
|
|
Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight,
|
|
ExternalLink, Copy, Shield, AlertTriangle, Cpu, CheckCircle, XCircle, Clock
|
|
} from 'lucide-react'
|
|
import Card from '../components/common/Card'
|
|
import Button from '../components/common/Button'
|
|
import { SeverityBadge } from '../components/common/Badge'
|
|
import { scansApi, reportsApi, agentTasksApi } from '../services/api'
|
|
import { wsService } from '../services/websocket'
|
|
import { useScanStore } from '../store'
|
|
import type { Endpoint, Vulnerability, WSMessage, ScanAgentTask, Report } from '../types'
|
|
|
|
export default function ScanDetailsPage() {
|
|
const { scanId } = useParams<{ scanId: string }>()
|
|
const navigate = useNavigate()
|
|
const {
|
|
currentScan, endpoints, vulnerabilities, logs, agentTasks,
|
|
setCurrentScan, setEndpoints, setVulnerabilities,
|
|
addEndpoint, addVulnerability, addLog, updateScan,
|
|
addAgentTask, updateAgentTask, setAgentTasks,
|
|
loadScanData, saveScanData, getVulnCounts
|
|
} = useScanStore()
|
|
|
|
const [isGeneratingReport, setIsGeneratingReport] = useState(false)
|
|
const [expandedVulns, setExpandedVulns] = useState<Set<string>>(new Set())
|
|
const [activeTab, setActiveTab] = useState<'vulns' | 'endpoints' | 'tasks'>('vulns')
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [autoGeneratedReport, setAutoGeneratedReport] = useState<Report | null>(null)
|
|
|
|
// Calculate vulnerability counts from actual data
|
|
const vulnCounts = useMemo(() => getVulnCounts(), [vulnerabilities])
|
|
|
|
useEffect(() => {
|
|
if (!scanId) return
|
|
|
|
// Try to load cached data first
|
|
loadScanData(scanId)
|
|
|
|
// Fetch initial data from API
|
|
const fetchData = async () => {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
try {
|
|
const scan = await scansApi.get(scanId)
|
|
setCurrentScan(scan)
|
|
|
|
const [endpointsData, vulnsData, tasksData, reportsData] = await Promise.all([
|
|
scansApi.getEndpoints(scanId),
|
|
scansApi.getVulnerabilities(scanId),
|
|
agentTasksApi.list(scanId).catch(() => ({ tasks: [] })),
|
|
reportsApi.list({ scanId, autoGenerated: true }).catch(() => ({ reports: [] }))
|
|
])
|
|
|
|
// Only set if we have data from API
|
|
if (endpointsData.endpoints?.length > 0) {
|
|
setEndpoints(endpointsData.endpoints)
|
|
}
|
|
if (vulnsData.vulnerabilities?.length > 0) {
|
|
setVulnerabilities(vulnsData.vulnerabilities)
|
|
}
|
|
if (tasksData.tasks?.length > 0) {
|
|
setAgentTasks(tasksData.tasks)
|
|
}
|
|
// Set auto-generated report if exists
|
|
if (reportsData.reports?.length > 0) {
|
|
setAutoGeneratedReport(reportsData.reports[0])
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Failed to fetch scan:', err)
|
|
setError(err?.response?.data?.detail || 'Failed to load scan')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
fetchData()
|
|
|
|
// Poll for updates while scan is running
|
|
const pollInterval = setInterval(async () => {
|
|
if (currentScan?.status === 'running' || !currentScan) {
|
|
try {
|
|
const scan = await scansApi.get(scanId)
|
|
setCurrentScan(scan)
|
|
|
|
const [endpointsData, vulnsData, tasksData] = await Promise.all([
|
|
scansApi.getEndpoints(scanId),
|
|
scansApi.getVulnerabilities(scanId),
|
|
agentTasksApi.list(scanId).catch(() => ({ tasks: [] }))
|
|
])
|
|
|
|
if (endpointsData.endpoints?.length > 0) {
|
|
setEndpoints(endpointsData.endpoints)
|
|
}
|
|
if (vulnsData.vulnerabilities?.length > 0) {
|
|
setVulnerabilities(vulnsData.vulnerabilities)
|
|
}
|
|
if (tasksData.tasks?.length > 0) {
|
|
setAgentTasks(tasksData.tasks)
|
|
}
|
|
} catch (err) {
|
|
console.error('Poll error:', err)
|
|
}
|
|
}
|
|
}, 3000)
|
|
|
|
// Connect WebSocket for running scans
|
|
wsService.connect(scanId)
|
|
|
|
// Subscribe to events
|
|
const unsubscribe = wsService.subscribe('*', (message: WSMessage) => {
|
|
switch (message.type) {
|
|
case 'progress_update':
|
|
updateScan(scanId, {
|
|
progress: message.progress as number,
|
|
current_phase: message.message as string
|
|
})
|
|
break
|
|
case 'phase_change':
|
|
updateScan(scanId, { current_phase: message.phase as string })
|
|
addLog('info', `Phase: ${message.phase}`)
|
|
break
|
|
case 'endpoint_found':
|
|
addEndpoint(message.endpoint as Endpoint)
|
|
break
|
|
case 'vuln_found':
|
|
addVulnerability(message.vulnerability as Vulnerability)
|
|
addLog('warning', `Found: ${(message.vulnerability as Vulnerability).title}`)
|
|
break
|
|
case 'stats_update':
|
|
// Real-time stats update from backend
|
|
if (message.stats) {
|
|
const stats = message.stats as {
|
|
total_vulnerabilities?: number
|
|
critical?: number
|
|
high?: number
|
|
medium?: number
|
|
low?: number
|
|
info?: number
|
|
total_endpoints?: number
|
|
}
|
|
updateScan(scanId, {
|
|
total_vulnerabilities: stats.total_vulnerabilities,
|
|
critical_count: stats.critical,
|
|
high_count: stats.high,
|
|
medium_count: stats.medium,
|
|
low_count: stats.low,
|
|
info_count: stats.info,
|
|
total_endpoints: stats.total_endpoints
|
|
})
|
|
}
|
|
break
|
|
case 'log_message':
|
|
addLog(message.level as string, message.message as string)
|
|
break
|
|
case 'scan_completed':
|
|
updateScan(scanId, { status: 'completed', progress: 100 })
|
|
addLog('info', 'Scan completed')
|
|
// Save data when scan completes
|
|
saveScanData(scanId)
|
|
break
|
|
case 'scan_stopped':
|
|
// Handle scan stopped by user
|
|
if (message.summary) {
|
|
const summary = message.summary as {
|
|
total_vulnerabilities?: number
|
|
critical?: number
|
|
high?: number
|
|
medium?: number
|
|
low?: number
|
|
info?: number
|
|
total_endpoints?: number
|
|
duration?: number
|
|
progress?: number
|
|
}
|
|
updateScan(scanId, {
|
|
status: 'stopped',
|
|
progress: summary.progress || currentScan?.progress,
|
|
total_vulnerabilities: summary.total_vulnerabilities,
|
|
critical_count: summary.critical,
|
|
high_count: summary.high,
|
|
medium_count: summary.medium,
|
|
low_count: summary.low,
|
|
info_count: summary.info,
|
|
total_endpoints: summary.total_endpoints,
|
|
duration: summary.duration
|
|
})
|
|
} else {
|
|
updateScan(scanId, { status: 'stopped' })
|
|
}
|
|
addLog('warning', 'Scan stopped by user')
|
|
saveScanData(scanId)
|
|
break
|
|
case 'scan_failed':
|
|
updateScan(scanId, { status: 'failed' })
|
|
addLog('error', `Scan failed: ${message.error || 'Unknown error'}`)
|
|
saveScanData(scanId)
|
|
break
|
|
case 'agent_task':
|
|
case 'agent_task_started':
|
|
// Handle new or updated agent task
|
|
if (message.task) {
|
|
addAgentTask(message.task as ScanAgentTask)
|
|
}
|
|
break
|
|
case 'agent_task_completed':
|
|
// Handle completed agent task
|
|
if (message.task) {
|
|
const task = message.task as ScanAgentTask
|
|
updateAgentTask(task.id, task)
|
|
}
|
|
break
|
|
case 'report_generated':
|
|
// Handle auto-generated report
|
|
if (message.report) {
|
|
const report = message.report as Report
|
|
setAutoGeneratedReport(report)
|
|
addLog('info', `Report generated: ${report.title}`)
|
|
}
|
|
break
|
|
case 'error':
|
|
addLog('error', message.error as string)
|
|
break
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
// Save data before unmounting
|
|
saveScanData(scanId)
|
|
unsubscribe()
|
|
wsService.disconnect()
|
|
clearInterval(pollInterval)
|
|
}
|
|
}, [scanId])
|
|
|
|
const handleStopScan = async () => {
|
|
if (!scanId) return
|
|
try {
|
|
await scansApi.stop(scanId)
|
|
updateScan(scanId, { status: 'stopped' })
|
|
saveScanData(scanId)
|
|
} catch (error) {
|
|
console.error('Failed to stop scan:', error)
|
|
}
|
|
}
|
|
|
|
const handleGenerateReport = async () => {
|
|
if (!scanId) return
|
|
setIsGeneratingReport(true)
|
|
try {
|
|
const report = await reportsApi.generate({
|
|
scan_id: scanId,
|
|
format: 'html',
|
|
include_poc: true,
|
|
include_remediation: true
|
|
})
|
|
window.open(reportsApi.getViewUrl(report.id), '_blank')
|
|
} catch (error) {
|
|
console.error('Failed to generate report:', error)
|
|
} finally {
|
|
setIsGeneratingReport(false)
|
|
}
|
|
}
|
|
|
|
const toggleVuln = (id: string) => {
|
|
const newExpanded = new Set(expandedVulns)
|
|
if (newExpanded.has(id)) {
|
|
newExpanded.delete(id)
|
|
} else {
|
|
newExpanded.add(id)
|
|
}
|
|
setExpandedVulns(newExpanded)
|
|
}
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<RefreshCw className="w-8 h-8 animate-spin text-primary-500" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-64">
|
|
<AlertTriangle className="w-12 h-12 text-red-500 mb-4" />
|
|
<p className="text-xl text-white mb-2">Failed to load scan</p>
|
|
<p className="text-dark-400 mb-4">{error}</p>
|
|
<Button onClick={() => navigate('/')}>Go to Dashboard</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!currentScan) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-64">
|
|
<AlertTriangle className="w-12 h-12 text-yellow-500 mb-4" />
|
|
<p className="text-xl text-white mb-2">Scan not found</p>
|
|
<p className="text-dark-400 mb-4">The scan may still be initializing or does not exist.</p>
|
|
<div className="flex gap-2">
|
|
<Button onClick={() => window.location.reload()}>Refresh</Button>
|
|
<Button variant="secondary" onClick={() => navigate('/')}>Go to Dashboard</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fadeIn">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
|
<Shield className="w-6 h-6 text-primary-500" />
|
|
{currentScan.name || 'Unnamed Scan'}
|
|
</h2>
|
|
<div className="flex items-center gap-3 mt-2">
|
|
<SeverityBadge severity={currentScan.status} />
|
|
<span className="text-dark-400">
|
|
Started {new Date(currentScan.created_at).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{currentScan.status === 'running' && (
|
|
<Button variant="danger" onClick={handleStopScan}>
|
|
<StopCircle className="w-4 h-4 mr-2" />
|
|
Stop Scan
|
|
</Button>
|
|
)}
|
|
{autoGeneratedReport && (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => window.open(reportsApi.getViewUrl(autoGeneratedReport.id), '_blank')}
|
|
>
|
|
<FileText className="w-4 h-4 mr-2" />
|
|
View Report
|
|
</Button>
|
|
)}
|
|
{(currentScan.status === 'completed' || currentScan.status === 'stopped') && (
|
|
<Button onClick={handleGenerateReport} isLoading={isGeneratingReport}>
|
|
<FileText className="w-4 h-4 mr-2" />
|
|
{autoGeneratedReport ? 'New Report' : 'Generate Report'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
{currentScan.status === 'running' && (
|
|
<Card>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-dark-300">{currentScan.current_phase || 'Initializing...'}</span>
|
|
<span className="text-white font-medium">{currentScan.progress}%</span>
|
|
</div>
|
|
<div className="h-2 bg-dark-900 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-primary-500 rounded-full transition-all duration-300"
|
|
style={{ width: `${currentScan.progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Auto-generated Report Notification */}
|
|
{autoGeneratedReport && (
|
|
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-green-500/20 rounded-full p-2">
|
|
<FileText className="w-5 h-5 text-green-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-white font-medium">
|
|
{autoGeneratedReport.is_partial ? 'Partial Report Generated' : 'Report Generated'}
|
|
</p>
|
|
<p className="text-sm text-dark-400">
|
|
{autoGeneratedReport.title || 'Scan report is ready to view'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={() => window.open(reportsApi.getViewUrl(autoGeneratedReport.id), '_blank')}
|
|
>
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
View Report
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setAutoGeneratedReport(null)}
|
|
>
|
|
Dismiss
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-white">{endpoints.length}</p>
|
|
<p className="text-sm text-dark-400">Endpoints</p>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-white">{vulnerabilities.length}</p>
|
|
<p className="text-sm text-dark-400">Total Vulns</p>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-red-500">{vulnCounts.critical}</p>
|
|
<p className="text-sm text-dark-400">Critical</p>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-orange-500">{vulnCounts.high}</p>
|
|
<p className="text-sm text-dark-400">High</p>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-yellow-500">{vulnCounts.medium}</p>
|
|
<p className="text-sm text-dark-400">Medium</p>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-blue-500">{vulnCounts.low}</p>
|
|
<p className="text-sm text-dark-400">Low</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-2 border-b border-dark-700 pb-2">
|
|
<Button
|
|
variant={activeTab === 'vulns' ? 'primary' : 'ghost'}
|
|
onClick={() => setActiveTab('vulns')}
|
|
>
|
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
|
Vulnerabilities ({vulnerabilities.length})
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === 'endpoints' ? 'primary' : 'ghost'}
|
|
onClick={() => setActiveTab('endpoints')}
|
|
>
|
|
<Globe className="w-4 h-4 mr-2" />
|
|
Endpoints ({endpoints.length})
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === 'tasks' ? 'primary' : 'ghost'}
|
|
onClick={() => setActiveTab('tasks')}
|
|
>
|
|
<Cpu className="w-4 h-4 mr-2" />
|
|
Agent Tasks ({agentTasks.length})
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Vulnerabilities Tab */}
|
|
{activeTab === 'vulns' && (
|
|
<div className="space-y-3">
|
|
{vulnerabilities.length === 0 ? (
|
|
<Card>
|
|
<p className="text-dark-400 text-center py-8">
|
|
{currentScan.status === 'running' ? 'Scanning for vulnerabilities...' : 'No vulnerabilities found'}
|
|
</p>
|
|
</Card>
|
|
) : (
|
|
vulnerabilities.map((vuln, idx) => (
|
|
<div
|
|
key={vuln.id || `vuln-${idx}`}
|
|
className="bg-dark-800 rounded-lg border border-dark-700 overflow-hidden"
|
|
>
|
|
{/* Vulnerability Header */}
|
|
<div
|
|
className="p-4 cursor-pointer hover:bg-dark-750 transition-colors"
|
|
onClick={() => toggleVuln(vuln.id || `vuln-${idx}`)}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-start gap-2 flex-1">
|
|
{expandedVulns.has(vuln.id || `vuln-${idx}`) ? (
|
|
<ChevronDown className="w-4 h-4 mt-1 text-dark-400" />
|
|
) : (
|
|
<ChevronRight className="w-4 h-4 mt-1 text-dark-400" />
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-white">{vuln.title}</p>
|
|
<p className="text-sm text-dark-400 truncate mt-1">{vuln.affected_endpoint}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{vuln.cvss_score && (
|
|
<span className={`text-sm font-bold px-2 py-0.5 rounded ${
|
|
vuln.cvss_score >= 9 ? 'bg-red-500/20 text-red-400' :
|
|
vuln.cvss_score >= 7 ? 'bg-orange-500/20 text-orange-400' :
|
|
vuln.cvss_score >= 4 ? 'bg-yellow-500/20 text-yellow-400' :
|
|
'bg-blue-500/20 text-blue-400'
|
|
}`}>
|
|
CVSS {vuln.cvss_score.toFixed(1)}
|
|
</span>
|
|
)}
|
|
<SeverityBadge severity={vuln.severity} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vulnerability Details */}
|
|
{expandedVulns.has(vuln.id || `vuln-${idx}`) && (
|
|
<div className="p-4 pt-0 space-y-4 border-t border-dark-700">
|
|
{/* Meta Info */}
|
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
|
{vuln.vulnerability_type && (
|
|
<span className="text-dark-400">
|
|
Type: <span className="text-white">{vuln.vulnerability_type}</span>
|
|
</span>
|
|
)}
|
|
{vuln.cwe_id && (
|
|
<a
|
|
href={`https://cwe.mitre.org/data/definitions/${vuln.cwe_id.replace('CWE-', '')}.html`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary-400 hover:underline flex items-center gap-1"
|
|
>
|
|
{vuln.cwe_id}
|
|
<ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
)}
|
|
{vuln.cvss_vector && (
|
|
<span className="text-xs bg-dark-700 px-2 py-1 rounded font-mono text-dark-300">
|
|
{vuln.cvss_vector}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{vuln.description && (
|
|
<div>
|
|
<p className="text-sm font-medium text-dark-300 mb-1">Description</p>
|
|
<p className="text-sm text-dark-400">{vuln.description}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Impact */}
|
|
{vuln.impact && (
|
|
<div>
|
|
<p className="text-sm font-medium text-dark-300 mb-1">Impact</p>
|
|
<p className="text-sm text-dark-400">{vuln.impact}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Proof of Concept */}
|
|
{(vuln.poc_request || vuln.poc_payload) && (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<p className="text-sm font-medium text-dark-300">Proof of Concept</p>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => copyToClipboard(vuln.poc_request || vuln.poc_payload || '')}
|
|
>
|
|
<Copy className="w-3 h-3 mr-1" />
|
|
Copy
|
|
</Button>
|
|
</div>
|
|
{vuln.poc_payload && (
|
|
<div className="mb-2">
|
|
<p className="text-xs text-dark-500 mb-1">Payload:</p>
|
|
<pre className="text-xs bg-dark-900 p-3 rounded overflow-x-auto text-yellow-400 font-mono">
|
|
{vuln.poc_payload}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
{vuln.poc_request && (
|
|
<div>
|
|
<p className="text-xs text-dark-500 mb-1">Request:</p>
|
|
<pre className="text-xs bg-dark-900 p-3 rounded overflow-x-auto text-dark-300 font-mono">
|
|
{vuln.poc_request}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
{vuln.poc_response && (
|
|
<div className="mt-2">
|
|
<p className="text-xs text-dark-500 mb-1">Response:</p>
|
|
<pre className="text-xs bg-dark-900 p-3 rounded overflow-x-auto text-dark-300 font-mono max-h-40">
|
|
{vuln.poc_response}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Remediation */}
|
|
{vuln.remediation && (
|
|
<div>
|
|
<p className="text-sm font-medium text-green-400 mb-1">Remediation</p>
|
|
<p className="text-sm text-dark-400">{vuln.remediation}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* AI Analysis */}
|
|
{vuln.ai_analysis && (
|
|
<div>
|
|
<p className="text-sm font-medium text-purple-400 mb-1">AI Analysis</p>
|
|
<p className="text-sm text-dark-400 whitespace-pre-wrap">{vuln.ai_analysis}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* References */}
|
|
{vuln.references?.length > 0 && (
|
|
<div>
|
|
<p className="text-sm font-medium text-dark-300 mb-1">References</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{vuln.references.map((ref, i) => (
|
|
<a
|
|
key={i}
|
|
href={ref}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-primary-400 hover:underline flex items-center gap-1"
|
|
>
|
|
{(() => {
|
|
try {
|
|
return new URL(ref).hostname
|
|
} catch {
|
|
return ref
|
|
}
|
|
})()}
|
|
<ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Endpoints Tab */}
|
|
{activeTab === 'endpoints' && (
|
|
<Card title="Discovered Endpoints" subtitle={`${endpoints.length} endpoints found`}>
|
|
<div className="space-y-2 max-h-[500px] overflow-auto">
|
|
{endpoints.length === 0 ? (
|
|
<p className="text-dark-400 text-center py-8">No endpoints discovered yet</p>
|
|
) : (
|
|
endpoints.map((endpoint, idx) => (
|
|
<div
|
|
key={endpoint.id || `endpoint-${idx}`}
|
|
className="flex items-center gap-3 p-3 bg-dark-900/50 rounded-lg hover:bg-dark-900 transition-colors"
|
|
>
|
|
<Globe className="w-4 h-4 text-dark-400 flex-shrink-0" />
|
|
<span className={`text-xs px-2 py-0.5 rounded font-medium ${
|
|
endpoint.method === 'GET' ? 'bg-green-500/20 text-green-400' :
|
|
endpoint.method === 'POST' ? 'bg-blue-500/20 text-blue-400' :
|
|
endpoint.method === 'PUT' ? 'bg-yellow-500/20 text-yellow-400' :
|
|
endpoint.method === 'DELETE' ? 'bg-red-500/20 text-red-400' :
|
|
'bg-dark-700 text-dark-300'
|
|
}`}>
|
|
{endpoint.method}
|
|
</span>
|
|
<span className="text-sm text-dark-200 truncate flex-1 font-mono">
|
|
{endpoint.path || endpoint.url}
|
|
</span>
|
|
{endpoint.parameters?.length > 0 && (
|
|
<span className="text-xs text-dark-500">
|
|
{endpoint.parameters.length} params
|
|
</span>
|
|
)}
|
|
{endpoint.content_type && (
|
|
<span className="text-xs text-dark-500">{endpoint.content_type}</span>
|
|
)}
|
|
{endpoint.response_status && (
|
|
<span className={`text-xs font-medium ${
|
|
endpoint.response_status < 300 ? 'text-green-400' :
|
|
endpoint.response_status < 400 ? 'text-yellow-400' :
|
|
'text-red-400'
|
|
}`}>
|
|
{endpoint.response_status}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Agent Tasks Tab */}
|
|
{activeTab === 'tasks' && (
|
|
<Card title="Agent Tasks" subtitle={`${agentTasks.length} tasks executed`}>
|
|
<div className="space-y-3 max-h-[500px] overflow-auto">
|
|
{agentTasks.length === 0 ? (
|
|
<p className="text-dark-400 text-center py-8">
|
|
{currentScan.status === 'running' ? 'Agent tasks will appear here...' : 'No agent tasks recorded'}
|
|
</p>
|
|
) : (
|
|
agentTasks.map((task, idx) => (
|
|
<div
|
|
key={task.id || `task-${idx}`}
|
|
className="p-4 bg-dark-900/50 rounded-lg border border-dark-700"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
{/* Status Icon */}
|
|
<div className={`mt-0.5 ${
|
|
task.status === 'completed' ? 'text-green-400' :
|
|
task.status === 'running' ? 'text-blue-400' :
|
|
task.status === 'failed' ? 'text-red-400' :
|
|
'text-dark-400'
|
|
}`}>
|
|
{task.status === 'completed' ? <CheckCircle className="w-5 h-5" /> :
|
|
task.status === 'running' ? <RefreshCw className="w-5 h-5 animate-spin" /> :
|
|
task.status === 'failed' ? <XCircle className="w-5 h-5" /> :
|
|
<Clock className="w-5 h-5" />}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-white">{task.task_name}</p>
|
|
{task.description && (
|
|
<p className="text-sm text-dark-400 mt-1">{task.description}</p>
|
|
)}
|
|
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs">
|
|
{task.tool_name && (
|
|
<span className="bg-dark-700 px-2 py-1 rounded text-dark-300">
|
|
{task.tool_name}
|
|
</span>
|
|
)}
|
|
<span className={`px-2 py-1 rounded ${
|
|
task.task_type === 'recon' ? 'bg-blue-500/20 text-blue-400' :
|
|
task.task_type === 'analysis' ? 'bg-purple-500/20 text-purple-400' :
|
|
task.task_type === 'testing' ? 'bg-orange-500/20 text-orange-400' :
|
|
'bg-green-500/20 text-green-400'
|
|
}`}>
|
|
{task.task_type}
|
|
</span>
|
|
{task.duration_ms !== null && (
|
|
<span className="text-dark-500">
|
|
{task.duration_ms < 1000
|
|
? `${task.duration_ms}ms`
|
|
: `${(task.duration_ms / 1000).toFixed(1)}s`}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className={`text-xs px-2 py-1 rounded font-medium ${
|
|
task.status === 'completed' ? 'bg-green-500/20 text-green-400' :
|
|
task.status === 'running' ? 'bg-blue-500/20 text-blue-400' :
|
|
task.status === 'failed' ? 'bg-red-500/20 text-red-400' :
|
|
'bg-dark-700 text-dark-300'
|
|
}`}>
|
|
{task.status}
|
|
</span>
|
|
{(task.items_processed > 0 || task.items_found > 0) && (
|
|
<p className="text-xs text-dark-500 mt-2">
|
|
{task.items_processed > 0 && `${task.items_processed} processed`}
|
|
{task.items_processed > 0 && task.items_found > 0 && ' / '}
|
|
{task.items_found > 0 && `${task.items_found} found`}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{task.result_summary && (
|
|
<p className="text-xs text-dark-400 mt-3 border-t border-dark-700 pt-3">
|
|
{task.result_summary}
|
|
</p>
|
|
)}
|
|
{task.error_message && (
|
|
<p className="text-xs text-red-400 mt-3 border-t border-dark-700 pt-3">
|
|
Error: {task.error_message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Activity Log */}
|
|
<Card title="Activity Log">
|
|
<div className="space-y-1 max-h-60 overflow-auto font-mono text-xs">
|
|
{logs.length === 0 ? (
|
|
<p className="text-dark-400 text-center py-4">Waiting for activity...</p>
|
|
) : (
|
|
logs.map((log, i) => (
|
|
<div key={i} className="flex gap-2">
|
|
<span className="text-dark-500">{new Date(log.time).toLocaleTimeString()}</span>
|
|
<span className={`${
|
|
log.level === 'error' ? 'text-red-400' :
|
|
log.level === 'warning' ? 'text-yellow-400' :
|
|
log.level === 'success' ? 'text-green-400' :
|
|
'text-dark-300'
|
|
}`}>
|
|
{log.message}
|
|
</span>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|