Add files via upload

This commit is contained in:
Joas A Santos
2026-01-23 15:49:46 -03:00
committed by GitHub
parent f9e4ec16ec
commit d4ce4d2ff7
5 changed files with 529 additions and 43 deletions

View File

@@ -1,34 +1,43 @@
import { useEffect } from 'react' import { useEffect, useCallback, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Activity, Shield, AlertTriangle, Plus, ArrowRight } from 'lucide-react' import { Activity, Shield, AlertTriangle, Plus, ArrowRight, CheckCircle, StopCircle, Clock, FileText, Cpu } from 'lucide-react'
import Card from '../components/common/Card' import Card from '../components/common/Card'
import Button from '../components/common/Button' import Button from '../components/common/Button'
import { SeverityBadge } from '../components/common/Badge' import { SeverityBadge } from '../components/common/Badge'
import { dashboardApi } from '../services/api' import { dashboardApi } from '../services/api'
import { useDashboardStore } from '../store' import { useDashboardStore } from '../store'
import type { ActivityFeedItem } from '../types'
export default function HomePage() { export default function HomePage() {
const { stats, recentScans, recentVulnerabilities, setStats, setRecentScans, setRecentVulnerabilities, setLoading } = useDashboardStore() const { stats, recentScans, recentVulnerabilities, setStats, setRecentScans, setRecentVulnerabilities, setLoading } = useDashboardStore()
const [activityFeed, setActivityFeed] = useState<ActivityFeedItem[]>([])
useEffect(() => { const fetchData = useCallback(async () => {
const fetchData = async () => {
setLoading(true)
try { try {
const [statsData, recentData] = await Promise.all([ const [statsData, recentData, activityData] = await Promise.all([
dashboardApi.getStats(), dashboardApi.getStats(),
dashboardApi.getRecent(5) dashboardApi.getRecent(5),
dashboardApi.getActivityFeed(15)
]) ])
setStats(statsData) setStats(statsData)
setRecentScans(recentData.recent_scans) setRecentScans(recentData.recent_scans)
setRecentVulnerabilities(recentData.recent_vulnerabilities) setRecentVulnerabilities(recentData.recent_vulnerabilities)
setActivityFeed(activityData.activities)
} catch (error) { } catch (error) {
console.error('Failed to fetch dashboard data:', error) console.error('Failed to fetch dashboard data:', error)
} finally {
setLoading(false)
} }
} }, [setStats, setRecentScans, setRecentVulnerabilities])
fetchData()
}, []) useEffect(() => {
// Initial fetch
setLoading(true)
fetchData().finally(() => setLoading(false))
// Periodic refresh every 30 seconds
const refreshInterval = setInterval(fetchData, 30000)
return () => clearInterval(refreshInterval)
}, [fetchData, setLoading])
const statCards = [ const statCards = [
{ {
@@ -39,26 +48,57 @@ export default function HomePage() {
bgColor: 'bg-blue-500/10', bgColor: 'bg-blue-500/10',
}, },
{ {
label: 'Running Scans', label: 'Running',
value: stats?.scans.running || 0, value: stats?.scans.running || 0,
icon: Shield, icon: Shield,
color: 'text-green-400', color: 'text-green-400',
bgColor: 'bg-green-500/10', bgColor: 'bg-green-500/10',
}, },
{ {
label: 'Vulnerabilities', label: 'Completed',
value: stats?.scans.completed || 0,
icon: CheckCircle,
color: 'text-emerald-400',
bgColor: 'bg-emerald-500/10',
},
{
label: 'Stopped',
value: stats?.scans.stopped || 0,
icon: StopCircle,
color: 'text-yellow-400',
bgColor: 'bg-yellow-500/10',
},
]
const vulnCards = [
{
label: 'Total Vulns',
value: stats?.vulnerabilities.total || 0, value: stats?.vulnerabilities.total || 0,
icon: AlertTriangle, icon: AlertTriangle,
color: 'text-red-400', color: 'text-red-400',
bgColor: 'bg-red-500/10', bgColor: 'bg-red-500/10',
}, },
{ {
label: 'Critical Issues', label: 'Critical',
value: stats?.vulnerabilities.critical || 0, value: stats?.vulnerabilities.critical || 0,
icon: AlertTriangle, icon: AlertTriangle,
color: 'text-red-500', color: 'text-red-500',
bgColor: 'bg-red-600/10', bgColor: 'bg-red-600/10',
}, },
{
label: 'High',
value: stats?.vulnerabilities.high || 0,
icon: AlertTriangle,
color: 'text-orange-400',
bgColor: 'bg-orange-500/10',
},
{
label: 'Medium',
value: stats?.vulnerabilities.medium || 0,
icon: AlertTriangle,
color: 'text-yellow-400',
bgColor: 'bg-yellow-500/10',
},
] ]
return ( return (
@@ -77,8 +117,8 @@ export default function HomePage() {
</Link> </Link>
</div> </div>
{/* Stats Grid */} {/* Scan Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{statCards.map((stat) => ( {statCards.map((stat) => (
<Card key={stat.label} className="hover:border-dark-700 transition-colors"> <Card key={stat.label} className="hover:border-dark-700 transition-colors">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -94,6 +134,23 @@ export default function HomePage() {
))} ))}
</div> </div>
{/* Vulnerability Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{vulnCards.map((stat) => (
<Card key={stat.label} className="hover:border-dark-700 transition-colors">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
<stat.icon className={`w-6 h-6 ${stat.color}`} />
</div>
<div>
<p className="text-2xl font-bold text-white">{stat.value}</p>
<p className="text-sm text-dark-400">{stat.label}</p>
</div>
</div>
</Card>
))}
</div>
{/* Severity Distribution */} {/* Severity Distribution */}
{stats && stats.vulnerabilities.total > 0 && ( {stats && stats.vulnerabilities.total > 0 && (
<Card title="Vulnerability Distribution"> <Card title="Vulnerability Distribution">
@@ -205,6 +262,76 @@ export default function HomePage() {
</div> </div>
</Card> </Card>
</div> </div>
{/* Activity Feed */}
<Card
title="Activity Feed"
subtitle="Recent activities across all scans"
>
<div className="space-y-2 max-h-[400px] overflow-auto">
{activityFeed.length === 0 ? (
<p className="text-dark-400 text-center py-4">No recent activity.</p>
) : (
activityFeed.map((activity, idx) => (
<Link
key={`${activity.type}-${activity.timestamp}-${idx}`}
to={activity.link}
className="flex items-start gap-3 p-3 bg-dark-900/50 rounded-lg hover:bg-dark-900 transition-colors"
>
{/* Activity Icon */}
<div className={`mt-0.5 p-2 rounded-lg ${
activity.type === 'scan' ? 'bg-blue-500/20 text-blue-400' :
activity.type === 'vulnerability' ? 'bg-red-500/20 text-red-400' :
activity.type === 'agent_task' ? 'bg-purple-500/20 text-purple-400' :
'bg-green-500/20 text-green-400'
}`}>
{activity.type === 'scan' ? <Shield className="w-4 h-4" /> :
activity.type === 'vulnerability' ? <AlertTriangle className="w-4 h-4" /> :
activity.type === 'agent_task' ? <Cpu className="w-4 h-4" /> :
<FileText className="w-4 h-4" />}
</div>
{/* Activity Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs text-dark-500 uppercase font-medium">
{activity.type.replace('_', ' ')}
</span>
<span className="text-xs text-dark-600"></span>
<span className="text-xs text-dark-500">{activity.action}</span>
</div>
<p className="font-medium text-white truncate mt-0.5">{activity.title}</p>
{activity.description && (
<p className="text-xs text-dark-400 truncate">{activity.description}</p>
)}
</div>
{/* Activity Meta */}
<div className="flex flex-col items-end gap-1">
{activity.severity && (
<SeverityBadge severity={activity.severity} />
)}
{activity.status && !activity.severity && (
<span className={`text-xs px-2 py-0.5 rounded font-medium ${
activity.status === 'completed' ? 'bg-green-500/20 text-green-400' :
activity.status === 'running' ? 'bg-blue-500/20 text-blue-400' :
activity.status === 'failed' ? 'bg-red-500/20 text-red-400' :
activity.status === 'stopped' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-dark-700 text-dark-300'
}`}>
{activity.status}
</span>
)}
<span className="text-xs text-dark-500 flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(activity.timestamp).toLocaleTimeString()}
</span>
</div>
</Link>
))
)}
</div>
</Card>
</div> </div>
) )
} }

View File

@@ -2,31 +2,33 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { import {
Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight, Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight,
ExternalLink, Copy, Shield, AlertTriangle ExternalLink, Copy, Shield, AlertTriangle, Cpu, CheckCircle, XCircle, Clock
} from 'lucide-react' } from 'lucide-react'
import Card from '../components/common/Card' import Card from '../components/common/Card'
import Button from '../components/common/Button' import Button from '../components/common/Button'
import { SeverityBadge } from '../components/common/Badge' import { SeverityBadge } from '../components/common/Badge'
import { scansApi, reportsApi } from '../services/api' import { scansApi, reportsApi, agentTasksApi } from '../services/api'
import { wsService } from '../services/websocket' import { wsService } from '../services/websocket'
import { useScanStore } from '../store' import { useScanStore } from '../store'
import type { Endpoint, Vulnerability, WSMessage } from '../types' import type { Endpoint, Vulnerability, WSMessage, ScanAgentTask, Report } from '../types'
export default function ScanDetailsPage() { export default function ScanDetailsPage() {
const { scanId } = useParams<{ scanId: string }>() const { scanId } = useParams<{ scanId: string }>()
const navigate = useNavigate() const navigate = useNavigate()
const { const {
currentScan, endpoints, vulnerabilities, logs, currentScan, endpoints, vulnerabilities, logs, agentTasks,
setCurrentScan, setEndpoints, setVulnerabilities, setCurrentScan, setEndpoints, setVulnerabilities,
addEndpoint, addVulnerability, addLog, updateScan, addEndpoint, addVulnerability, addLog, updateScan,
addAgentTask, updateAgentTask, setAgentTasks,
loadScanData, saveScanData, getVulnCounts loadScanData, saveScanData, getVulnCounts
} = useScanStore() } = useScanStore()
const [isGeneratingReport, setIsGeneratingReport] = useState(false) const [isGeneratingReport, setIsGeneratingReport] = useState(false)
const [expandedVulns, setExpandedVulns] = useState<Set<string>>(new Set()) const [expandedVulns, setExpandedVulns] = useState<Set<string>>(new Set())
const [activeTab, setActiveTab] = useState<'endpoints' | 'vulns'>('vulns') const [activeTab, setActiveTab] = useState<'vulns' | 'endpoints' | 'tasks'>('vulns')
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [autoGeneratedReport, setAutoGeneratedReport] = useState<Report | null>(null)
// Calculate vulnerability counts from actual data // Calculate vulnerability counts from actual data
const vulnCounts = useMemo(() => getVulnCounts(), [vulnerabilities]) const vulnCounts = useMemo(() => getVulnCounts(), [vulnerabilities])
@@ -45,9 +47,11 @@ export default function ScanDetailsPage() {
const scan = await scansApi.get(scanId) const scan = await scansApi.get(scanId)
setCurrentScan(scan) setCurrentScan(scan)
const [endpointsData, vulnsData] = await Promise.all([ const [endpointsData, vulnsData, tasksData, reportsData] = await Promise.all([
scansApi.getEndpoints(scanId), scansApi.getEndpoints(scanId),
scansApi.getVulnerabilities(scanId) scansApi.getVulnerabilities(scanId),
agentTasksApi.list(scanId).catch(() => ({ tasks: [] })),
reportsApi.list({ scanId, autoGenerated: true }).catch(() => ({ reports: [] }))
]) ])
// Only set if we have data from API // Only set if we have data from API
@@ -57,6 +61,13 @@ export default function ScanDetailsPage() {
if (vulnsData.vulnerabilities?.length > 0) { if (vulnsData.vulnerabilities?.length > 0) {
setVulnerabilities(vulnsData.vulnerabilities) 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) { } catch (err: any) {
console.error('Failed to fetch scan:', err) console.error('Failed to fetch scan:', err)
setError(err?.response?.data?.detail || 'Failed to load scan') setError(err?.response?.data?.detail || 'Failed to load scan')
@@ -73,9 +84,10 @@ export default function ScanDetailsPage() {
const scan = await scansApi.get(scanId) const scan = await scansApi.get(scanId)
setCurrentScan(scan) setCurrentScan(scan)
const [endpointsData, vulnsData] = await Promise.all([ const [endpointsData, vulnsData, tasksData] = await Promise.all([
scansApi.getEndpoints(scanId), scansApi.getEndpoints(scanId),
scansApi.getVulnerabilities(scanId) scansApi.getVulnerabilities(scanId),
agentTasksApi.list(scanId).catch(() => ({ tasks: [] }))
]) ])
if (endpointsData.endpoints?.length > 0) { if (endpointsData.endpoints?.length > 0) {
@@ -84,6 +96,9 @@ export default function ScanDetailsPage() {
if (vulnsData.vulnerabilities?.length > 0) { if (vulnsData.vulnerabilities?.length > 0) {
setVulnerabilities(vulnsData.vulnerabilities) setVulnerabilities(vulnsData.vulnerabilities)
} }
if (tasksData.tasks?.length > 0) {
setAgentTasks(tasksData.tasks)
}
} catch (err) { } catch (err) {
console.error('Poll error:', err) console.error('Poll error:', err)
} }
@@ -113,6 +128,29 @@ export default function ScanDetailsPage() {
addVulnerability(message.vulnerability as Vulnerability) addVulnerability(message.vulnerability as Vulnerability)
addLog('warning', `Found: ${(message.vulnerability as Vulnerability).title}`) addLog('warning', `Found: ${(message.vulnerability as Vulnerability).title}`)
break 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': case 'log_message':
addLog(message.level as string, message.message as string) addLog(message.level as string, message.message as string)
break break
@@ -122,6 +160,65 @@ export default function ScanDetailsPage() {
// Save data when scan completes // Save data when scan completes
saveScanData(scanId) saveScanData(scanId)
break 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': case 'error':
addLog('error', message.error as string) addLog('error', message.error as string)
break break
@@ -236,10 +333,19 @@ export default function ScanDetailsPage() {
Stop Scan Stop Scan
</Button> </Button>
)} )}
{currentScan.status === 'completed' && ( {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}> <Button onClick={handleGenerateReport} isLoading={isGeneratingReport}>
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
Generate Report {autoGeneratedReport ? 'New Report' : 'Generate Report'}
</Button> </Button>
)} )}
</div> </div>
@@ -263,6 +369,41 @@ export default function ScanDetailsPage() {
</Card> </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 */} {/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4"> <div className="grid grid-cols-2 md:grid-cols-6 gap-4">
<Card> <Card>
@@ -319,6 +460,13 @@ export default function ScanDetailsPage() {
<Globe className="w-4 h-4 mr-2" /> <Globe className="w-4 h-4 mr-2" />
Endpoints ({endpoints.length}) Endpoints ({endpoints.length})
</Button> </Button>
<Button
variant={activeTab === 'tasks' ? 'primary' : 'ghost'}
onClick={() => setActiveTab('tasks')}
>
<Cpu className="w-4 h-4 mr-2" />
Agent Tasks ({agentTasks.length})
</Button>
</div> </div>
{/* Vulnerabilities Tab */} {/* Vulnerabilities Tab */}
@@ -553,6 +701,98 @@ export default function ScanDetailsPage() {
</Card> </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 */} {/* Activity Log */}
<Card title="Activity Log"> <Card title="Activity Log">
<div className="space-y-1 max-h-60 overflow-auto font-mono text-xs"> <div className="space-y-1 max-h-60 overflow-auto font-mono text-xs">

View File

@@ -1,7 +1,8 @@
import axios from 'axios' import axios from 'axios'
import type { import type {
Scan, Vulnerability, Prompt, PromptPreset, Report, DashboardStats, Scan, Vulnerability, Prompt, PromptPreset, Report, DashboardStats,
AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode,
ScanAgentTask, ActivityFeedItem
} from '../types' } from '../types'
const api = axios.create({ const api = axios.create({
@@ -127,9 +128,12 @@ export const promptsApi = {
// Reports API // Reports API
export const reportsApi = { export const reportsApi = {
list: async (scanId?: string): Promise<{ reports: Report[]; total: number }> => { list: async (options?: { scanId?: string; autoGenerated?: boolean }): Promise<{ reports: Report[]; total: number }> => {
const params = scanId ? `?scan_id=${scanId}` : '' const params = new URLSearchParams()
const response = await api.get(`/reports${params}`) if (options?.scanId) params.append('scan_id', options.scanId)
if (options?.autoGenerated !== undefined) params.append('auto_generated', String(options.autoGenerated))
const queryString = params.toString()
const response = await api.get(`/reports${queryString ? `?${queryString}` : ''}`)
return response.data return response.data
}, },
@@ -183,6 +187,16 @@ export const dashboardApi = {
const response = await api.get('/dashboard/vulnerability-types') const response = await api.get('/dashboard/vulnerability-types')
return response.data return response.data
}, },
getAgentTasks: async (limit = 20) => {
const response = await api.get(`/dashboard/agent-tasks?limit=${limit}`)
return response.data
},
getActivityFeed: async (limit = 30): Promise<{ activities: ActivityFeedItem[]; total: number }> => {
const response = await api.get(`/dashboard/activity-feed?limit=${limit}`)
return response.data
},
} }
// Vulnerabilities API // Vulnerabilities API
@@ -198,6 +212,41 @@ export const vulnerabilitiesApi = {
}, },
} }
// Scan Agent Tasks API (for tracking scan-specific tasks)
export const agentTasksApi = {
list: async (scanId: string, status?: string, taskType?: string): Promise<{ tasks: ScanAgentTask[]; total: number; scan_id: string }> => {
const params = new URLSearchParams()
params.append('scan_id', scanId)
if (status) params.append('status', status)
if (taskType) params.append('task_type', taskType)
const response = await api.get(`/agent-tasks?${params}`)
return response.data
},
get: async (taskId: string): Promise<ScanAgentTask> => {
const response = await api.get(`/agent-tasks/${taskId}`)
return response.data
},
getSummary: async (scanId: string): Promise<{
total: number
pending: number
running: number
completed: number
failed: number
by_type: Record<string, number>
by_tool: Record<string, number>
}> => {
const response = await api.get(`/agent-tasks/summary?scan_id=${scanId}`)
return response.data
},
getTimeline: async (scanId: string): Promise<{ scan_id: string; timeline: ScanAgentTask[]; total: number }> => {
const response = await api.get(`/agent-tasks/scan/${scanId}/timeline`)
return response.data
},
}
// Agent API (Autonomous AI Agent like PentAGI) // Agent API (Autonomous AI Agent like PentAGI)
export const agentApi = { export const agentApi = {
// Run the autonomous agent // Run the autonomous agent

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import type { Scan, Vulnerability, Endpoint, DashboardStats } from '../types' import type { Scan, Vulnerability, Endpoint, DashboardStats, ScanAgentTask } from '../types'
interface LogEntry { interface LogEntry {
level: string level: string
@@ -12,6 +12,7 @@ interface ScanDataCache {
endpoints: Endpoint[] endpoints: Endpoint[]
vulnerabilities: Vulnerability[] vulnerabilities: Vulnerability[]
logs: LogEntry[] logs: LogEntry[]
agentTasks: ScanAgentTask[]
} }
interface ScanState { interface ScanState {
@@ -20,6 +21,7 @@ interface ScanState {
endpoints: Endpoint[] endpoints: Endpoint[]
vulnerabilities: Vulnerability[] vulnerabilities: Vulnerability[]
logs: LogEntry[] logs: LogEntry[]
agentTasks: ScanAgentTask[]
scanDataCache: Record<string, ScanDataCache> scanDataCache: Record<string, ScanDataCache>
isLoading: boolean isLoading: boolean
error: string | null error: string | null
@@ -33,6 +35,9 @@ interface ScanState {
setVulnerabilities: (vulnerabilities: Vulnerability[]) => void setVulnerabilities: (vulnerabilities: Vulnerability[]) => void
addLog: (level: string, message: string) => void addLog: (level: string, message: string) => void
setLogs: (logs: LogEntry[]) => void setLogs: (logs: LogEntry[]) => void
addAgentTask: (task: ScanAgentTask) => void
updateAgentTask: (taskId: string, updates: Partial<ScanAgentTask>) => void
setAgentTasks: (tasks: ScanAgentTask[]) => void
setLoading: (loading: boolean) => void setLoading: (loading: boolean) => void
setError: (error: string | null) => void setError: (error: string | null) => void
loadScanData: (scanId: string) => void loadScanData: (scanId: string) => void
@@ -51,6 +56,7 @@ export const useScanStore = create<ScanState>()(
endpoints: [], endpoints: [],
vulnerabilities: [], vulnerabilities: [],
logs: [], logs: [],
agentTasks: [],
scanDataCache: {}, scanDataCache: {},
isLoading: false, isLoading: false,
error: null, error: null,
@@ -84,6 +90,27 @@ export const useScanStore = create<ScanState>()(
logs: [...state.logs, { level, message, time: new Date().toISOString() }].slice(-200) logs: [...state.logs, { level, message, time: new Date().toISOString() }].slice(-200)
})), })),
setLogs: (logs) => set({ logs }), setLogs: (logs) => set({ logs }),
// Agent Tasks
addAgentTask: (task) =>
set((state) => {
const exists = state.agentTasks.some(t => t.id === task.id)
if (exists) {
// Update existing task
return {
agentTasks: state.agentTasks.map(t => t.id === task.id ? task : t)
}
}
return { agentTasks: [...state.agentTasks, task] }
}),
updateAgentTask: (taskId, updates) =>
set((state) => ({
agentTasks: state.agentTasks.map(t =>
t.id === taskId ? { ...t, ...updates } : t
)
})),
setAgentTasks: (agentTasks) => set({ agentTasks }),
setLoading: (isLoading) => set({ isLoading }), setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error }), setError: (error) => set({ error }),
@@ -94,7 +121,8 @@ export const useScanStore = create<ScanState>()(
set({ set({
endpoints: cached.endpoints, endpoints: cached.endpoints,
vulnerabilities: cached.vulnerabilities, vulnerabilities: cached.vulnerabilities,
logs: cached.logs logs: cached.logs,
agentTasks: cached.agentTasks || []
}) })
} }
}, },
@@ -107,7 +135,8 @@ export const useScanStore = create<ScanState>()(
[scanId]: { [scanId]: {
endpoints: state.endpoints, endpoints: state.endpoints,
vulnerabilities: state.vulnerabilities, vulnerabilities: state.vulnerabilities,
logs: state.logs logs: state.logs,
agentTasks: state.agentTasks
} }
} }
}) })
@@ -120,6 +149,7 @@ export const useScanStore = create<ScanState>()(
endpoints: [], endpoints: [],
vulnerabilities: [], vulnerabilities: [],
logs: [], logs: [],
agentTasks: [],
scanDataCache: {}, scanDataCache: {},
isLoading: false, isLoading: false,
error: null, error: null,
@@ -130,6 +160,7 @@ export const useScanStore = create<ScanState>()(
endpoints: [], endpoints: [],
vulnerabilities: [], vulnerabilities: [],
logs: [], logs: [],
agentTasks: [],
}), }),
getVulnCounts: () => { getVulnCounts: () => {

View File

@@ -13,6 +13,7 @@ export interface Scan {
created_at: string created_at: string
started_at: string | null started_at: string | null
completed_at: string | null completed_at: string | null
duration: number | null // Duration in seconds
error_message: string | null error_message: string | null
total_endpoints: number total_endpoints: number
total_vulnerabilities: number total_vulnerabilities: number
@@ -101,6 +102,8 @@ export interface Report {
format: 'html' | 'pdf' | 'json' format: 'html' | 'pdf' | 'json'
file_path: string | null file_path: string | null
executive_summary: string | null executive_summary: string | null
auto_generated: boolean
is_partial: boolean
generated_at: string generated_at: string
} }
@@ -110,6 +113,9 @@ export interface DashboardStats {
total: number total: number
running: number running: number
completed: number completed: number
stopped: number
failed: number
pending: number
recent: number recent: number
} }
vulnerabilities: { vulnerabilities: {
@@ -133,6 +139,26 @@ export interface WSMessage {
[key: string]: unknown [key: string]: unknown
} }
// Scan Agent Task (different from AgentTask which is for the task library)
export interface ScanAgentTask {
id: string
scan_id: string
task_type: 'recon' | 'analysis' | 'testing' | 'reporting'
task_name: string
description: string | null
tool_name: string | null
tool_category: string | null
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
started_at: string | null
completed_at: string | null
duration_ms: number | null
items_processed: number
items_found: number
result_summary: string | null
error_message: string | null
created_at: string
}
// Agent types // Agent types
export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only' export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only'
@@ -289,3 +315,16 @@ export interface RealtimeSessionSummary {
findings_count: number findings_count: number
messages_count: number messages_count: number
} }
// Activity Feed types
export interface ActivityFeedItem {
type: 'scan' | 'vulnerability' | 'agent_task' | 'report'
action: string
title: string
description: string
status: string | null
severity: 'critical' | 'high' | 'medium' | 'low' | 'info' | null
timestamp: string
scan_id: string
link: string
}