mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-02-12 14:02:45 +00:00
Add files via upload
This commit is contained in:
45
docker-compose.lite.yml
Normal file
45
docker-compose.lite.yml
Normal 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
|
||||
@@ -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
13
frontend/index.html
Normal 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
34
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
5
frontend/public/favicon.svg
Normal file
5
frontend/public/favicon.svg
Normal 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
31
frontend/src/App.tsx
Normal 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
|
||||
37
frontend/src/components/common/Badge.tsx
Normal file
37
frontend/src/components/common/Badge.tsx
Normal 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>
|
||||
}
|
||||
70
frontend/src/components/common/Button.tsx
Normal file
70
frontend/src/components/common/Button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
frontend/src/components/common/Card.tsx
Normal file
27
frontend/src/components/common/Card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
frontend/src/components/common/Input.tsx
Normal file
41
frontend/src/components/common/Input.tsx
Normal 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
|
||||
41
frontend/src/components/common/Textarea.tsx
Normal file
41
frontend/src/components/common/Textarea.tsx
Normal 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
|
||||
29
frontend/src/components/layout/Header.tsx
Normal file
29
frontend/src/components/layout/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
frontend/src/components/layout/Layout.tsx
Normal file
21
frontend/src/components/layout/Layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
74
frontend/src/components/layout/Sidebar.tsx
Normal file
74
frontend/src/components/layout/Sidebar.tsx
Normal 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
13
frontend/src/main.tsx
Normal 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>,
|
||||
)
|
||||
1169
frontend/src/pages/AgentStatusPage.tsx
Normal file
1169
frontend/src/pages/AgentStatusPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
210
frontend/src/pages/HomePage.tsx
Normal file
210
frontend/src/pages/HomePage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
572
frontend/src/pages/NewScanPage.tsx
Normal file
572
frontend/src/pages/NewScanPage.tsx
Normal 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: https://example1.com 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... 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>
|
||||
)
|
||||
}
|
||||
930
frontend/src/pages/RealtimeTaskPage.tsx
Normal file
930
frontend/src/pages/RealtimeTaskPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
68
frontend/src/pages/ReportViewPage.tsx
Normal file
68
frontend/src/pages/ReportViewPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
148
frontend/src/pages/ReportsPage.tsx
Normal file
148
frontend/src/pages/ReportsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
580
frontend/src/pages/ScanDetailsPage.tsx
Normal file
580
frontend/src/pages/ScanDetailsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
308
frontend/src/pages/SettingsPage.tsx
Normal file
308
frontend/src/pages/SettingsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
459
frontend/src/pages/TaskLibraryPage.tsx
Normal file
459
frontend/src/pages/TaskLibraryPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
347
frontend/src/services/api.ts
Normal file
347
frontend/src/services/api.ts
Normal 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
|
||||
116
frontend/src/services/websocket.ts
Normal file
116
frontend/src/services/websocket.ts
Normal 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
178
frontend/src/store/index.ts
Normal 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 }),
|
||||
}))
|
||||
63
frontend/src/styles/globals.css
Normal file
63
frontend/src/styles/globals.css
Normal 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
291
frontend/src/types/index.ts
Normal 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
|
||||
}
|
||||
39
frontend/tailwind.config.js
Normal file
39
frontend/tailwind.config.js
Normal 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
25
frontend/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal 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
23
frontend/vite.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user