Add files via upload

This commit is contained in:
Joas A Santos
2026-01-19 19:23:10 -03:00
committed by GitHub
parent bdd6c91f50
commit e7f1e75803
34 changed files with 6064 additions and 6 deletions

45
docker-compose.lite.yml Normal file
View File

@@ -0,0 +1,45 @@
# NeuroSploit v3 - LITE Docker Compose
# Fast builds without external security tools
# Usage: docker compose -f docker-compose.lite.yml up --build
services:
backend:
build:
context: .
dockerfile: docker/Dockerfile.backend.lite
container_name: neurosploit-backend
env_file:
- .env
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- DATABASE_URL=sqlite+aiosqlite:///./data/neurosploit.db
volumes:
- neurosploit-data:/app/data
ports:
- "8000:8000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
interval: 30s
timeout: 10s
retries: 3
frontend:
build:
context: .
dockerfile: docker/Dockerfile.frontend
container_name: neurosploit-frontend
ports:
- "3000:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
volumes:
neurosploit-data:
networks:
default:
name: neurosploit-network

View File

@@ -1,9 +1,44 @@
services:
dvwa:
image: vulnerables/web-dvwa
container_name: dvwa
ports:
- "8080:80"
backend:
build:
context: .
# Use Dockerfile.backend.lite for faster builds (no security tools)
# Use Dockerfile.backend for full version with all tools
dockerfile: docker/Dockerfile.backend
container_name: neurosploit-backend
env_file:
- .env
environment:
- MYSQL_PASS=password
# These override .env if set
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- DATABASE_URL=sqlite+aiosqlite:///./data/neurosploit.db
volumes:
- neurosploit-data:/app/data
ports:
- "8000:8000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
interval: 30s
timeout: 10s
retries: 3
frontend:
build:
context: .
dockerfile: docker/Dockerfile.frontend
container_name: neurosploit-frontend
ports:
- "3000:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
volumes:
neurosploit-data:
networks:
default:
name: neurosploit-network

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NeuroSploit v3 - AI-Powered Penetration Testing</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

34
frontend/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "neurosploit-frontend",
"version": "3.0.0",
"description": "NeuroSploit v3 - AI-Powered Penetration Testing Platform",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"zustand": "^4.4.0",
"axios": "^1.6.0",
"socket.io-client": "^4.6.0",
"recharts": "^2.10.0",
"lucide-react": "^0.303.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="#1a1a2e" stroke="#e94560" stroke-width="3"/>
<path d="M30 50 L45 35 L45 45 L70 45 L70 55 L45 55 L45 65 Z" fill="#e94560"/>
<circle cx="50" cy="50" r="8" fill="none" stroke="#e94560" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

31
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { Routes, Route } from 'react-router-dom'
import Layout from './components/layout/Layout'
import HomePage from './pages/HomePage'
import NewScanPage from './pages/NewScanPage'
import ScanDetailsPage from './pages/ScanDetailsPage'
import AgentStatusPage from './pages/AgentStatusPage'
import TaskLibraryPage from './pages/TaskLibraryPage'
import RealtimeTaskPage from './pages/RealtimeTaskPage'
import ReportsPage from './pages/ReportsPage'
import ReportViewPage from './pages/ReportViewPage'
import SettingsPage from './pages/SettingsPage'
function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/scan/new" element={<NewScanPage />} />
<Route path="/scan/:scanId" element={<ScanDetailsPage />} />
<Route path="/agent/:agentId" element={<AgentStatusPage />} />
<Route path="/tasks" element={<TaskLibraryPage />} />
<Route path="/realtime" element={<RealtimeTaskPage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/reports/:reportId" element={<ReportViewPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Layout>
)
}
export default App

View File

@@ -0,0 +1,37 @@
import { clsx } from 'clsx'
interface BadgeProps {
variant?: 'critical' | 'high' | 'medium' | 'low' | 'info' | 'success' | 'warning' | 'default'
children: React.ReactNode
className?: string
}
const variants = {
critical: 'bg-red-500/20 text-red-400 border-red-500/30',
high: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
medium: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
low: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
info: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
success: 'bg-green-500/20 text-green-400 border-green-500/30',
warning: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
default: 'bg-dark-900/50 text-dark-300 border-dark-700',
}
export default function Badge({ variant = 'default', children, className }: BadgeProps) {
return (
<span
className={clsx(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
variants[variant],
className
)}
>
{children}
</span>
)
}
export function SeverityBadge({ severity }: { severity: string }) {
const variant = severity.toLowerCase() as BadgeProps['variant']
return <Badge variant={variant}>{severity.toUpperCase()}</Badge>
}

View File

@@ -0,0 +1,70 @@
import { ButtonHTMLAttributes, ReactNode } from 'react'
import { clsx } from 'clsx'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
children: ReactNode
}
export default function Button({
variant = 'primary',
size = 'md',
isLoading = false,
children,
className,
disabled,
...props
}: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-700 disabled:opacity-50 disabled:cursor-not-allowed'
const variants = {
primary: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500',
secondary: 'bg-dark-900 text-white hover:bg-dark-800 focus:ring-dark-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-dark-300 hover:text-white hover:bg-dark-900/50 focus:ring-dark-500',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
}
return (
<button
className={clsx(baseStyles, variants[variant], sizes[size], className)}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Loading...
</>
) : (
children
)}
</button>
)
}

View File

@@ -0,0 +1,27 @@
import { ReactNode } from 'react'
import { clsx } from 'clsx'
interface CardProps {
children: ReactNode
className?: string
title?: ReactNode
subtitle?: string
action?: ReactNode
}
export default function Card({ children, className, title, subtitle, action }: CardProps) {
return (
<div className={clsx('bg-dark-800 rounded-xl border border-dark-900/50', className)}>
{(title || action) && (
<div className="flex items-center justify-between p-4 border-b border-dark-900/50">
<div>
{title && <h3 className="text-lg font-semibold text-white">{title}</h3>}
{subtitle && <p className="text-sm text-dark-400 mt-1">{subtitle}</p>}
</div>
{action}
</div>
)}
<div className="p-4">{children}</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import { InputHTMLAttributes, forwardRef } from 'react'
import { clsx } from 'clsx'
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
error?: string
helperText?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, helperText, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-dark-200 mb-1.5">
{label}
</label>
)}
<input
ref={ref}
className={clsx(
'w-full px-4 py-2.5 bg-dark-900 border rounded-lg text-white placeholder-dark-500',
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
'transition-colors',
error ? 'border-red-500' : 'border-dark-700',
className
)}
{...props}
/>
{error && <p className="mt-1 text-sm text-red-400">{error}</p>}
{helperText && !error && (
<p className="mt-1 text-sm text-dark-400">{helperText}</p>
)}
</div>
)
}
)
Input.displayName = 'Input'
export default Input

View File

@@ -0,0 +1,41 @@
import { TextareaHTMLAttributes, forwardRef } from 'react'
import { clsx } from 'clsx'
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string
error?: string
helperText?: string
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ label, error, helperText, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-dark-200 mb-1.5">
{label}
</label>
)}
<textarea
ref={ref}
className={clsx(
'w-full px-4 py-2.5 bg-dark-900 border rounded-lg text-white placeholder-dark-500',
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
'transition-colors resize-none',
error ? 'border-red-500' : 'border-dark-700',
className
)}
{...props}
/>
{error && <p className="mt-1 text-sm text-red-400">{error}</p>}
{helperText && !error && (
<p className="mt-1 text-sm text-dark-400">{helperText}</p>
)}
</div>
)
}
)
Textarea.displayName = 'Textarea'
export default Textarea

View File

