|
|
|
@@ -14,15 +14,15 @@ import VulnAgentGrid from '../components/VulnAgentGrid'
|
|
|
|
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const PHASES = [
|
|
|
|
|
{ key: 'parallel', label: 'Parallel Streams', icon: Layers, range: [0, 50] as const },
|
|
|
|
|
{ key: 'deep', label: 'Deep Analysis', icon: Brain, range: [50, 75] as const },
|
|
|
|
|
{ key: 'final', label: 'Finalization', icon: Shield, range: [75, 100] as const },
|
|
|
|
|
{ key: 'recon', label: 'Reconnaissance', icon: Globe, range: [0, 20] as const },
|
|
|
|
|
{ key: 'agents', label: 'Agent Grid (108 agents)', icon: Layers, range: [20, 85] as const },
|
|
|
|
|
{ key: 'final', label: 'Finalization', icon: Shield, range: [85, 100] as const },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const STREAMS = [
|
|
|
|
|
{ key: 'recon', label: 'Recon', icon: Globe, color: 'blue', activeUntil: 25 },
|
|
|
|
|
{ key: 'junior', label: 'Junior AI', icon: Brain, color: 'purple', activeUntil: 35 },
|
|
|
|
|
{ key: 'tools', label: 'Tools', icon: Wrench, color: 'orange', activeUntil: 50 },
|
|
|
|
|
{ key: 'recon', label: 'Recon', icon: Globe, color: 'blue', activeUntil: 20 },
|
|
|
|
|
{ key: 'agents', label: 'Agent Grid', icon: Brain, color: 'purple', activeUntil: 85 },
|
|
|
|
|
{ key: 'final', label: 'Report', icon: Wrench, color: 'orange', activeUntil: 100 },
|
|
|
|
|
] as const
|
|
|
|
|
|
|
|
|
|
const STREAM_COLORS: Record<string, { bg: string; text: string; border: string; pulse: string }> = {
|
|
|
|
@@ -53,12 +53,10 @@ const CONFIDENCE_STYLES: Record<string, string> = {
|
|
|
|
|
|
|
|
|
|
const LOG_FILTERS = [
|
|
|
|
|
{ key: 'all', label: 'All', color: '' },
|
|
|
|
|
{ key: 'stream1', label: 'Recon', color: 'text-blue-400' },
|
|
|
|
|
{ key: 'stream2', label: 'Junior', color: 'text-purple-400' },
|
|
|
|
|
{ key: 'stream3', label: 'Tools', color: 'text-orange-400' },
|
|
|
|
|
{ key: 'deep', label: 'Deep', color: 'text-cyan-400' },
|
|
|
|
|
{ key: 'container', label: 'Container', color: 'text-cyan-300' },
|
|
|
|
|
{ key: 'cli_agent', label: 'CLI Agent', color: 'text-pink-400' },
|
|
|
|
|
{ key: 'recon', label: 'Recon', color: 'text-blue-400' },
|
|
|
|
|
{ key: 'agents', label: 'Agents', color: 'text-green-400' },
|
|
|
|
|
{ key: 'judge', label: 'Validation', color: 'text-amber-300' },
|
|
|
|
|
{ key: 'final', label: 'Final', color: 'text-cyan-400' },
|
|
|
|
|
{ key: 'error', label: 'Errors', color: 'text-red-400' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
@@ -88,8 +86,8 @@ interface Toast {
|
|
|
|
|
// ─── Utility Functions ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function phaseFromProgress(progress: number): number {
|
|
|
|
|
if (progress < 50) return 0
|
|
|
|
|
if (progress < 75) return 1
|
|
|
|
|
if (progress < 20) return 0
|
|
|
|
|
if (progress < 85) return 1
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -116,6 +114,15 @@ function logMessageColor(message: string): string {
|
|
|
|
|
if (message.startsWith('[PLAYBOOK]')) return 'text-indigo-400'
|
|
|
|
|
if (message.startsWith('[SITE ANALYZER]')) return 'text-emerald-400'
|
|
|
|
|
if (message.startsWith('[MD-AGENTS]')) return 'text-cyan-300'
|
|
|
|
|
if (message.startsWith('[AGENT GRID]')) return 'text-green-400'
|
|
|
|
|
if (message.startsWith('[PHASE 1]')) return 'text-blue-300'
|
|
|
|
|
if (message.startsWith('[PHASE 2]')) return 'text-purple-300'
|
|
|
|
|
if (message.startsWith('[PHASE 3]')) return 'text-yellow-300'
|
|
|
|
|
if (message.startsWith('[RECON]')) return 'text-blue-400'
|
|
|
|
|
if (message.startsWith('[CVE]')) return 'text-red-300'
|
|
|
|
|
if (message.startsWith('[CHAIN]')) return 'text-orange-300'
|
|
|
|
|
if (message.startsWith('[JUDGE]')) return 'text-amber-300'
|
|
|
|
|
if (message.includes('Starting (real HTTP)')) return 'text-green-300'
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -422,8 +429,8 @@ export default function AutoPentestPage() {
|
|
|
|
|
|
|
|
|
|
// Model selection
|
|
|
|
|
const [availableModels, setAvailableModels] = useState<Array<{ provider_id: string; provider_name: string; default_model: string; tier: number; available_models: string[] }>>([])
|
|
|
|
|
const [selectedProvider, setSelectedProvider] = useState('')
|
|
|
|
|
const [selectedModel, setSelectedModel] = useState('')
|
|
|
|
|
const [selectedProvider, setSelectedProvider] = useState('anthropic')
|
|
|
|
|
const [selectedModel, setSelectedModel] = useState('claude-sonnet-4-20250514')
|
|
|
|
|
|
|
|
|
|
// MD Agent selection
|
|
|
|
|
const [availableMdAgents, setAvailableMdAgents] = useState<Array<{ name: string; display_name: string; category: string }>>([])
|
|
|
|
@@ -739,12 +746,7 @@ export default function AutoPentestPage() {
|
|
|
|
|
return () => { if (pollRef.current) clearInterval(pollRef.current) }
|
|
|
|
|
}, [sessions, agentId, connectionLost, addToast])
|
|
|
|
|
|
|
|
|
|
// Auto-scroll logs
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (activeTab === 'logs' && logsEndRef.current) {
|
|
|
|
|
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
|
|
|
|
}
|
|
|
|
|
}, [logs, activeTab])
|
|
|
|
|
// Auto-scroll logs disabled — user controls scroll position
|
|
|
|
|
|
|
|
|
|
// ─── History ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
@@ -1376,48 +1378,60 @@ export default function AutoPentestPage() {
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* LLM Provider / Model Selection */}
|
|
|
|
|
{availableModels.length > 0 && (
|
|
|
|
|
<div className="mb-6 flex flex-col sm:flex-row gap-3 sm:gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-xs font-medium text-dark-400 mb-1">LLM Provider</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedProvider}
|
|
|
|
|
onChange={e => {
|
|
|
|
|
setSelectedProvider(e.target.value)
|
|
|
|
|
const m = availableModels.find(m => m.provider_id === e.target.value)
|
|
|
|
|
if (m) setSelectedModel(m.default_model)
|
|
|
|
|
else setSelectedModel('')
|
|
|
|
|
}}
|
|
|
|
|
disabled={isRunning}
|
|
|
|
|
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-sm text-white focus:outline-none focus:border-green-500 disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Auto (best available)</option>
|
|
|
|
|
{availableModels.map(m => (
|
|
|
|
|
<option key={m.provider_id} value={m.provider_id}>
|
|
|
|
|
{m.provider_name} (Tier {m.tier})
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-xs font-medium text-dark-400 mb-1">Model</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedModel}
|
|
|
|
|
onChange={e => setSelectedModel(e.target.value)}
|
|
|
|
|
disabled={isRunning}
|
|
|
|
|
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-sm text-white focus:outline-none focus:border-green-500 disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Auto (default)</option>
|
|
|
|
|
{(selectedProvider
|
|
|
|
|
? (availableModels.find(m => m.provider_id === selectedProvider)?.available_models || [])
|
|
|
|
|
: availableModels.flatMap(m => m.available_models).filter((v, i, a) => a.indexOf(v) === i)
|
|
|
|
|
).map(model => (
|
|
|
|
|
<option key={model} value={model}>{model}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-6 flex flex-col sm:flex-row gap-3 sm:gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-xs font-medium text-dark-400 mb-1">LLM Provider</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedProvider}
|
|
|
|
|
onChange={e => {
|
|
|
|
|
setSelectedProvider(e.target.value)
|
|
|
|
|
const m = availableModels.find(m => m.provider_id === e.target.value)
|
|
|
|
|
if (m) setSelectedModel(m.default_model)
|
|
|
|
|
else if (e.target.value === 'anthropic') setSelectedModel('claude-sonnet-4-20250514')
|
|
|
|
|
else setSelectedModel('')
|
|
|
|
|
}}
|
|
|
|
|
disabled={isRunning}
|
|
|
|
|
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-sm text-white focus:outline-none focus:border-green-500 disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Auto (best available)</option>
|
|
|
|
|
<option value="anthropic">Anthropic (Claude API)</option>
|
|
|
|
|
<option value="claude_code">Claude Code (OAuth)</option>
|
|
|
|
|
<option value="openai">OpenAI</option>
|
|
|
|
|
<option value="gemini">Gemini</option>
|
|
|
|
|
<option value="openrouter">OpenRouter</option>
|
|
|
|
|
{availableModels.filter(m => !['anthropic','claude_code','openai','gemini','openrouter'].includes(m.provider_id)).map(m => (
|
|
|
|
|
<option key={m.provider_id} value={m.provider_id}>
|
|
|
|
|
{m.provider_name} (Tier {m.tier})
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-xs font-medium text-dark-400 mb-1">Model</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedModel}
|
|
|
|
|
onChange={e => setSelectedModel(e.target.value)}
|
|
|
|
|
disabled={isRunning}
|
|
|
|
|
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-sm text-white focus:outline-none focus:border-green-500 disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Auto (default)</option>
|
|
|
|
|
{selectedProvider === 'anthropic' || selectedProvider === 'claude_code' || selectedProvider === '' ? (
|
|
|
|
|
<>
|
|
|
|
|
<option value="claude-opus-4-20250514">Claude Opus 4</option>
|
|
|
|
|
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
|
|
|
|
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
|
|
|
|
|
<option value="claude-haiku-4-5-20251001">Claude Haiku 4.5</option>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
{(selectedProvider && availableModels.find(m => m.provider_id === selectedProvider)?.available_models || [])
|
|
|
|
|
.filter(m => !m.startsWith('claude-'))
|
|
|
|
|
.map(model => (
|
|
|
|
|
<option key={model} value={model}>{model}</option>
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Multi-target textarea */}
|
|
|
|
|
{multiTarget && (
|
|
|
|
|