From d4ce4d2ff77b7e24a1dcb06af63127e35a02fbf2 Mon Sep 17 00:00:00 2001 From: Joas A Santos <34966120+CyberSecurityUP@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:49:46 -0300 Subject: [PATCH] Add files via upload --- frontend/src/pages/HomePage.tsx | 177 ++++++++++++++--- frontend/src/pages/ScanDetailsPage.tsx | 262 +++++++++++++++++++++++-- frontend/src/services/api.ts | 57 +++++- frontend/src/store/index.ts | 37 +++- frontend/src/types/index.ts | 39 ++++ 5 files changed, 529 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 2be9d13..84cdc95 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,34 +1,43 @@ -import { useEffect } from 'react' +import { useEffect, useCallback, useState } from 'react' 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 Button from '../components/common/Button' import { SeverityBadge } from '../components/common/Badge' import { dashboardApi } from '../services/api' import { useDashboardStore } from '../store' +import type { ActivityFeedItem } from '../types' export default function HomePage() { const { stats, recentScans, recentVulnerabilities, setStats, setRecentScans, setRecentVulnerabilities, setLoading } = useDashboardStore() + const [activityFeed, setActivityFeed] = useState([]) + + const fetchData = useCallback(async () => { + try { + const [statsData, recentData, activityData] = await Promise.all([ + dashboardApi.getStats(), + dashboardApi.getRecent(5), + dashboardApi.getActivityFeed(15) + ]) + setStats(statsData) + setRecentScans(recentData.recent_scans) + setRecentVulnerabilities(recentData.recent_vulnerabilities) + setActivityFeed(activityData.activities) + } catch (error) { + console.error('Failed to fetch dashboard data:', error) + } + }, [setStats, setRecentScans, setRecentVulnerabilities]) useEffect(() => { - const fetchData = async () => { - setLoading(true) - try { - const [statsData, recentData] = await Promise.all([ - dashboardApi.getStats(), - dashboardApi.getRecent(5) - ]) - setStats(statsData) - setRecentScans(recentData.recent_scans) - setRecentVulnerabilities(recentData.recent_vulnerabilities) - } catch (error) { - console.error('Failed to fetch dashboard data:', error) - } finally { - setLoading(false) - } - } - fetchData() - }, []) + // 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 = [ { @@ -39,26 +48,57 @@ export default function HomePage() { bgColor: 'bg-blue-500/10', }, { - label: 'Running Scans', + label: 'Running', value: stats?.scans.running || 0, icon: Shield, color: 'text-green-400', 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, icon: AlertTriangle, color: 'text-red-400', bgColor: 'bg-red-500/10', }, { - label: 'Critical Issues', + label: 'Critical', value: stats?.vulnerabilities.critical || 0, icon: AlertTriangle, color: 'text-red-500', 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 ( @@ -77,8 +117,8 @@ export default function HomePage() { - {/* Stats Grid */} -
+ {/* Scan Stats Grid */} +
{statCards.map((stat) => (
@@ -94,6 +134,23 @@ export default function HomePage() { ))}
+ {/* Vulnerability Stats Grid */} +
+ {vulnCards.map((stat) => ( + +
+
+ +
+
+

{stat.value}

+

{stat.label}

+
+
+
+ ))} +
+ {/* Severity Distribution */} {stats && stats.vulnerabilities.total > 0 && ( @@ -205,6 +262,76 @@ export default function HomePage() {
+ + {/* Activity Feed */} + +
+ {activityFeed.length === 0 ? ( +

No recent activity.

+ ) : ( + activityFeed.map((activity, idx) => ( + + {/* Activity Icon */} +
+ {activity.type === 'scan' ? : + activity.type === 'vulnerability' ? : + activity.type === 'agent_task' ? : + } +
+ + {/* Activity Content */} +
+
+ + {activity.type.replace('_', ' ')} + + + {activity.action} +
+

{activity.title}

+ {activity.description && ( +

{activity.description}

+ )} +
+ + {/* Activity Meta */} +
+ {activity.severity && ( + + )} + {activity.status && !activity.severity && ( + + {activity.status} + + )} + + + {new Date(activity.timestamp).toLocaleTimeString()} + +
+ + )) + )} +
+
) } diff --git a/frontend/src/pages/ScanDetailsPage.tsx b/frontend/src/pages/ScanDetailsPage.tsx index c56ecc2..826d3e6 100644 --- a/frontend/src/pages/ScanDetailsPage.tsx +++ b/frontend/src/pages/ScanDetailsPage.tsx @@ -2,31 +2,33 @@ import { useEffect, useMemo, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight, - ExternalLink, Copy, Shield, AlertTriangle + 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 } from '../services/api' +import { scansApi, reportsApi, agentTasksApi } from '../services/api' import { wsService } from '../services/websocket' import { useScanStore } from '../store' -import type { Endpoint, Vulnerability, WSMessage } from '../types' +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, + 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>(new Set()) - const [activeTab, setActiveTab] = useState<'endpoints' | 'vulns'>('vulns') + const [activeTab, setActiveTab] = useState<'vulns' | 'endpoints' | 'tasks'>('vulns') const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) + const [autoGeneratedReport, setAutoGeneratedReport] = useState(null) // Calculate vulnerability counts from actual data const vulnCounts = useMemo(() => getVulnCounts(), [vulnerabilities]) @@ -45,9 +47,11 @@ export default function ScanDetailsPage() { const scan = await scansApi.get(scanId) setCurrentScan(scan) - const [endpointsData, vulnsData] = await Promise.all([ + const [endpointsData, vulnsData, tasksData, reportsData] = await Promise.all([ 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 @@ -57,6 +61,13 @@ export default function ScanDetailsPage() { 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') @@ -73,9 +84,10 @@ export default function ScanDetailsPage() { const scan = await scansApi.get(scanId) setCurrentScan(scan) - const [endpointsData, vulnsData] = await Promise.all([ + const [endpointsData, vulnsData, tasksData] = await Promise.all([ scansApi.getEndpoints(scanId), - scansApi.getVulnerabilities(scanId) + scansApi.getVulnerabilities(scanId), + agentTasksApi.list(scanId).catch(() => ({ tasks: [] })) ]) if (endpointsData.endpoints?.length > 0) { @@ -84,6 +96,9 @@ export default function ScanDetailsPage() { if (vulnsData.vulnerabilities?.length > 0) { setVulnerabilities(vulnsData.vulnerabilities) } + if (tasksData.tasks?.length > 0) { + setAgentTasks(tasksData.tasks) + } } catch (err) { console.error('Poll error:', err) } @@ -113,6 +128,29 @@ export default function ScanDetailsPage() { 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 @@ -122,6 +160,65 @@ export default function ScanDetailsPage() { // 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 @@ -236,10 +333,19 @@ export default function ScanDetailsPage() { Stop Scan )} - {currentScan.status === 'completed' && ( + {autoGeneratedReport && ( + + )} + {(currentScan.status === 'completed' || currentScan.status === 'stopped') && ( )} @@ -263,6 +369,41 @@ export default function ScanDetailsPage() { )} + {/* Auto-generated Report Notification */} + {autoGeneratedReport && ( +
+
+
+ +
+
+

+ {autoGeneratedReport.is_partial ? 'Partial Report Generated' : 'Report Generated'} +

+

+ {autoGeneratedReport.title || 'Scan report is ready to view'} +

+
+
+
+ + +
+
+ )} + {/* Stats */}
@@ -319,6 +460,13 @@ export default function ScanDetailsPage() { Endpoints ({endpoints.length}) +
{/* Vulnerabilities Tab */} @@ -553,6 +701,98 @@ export default function ScanDetailsPage() { )} + {/* Agent Tasks Tab */} + {activeTab === 'tasks' && ( + +
+ {agentTasks.length === 0 ? ( +

+ {currentScan.status === 'running' ? 'Agent tasks will appear here...' : 'No agent tasks recorded'} +

+ ) : ( + agentTasks.map((task, idx) => ( +
+
+
+ {/* Status Icon */} +
+ {task.status === 'completed' ? : + task.status === 'running' ? : + task.status === 'failed' ? : + } +
+
+

{task.task_name}

+ {task.description && ( +

{task.description}

+ )} +
+ {task.tool_name && ( + + {task.tool_name} + + )} + + {task.task_type} + + {task.duration_ms !== null && ( + + {task.duration_ms < 1000 + ? `${task.duration_ms}ms` + : `${(task.duration_ms / 1000).toFixed(1)}s`} + + )} +
+
+
+
+ + {task.status} + + {(task.items_processed > 0 || task.items_found > 0) && ( +

+ {task.items_processed > 0 && `${task.items_processed} processed`} + {task.items_processed > 0 && task.items_found > 0 && ' / '} + {task.items_found > 0 && `${task.items_found} found`} +

+ )} +
+
+ {task.result_summary && ( +

+ {task.result_summary} +

+ )} + {task.error_message && ( +

+ Error: {task.error_message} +

+ )} +
+ )) + )} +
+
+ )} + {/* Activity Log */}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 496fce9..7a8db0e 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,8 @@ import axios from 'axios' import type { Scan, Vulnerability, Prompt, PromptPreset, Report, DashboardStats, - AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode + AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode, + ScanAgentTask, ActivityFeedItem } from '../types' const api = axios.create({ @@ -127,9 +128,12 @@ export const promptsApi = { // Reports API export const reportsApi = { - list: async (scanId?: string): Promise<{ reports: Report[]; total: number }> => { - const params = scanId ? `?scan_id=${scanId}` : '' - const response = await api.get(`/reports${params}`) + list: async (options?: { scanId?: string; autoGenerated?: boolean }): Promise<{ reports: Report[]; total: number }> => { + const params = new URLSearchParams() + 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 }, @@ -183,6 +187,16 @@ export const dashboardApi = { const response = await api.get('/dashboard/vulnerability-types') 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 @@ -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 => { + 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 + by_tool: Record + }> => { + 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) export const agentApi = { // Run the autonomous agent diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 267ae68..720295a 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import type { Scan, Vulnerability, Endpoint, DashboardStats } from '../types' +import type { Scan, Vulnerability, Endpoint, DashboardStats, ScanAgentTask } from '../types' interface LogEntry { level: string @@ -12,6 +12,7 @@ interface ScanDataCache { endpoints: Endpoint[] vulnerabilities: Vulnerability[] logs: LogEntry[] + agentTasks: ScanAgentTask[] } interface ScanState { @@ -20,6 +21,7 @@ interface ScanState { endpoints: Endpoint[] vulnerabilities: Vulnerability[] logs: LogEntry[] + agentTasks: ScanAgentTask[] scanDataCache: Record isLoading: boolean error: string | null @@ -33,6 +35,9 @@ interface ScanState { setVulnerabilities: (vulnerabilities: Vulnerability[]) => void addLog: (level: string, message: string) => void setLogs: (logs: LogEntry[]) => void + addAgentTask: (task: ScanAgentTask) => void + updateAgentTask: (taskId: string, updates: Partial) => void + setAgentTasks: (tasks: ScanAgentTask[]) => void setLoading: (loading: boolean) => void setError: (error: string | null) => void loadScanData: (scanId: string) => void @@ -51,6 +56,7 @@ export const useScanStore = create()( endpoints: [], vulnerabilities: [], logs: [], + agentTasks: [], scanDataCache: {}, isLoading: false, error: null, @@ -84,6 +90,27 @@ export const useScanStore = create()( logs: [...state.logs, { level, message, time: new Date().toISOString() }].slice(-200) })), 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 }), setError: (error) => set({ error }), @@ -94,7 +121,8 @@ export const useScanStore = create()( set({ endpoints: cached.endpoints, vulnerabilities: cached.vulnerabilities, - logs: cached.logs + logs: cached.logs, + agentTasks: cached.agentTasks || [] }) } }, @@ -107,7 +135,8 @@ export const useScanStore = create()( [scanId]: { endpoints: state.endpoints, vulnerabilities: state.vulnerabilities, - logs: state.logs + logs: state.logs, + agentTasks: state.agentTasks } } }) @@ -120,6 +149,7 @@ export const useScanStore = create()( endpoints: [], vulnerabilities: [], logs: [], + agentTasks: [], scanDataCache: {}, isLoading: false, error: null, @@ -130,6 +160,7 @@ export const useScanStore = create()( endpoints: [], vulnerabilities: [], logs: [], + agentTasks: [], }), getVulnCounts: () => { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d87d92d..80bb39a 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -13,6 +13,7 @@ export interface Scan { created_at: string started_at: string | null completed_at: string | null + duration: number | null // Duration in seconds error_message: string | null total_endpoints: number total_vulnerabilities: number @@ -101,6 +102,8 @@ export interface Report { format: 'html' | 'pdf' | 'json' file_path: string | null executive_summary: string | null + auto_generated: boolean + is_partial: boolean generated_at: string } @@ -110,6 +113,9 @@ export interface DashboardStats { total: number running: number completed: number + stopped: number + failed: number + pending: number recent: number } vulnerabilities: { @@ -133,6 +139,26 @@ export interface WSMessage { [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 export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only' @@ -289,3 +315,16 @@ export interface RealtimeSessionSummary { findings_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 +}