@@ -0,0 +1,29 @@
import { useLocation } from 'react-router-dom'
const pageTitles: Record<string, string> = {
'/': 'Dashboard',
'/scan/new': 'New Security Scan',
'/reports': 'Reports',
'/settings': 'Settings',
}
export default function Header() {
const location = useLocation()
const title = pageTitles[location.pathname] || 'NeuroSploit'
return (
<header className="h-16 bg-dark-800 border-b border-dark-900/50 flex items-center justify-between px-6">
<h1 className="text-xl font-semibold text-white">{title}</h1>
<div className="flex items-center gap-4">
<span className="text-sm text-dark-400">
{new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
</div>
</header>
)
}

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react'
import Sidebar from './Sidebar'
import Header from './Header'
interface LayoutProps {
children: ReactNode
}
export default function Layout({ children }: LayoutProps) {
return (
<div className="flex min-h-screen bg-dark-700">
<Sidebar />
<div className="flex-1 flex flex-col">
<Header />
<main className="flex-1 p-6 overflow-auto">
{children}
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,74 @@
import { Link, useLocation } from 'react-router-dom'
import {
Home,
Bot,
BookOpen,
FileText,
Settings,
Activity,
Shield,
Zap
} from 'lucide-react'
const navItems = [
{ path: '/', icon: Home, label: 'Dashboard' },
{ path: '/scan/new', icon: Bot, label: 'AI Agent' },
{ path: '/realtime', icon: Zap, label: 'Real-time Task' },
{ path: '/tasks', icon: BookOpen, label: 'Task Library' },
{ path: '/reports', icon: FileText, label: 'Reports' },
{ path: '/settings', icon: Settings, label: 'Settings' },
]
export default function Sidebar() {
const location = useLocation()
return (
<aside className="w-64 bg-dark-800 border-r border-dark-900/50 flex flex-col">
{/* Logo */}
<div className="p-6 border-b border-dark-900/50">
<Link to="/" className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
<Shield className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-white">NeuroSploit</h1>
<p className="text-xs text-dark-400">v3.0 AI Pentest</p>
</div>
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 p-4">
<ul className="space-y-2">
{navItems.map((item) => {
const isActive = location.pathname === item.path
const Icon = item.icon
return (
<li key={item.path}>
<Link
to={item.path}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
isActive
? 'bg-primary-500/20 text-primary-500'
: 'text-dark-300 hover:bg-dark-900/50 hover:text-white'
}`}
>
<Icon className="w-5 h-5" />
<span>{item.label}</span>
</Link>
</li>
)
})}
</ul>
</nav>
{/* Status */}
<div className="p-4 border-t border-dark-900/50">
<div className="flex items-center gap-2 text-sm">
<Activity className="w-4 h-4 text-green-500" />
<span className="text-dark-400">System Online</span>
</div>
</div>
</aside>
)
}

13
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './styles/globals.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,210 @@
import { useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Activity, Shield, AlertTriangle, Plus, ArrowRight } 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'
export default function HomePage() {
const { stats, recentScans, recentVulnerabilities, setStats, setRecentScans, setRecentVulnerabilities, setLoading } = useDashboardStore()
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()
}, [])
const statCards = [
{
label: 'Total Scans',
value: stats?.scans.total || 0,
icon: Activity,
color: 'text-blue-400',
bgColor: 'bg-blue-500/10',
},
{
label: 'Running Scans',
value: stats?.scans.running || 0,
icon: Shield,
color: 'text-green-400',
bgColor: 'bg-green-500/10',
},
{
label: 'Vulnerabilities',
value: stats?.vulnerabilities.total || 0,
icon: AlertTriangle,
color: 'text-red-400',
bgColor: 'bg-red-500/10',
},
{
label: 'Critical Issues',
value: stats?.vulnerabilities.critical || 0,
icon: AlertTriangle,
color: 'text-red-500',
bgColor: 'bg-red-600/10',
},
]
return (
<div className="space-y-6 animate-fadeIn">
{/* Quick Actions */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Welcome to NeuroSploit</h2>
<p className="text-dark-400 mt-1">AI-Powered Penetration Testing Platform</p>
</div>
<Link to="/scan/new">
<Button size="lg">
<Plus className="w-5 h-5 mr-2" />
New Scan
</Button>
</Link>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{statCards.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 */}
{stats && stats.vulnerabilities.total > 0 && (
<Card title="Vulnerability Distribution">
<div className="flex h-8 rounded-lg overflow-hidden">
{stats.vulnerabilities.critical > 0 && (
<div
className="bg-red-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(stats.vulnerabilities.critical / stats.vulnerabilities.total) * 100}%` }}
>
{stats.vulnerabilities.critical}
</div>
)}
{stats.vulnerabilities.high > 0 && (
<div
className="bg-orange-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(stats.vulnerabilities.high / stats.vulnerabilities.total) * 100}%` }}
>
{stats.vulnerabilities.high}
</div>
)}
{stats.vulnerabilities.medium > 0 && (
<div
className="bg-yellow-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(stats.vulnerabilities.medium / stats.vulnerabilities.total) * 100}%` }}
>
{stats.vulnerabilities.medium}
</div>
)}
{stats.vulnerabilities.low > 0 && (
<div
className="bg-blue-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(stats.vulnerabilities.low / stats.vulnerabilities.total) * 100}%` }}
>
{stats.vulnerabilities.low}
</div>
)}
</div>
<div className="flex gap-4 mt-3 text-xs">
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-red-500" /> Critical</span>
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-orange-500" /> High</span>
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-yellow-500" /> Medium</span>
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-blue-500" /> Low</span>
</div>
</Card>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Scans */}
<Card
title="Recent Scans"
action={
<Link to="/reports" className="text-sm text-primary-500 hover:text-primary-400 flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</Link>
}
>
<div className="space-y-3">
{recentScans.length === 0 ? (
<p className="text-dark-400 text-center py-4">No scans yet. Start your first scan!</p>
) : (
recentScans.map((scan) => (
<Link
key={scan.id}
to={`/scan/${scan.id}`}
className="flex items-center justify-between p-3 bg-dark-900/50 rounded-lg hover:bg-dark-900 transition-colors"
>
<div>
<p className="font-medium text-white">{scan.name || 'Unnamed Scan'}</p>
<p className="text-xs text-dark-400">
{new Date(scan.created_at).toLocaleDateString()}
</p>
</div>
<div className="flex items-center gap-2">
<SeverityBadge severity={scan.status} />
<span className="text-sm text-dark-400">{scan.total_vulnerabilities} vulns</span>
</div>
</Link>
))
)}
</div>
</Card>
{/* Recent Vulnerabilities */}
<Card
title="Recent Findings"
action={
<Link to="/reports" className="text-sm text-primary-500 hover:text-primary-400 flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</Link>
}
>
<div className="space-y-3">
{recentVulnerabilities.length === 0 ? (
<p className="text-dark-400 text-center py-4">No vulnerabilities found yet.</p>
) : (
recentVulnerabilities.slice(0, 5).map((vuln) => (
<div
key={vuln.id}
className="flex items-center justify-between p-3 bg-dark-900/50 rounded-lg"
>
<div className="flex-1 min-w-0">
<p className="font-medium text-white truncate">{vuln.title}</p>
<p className="text-xs text-dark-400 truncate">{vuln.affected_endpoint}</p>
</div>
<SeverityBadge severity={vuln.severity} />
</div>
))
)}
</div>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,572 @@
import { useState, useRef, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Upload, Link as LinkIcon, FileText, Play, AlertTriangle,
Bot, Search, Target, Brain, BookOpen, ChevronDown, Key, Settings
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import Input from '../components/common/Input'
import Textarea from '../components/common/Textarea'
import { agentApi, targetsApi } from '../services/api'
import type { AgentTask, AgentMode } from '../types'
type TargetInputMode = 'single' | 'multiple' | 'file'
interface OperationModeInfo {
id: AgentMode
name: string
icon: React.ReactNode
description: string
warning?: string
color: string
}
const OPERATION_MODES: OperationModeInfo[] = [
{
id: 'full_auto',
name: 'Full Auto',
icon: <Bot className="w-5 h-5" />,
description: 'Complete workflow: Recon -> Analyze -> Test -> Report',
color: 'primary'
},
{
id: 'recon_only',
name: 'Recon Only',
icon: <Search className="w-5 h-5" />,
description: 'Reconnaissance and enumeration only, no vulnerability testing',
color: 'blue'
},
{
id: 'prompt_only',
name: 'AI Prompt Mode',
icon: <Brain className="w-5 h-5" />,
description: 'AI decides everything based on your prompt - full autonomy',
warning: 'HIGH TOKEN USAGE - The AI will use more API calls to decide what to do',
color: 'purple'
},
{
id: 'analyze_only',
name: 'Analyze Only',
icon: <Target className="w-5 h-5" />,
description: 'Analyze provided data without active testing',
color: 'green'
}
]
const TASK_CATEGORIES = [
{ id: 'all', name: 'All Tasks' },
{ id: 'full_auto', name: 'Full Auto' },
{ id: 'recon', name: 'Reconnaissance' },
{ id: 'vulnerability', name: 'Vulnerability' },
{ id: 'custom', name: 'Custom' },
{ id: 'reporting', name: 'Reporting' }
]
export default function NewScanPage() {
const navigate = useNavigate()
const fileInputRef = useRef<HTMLInputElement>(null)
// Target state
const [targetMode, setTargetMode] = useState<TargetInputMode>('single')
const [singleUrl, setSingleUrl] = useState('')
const [multipleUrls, setMultipleUrls] = useState('')
const [uploadedUrls, setUploadedUrls] = useState<string[]>([])
const [urlError, setUrlError] = useState('')
// Operation mode
const [operationMode, setOperationMode] = useState<AgentMode>('full_auto')
// Task library
const [tasks, setTasks] = useState<AgentTask[]>([])
const [selectedTask, setSelectedTask] = useState<AgentTask | null>(null)
const [taskCategory, setTaskCategory] = useState('all')
const [showTaskLibrary, setShowTaskLibrary] = useState(false)
const [loadingTasks, setLoadingTasks] = useState(false)
// Custom prompt
const [useCustomPrompt, setUseCustomPrompt] = useState(false)
const [customPrompt, setCustomPrompt] = useState('')
// Auth options
const [showAuthOptions, setShowAuthOptions] = useState(false)
const [authType, setAuthType] = useState<'none' | 'cookie' | 'bearer' | 'basic' | 'header'>('none')
const [authValue, setAuthValue] = useState('')
// Advanced options
const [maxDepth, setMaxDepth] = useState(5)
// UI state
const [isLoading, setIsLoading] = useState(false)
// Load tasks on mount
useEffect(() => {
loadTasks()
}, [])
const loadTasks = async (category?: string) => {
setLoadingTasks(true)
try {
const taskList = await agentApi.tasks.list(category === 'all' ? undefined : category)
setTasks(taskList)
} catch (error) {
console.error('Failed to load tasks:', error)
} finally {
setLoadingTasks(false)
}
}
const handleCategoryChange = (category: string) => {
setTaskCategory(category)
loadTasks(category)
}
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
try {
const result = await targetsApi.upload(file)
const validUrls = result.filter((r: any) => r.valid).map((r: any) => r.normalized_url)
setUploadedUrls(validUrls)
setUrlError('')
} catch (error) {
setUrlError('Failed to parse file')
}
}
const getTargetUrl = (): string => {
switch (targetMode) {
case 'single':
return singleUrl.trim()
case 'multiple':
return multipleUrls.split(/[,\n]/)[0]?.trim() || ''
case 'file':
return uploadedUrls[0] || ''
default:
return ''
}
}
const handleStartAgent = async () => {
const target = getTargetUrl()
if (!target) {
setUrlError('Please enter a target URL')
return
}
setIsLoading(true)
try {
// Validate URL
const validation = await targetsApi.validateBulk([target])
if (!validation[0]?.valid) {
setUrlError('Invalid URL format')
setIsLoading(false)
return
}
// Build request
const request: any = {
target: validation[0].normalized_url,
mode: operationMode,
max_depth: maxDepth
}
// Add task or custom prompt
if (selectedTask && !useCustomPrompt) {
request.task_id = selectedTask.id
} else if (useCustomPrompt && customPrompt.trim()) {
request.prompt = customPrompt
}
// Add auth if specified
if (authType !== 'none' && authValue.trim()) {
request.auth_type = authType
request.auth_value = authValue
}
// Start agent
const response = await agentApi.run(request)
// Navigate to agent status page
navigate(`/agent/${response.agent_id}`)
} catch (error) {
console.error('Failed to start agent:', error)
setUrlError('Failed to start agent. Please try again.')
} finally {
setIsLoading(false)
}
}
const currentModeInfo = OPERATION_MODES.find(m => m.id === operationMode)!
return (
<div className="max-w-5xl mx-auto space-y-6 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
<Bot className="w-8 h-8 text-primary-500" />
AI Security Agent
</h1>
<p className="text-dark-400 mt-1">Autonomous penetration testing powered by AI</p>
</div>
</div>
{/* Operation Mode Selector */}
<Card title="Operation Mode" subtitle="Select how the AI agent should operate">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{OPERATION_MODES.map((mode) => (
<div
key={mode.id}
onClick={() => setOperationMode(mode.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
operationMode === mode.id
? `border-${mode.color}-500 bg-${mode.color}-500/10`
: 'border-dark-700 hover:border-dark-500 bg-dark-900/50'
}`}
>
<div className={`flex items-center gap-2 mb-2 ${
operationMode === mode.id ? `text-${mode.color}-400` : 'text-dark-300'
}`}>
{mode.icon}
<span className="font-semibold">{mode.name}</span>
</div>
<p className="text-sm text-dark-400">{mode.description}</p>
{mode.warning && operationMode === mode.id && (
<div className="mt-2 flex items-start gap-2 text-yellow-400 text-xs">
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
<span>{mode.warning}</span>
</div>
)}
</div>
))}
</div>
</Card>
{/* Target Input */}
<Card title="Target" subtitle="Enter the URL to test">
<div className="space-y-4">
{/* Mode Selector */}
<div className="flex gap-2">
<Button
variant={targetMode === 'single' ? 'primary' : 'secondary'}
onClick={() => setTargetMode('single')}
>
<LinkIcon className="w-4 h-4 mr-2" />
Single URL
</Button>
<Button
variant={targetMode === 'multiple' ? 'primary' : 'secondary'}
onClick={() => setTargetMode('multiple')}
>
<FileText className="w-4 h-4 mr-2" />
Multiple URLs
</Button>
<Button
variant={targetMode === 'file' ? 'primary' : 'secondary'}
onClick={() => setTargetMode('file')}
>
<Upload className="w-4 h-4 mr-2" />
Upload File
</Button>
</div>
{/* Input Fields */}
{targetMode === 'single' && (
<Input
placeholder="https://example.com"
value={singleUrl}
onChange={(e) => {
setSingleUrl(e.target.value)
setUrlError('')
}}
error={urlError}
/>
)}
{targetMode === 'multiple' && (
<div>
<Textarea
placeholder="Enter URLs separated by commas or new lines:&#10;https://example1.com&#10;https://example2.com"
rows={5}
value={multipleUrls}
onChange={(e) => {
setMultipleUrls(e.target.value)
setUrlError('')
}}
/>
<p className="text-xs text-dark-500 mt-1">Note: Agent will test the first URL. Multiple URL support coming soon.</p>
{urlError && <p className="mt-1 text-sm text-red-400">{urlError}</p>}
</div>
)}
{targetMode === 'file' && (
<div>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
accept=".txt,.csv,.lst"
className="hidden"
/>
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-dark-700 rounded-lg p-8 text-center cursor-pointer hover:border-primary-500 transition-colors"
>
<Upload className="w-10 h-10 mx-auto text-dark-400 mb-3" />
<p className="text-dark-300">Click to upload a file with URLs</p>
<p className="text-sm text-dark-500 mt-1">Supports .txt, .csv, .lst files</p>
</div>
{uploadedUrls.length > 0 && (
<p className="mt-2 text-sm text-green-400">
{uploadedUrls.length} valid URLs loaded - using first URL
</p>
)}
{urlError && <p className="mt-2 text-sm text-red-400">{urlError}</p>}
</div>
)}
</div>
</Card>
{/* Task Library */}
<Card
title={
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<BookOpen className="w-5 h-5 text-primary-500" />
<span>Task Library</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowTaskLibrary(!showTaskLibrary)}
>
<ChevronDown className={`w-4 h-4 transition-transform ${showTaskLibrary ? 'rotate-180' : ''}`} />
</Button>
</div>
}
subtitle="Select a preset task or create a custom prompt"
>
{/* Custom Prompt Toggle */}
<div className="flex items-center justify-between mb-4 pb-4 border-b border-dark-700">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="customPrompt"
checked={useCustomPrompt}
onChange={(e) => setUseCustomPrompt(e.target.checked)}
className="w-4 h-4 rounded border-dark-600 bg-dark-800 text-primary-500 focus:ring-primary-500"
/>
<label htmlFor="customPrompt" className="text-white">Use custom prompt instead of task</label>
</div>
</div>
{useCustomPrompt ? (
<Textarea
placeholder="Enter your custom prompt for the AI agent...&#10;&#10;Example: Test for SQL injection on all form inputs, check for authentication bypass on the login endpoint, and look for IDOR vulnerabilities in user profile APIs."
rows={6}
value={customPrompt}
onChange={(e) => setCustomPrompt(e.target.value)}
/>
) : (
<>
{showTaskLibrary && (
<>
{/* Category Filter */}
<div className="flex gap-2 mb-4 flex-wrap">
{TASK_CATEGORIES.map((cat) => (
<Button
key={cat.id}
variant={taskCategory === cat.id ? 'primary' : 'secondary'}
size="sm"
onClick={() => handleCategoryChange(cat.id)}
>
{cat.name}
</Button>
))}
</div>
</>
)}
{/* Tasks Grid */}
<div className={`grid grid-cols-1 md:grid-cols-2 gap-3 ${showTaskLibrary ? 'max-h-80 overflow-auto' : ''}`}>
{loadingTasks ? (
<p className="text-dark-400 col-span-2 text-center py-4">Loading tasks...</p>
) : (
(showTaskLibrary ? tasks : tasks.slice(0, 4)).map((task) => (
<div
key={task.id}
onClick={() => setSelectedTask(task)}
className={`p-4 rounded-lg border cursor-pointer transition-all ${
selectedTask?.id === task.id
? 'border-primary-500 bg-primary-500/10'
: 'border-dark-700 hover:border-dark-500 bg-dark-900/50'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-white">{task.name}</span>
{task.is_preset && (
<span className="text-xs bg-primary-500/20 text-primary-400 px-2 py-0.5 rounded">Preset</span>
)}
</div>
<p className="text-sm text-dark-400 line-clamp-2">{task.description}</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-xs text-dark-500">{task.category}</span>
{task.estimated_tokens > 0 && (
<span className="text-xs text-dark-500">~{task.estimated_tokens} tokens</span>
)}
</div>
{task.tags?.length > 0 && (
<div className="flex gap-1 mt-2 flex-wrap">
{task.tags.slice(0, 3).map((tag) => (
<span key={tag} className="text-xs bg-dark-700 text-dark-300 px-2 py-0.5 rounded">
{tag}
</span>
))}
</div>
)}
</div>
))
)}
</div>
{!showTaskLibrary && tasks.length > 4 && (
<Button
variant="ghost"
className="w-full mt-3"
onClick={() => setShowTaskLibrary(true)}
>
Show all {tasks.length} tasks
</Button>
)}
</>
)}
{/* Selected Task Preview */}
{selectedTask && !useCustomPrompt && (
<div className="mt-4 p-4 bg-dark-800 rounded-lg border border-dark-700">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-white">Selected: {selectedTask.name}</span>
<Button variant="ghost" size="sm" onClick={() => setSelectedTask(null)}>
Clear
</Button>
</div>
<p className="text-sm text-dark-400 whitespace-pre-wrap line-clamp-4">
{selectedTask.prompt}
</p>
</div>
)}
</Card>
{/* Authentication Options */}
<Card
title={
<div className="flex items-center gap-2">
<Key className="w-5 h-5 text-primary-500" />
<span>Authentication</span>
<span className="text-xs text-dark-500">(Optional)</span>
</div>
}
>
<div className="space-y-4">
<div className="flex gap-2 flex-wrap">
{[
{ id: 'none', label: 'None' },
{ id: 'cookie', label: 'Cookie' },
{ id: 'bearer', label: 'Bearer Token' },
{ id: 'basic', label: 'Basic Auth' },
{ id: 'header', label: 'Custom Header' }
].map((type) => (
<Button
key={type.id}
variant={authType === type.id ? 'primary' : 'secondary'}
size="sm"
onClick={() => setAuthType(type.id as any)}
>
{type.label}
</Button>
))}
</div>
{authType !== 'none' && (
<Input
placeholder={
authType === 'cookie' ? 'session=abc123; token=xyz789' :
authType === 'bearer' ? 'eyJhbGciOiJIUzI1NiIs...' :
authType === 'basic' ? 'username:password' :
'X-API-Key: your-api-key'
}
value={authValue}
onChange={(e) => setAuthValue(e.target.value)}
label={
authType === 'cookie' ? 'Cookie String' :
authType === 'bearer' ? 'Bearer Token' :
authType === 'basic' ? 'Username:Password' :
'Header:Value'
}
/>
)}
</div>
</Card>
{/* Advanced Options */}
<Card
title={
<div className="flex items-center gap-2 cursor-pointer" onClick={() => setShowAuthOptions(!showAuthOptions)}>
<Settings className="w-5 h-5 text-primary-500" />
<span>Advanced Options</span>
<ChevronDown className={`w-4 h-4 transition-transform ${showAuthOptions ? 'rotate-180' : ''}`} />
</div>
}
>
{showAuthOptions && (
<div className="space-y-4">
<div>
<label className="text-sm text-dark-300 mb-1 block">Max Crawl Depth</label>
<div className="flex items-center gap-4">
<input
type="range"
min="1"
max="10"
value={maxDepth}
onChange={(e) => setMaxDepth(parseInt(e.target.value))}
className="flex-1"
/>
<span className="text-white font-medium w-8">{maxDepth}</span>
</div>
</div>
</div>
)}
{!showAuthOptions && (
<p className="text-dark-500 text-sm">Click to expand advanced options</p>
)}
</Card>
{/* Warning for Prompt Only Mode */}
{operationMode === 'prompt_only' && (
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4 flex items-start gap-3">
<AlertTriangle className="w-6 h-6 text-yellow-500 flex-shrink-0" />
<div>
<p className="font-medium text-yellow-400">High Token Usage Warning</p>
<p className="text-sm text-yellow-300/80 mt-1">
In AI Prompt Mode, the agent has full autonomy to decide what tools to use and what tests to run.
This results in significantly higher API token consumption. Consider using Full Auto mode for most use cases.
</p>
</div>
</div>
)}
{/* Start Button */}
<div className="flex justify-end gap-3 sticky bottom-4 bg-dark-950/90 backdrop-blur p-4 -mx-4 rounded-lg">
<Button variant="secondary" onClick={() => navigate('/')}>
Cancel
</Button>
<Button onClick={handleStartAgent} isLoading={isLoading} size="lg">
<Play className="w-5 h-5 mr-2" />
Deploy Agent ({currentModeInfo.name})
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,930 @@
import { useEffect, useState, useRef } from 'react'
import {
MessageSquare, Send, Target, Shield, Trash2,
RefreshCw, XCircle, Globe, Bot, ChevronDown, ChevronRight, Plus,
Terminal, Zap, Search, AlertCircle, CheckCircle2, FileText, Wrench,
ExternalLink, Code
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import { SeverityBadge } from '../components/common/Badge'
import { agentApi } from '../services/api'
import type { RealtimeSession, RealtimeMessage, RealtimeSessionSummary } from '../types'
const SEVERITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }
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'
}
export default function RealtimeTaskPage() {
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
// Session state
const [sessions, setSessions] = useState<RealtimeSessionSummary[]>([])
const [activeSession, setActiveSession] = useState<RealtimeSession | null>(null)
const [error, setError] = useState<string | null>(null)
// New session form
const [showNewSession, setShowNewSession] = useState(false)
const [newTarget, setNewTarget] = useState('')
const [newSessionName, setNewSessionName] = useState('')
const [isCreating, setIsCreating] = useState(false)
// Chat state
const [message, setMessage] = useState('')
const [isSending, setIsSending] = useState(false)
const [expandedFindings, setExpandedFindings] = useState<Set<number>>(new Set())
const [expandedMessages, setExpandedMessages] = useState<Set<number>>(new Set())
// LLM status
const [llmStatus, setLlmStatus] = useState<{
available: boolean
provider: string | null
error: string | null
} | null>(null)
// Tools state
const [showToolsModal, setShowToolsModal] = useState(false)
const [toolsStatus, setToolsStatus] = useState<{ available: boolean; docker_status: string } | null>(null)
const [executingTool, setExecutingTool] = useState<string | null>(null)
// Report state
const [generatingReport, setGeneratingReport] = useState(false)
// Message truncation config
const MESSAGE_MAX_LENGTH = 600
// Quick prompts for common tasks
const quickPrompts = [
{ label: 'Security Headers', prompt: 'Analyze security headers and identify misconfigurations', icon: '🛡️' },
{ label: 'Full Scan', prompt: 'Perform a comprehensive security assessment including headers, cookies, CORS, and endpoint discovery', icon: '🔍' },
{ label: 'XSS Test', prompt: 'Test for Cross-Site Scripting (XSS) vulnerabilities in all input fields', icon: '💉' },
{ label: 'SQL Injection', prompt: 'Check for SQL injection vulnerabilities in parameters and forms', icon: '🗃️' },
{ label: 'Directory Enum', prompt: 'Discover hidden directories, files, and endpoints using common wordlists', icon: '📁' },
{ label: 'Tech Stack', prompt: 'Detect technologies, frameworks, and versions used by this application', icon: '⚙️' },
]
// Tool categories with icons
const toolPrompts = [
{ tool: 'ffuf', label: 'FFUF', description: 'Fast web fuzzer', icon: '⚡' },
{ tool: 'feroxbuster', label: 'Feroxbuster', description: 'Directory brute-force', icon: '🦀' },
{ tool: 'nuclei', label: 'Nuclei', description: 'Vulnerability scanner', icon: '☢️' },
{ tool: 'nmap', label: 'Nmap', description: 'Port scanner', icon: '🔌' },
{ tool: 'nikto', label: 'Nikto', description: 'Web server scanner', icon: '🕷️' },
{ tool: 'httpx', label: 'HTTPX', description: 'HTTP toolkit', icon: '🌐' },
]
// Load sessions and check status on mount
useEffect(() => {
loadSessions()
checkLlmStatus()
loadToolsInfo()
}, [])
const checkLlmStatus = async () => {
try {
const status = await agentApi.realtime.getLlmStatus()
setLlmStatus({
available: status.available,
provider: status.provider,
error: status.error
})
} catch (err) {
console.error('Failed to check LLM status:', err)
setLlmStatus({
available: false,
provider: null,
error: 'Failed to connect to backend'
})
}
}
const loadToolsInfo = async () => {
try {
const status = await agentApi.realtime.getToolsStatus()
setToolsStatus(status)
} catch (err) {
console.error('Failed to load tools info:', err)
}
}
// Auto-scroll to bottom when messages change
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [activeSession?.messages])
const loadSessions = async () => {
try {
const data = await agentApi.realtime.listSessions()
setSessions(data.sessions || [])
} catch (err) {
console.error('Failed to load sessions:', err)
}
}
const createSession = async () => {
if (!newTarget.trim()) return
setIsCreating(true)
setError(null)
try {
const result = await agentApi.realtime.createSession(newTarget, newSessionName || undefined)
await loadSessions()
await loadSession(result.session_id)
setShowNewSession(false)
setNewTarget('')
setNewSessionName('')
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to create session')
} finally {
setIsCreating(false)
}
}
const loadSession = async (sessionId: string) => {
setError(null)
try {
const data = await agentApi.realtime.getSession(sessionId)
setActiveSession(data)
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to load session')
}
}
const sendMessage = async (prompt?: string) => {
const messageToSend = prompt || message
if (!messageToSend.trim() || !activeSession) return
setIsSending(true)
setMessage('')
// Optimistically add user message
const userMessage: RealtimeMessage = {
role: 'user',
content: messageToSend,
timestamp: new Date().toISOString()
}
setActiveSession(prev => prev ? {
...prev,
messages: [...prev.messages, userMessage]
} : null)
try {
const result = await agentApi.realtime.sendMessage(activeSession.session_id, messageToSend)
// Add assistant response
const assistantMessage: RealtimeMessage = {
role: 'assistant',
content: result.response,
timestamp: new Date().toISOString(),
metadata: { tests_executed: result.tests_executed }
}
setActiveSession(prev => prev ? {
...prev,
messages: [...prev.messages, assistantMessage],
findings: result.findings || prev.findings
} : null)
// Focus input for next message
inputRef.current?.focus()
} catch (err: any) {
// Add error message
const errorMessage: RealtimeMessage = {
role: 'assistant',
content: `Error: ${err.response?.data?.detail || err.message || 'Failed to send message'}`,
timestamp: new Date().toISOString(),
metadata: { error: true }
}
setActiveSession(prev => prev ? {
...prev,
messages: [...prev.messages, errorMessage]
} : null)
} finally {
setIsSending(false)
}
}
const executeTool = async (toolId: string) => {
if (!activeSession) return
setExecutingTool(toolId)
setShowToolsModal(false)
// Add user message about tool execution
const userMessage: RealtimeMessage = {
role: 'user',
content: `Execute ${toolId} scan on target`,
timestamp: new Date().toISOString()
}
setActiveSession(prev => prev ? {
...prev,
messages: [...prev.messages, userMessage]
} : null)
try {
await agentApi.realtime.executeTool(activeSession.session_id, toolId)
// Reload session to get updated messages and findings
await loadSession(activeSession.session_id)
} catch (err: any) {
const errorMessage: RealtimeMessage = {
role: 'assistant',
content: `Tool execution failed: ${err.response?.data?.detail || err.message}`,
timestamp: new Date().toISOString(),
metadata: { error: true }
}
setActiveSession(prev => prev ? {
...prev,
messages: [...prev.messages, errorMessage]
} : null)
} finally {
setExecutingTool(null)
}
}
const deleteSession = async (sessionId: string) => {
if (!confirm('Delete this session?')) return
try {
await agentApi.realtime.deleteSession(sessionId)
if (activeSession?.session_id === sessionId) {
setActiveSession(null)
}
await loadSessions()
} catch (err) {
console.error('Failed to delete session:', err)
}
}
const downloadReportHtml = async () => {
if (!activeSession) return
setGeneratingReport(true)
try {
const htmlContent = await agentApi.realtime.getReportHtml(activeSession.session_id)
// Open in new tab
const newWindow = window.open('', '_blank')
if (newWindow) {
newWindow.document.write(htmlContent)
newWindow.document.close()
}
} catch (err) {
console.error('Failed to generate HTML report:', err)
} finally {
setGeneratingReport(false)
}
}
const downloadReportJson = async () => {
if (!activeSession) return
try {
const report = await agentApi.realtime.getReport(activeSession.session_id)
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `report-${activeSession.session_id}-${new Date().toISOString().split('T')[0]}.json`
a.click()
URL.revokeObjectURL(url)
} catch (err) {
console.error('Failed to generate report:', err)
}
}
const toggleFinding = (index: number) => {
const newExpanded = new Set(expandedFindings)
if (newExpanded.has(index)) {
newExpanded.delete(index)
} else {
newExpanded.add(index)
}
setExpandedFindings(newExpanded)
}
const toggleMessage = (index: number) => {
const newExpanded = new Set(expandedMessages)
if (newExpanded.has(index)) {
newExpanded.delete(index)
} else {
newExpanded.add(index)
}
setExpandedMessages(newExpanded)
}
const renderMessage = (msg: RealtimeMessage, index: number) => {
const isUser = msg.role === 'user'
const isError = msg.metadata?.error
const isApiError = msg.metadata?.api_error
const isToolExecution = msg.metadata?.tool_execution
const isExpanded = expandedMessages.has(index)
const shouldTruncate = !isUser && msg.content.length > MESSAGE_MAX_LENGTH && !isExpanded
const displayContent = shouldTruncate
? msg.content.substring(0, MESSAGE_MAX_LENGTH) + '...'
: msg.content
return (
<div
key={index}
className={`flex ${isUser ? 'justify-end' : 'justify-start'} animate-fadeIn`}
>
<div
className={`max-w-[85%] rounded-xl px-4 py-3 shadow-lg overflow-hidden ${
isUser
? 'bg-gradient-to-r from-primary-600 to-primary-500 text-white'
: isError || isApiError
? 'bg-red-500/10 border border-red-500/30 text-red-300'
: isToolExecution
? 'bg-purple-500/10 border border-purple-500/30 text-purple-200'
: 'bg-dark-800/80 border border-dark-700 text-dark-200'
}`}
>
{!isUser && (
<div className="flex items-center gap-2 mb-2 text-xs text-dark-400">
{isToolExecution ? (
<Wrench className="w-3 h-3 text-purple-400" />
) : (
<Bot className="w-3 h-3" />
)}
<span>{isToolExecution ? 'Tool Execution' : 'NeuroSploit AI'}</span>
{isApiError && (
<span className="bg-red-500/20 text-red-400 px-1.5 py-0.5 rounded text-[10px]">
API Error
</span>
)}
{msg.metadata?.tests_executed && (
<span className="bg-green-500/20 text-green-400 px-1.5 py-0.5 rounded text-[10px]">
Tests Executed
</span>
)}
{msg.metadata?.new_findings && msg.metadata.new_findings > 0 && (
<span className="bg-orange-500/20 text-orange-400 px-1.5 py-0.5 rounded text-[10px]">
+{msg.metadata.new_findings} findings
</span>
)}
</div>
)}
<div className="whitespace-pre-wrap text-sm leading-relaxed prose prose-invert prose-sm max-w-none break-words overflow-x-auto">
{displayContent}
</div>
{!isUser && msg.content.length > MESSAGE_MAX_LENGTH && (
<button
onClick={() => toggleMessage(index)}
className="mt-3 text-xs text-primary-400 hover:text-primary-300 transition-colors flex items-center gap-1 font-medium"
>
{isExpanded ? (
<>
<ChevronDown className="w-3 h-3" />
Show less
</>
) : (
<>
<ChevronRight className="w-3 h-3" />
Show more ({Math.ceil((msg.content.length - MESSAGE_MAX_LENGTH) / 100) * 100}+ chars)
</>
)}
</button>
)}
<div className={`text-[10px] mt-2 ${isUser ? 'text-primary-200' : 'text-dark-500'}`}>
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
)
}
const sortedFindings = activeSession?.findings
? [...activeSession.findings].sort((a, b) =>
(SEVERITY_ORDER[a.severity] || 4) - (SEVERITY_ORDER[b.severity] || 4)
)
: []
// Calculate severity stats
const severityStats = sortedFindings.reduce((acc, f) => {
const sev = f.severity?.toLowerCase() || 'info'
acc[sev] = (acc[sev] || 0) + 1
return acc
}, {} as Record<string, number>)
return (
<div className="space-y-6 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-3">
<Zap className="w-7 h-7 text-yellow-500" />
Real-time Task
</h2>
<p className="text-dark-400 mt-1">
Interactive AI-powered security testing with real tool execution
</p>
</div>
<div className="flex items-center gap-3 flex-wrap">
{/* LLM Status Indicator */}
{llmStatus && (
<div
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs cursor-help ${
llmStatus.available
? 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'bg-red-500/20 text-red-400 border border-red-500/30'
}`}
title={llmStatus.error || `Connected to ${llmStatus.provider}`}
>
{llmStatus.available ? (
<>
<CheckCircle2 className="w-3.5 h-3.5" />
<span className="font-medium">{llmStatus.provider?.toUpperCase()}</span>
</>
) : (
<>
<AlertCircle className="w-3.5 h-3.5" />
<span>No AI</span>
</>
)}
</div>
)}
{/* Docker Status */}
{toolsStatus && (
<div
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs cursor-help ${
toolsStatus.available
? 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
: 'bg-dark-700 text-dark-400 border border-dark-600'
}`}
title={toolsStatus.available ? 'Docker tools ready' : 'Docker not available'}
>
<Terminal className="w-3.5 h-3.5" />
<span>{toolsStatus.available ? 'Tools Ready' : 'No Docker'}</span>
</div>
)}
<Button onClick={() => setShowNewSession(true)}>
<Plus className="w-4 h-4 mr-2" />
New Session
</Button>
</div>
</div>
{/* New Session Modal */}
{showNewSession && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
<Card className="w-full max-w-md mx-4 shadow-2xl">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Target className="w-5 h-5 text-primary-500" />
Create New Session
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm text-dark-300 mb-1">Target URL *</label>
<input
type="text"
value={newTarget}
onChange={(e) => setNewTarget(e.target.value)}
placeholder="https://example.com"
className="w-full bg-dark-800 border border-dark-600 rounded-lg px-4 py-2.5 text-white placeholder-dark-400 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
autoFocus
/>
</div>
<div>
<label className="block text-sm text-dark-300 mb-1">Session Name (optional)</label>
<input
type="text"
value={newSessionName}
onChange={(e) => setNewSessionName(e.target.value)}
placeholder="My Security Test"
className="w-full bg-dark-800 border border-dark-600 rounded-lg px-4 py-2.5 text-white placeholder-dark-400 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
/>
</div>
{error && (
<div className="text-red-400 text-sm flex items-center gap-2 bg-red-500/10 p-3 rounded-lg">
<XCircle className="w-4 h-4 flex-shrink-0" />
{error}
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={() => setShowNewSession(false)}>
Cancel
</Button>
<Button onClick={createSession} isLoading={isCreating} disabled={!newTarget.trim()}>
Create Session
</Button>
</div>
</div>
</Card>
</div>
)}
{/* Tools Modal */}
{showToolsModal && activeSession && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
<Card className="w-full max-w-2xl mx-4 shadow-2xl">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Wrench className="w-5 h-5 text-purple-500" />
Execute Security Tool
</h3>
<button onClick={() => setShowToolsModal(false)} className="text-dark-400 hover:text-white">
<XCircle className="w-5 h-5" />
</button>
</div>
<p className="text-dark-400 text-sm mb-4">
Target: <span className="text-white font-mono">{activeSession.target}</span>
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{toolPrompts.map((tool) => (
<button
key={tool.tool}
onClick={() => executeTool(tool.tool)}
disabled={executingTool !== null || !toolsStatus?.available}
className="p-4 bg-dark-800 hover:bg-dark-700 border border-dark-600 hover:border-purple-500/50 rounded-xl transition-all text-left disabled:opacity-50 disabled:cursor-not-allowed group"
>
<div className="text-2xl mb-2">{tool.icon}</div>
<div className="font-medium text-white group-hover:text-purple-400 transition-colors">
{tool.label}
</div>
<div className="text-xs text-dark-400 mt-1">{tool.description}</div>
</button>
))}
</div>
{!toolsStatus?.available && (
<div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-yellow-400 text-sm">
<AlertCircle className="w-4 h-4 inline mr-2" />
Docker is not available. Tools require Docker to be running.
</div>
)}
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sessions List */}
<div className="lg:col-span-1">
<Card title="Sessions" className="h-fit">
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{sessions.length === 0 ? (
<p className="text-dark-400 text-sm text-center py-4">
No sessions yet. Create one to start testing.
</p>
) : (
sessions.map((s) => (
<div
key={s.session_id}
className={`p-3 rounded-xl cursor-pointer transition-all ${
activeSession?.session_id === s.session_id
? 'bg-primary-500/20 border border-primary-500/50 shadow-lg shadow-primary-500/10'
: 'bg-dark-800/50 hover:bg-dark-800 border border-transparent hover:border-dark-600'
}`}
onClick={() => loadSession(s.session_id)}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate text-sm">{s.name}</p>
<p className="text-dark-400 text-xs truncate flex items-center gap-1 mt-0.5">
<Globe className="w-3 h-3" />
{s.target}
</p>
</div>
<button
onClick={(e) => {
e.stopPropagation()
deleteSession(s.session_id)
}}
className="p-1.5 text-dark-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex items-center gap-2 mt-2 text-xs">
<span className="text-dark-400">{s.messages_count} msgs</span>
{s.findings_count > 0 && (
<span className="bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-medium">
{s.findings_count} findings
</span>
)}
</div>
</div>
))
)}
</div>
</Card>
</div>
{/* Chat Area */}
<div className="lg:col-span-2">
{activeSession ? (
<Card className="flex flex-col h-[calc(100vh-220px)]">
{/* Session Header */}
<div className="flex items-center justify-between pb-4 border-b border-dark-700">
<div className="flex-1 min-w-0">
<h3 className="text-white font-medium flex items-center gap-2">
<Target className="w-4 h-4 text-primary-500 flex-shrink-0" />
<span className="truncate">{activeSession.name}</span>
</h3>
<p className="text-dark-400 text-sm truncate">{activeSession.target}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="secondary"
size="sm"
onClick={() => setShowToolsModal(true)}
disabled={executingTool !== null}
>
<Wrench className="w-4 h-4 mr-1" />
Tools
</Button>
<div className="relative group">
<Button
variant="secondary"
size="sm"
onClick={downloadReportHtml}
isLoading={generatingReport}
>
<FileText className="w-4 h-4 mr-1" />
Report
</Button>
{/* Dropdown for report options */}
<div className="absolute right-0 mt-1 w-40 bg-dark-800 border border-dark-600 rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-10">
<button
onClick={downloadReportHtml}
className="w-full px-3 py-2 text-left text-sm text-dark-300 hover:bg-dark-700 hover:text-white rounded-t-lg flex items-center gap-2"
>
<ExternalLink className="w-3 h-3" />
Open HTML Report
</button>
<button
onClick={downloadReportJson}
className="w-full px-3 py-2 text-left text-sm text-dark-300 hover:bg-dark-700 hover:text-white rounded-b-lg flex items-center gap-2"
>
<Code className="w-3 h-3" />
Download JSON
</button>
</div>
</div>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto py-4 space-y-4 scroll-smooth">
{activeSession.messages.length === 0 ? (
<div className="text-center py-12">
<MessageSquare className="w-12 h-12 text-dark-600 mx-auto mb-3" />
<p className="text-dark-400">
Start by typing a security testing instruction or use a quick prompt
</p>
</div>
) : (
activeSession.messages.map((msg, i) => renderMessage(msg, i))
)}
{(isSending || executingTool) && (
<div className="flex justify-start">
<div className="bg-dark-800 border border-dark-700 rounded-xl px-4 py-3 flex items-center gap-3 text-dark-400">
<RefreshCw className="w-4 h-4 animate-spin text-primary-500" />
<span>{executingTool ? `Running ${executingTool}...` : 'Analyzing and testing...'}</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick Prompts */}
<div className="flex flex-wrap gap-2 py-3 border-t border-dark-700">
{quickPrompts.map((qp, i) => (
<button
key={i}
onClick={() => sendMessage(qp.prompt)}
disabled={isSending || executingTool !== null}
className="px-3 py-1.5 text-xs bg-dark-800 hover:bg-dark-700 text-dark-300 hover:text-white rounded-full transition-all disabled:opacity-50 flex items-center gap-1.5 border border-dark-700 hover:border-dark-600"
>
<span>{qp.icon}</span>
{qp.label}
</button>
))}
</div>
{/* Input */}
<div className="flex gap-2 pt-2">
<input
ref={inputRef}
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && sendMessage()}
placeholder="Type your security testing instruction..."
disabled={isSending || executingTool !== null}
className="flex-1 bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white placeholder-dark-400 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 disabled:opacity-50"
/>
<Button
onClick={() => sendMessage()}
isLoading={isSending}
disabled={!message.trim() || executingTool !== null}
className="px-4"
>
<Send className="w-4 h-4" />
</Button>
</div>
</Card>
) : (
<Card className="flex flex-col items-center justify-center h-[calc(100vh-220px)]">
<div className="text-center">
<div className="w-20 h-20 bg-gradient-to-br from-primary-500/20 to-purple-500/20 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Terminal className="w-10 h-10 text-primary-400" />
</div>
<h3 className="text-white text-lg font-medium mb-2">No Session Selected</h3>
<p className="text-dark-400 text-center mb-6 max-w-sm">
Select a session from the list or create a new one to start interactive security testing
</p>
<Button onClick={() => setShowNewSession(true)} className="mx-auto">
<Plus className="w-4 h-4 mr-2" />
Create New Session
</Button>
</div>
</Card>
)}
</div>
{/* Findings Panel */}
<div className="lg:col-span-1 space-y-4">
{/* Severity Summary */}
{sortedFindings.length > 0 && (
<Card className="!p-3">
<div className="flex items-center gap-2 flex-wrap">
{['critical', 'high', 'medium', 'low', 'info'].map(sev => {
const count = severityStats[sev] || 0
if (count === 0) return null
return (
<div
key={sev}
className={`px-2 py-1 rounded-lg text-xs font-medium flex items-center gap-1 ${
sev === 'critical' ? 'bg-red-500/20 text-red-400' :
sev === 'high' ? 'bg-orange-500/20 text-orange-400' :
sev === 'medium' ? 'bg-yellow-500/20 text-yellow-400' :
sev === 'low' ? 'bg-blue-500/20 text-blue-400' :
'bg-gray-500/20 text-gray-400'
}`}
>
<span className={`w-2 h-2 rounded-full ${SEVERITY_COLORS[sev]}`} />
{count} {sev}
</div>
)
})}
</div>
</Card>
)}
<Card
title={
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-red-400" />
<span>Findings</span>
{sortedFindings.length > 0 && (
<span className="bg-red-500/20 text-red-400 text-xs px-2 py-0.5 rounded-full font-medium">
{sortedFindings.length}
</span>
)}
</div>
}
className="h-fit max-h-[calc(100vh-320px)] overflow-y-auto"
>
{sortedFindings.length === 0 ? (
<div className="text-center py-8">
<Search className="w-8 h-8 text-dark-600 mx-auto mb-2" />
<p className="text-dark-400 text-sm">
No findings yet. Send a testing instruction to discover vulnerabilities.
</p>
</div>
) : (
<div className="space-y-2">
{sortedFindings.map((finding, i) => (
<div
key={i}
className="bg-dark-900/50 rounded-xl border border-dark-700 overflow-hidden hover:border-dark-600 transition-colors"
>
<div
className="p-3 cursor-pointer hover:bg-dark-800/50 transition-colors"
onClick={() => toggleFinding(i)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-2 flex-1 min-w-0">
{expandedFindings.has(i) ? (
<ChevronDown className="w-4 h-4 mt-0.5 text-dark-400 flex-shrink-0" />
) : (
<ChevronRight className="w-4 h-4 mt-0.5 text-dark-400 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{finding.title}</p>
<div className="flex items-center gap-2 mt-0.5">
<p className="text-xs text-dark-400 truncate flex-1">{finding.affected_endpoint}</p>
{finding.cvss_score && (
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded ${
finding.cvss_score >= 9.0 ? 'bg-red-500/20 text-red-400' :
finding.cvss_score >= 7.0 ? 'bg-orange-500/20 text-orange-400' :
finding.cvss_score >= 4.0 ? 'bg-yellow-500/20 text-yellow-400' :
'bg-blue-500/20 text-blue-400'
}`}>
{finding.cvss_score}
</span>
)}
</div>
</div>
</div>
<SeverityBadge severity={finding.severity} />
</div>
</div>
{expandedFindings.has(i) && (
<div className="px-3 pb-3 pt-0 space-y-3 border-t border-dark-700 overflow-hidden">
{/* CVSS/CWE/OWASP badges */}
{(finding.cvss_score || finding.cwe_id || finding.owasp) && (
<div className="mt-3 flex flex-wrap gap-2">
{finding.cvss_score && (
<div className="bg-dark-800 px-2 py-1 rounded text-xs">
<span className="text-dark-500">CVSS:</span>{' '}
<span className={`font-bold ${
finding.cvss_score >= 9.0 ? 'text-red-400' :
finding.cvss_score >= 7.0 ? 'text-orange-400' :
finding.cvss_score >= 4.0 ? 'text-yellow-400' :
'text-blue-400'
}`}>{finding.cvss_score}</span>
</div>
)}
{finding.cwe_id && (
<div className="bg-dark-800 px-2 py-1 rounded text-xs">
<span className="text-dark-500">CWE:</span>{' '}
<span className="text-blue-400">{finding.cwe_id}</span>
</div>
)}
{finding.owasp && (
<div className="bg-dark-800 px-2 py-1 rounded text-xs">
<span className="text-dark-500">OWASP:</span>{' '}
<span className="text-yellow-400 truncate">{finding.owasp.split(' - ')[0]}</span>
</div>
)}
</div>
)}
<div className="mt-3">
<p className="text-[10px] text-dark-500 uppercase tracking-wider font-medium">Type</p>
<p className="text-sm text-dark-300 break-words">{finding.vulnerability_type}</p>
</div>
<div>
<p className="text-[10px] text-dark-500 uppercase tracking-wider font-medium">Description</p>
<p className="text-sm text-dark-300 break-words">{finding.description}</p>
</div>
{finding.evidence && (
<div>
<p className="text-[10px] text-dark-500 uppercase tracking-wider font-medium">Evidence</p>
<p className="text-sm text-dark-300 font-mono bg-dark-800 p-2 rounded-lg text-xs overflow-x-auto break-all max-h-32 overflow-y-auto">
{finding.evidence}
</p>
</div>
)}
<div>
<p className="text-[10px] text-dark-500 uppercase tracking-wider font-medium">Remediation</p>
<p className="text-sm text-green-400 break-words">{finding.remediation}</p>
</div>
</div>
)}
</div>
))}
</div>
)}
</Card>
{/* Technologies Detected */}
{activeSession && activeSession.recon_data?.technologies && activeSession.recon_data.technologies.length > 0 && (
<Card title="Technologies" className="!p-4">
<div className="flex flex-wrap gap-2">
{activeSession.recon_data.technologies.map((tech, i) => (
<span
key={i}
className="px-2.5 py-1 text-xs bg-dark-800 text-dark-300 rounded-lg border border-dark-700"
>
{tech}
</span>
))}
</div>
</Card>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Download, ExternalLink } from 'lucide-react'
import Button from '../components/common/Button'
import { reportsApi } from '../services/api'
export default function ReportViewPage() {
const { reportId } = useParams<{ reportId: string }>()
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
if (!reportId) {
navigate('/reports')
return
}
setIsLoading(false)
}, [reportId])
if (isLoading || !reportId) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full" />
</div>
)
}
return (
<div className="space-y-4 animate-fadeIn">
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => navigate('/reports')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Reports
</Button>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={() => window.open(reportsApi.getDownloadUrl(reportId, 'html'), '_blank')}
>
<Download className="w-4 h-4 mr-2" />
Download HTML
</Button>
<Button
variant="secondary"
onClick={() => window.open(reportsApi.getDownloadUrl(reportId, 'json'), '_blank')}
>
<Download className="w-4 h-4 mr-2" />
Download JSON
</Button>
<Button
onClick={() => window.open(reportsApi.getViewUrl(reportId), '_blank')}
>
<ExternalLink className="w-4 h-4 mr-2" />
Open in New Tab
</Button>
</div>
</div>
<div className="bg-dark-800 rounded-xl overflow-hidden border border-dark-900/50">
<iframe
src={reportsApi.getViewUrl(reportId)}
className="w-full h-[calc(100vh-200px)]"
title="Report"
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,148 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { FileText, Download, Eye, Trash2, Calendar } from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import { reportsApi, scansApi } from '../services/api'
import type { Report, Scan } from '../types'
export default function ReportsPage() {
const [reports, setReports] = useState<Report[]>([])
const [scans, setScans] = useState<Map<string, Scan>>(new Map())
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const fetchData = async () => {
try {
const [reportsData, scansData] = await Promise.all([
reportsApi.list(),
scansApi.list(1, 100)
])
setReports(reportsData.reports)
const scansMap = new Map<string, Scan>()
scansData.scans.forEach((scan: Scan) => scansMap.set(scan.id, scan))
setScans(scansMap)
} catch (error) {
console.error('Failed to fetch reports:', error)
} finally {
setIsLoading(false)
}
}
fetchData()
}, [])
const handleDelete = async (reportId: string) => {
if (!confirm('Are you sure you want to delete this report?')) return
try {
await reportsApi.delete(reportId)
setReports(reports.filter((r) => r.id !== reportId))
} catch (error) {
console.error('Failed to delete report:', error)
}
}
const handleDownload = (reportId: string, format: string) => {
window.open(reportsApi.getDownloadUrl(reportId, format), '_blank')
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full" />
</div>
)
}
return (
<div className="space-y-6 animate-fadeIn">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Reports</h2>
<p className="text-dark-400 mt-1">View and download security assessment reports</p>
</div>
</div>
{reports.length === 0 ? (
<Card>
<div className="text-center py-12">
<FileText className="w-16 h-16 mx-auto text-dark-500 mb-4" />
<h3 className="text-lg font-medium text-white mb-2">No Reports Yet</h3>
<p className="text-dark-400 mb-4">
Reports are generated after completing a security scan.
</p>
<Link to="/scan/new">
<Button>Start a New Scan</Button>
</Link>
</div>
</Card>
) : (
<div className="grid gap-4">
{reports.map((report) => {
const scan = scans.get(report.scan_id)
return (
<Card key={report.id}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-primary-500/10 rounded-lg">
<FileText className="w-6 h-6 text-primary-500" />
</div>
<div>
<h3 className="font-medium text-white">
{report.title || scan?.name || 'Security Report'}
</h3>
<div className="flex items-center gap-3 mt-1 text-sm text-dark-400">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{new Date(report.generated_at).toLocaleDateString()}
</span>
<span className="uppercase text-xs bg-dark-700 px-2 py-0.5 rounded">
{report.format}
</span>
{scan && (
<span>
{scan.total_vulnerabilities} vulnerabilities
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
onClick={() => window.open(reportsApi.getViewUrl(report.id), '_blank')}
>
<Eye className="w-4 h-4 mr-2" />
View
</Button>
<Button
variant="secondary"
onClick={() => handleDownload(report.id, 'html')}
>
<Download className="w-4 h-4 mr-2" />
HTML
</Button>
<Button
variant="secondary"
onClick={() => handleDownload(report.id, 'json')}
>
<Download className="w-4 h-4 mr-2" />
JSON
</Button>
<Button
variant="ghost"
onClick={() => handleDelete(report.id)}
className="text-red-400 hover:text-red-300"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
)
})}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,580 @@
import { useEffect, useMemo, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight,
ExternalLink, Copy, Shield, AlertTriangle
} 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 { wsService } from '../services/websocket'
import { useScanStore } from '../store'
import type { Endpoint, Vulnerability, WSMessage } from '../types'
export default function ScanDetailsPage() {
const { scanId } = useParams<{ scanId: string }>()
const navigate = useNavigate()
const {
currentScan, endpoints, vulnerabilities, logs,
setCurrentScan, setEndpoints, setVulnerabilities,
addEndpoint, addVulnerability, addLog, updateScan,
loadScanData, saveScanData, getVulnCounts
} = useScanStore()
const [isGeneratingReport, setIsGeneratingReport] = useState(false)
const [expandedVulns, setExpandedVulns] = useState<Set<string>>(new Set())
const [activeTab, setActiveTab] = useState<'endpoints' | 'vulns'>('vulns')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | 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] = await Promise.all([
scansApi.getEndpoints(scanId),
scansApi.getVulnerabilities(scanId)
])
// Only set if we have data from API
if (endpointsData.endpoints?.length > 0) {
setEndpoints(endpointsData.endpoints)
}
if (vulnsData.vulnerabilities?.length > 0) {
setVulnerabilities(vulnsData.vulnerabilities)
}
} 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] = await Promise.all([
scansApi.getEndpoints(scanId),
scansApi.getVulnerabilities(scanId)
])
if (endpointsData.endpoints?.length > 0) {
setEndpoints(endpointsData.endpoints)
}
if (vulnsData.vulnerabilities?.length > 0) {
setVulnerabilities(vulnsData.vulnerabilities)
}
} 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 '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 '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>
)}
{currentScan.status === 'completed' && (
<Button onClick={handleGenerateReport} isLoading={isGeneratingReport}>
<FileText className="w-4 h-4 mr-2" />
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>
)}
{/* 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>
</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>
)}
{/* 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>
)
}

View File

@@ -0,0 +1,308 @@
import { useState, useEffect } from 'react'
import { Save, Shield, Trash2, RefreshCw, AlertTriangle } from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import Input from '../components/common/Input'
interface Settings {
llm_provider: string
has_anthropic_key: boolean
has_openai_key: boolean
max_concurrent_scans: number
aggressive_mode: boolean
default_scan_type: string
recon_enabled_by_default: boolean
}
interface DbStats {
scans: number
vulnerabilities: number
endpoints: number
reports: number
}
export default function SettingsPage() {
const [settings, setSettings] = useState<Settings | null>(null)
const [dbStats, setDbStats] = useState<DbStats | null>(null)
const [apiKey, setApiKey] = useState('')
const [openaiKey, setOpenaiKey] = useState('')
const [llmProvider, setLlmProvider] = useState('claude')
const [maxConcurrentScans, setMaxConcurrentScans] = useState('3')
const [aggressiveMode, setAggressiveMode] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [isClearing, setIsClearing] = useState(false)
const [showClearConfirm, setShowClearConfirm] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
useEffect(() => {
fetchSettings()
fetchDbStats()
}, [])
const fetchSettings = async () => {
try {
const response = await fetch('/api/v1/settings')
if (response.ok) {
const data = await response.json()
setSettings(data)
setLlmProvider(data.llm_provider)
setMaxConcurrentScans(String(data.max_concurrent_scans))
setAggressiveMode(data.aggressive_mode)
}
} catch (error) {
console.error('Failed to fetch settings:', error)
}
}
const fetchDbStats = async () => {
try {
const response = await fetch('/api/v1/settings/stats')
if (response.ok) {
const data = await response.json()
setDbStats(data)
}
} catch (error) {
console.error('Failed to fetch db stats:', error)
}
}
const handleSave = async () => {
setIsSaving(true)
setMessage(null)
try {
const response = await fetch('/api/v1/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
llm_provider: llmProvider,
anthropic_api_key: apiKey || undefined,
openai_api_key: openaiKey || undefined,
max_concurrent_scans: parseInt(maxConcurrentScans),
aggressive_mode: aggressiveMode
})
})
if (response.ok) {
const data = await response.json()
setSettings(data)
setApiKey('')
setOpenaiKey('')
setMessage({ type: 'success', text: 'Settings saved successfully!' })
} else {
setMessage({ type: 'error', text: 'Failed to save settings' })
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to save settings' })
} finally {
setIsSaving(false)
}
}
const handleClearDatabase = async () => {
setIsClearing(true)
setMessage(null)
try {
const response = await fetch('/api/v1/settings/clear-database', {
method: 'POST'
})
if (response.ok) {
setMessage({ type: 'success', text: 'Database cleared successfully!' })
setShowClearConfirm(false)
fetchDbStats()
} else {
const data = await response.json()
setMessage({ type: 'error', text: data.detail || 'Failed to clear database' })
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to clear database' })
} finally {
setIsClearing(false)
}
}
return (
<div className="max-w-2xl mx-auto space-y-6 animate-fadeIn">
{/* Status Message */}
{message && (
<div className={`p-4 rounded-lg ${message.type === 'success' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
{message.text}
</div>
)}
{/* LLM Configuration */}
<Card title="LLM Configuration" subtitle="Configure AI model for vulnerability analysis">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-dark-200 mb-2">
LLM Provider
</label>
<div className="flex gap-2">
{['claude', 'openai', 'ollama'].map((provider) => (
<Button
key={provider}
variant={llmProvider === provider ? 'primary' : 'secondary'}
onClick={() => setLlmProvider(provider)}
>
{provider.charAt(0).toUpperCase() + provider.slice(1)}
</Button>
))}
</div>
</div>
{llmProvider === 'claude' && (
<Input
label="Anthropic API Key"
type="password"
placeholder={settings?.has_anthropic_key ? '••••••••••••••••' : 'sk-ant-...'}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
helperText={settings?.has_anthropic_key ? 'API key is configured. Enter a new key to update.' : 'Required for Claude-powered analysis'}
/>
)}
{llmProvider === 'openai' && (
<Input
label="OpenAI API Key"
type="password"
placeholder={settings?.has_openai_key ? '••••••••••••••••' : 'sk-...'}
value={openaiKey}
onChange={(e) => setOpenaiKey(e.target.value)}
helperText={settings?.has_openai_key ? 'API key is configured. Enter a new key to update.' : 'Required for OpenAI-powered analysis'}
/>
)}
</div>
</Card>
{/* Scan Settings */}
<Card title="Scan Settings" subtitle="Configure default scan behavior">
<div className="space-y-4">
<Input
label="Max Concurrent Scans"
type="number"
min="1"
max="10"
value={maxConcurrentScans}
onChange={(e) => setMaxConcurrentScans(e.target.value)}
helperText="Maximum number of scans that can run simultaneously"
/>
<div className="flex items-center justify-between p-4 bg-dark-900/50 rounded-lg">
<div>
<p className="font-medium text-white">Enable Aggressive Mode</p>
<p className="text-sm text-dark-400">
Use more payloads and bypass techniques (may be slower)
</p>
</div>
<button
onClick={() => setAggressiveMode(!aggressiveMode)}
className={`w-12 h-6 rounded-full transition-colors ${aggressiveMode ? 'bg-primary-500' : 'bg-dark-700'}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow-md transform transition-transform ${aggressiveMode ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
</div>
</div>
</Card>
{/* Database Management */}
<Card title="Database Management" subtitle="Manage stored data">
<div className="space-y-4">
{/* Stats */}
{dbStats && (
<div className="grid grid-cols-4 gap-4 p-4 bg-dark-900/50 rounded-lg">
<div className="text-center">
<p className="text-2xl font-bold text-white">{dbStats.scans}</p>
<p className="text-xs text-dark-400">Scans</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-white">{dbStats.vulnerabilities}</p>
<p className="text-xs text-dark-400">Vulnerabilities</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-white">{dbStats.endpoints}</p>
<p className="text-xs text-dark-400">Endpoints</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-white">{dbStats.reports}</p>
<p className="text-xs text-dark-400">Reports</p>
</div>
</div>
)}
{/* Clear Database */}
{!showClearConfirm ? (
<div className="flex items-center justify-between p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
<div>
<p className="font-medium text-white">Clear All Data</p>
<p className="text-sm text-dark-400">
Remove all scans, vulnerabilities, and reports
</p>
</div>
<Button variant="danger" onClick={() => setShowClearConfirm(true)}>
<Trash2 className="w-4 h-4 mr-2" />
Clear Database
</Button>
</div>
) : (
<div className="p-4 bg-red-500/20 border border-red-500/50 rounded-lg space-y-4">
<div className="flex items-start gap-3">
<AlertTriangle className="w-6 h-6 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-red-400">Are you sure?</p>
<p className="text-sm text-dark-300 mt-1">
This will permanently delete all scans, vulnerabilities, endpoints, and reports.
This action cannot be undone.
</p>
</div>
</div>
<div className="flex gap-3 justify-end">
<Button variant="secondary" onClick={() => setShowClearConfirm(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleClearDatabase} isLoading={isClearing}>
<Trash2 className="w-4 h-4 mr-2" />
Yes, Clear Everything
</Button>
</div>
</div>
)}
{/* Refresh Stats */}
<Button variant="secondary" onClick={fetchDbStats} className="w-full">
<RefreshCw className="w-4 h-4 mr-2" />
Refresh Statistics
</Button>
</div>
</Card>
{/* About */}
<Card title="About NeuroSploit">
<div className="space-y-3">
<div className="flex items-center gap-3">
<Shield className="w-8 h-8 text-primary-500" />
<div>
<p className="font-bold text-white text-lg">NeuroSploit v3.0</p>
<p className="text-sm text-dark-400">AI-Powered Penetration Testing Platform</p>
</div>
</div>
<div className="text-sm text-dark-400 space-y-1">
<p>Dynamic vulnerability testing driven by AI prompts</p>
<p>50+ vulnerability types across 10 categories</p>
<p>Real-time dashboard with WebSocket updates</p>
<p>Professional HTML/PDF/JSON reports</p>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button onClick={handleSave} isLoading={isSaving} size="lg">
<Save className="w-5 h-5 mr-2" />
Save Settings
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,459 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
BookOpen, Plus, Trash2, Play, Search, Tag, Zap, X, Save
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import Input from '../components/common/Input'
import Textarea from '../components/common/Textarea'
import { agentApi } from '../services/api'
import type { AgentTask } from '../types'
const CATEGORIES = [
{ id: 'all', name: 'All Tasks', color: 'dark' },
{ id: 'full_auto', name: 'Full Auto', color: 'primary' },
{ id: 'recon', name: 'Reconnaissance', color: 'blue' },
{ id: 'vulnerability', name: 'Vulnerability', color: 'orange' },
{ id: 'custom', name: 'Custom', color: 'purple' },
{ id: 'reporting', name: 'Reporting', color: 'green' }
]
export default function TaskLibraryPage() {
const navigate = useNavigate()
const [tasks, setTasks] = useState<AgentTask[]>([])
const [filteredTasks, setFilteredTasks] = useState<AgentTask[]>([])
const [loading, setLoading] = useState(true)
const [selectedCategory, setSelectedCategory] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [selectedTask, setSelectedTask] = useState<AgentTask | null>(null)
// Create task modal
const [showCreateModal, setShowCreateModal] = useState(false)
const [newTask, setNewTask] = useState({
name: '',
description: '',
category: 'custom',
prompt: '',
system_prompt: '',
tags: ''
})
const [creating, setCreating] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
useEffect(() => {
loadTasks()
}, [])
useEffect(() => {
filterTasks()
}, [tasks, selectedCategory, searchQuery])
const loadTasks = async () => {
setLoading(true)
try {
const taskList = await agentApi.tasks.list()
setTasks(taskList)
} catch (error) {
console.error('Failed to load tasks:', error)
} finally {
setLoading(false)
}
}
const filterTasks = () => {
let filtered = [...tasks]
// Category filter
if (selectedCategory !== 'all') {
filtered = filtered.filter(t => t.category === selectedCategory)
}
// Search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(t =>
t.name.toLowerCase().includes(query) ||
t.description.toLowerCase().includes(query) ||
t.tags?.some(tag => tag.toLowerCase().includes(query))
)
}
setFilteredTasks(filtered)
}
const handleCreateTask = async () => {
if (!newTask.name.trim() || !newTask.prompt.trim()) return
setCreating(true)
try {
await agentApi.tasks.create({
name: newTask.name,
description: newTask.description,
category: newTask.category,
prompt: newTask.prompt,
system_prompt: newTask.system_prompt || undefined,
tags: newTask.tags.split(',').map(t => t.trim()).filter(t => t)
})
// Reload tasks
await loadTasks()
setShowCreateModal(false)
setNewTask({
name: '',
description: '',
category: 'custom',
prompt: '',
system_prompt: '',
tags: ''
})
} catch (error) {
console.error('Failed to create task:', error)
} finally {
setCreating(false)
}
}
const handleDeleteTask = async (taskId: string) => {
try {
await agentApi.tasks.delete(taskId)
await loadTasks()
setDeleteConfirm(null)
if (selectedTask?.id === taskId) {
setSelectedTask(null)
}
} catch (error) {
console.error('Failed to delete task:', error)
}
}
const handleRunTask = (task: AgentTask) => {
// Navigate to new scan page with task pre-selected
navigate('/scan/new', { state: { selectedTaskId: task.id } })
}
return (
<div className="space-y-6 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
<BookOpen className="w-8 h-8 text-primary-500" />
Task Library
</h1>
<p className="text-dark-400 mt-1">Manage and create reusable security testing tasks</p>
</div>
<Button onClick={() => setShowCreateModal(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Task
</Button>
</div>
{/* Filters */}
<Card>
<div className="flex flex-wrap gap-4">
{/* Search */}
<div className="flex-1 min-w-[200px]">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-dark-400" />
<input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-dark-900 border border-dark-700 rounded-lg text-white placeholder-dark-500 focus:border-primary-500 focus:outline-none"
/>
</div>
</div>
{/* Category Filter */}
<div className="flex gap-2 flex-wrap">
{CATEGORIES.map((cat) => (
<Button
key={cat.id}
variant={selectedCategory === cat.id ? 'primary' : 'secondary'}
size="sm"
onClick={() => setSelectedCategory(cat.id)}
>
{cat.name}
</Button>
))}
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Task List */}
<div className="lg:col-span-2 space-y-3">
{loading ? (
<Card>
<p className="text-dark-400 text-center py-8">Loading tasks...</p>
</Card>
) : filteredTasks.length === 0 ? (
<Card>
<p className="text-dark-400 text-center py-8">
{searchQuery || selectedCategory !== 'all'
? 'No tasks match your filters'
: 'No tasks found. Create your first task!'}
</p>
</Card>
) : (
filteredTasks.map((task) => (
<div
key={task.id}
onClick={() => setSelectedTask(task)}
className={`bg-dark-800 rounded-lg border p-4 cursor-pointer transition-all ${
selectedTask?.id === task.id
? 'border-primary-500 bg-primary-500/5'
: 'border-dark-700 hover:border-dark-500'
}`}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-white">{task.name}</span>
{task.is_preset && (
<span className="text-xs bg-primary-500/20 text-primary-400 px-2 py-0.5 rounded">
Preset
</span>
)}
</div>
<p className="text-sm text-dark-400 line-clamp-2">{task.description}</p>
<div className="flex items-center gap-3 mt-3">
<span className={`text-xs px-2 py-0.5 rounded ${
task.category === 'full_auto' ? 'bg-primary-500/20 text-primary-400' :
task.category === 'recon' ? 'bg-blue-500/20 text-blue-400' :
task.category === 'vulnerability' ? 'bg-orange-500/20 text-orange-400' :
task.category === 'reporting' ? 'bg-green-500/20 text-green-400' :
'bg-purple-500/20 text-purple-400'
}`}>
{task.category}
</span>
{task.estimated_tokens > 0 && (
<span className="text-xs text-dark-500 flex items-center gap-1">
<Zap className="w-3 h-3" />
~{task.estimated_tokens} tokens
</span>
)}
</div>
{task.tags?.length > 0 && (
<div className="flex gap-1 mt-2 flex-wrap">
{task.tags.slice(0, 5).map((tag) => (
<span key={tag} className="text-xs bg-dark-700 text-dark-300 px-2 py-0.5 rounded flex items-center gap-1">
<Tag className="w-3 h-3" />
{tag}
</span>
))}
{task.tags.length > 5 && (
<span className="text-xs text-dark-500">+{task.tags.length - 5} more</span>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleRunTask(task)
}}
>
<Play className="w-4 h-4" />
</Button>
{!task.is_preset && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteConfirm(task.id)
}}
>
<Trash2 className="w-4 h-4 text-red-400" />
</Button>
)}
</div>
</div>
</div>
))
)}
</div>
{/* Task Details */}
<div>
<Card title="Task Details">
{selectedTask ? (
<div className="space-y-4">
<div>
<p className="text-sm text-dark-400">Name</p>
<p className="text-white font-medium">{selectedTask.name}</p>
</div>
<div>
<p className="text-sm text-dark-400">Description</p>
<p className="text-dark-300">{selectedTask.description}</p>
</div>
<div>
<p className="text-sm text-dark-400">Category</p>
<p className="text-white">{selectedTask.category}</p>
</div>
<div>
<p className="text-sm text-dark-400">Prompt</p>
<pre className="text-xs bg-dark-900 p-3 rounded-lg overflow-auto max-h-60 text-dark-300 whitespace-pre-wrap">
{selectedTask.prompt}
</pre>
</div>
{selectedTask.system_prompt && (
<div>
<p className="text-sm text-dark-400">System Prompt</p>
<pre className="text-xs bg-dark-900 p-3 rounded-lg overflow-auto max-h-40 text-dark-300 whitespace-pre-wrap">
{selectedTask.system_prompt}
</pre>
</div>
)}
{selectedTask.tools_required?.length > 0 && (
<div>
<p className="text-sm text-dark-400">Required Tools</p>
<div className="flex gap-1 flex-wrap mt-1">
{selectedTask.tools_required.map((tool) => (
<span key={tool} className="text-xs bg-dark-700 text-dark-300 px-2 py-1 rounded">
{tool}
</span>
))}
</div>
</div>
)}
<div className="pt-4 border-t border-dark-700">
<Button
className="w-full"
onClick={() => handleRunTask(selectedTask)}
>
<Play className="w-4 h-4 mr-2" />
Run This Task
</Button>
</div>
</div>
) : (
<p className="text-dark-400 text-center py-8">
Select a task to view details
</p>
)}
</Card>
</div>
</div>
{/* Create Task Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl border border-dark-700 w-full max-w-2xl max-h-[90vh] overflow-auto">
<div className="flex items-center justify-between p-4 border-b border-dark-700">
<h3 className="text-xl font-bold text-white">Create New Task</h3>
<Button variant="ghost" size="sm" onClick={() => setShowCreateModal(false)}>
<X className="w-5 h-5" />
</Button>
</div>
<div className="p-4 space-y-4">
<Input
label="Task Name"
placeholder="My Custom Task"
value={newTask.name}
onChange={(e) => setNewTask({ ...newTask, name: e.target.value })}
/>
<Input
label="Description"
placeholder="Brief description of what this task does"
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
/>
<div>
<label className="block text-sm font-medium text-dark-300 mb-2">Category</label>
<select
value={newTask.category}
onChange={(e) => setNewTask({ ...newTask, category: e.target.value })}
className="w-full px-4 py-2 bg-dark-900 border border-dark-700 rounded-lg text-white focus:border-primary-500 focus:outline-none"
>
<option value="custom">Custom</option>
<option value="recon">Reconnaissance</option>
<option value="vulnerability">Vulnerability</option>
<option value="full_auto">Full Auto</option>
<option value="reporting">Reporting</option>
</select>
</div>
<Textarea
label="Prompt"
placeholder="Enter the prompt for the AI agent..."
rows={8}
value={newTask.prompt}
onChange={(e) => setNewTask({ ...newTask, prompt: e.target.value })}
/>
<Textarea
label="System Prompt (Optional)"
placeholder="Enter a system prompt to guide the AI's behavior..."
rows={4}
value={newTask.system_prompt}
onChange={(e) => setNewTask({ ...newTask, system_prompt: e.target.value })}
/>
<Input
label="Tags (comma separated)"
placeholder="pentest, api, auth, custom"
value={newTask.tags}
onChange={(e) => setNewTask({ ...newTask, tags: e.target.value })}
/>
</div>
<div className="flex justify-end gap-3 p-4 border-t border-dark-700">
<Button variant="secondary" onClick={() => setShowCreateModal(false)}>
Cancel
</Button>
<Button
onClick={handleCreateTask}
isLoading={creating}
disabled={!newTask.name.trim() || !newTask.prompt.trim()}
>
<Save className="w-4 h-4 mr-2" />
Create Task
</Button>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{deleteConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl border border-dark-700 p-6 max-w-md">
<h3 className="text-xl font-bold text-white mb-2">Delete Task?</h3>
<p className="text-dark-400 mb-6">
Are you sure you want to delete this task? This action cannot be undone.
</p>
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setDeleteConfirm(null)}>
Cancel
</Button>
<Button variant="danger" onClick={() => handleDeleteTask(deleteConfirm)}>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</Button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,347 @@
import axios from 'axios'
import type {
Scan, Vulnerability, Prompt, PromptPreset, Report, DashboardStats,
AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode
} from '../types'
const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
})
// Scans API
export const scansApi = {
list: async (page = 1, perPage = 10, status?: string) => {
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) })
if (status) params.append('status', status)
const response = await api.get(`/scans?${params}`)
return response.data
},
get: async (scanId: string): Promise<Scan> => {
const response = await api.get(`/scans/${scanId}`)
return response.data
},
create: async (data: {
name?: string
targets: string[]
scan_type?: string
recon_enabled?: boolean
custom_prompt?: string
prompt_id?: string
}): Promise<Scan> => {
const response = await api.post('/scans', data)
return response.data
},
start: async (scanId: string) => {
const response = await api.post(`/scans/${scanId}/start`)
return response.data
},
stop: async (scanId: string) => {
const response = await api.post(`/scans/${scanId}/stop`)
return response.data
},
delete: async (scanId: string) => {
const response = await api.delete(`/scans/${scanId}`)
return response.data
},
getEndpoints: async (scanId: string, page = 1, perPage = 50) => {
const response = await api.get(`/scans/${scanId}/endpoints?page=${page}&per_page=${perPage}`)
return response.data
},
getVulnerabilities: async (scanId: string, severity?: string, page = 1, perPage = 50) => {
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) })
if (severity) params.append('severity', severity)
const response = await api.get(`/scans/${scanId}/vulnerabilities?${params}`)
return response.data
},
}
// Targets API
export const targetsApi = {
validate: async (url: string) => {
const response = await api.post('/targets/validate', { url })
return response.data
},
validateBulk: async (urls: string[]) => {
const response = await api.post('/targets/validate/bulk', { urls })
return response.data
},
upload: async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/targets/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
},
}
// Prompts API
export const promptsApi = {
getPresets: async (): Promise<PromptPreset[]> => {
const response = await api.get('/prompts/presets')
return response.data
},
getPreset: async (presetId: string) => {
const response = await api.get(`/prompts/presets/${presetId}`)
return response.data
},
parse: async (content: string) => {
const response = await api.post('/prompts/parse', { content })
return response.data
},
list: async (category?: string): Promise<Prompt[]> => {
const params = category ? `?category=${category}` : ''
const response = await api.get(`/prompts${params}`)
return response.data
},
create: async (data: { name: string; description?: string; content: string; category?: string }): Promise<Prompt> => {
const response = await api.post('/prompts', data)
return response.data
},
upload: async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/prompts/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
},
}
// 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}`)
return response.data
},
get: async (reportId: string): Promise<Report> => {
const response = await api.get(`/reports/${reportId}`)
return response.data
},
generate: async (data: {
scan_id: string
format?: string
title?: string
include_executive_summary?: boolean
include_poc?: boolean
include_remediation?: boolean
}): Promise<Report> => {
const response = await api.post('/reports', data)
return response.data
},
getViewUrl: (reportId: string) => `/api/v1/reports/${reportId}/view`,
getDownloadUrl: (reportId: string, format: string) => `/api/v1/reports/${reportId}/download/${format}`,
delete: async (reportId: string) => {
const response = await api.delete(`/reports/${reportId}`)
return response.data
},
}
// Dashboard API
export const dashboardApi = {
getStats: async (): Promise<DashboardStats> => {
const response = await api.get('/dashboard/stats')
return response.data
},
getRecent: async (limit = 10) => {
const response = await api.get(`/dashboard/recent?limit=${limit}`)
return response.data
},
getFindings: async (limit = 20, severity?: string) => {
const params = new URLSearchParams({ limit: String(limit) })
if (severity) params.append('severity', severity)
const response = await api.get(`/dashboard/findings?${params}`)
return response.data
},
getVulnerabilityTypes: async () => {
const response = await api.get('/dashboard/vulnerability-types')
return response.data
},
}
// Vulnerabilities API
export const vulnerabilitiesApi = {
getTypes: async () => {
const response = await api.get('/vulnerabilities/types')
return response.data
},
get: async (vulnId: string): Promise<Vulnerability> => {
const response = await api.get(`/vulnerabilities/${vulnId}`)
return response.data
},
}
// Agent API (Autonomous AI Agent like PentAGI)
export const agentApi = {
// Run the autonomous agent
run: async (request: AgentRequest): Promise<AgentResponse> => {
const response = await api.post('/agent/run', request)
return response.data
},
// Get agent status and results
getStatus: async (agentId: string): Promise<AgentStatus> => {
const response = await api.get(`/agent/status/${agentId}`)
return response.data
},
// Get agent logs
getLogs: async (agentId: string, limit = 100): Promise<{ agent_id: string; total_logs: number; logs: AgentLog[] }> => {
const response = await api.get(`/agent/logs/${agentId}?limit=${limit}`)
return response.data
},
// Get findings with details
getFindings: async (agentId: string) => {
const response = await api.get(`/agent/findings/${agentId}`)
return response.data
},
// Delete agent results
delete: async (agentId: string) => {
const response = await api.delete(`/agent/${agentId}`)
return response.data
},
// Stop a running agent
stop: async (agentId: string) => {
const response = await api.post(`/agent/stop/${agentId}`)
return response.data
},
// Send custom prompt to agent
sendPrompt: async (agentId: string, prompt: string) => {
const response = await api.post(`/agent/prompt/${agentId}`, { prompt })
return response.data
},
// Quick synchronous run (for small targets)
quickRun: async (target: string, mode: AgentMode = 'full_auto') => {
const response = await api.post(`/agent/quick?target=${encodeURIComponent(target)}&mode=${mode}`)
return response.data
},
// Task Library
tasks: {
list: async (category?: string): Promise<AgentTask[]> => {
const params = category ? `?category=${category}` : ''
const response = await api.get(`/agent/tasks${params}`)
return response.data
},
get: async (taskId: string): Promise<AgentTask> => {
const response = await api.get(`/agent/tasks/${taskId}`)
return response.data
},
create: async (task: {
name: string
description: string
category?: string
prompt: string
system_prompt?: string
tags?: string[]
}): Promise<{ message: string; task_id: string }> => {
const response = await api.post('/agent/tasks', task)
return response.data
},
delete: async (taskId: string) => {
const response = await api.delete(`/agent/tasks/${taskId}`)
return response.data
},
},
// Real-time Task API
realtime: {
createSession: async (target: string, name?: string) => {
const response = await api.post('/agent/realtime/session', { target, name })
return response.data
},
sendMessage: async (sessionId: string, message: string) => {
const response = await api.post(`/agent/realtime/${sessionId}/message`, { message })
return response.data
},
getSession: async (sessionId: string) => {
const response = await api.get(`/agent/realtime/${sessionId}`)
return response.data
},
getReport: async (sessionId: string) => {
const response = await api.get(`/agent/realtime/${sessionId}/report`)
return response.data
},
deleteSession: async (sessionId: string) => {
const response = await api.delete(`/agent/realtime/${sessionId}`)
return response.data
},
listSessions: async () => {
const response = await api.get('/agent/realtime/sessions/list')
return response.data
},
getLlmStatus: async () => {
const response = await api.get('/agent/realtime/llm-status')
return response.data
},
getReportHtml: async (sessionId: string) => {
const response = await api.get(`/agent/realtime/${sessionId}/report?format=html`, {
responseType: 'text'
})
return response.data
},
getToolsList: async () => {
const response = await api.get('/agent/realtime/tools/list')
return response.data
},
getToolsStatus: async () => {
const response = await api.get('/agent/realtime/tools/status')
return response.data
},
executeTool: async (sessionId: string, tool: string, options?: Record<string, any>, timeout?: number) => {
const response = await api.post(`/agent/realtime/${sessionId}/execute-tool`, {
tool,
options,
timeout: timeout || 300
})
return response.data
},
},
}
export default api

View File

@@ -0,0 +1,116 @@
import type { WSMessage } from '../types'
type MessageHandler = (message: WSMessage) => void
class WebSocketService {
private ws: WebSocket | null = null
private handlers: Map<string, Set<MessageHandler>> = new Map()
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectDelay = 1000
private scanId: string | null = null
connect(scanId: string): void {
if (this.ws?.readyState === WebSocket.OPEN && this.scanId === scanId) {
return
}
this.disconnect()
this.scanId = scanId
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/ws/scan/${scanId}`
try {
this.ws = new WebSocket(wsUrl)
this.ws.onopen = () => {
console.log('WebSocket connected')
this.reconnectAttempts = 0
}
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data) as WSMessage
this.notifyHandlers(message.type, message)
this.notifyHandlers('*', message) // Wildcard handlers
} catch (e) {
console.error('Failed to parse WebSocket message:', e)
}
}
this.ws.onclose = () => {
console.log('WebSocket disconnected')
this.attemptReconnect()
}
this.ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
} catch (e) {
console.error('Failed to create WebSocket:', e)
}
}
disconnect(): void {
if (this.ws) {
this.ws.close()
this.ws = null
}
this.scanId = null
}
private attemptReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts || !this.scanId) {
return
}
this.reconnectAttempts++
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
setTimeout(() => {
if (this.scanId) {
console.log(`Attempting reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
this.connect(this.scanId)
}
}, delay)
}
subscribe(eventType: string, handler: MessageHandler): () => void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, new Set())
}
this.handlers.get(eventType)!.add(handler)
// Return unsubscribe function
return () => {
this.handlers.get(eventType)?.delete(handler)
}
}
private notifyHandlers(eventType: string, message: WSMessage): void {
const handlers = this.handlers.get(eventType)
if (handlers) {
handlers.forEach((handler) => {
try {
handler(message)
} catch (e) {
console.error('Handler error:', e)
}
})
}
}
send(data: unknown): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data))
}
}
ping(): void {
this.send('ping')
}
}
export const wsService = new WebSocketService()
export default wsService

178
frontend/src/store/index.ts Normal file
View File

@@ -0,0 +1,178 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Scan, Vulnerability, Endpoint, DashboardStats } from '../types'
interface LogEntry {
level: string
message: string
time: string
}
interface ScanDataCache {
endpoints: Endpoint[]
vulnerabilities: Vulnerability[]
logs: LogEntry[]
}
interface ScanState {
currentScan: Scan | null
scans: Scan[]
endpoints: Endpoint[]
vulnerabilities: Vulnerability[]
logs: LogEntry[]
scanDataCache: Record<string, ScanDataCache>
isLoading: boolean
error: string | null
setCurrentScan: (scan: Scan | null) => void
setScans: (scans: Scan[]) => void
updateScan: (scanId: string, updates: Partial<Scan>) => void
addEndpoint: (endpoint: Endpoint) => void
addVulnerability: (vulnerability: Vulnerability) => void
setEndpoints: (endpoints: Endpoint[]) => void
setVulnerabilities: (vulnerabilities: Vulnerability[]) => void
addLog: (level: string, message: string) => void
setLogs: (logs: LogEntry[]) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
loadScanData: (scanId: string) => void
saveScanData: (scanId: string) => void
reset: () => void
resetCurrentScan: () => void
getVulnCounts: () => { critical: number; high: number; medium: number; low: number; info: number }
}
export const useScanStore = create<ScanState>()(
persist(
(set, get) => ({
currentScan: null,
scans: [],
endpoints: [],
vulnerabilities: [],
logs: [],
scanDataCache: {},
isLoading: false,
error: null,
setCurrentScan: (scan) => set({ currentScan: scan }),
setScans: (scans) => set({ scans }),
updateScan: (scanId, updates) =>
set((state) => ({
currentScan:
state.currentScan?.id === scanId
? { ...state.currentScan, ...updates }
: state.currentScan,
scans: state.scans.map((s) => (s.id === scanId ? { ...s, ...updates } : s)),
})),
addEndpoint: (endpoint) =>
set((state) => {
const exists = state.endpoints.some(e => e.id === endpoint.id || (e.url === endpoint.url && e.method === endpoint.method))
if (exists) return state
return { endpoints: [...state.endpoints, endpoint] }
}),
addVulnerability: (vulnerability) =>
set((state) => {
const exists = state.vulnerabilities.some(v => v.id === vulnerability.id)
if (exists) return state
return { vulnerabilities: [...state.vulnerabilities, vulnerability] }
}),
setEndpoints: (endpoints) => set({ endpoints }),
setVulnerabilities: (vulnerabilities) => set({ vulnerabilities }),
addLog: (level, message) =>
set((state) => ({
logs: [...state.logs, { level, message, time: new Date().toISOString() }].slice(-200)
})),
setLogs: (logs) => set({ logs }),
setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error }),
loadScanData: (scanId) => {
const state = get()
const cached = state.scanDataCache[scanId]
if (cached) {
set({
endpoints: cached.endpoints,
vulnerabilities: cached.vulnerabilities,
logs: cached.logs
})
}
},
saveScanData: (scanId) => {
const state = get()
set({
scanDataCache: {
...state.scanDataCache,
[scanId]: {
endpoints: state.endpoints,
vulnerabilities: state.vulnerabilities,
logs: state.logs
}
}
})
},
reset: () =>
set({
currentScan: null,
scans: [],
endpoints: [],
vulnerabilities: [],
logs: [],
scanDataCache: {},
isLoading: false,
error: null,
}),
resetCurrentScan: () =>
set({
endpoints: [],
vulnerabilities: [],
logs: [],
}),
getVulnCounts: () => {
const vulns = get().vulnerabilities
return {
critical: vulns.filter(v => v.severity === 'critical').length,
high: vulns.filter(v => v.severity === 'high').length,
medium: vulns.filter(v => v.severity === 'medium').length,
low: vulns.filter(v => v.severity === 'low').length,
info: vulns.filter(v => v.severity === 'info').length,
}
}
}),
{
name: 'neurosploit-scan-store',
partialize: (state) => ({
scanDataCache: state.scanDataCache,
scans: state.scans
})
}
)
)
interface DashboardState {
stats: DashboardStats | null
recentScans: Scan[]
recentVulnerabilities: Vulnerability[]
isLoading: boolean
setStats: (stats: DashboardStats) => void
setRecentScans: (scans: Scan[]) => void
setRecentVulnerabilities: (vulns: Vulnerability[]) => void
setLoading: (loading: boolean) => void
}
export const useDashboardStore = create<DashboardState>((set) => ({
stats: null,
recentScans: [],
recentVulnerabilities: [],
isLoading: false,
setStats: (stats) => set({ stats }),
setRecentScans: (recentScans) => set({ recentScans }),
setRecentVulnerabilities: (recentVulnerabilities) => set({ recentVulnerabilities }),
setLoading: (isLoading) => set({ isLoading }),
}))

View File

@@ -0,0 +1,63 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-card: #0f3460;
--text-primary: #eee;
--text-secondary: #aaa;
--accent: #e94560;
--border: #333;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
* {
box-sizing: border-box;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-card);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s infinite;
}

291
frontend/src/types/index.ts Normal file
View File

@@ -0,0 +1,291 @@
// Scan types
export interface Scan {
id: string
name: string | null
status: 'pending' | 'running' | 'completed' | 'failed' | 'stopped'
scan_type: 'quick' | 'full' | 'custom'
recon_enabled: boolean
progress: number
current_phase: string | null
config: Record<string, unknown>
custom_prompt: string | null
prompt_id: string | null
created_at: string
started_at: string | null
completed_at: string | null
error_message: string | null
total_endpoints: number
total_vulnerabilities: number
critical_count: number
high_count: number
medium_count: number
low_count: number
info_count: number
targets: Target[]
}
export interface Target {
id: string
scan_id: string
url: string
hostname: string | null
port: number | null
protocol: string | null
path: string | null
status: string
created_at: string
}
// Vulnerability types
export interface Vulnerability {
id: string
scan_id: string
title: string
vulnerability_type: string
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
cvss_score: number | null
cvss_vector: string | null
cwe_id: string | null
description: string | null
affected_endpoint: string | null
poc_request: string | null
poc_response: string | null
poc_payload: string | null
impact: string | null
remediation: string | null
references: string[]
ai_analysis: string | null
created_at: string
}
// Endpoint types
export interface Endpoint {
id: string
scan_id: string
url: string
method: string
path: string | null
parameters: unknown[]
response_status: number | null
content_type: string | null
technologies: string[]
discovered_at: string
}
// Prompt types
export interface Prompt {
id: string
name: string
description: string | null
content: string
is_preset: boolean
category: string | null
parsed_vulnerabilities: unknown[]
created_at: string
updated_at: string
}
export interface PromptPreset {
id: string
name: string
description: string
category: string
vulnerability_count: number
}
// Report types
export interface Report {
id: string
scan_id: string
title: string | null
format: 'html' | 'pdf' | 'json'
file_path: string | null
executive_summary: string | null
generated_at: string
}
// Dashboard types
export interface DashboardStats {
scans: {
total: number
running: number
completed: number
recent: number
}
vulnerabilities: {
total: number
critical: number
high: number
medium: number
low: number
info: number
recent: number
}
endpoints: {
total: number
}
}
// WebSocket message types
export interface WSMessage {
type: string
scan_id: string
[key: string]: unknown
}
// Agent types
export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only'
export interface AgentTask {
id: string
name: string
description: string
category: string
prompt: string
system_prompt?: string
tools_required: string[]
tags: string[]
is_preset: boolean
estimated_tokens: number
created_at?: string
updated_at?: string
}
export interface AgentRequest {
target: string
mode: AgentMode
task_id?: string
prompt?: string
auth_type?: 'cookie' | 'bearer' | 'basic' | 'header'
auth_value?: string
custom_headers?: Record<string, string>
max_depth?: number
}
export interface AgentResponse {
agent_id: string
status: string
mode: string
message: string
}
export interface AgentStatus {
agent_id: string
scan_id?: string // Link to database scan
status: 'running' | 'completed' | 'error' | 'stopped'
mode: string
target: string
task?: string
progress: number
phase: string
started_at?: string
completed_at?: string
logs_count: number
findings_count: number
findings: AgentFinding[]
report?: AgentReport
error?: string
}
export interface AgentFinding {
id: string
title: string
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
vulnerability_type: string
cvss_score: number
cvss_vector: string
cwe_id: string
description: string
affected_endpoint: string
parameter?: string
payload?: string
evidence?: string
request?: string
response?: string
impact: string
poc_code: string
remediation: string
references: string[]
ai_verified: boolean
confidence?: string
}
export interface AgentReport {
summary: {
target: string
mode: string
duration: string
total_findings: number
severity_breakdown: Record<string, number>
}
findings: AgentFinding[]
recommendations: string[]
executive_summary?: string
}
export interface AgentLog {
level: string
message: string
time: string
source?: 'script' | 'llm' // Identifies if log is from script or LLM
}
// Real-time Task types
export interface RealtimeMessageMetadata {
error?: boolean
api_error?: boolean
tests_executed?: boolean
new_findings?: number
provider?: string
tool_execution?: boolean
tool?: string
}
export interface RealtimeMessage {
role: 'user' | 'assistant' | 'system' | 'tool'
content: string
timestamp: string
metadata?: RealtimeMessageMetadata
}
export interface RealtimeSession {
session_id: string
name: string
target: string
status: 'active' | 'completed' | 'error'
created_at: string
messages: RealtimeMessage[]
findings: RealtimeFinding[]
recon_data: {
endpoints: Array<{ url: string; status: number; path: string }>
parameters: Record<string, string[]>
technologies: string[]
headers: Record<string, string>
}
}
export interface RealtimeFinding {
title: string
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
vulnerability_type: string
description: string
affected_endpoint: string
remediation: string
evidence?: string
references?: string[]
cvss_score?: number
cvss_vector?: string
cwe_id?: string
owasp?: string
impact?: string
}
export interface RealtimeSessionSummary {
session_id: string
name: string
target: string
status: string
created_at: string
findings_count: number
messages_count: number
}

View File

@@ -0,0 +1,39 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#e94560',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#1a1a2e',
800: '#16213e',
900: '#0f3460',
950: '#0a0a15',
},
},
},
},
plugins: [],
}

25
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

23
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/ws': {
target: 'ws://localhost:8000',
ws: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
})