Files
th-analyzer/gui/templates/index.html

690 lines
19 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>THE ANALYZER — Autonomous Security Engine</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap');
:root {
--bg: #0a0e17;
--surface: #111827;
--surface2: #1e293b;
--border: #1e293b;
--text: #e2e8f0;
--text-dim: #64748b;
--accent: #f97316;
--accent2: #22d3ee;
--green: #22c55e;
--red: #ef4444;
--yellow: #eab308;
--blue: #3b82f6;
--purple: #a855f7;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
overflow: hidden;
}
/* ─── Sidebar ─── */
.sidebar {
width: 260px;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
flex-shrink: 0;
}
.logo {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 14px;
line-height: 1.4;
color: var(--accent);
padding: 12px;
background: var(--bg);
border-radius: 8px;
border: 1px solid var(--border);
text-align: center;
}
.logo span { color: var(--text-dim); font-weight: 400; }
.nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-btn {
background: none;
border: none;
color: var(--text-dim);
padding: 10px 12px;
text-align: left;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-family: 'Inter', sans-serif;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 8px;
}
.nav-btn:hover { background: var(--surface2); color: var(--text); }
.nav-btn.active { background: var(--surface2); color: var(--accent); }
.nav-btn .icon { font-size: 16px; width: 20px; text-align: center; }
/* ─── Main ─── */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
padding: 16px 24px;
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar h1 {
font-size: 18px;
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-dot.idle { background: var(--text-dim); }
.status-dot.running { background: var(--green); animation: pulse 1.5s infinite; }
.status-dot.error { background: var(--red); }
.status-dot.done { background: var(--blue); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ─── Scan Panel ─── */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
.panel-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-dim);
margin-bottom: 16px;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.input-group input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
color: var(--text);
font-size: 14px;
font-family: 'JetBrains Mono', monospace;
outline: none;
transition: border 0.15s;
}
.input-group input:focus { border-color: var(--accent); }
.input-group input::placeholder { color: var(--text-dim); }
.mode-selector {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.mode-btn {
flex: 1;
padding: 8px 16px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-dim);
cursor: pointer;
font-size: 13px;
font-family: 'Inter', sans-serif;
transition: all 0.15s;
text-align: center;
}
.mode-btn:hover { border-color: var(--accent2); color: var(--text); }
.mode-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(249,115,22,0.1); }
.btn-primary {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, var(--accent), #ea580c);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.15s;
font-family: 'Inter', sans-serif;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(249,115,22,0.3); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-danger { background: var(--red); }
.btn-danger:hover { box-shadow: 0 4px 20px rgba(239,68,68,0.3); }
/* ─── Output Terminal ─── */
.terminal {
background: #000;
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.5;
}
.terminal-header {
padding: 10px 16px;
background: #0a0a0a;
border-bottom: 1px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: space-between;
}
.terminal-header span { color: var(--text-dim); font-size: 12px; }
.terminal-dots { display: flex; gap: 6px; }
.terminal-dot { width: 10px; height: 10px; border-radius: 50%; }
.terminal-dot.r { background: #ff5f56; }
.terminal-dot.y { background: #ffbd2e; }
.terminal-dot.g { background: #27c93f; }
.terminal-body {
padding: 16px;
height: 350px;
overflow-y: auto;
color: #a8b2c1;
}
.terminal-body::-webkit-scrollbar { width: 6px; }
.terminal-body::-webkit-scrollbar-track { background: #000; }
.terminal-body::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 3px; }
.terminal-line { white-space: pre-wrap; word-break: break-all; margin-bottom: 2px; }
.terminal-line .find { color: var(--red); font-weight: 600; }
.terminal-line .ok { color: var(--green); }
.terminal-line .info { color: var(--blue); }
.terminal-line .warn { color: var(--yellow); }
.terminal-line .brain { color: var(--purple); }
.terminal-line .dim { color: var(--text-dim); }
.terminal-line .accent { color: var(--accent); }
/* ─── Vector Grid ─── */
.vector-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px;
}
.vector-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.vector-card:hover { border-color: var(--accent2); }
.vector-card.selected { border-color: var(--accent); background: rgba(249,115,22,0.08); }
.vector-card .num {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-dim);
}
.vector-card .name {
font-size: 13px;
font-weight: 500;
margin: 4px 0;
}
.vector-card .sev {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.sev-critical { color: var(--red); }
.sev-high { color: var(--accent); }
.sev-medium { color: var(--yellow); }
.sev-low { color: var(--blue); }
/* ─── Report List ─── */
.report-list { display: flex; flex-direction: column; gap: 8px; }
.report-item {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.15s;
}
.report-item:hover { border-color: var(--accent2); }
.report-item .target { font-weight: 500; font-size: 14px; }
.report-item .meta { font-size: 12px; color: var(--text-dim); }
.report-item .vulns { font-family: 'JetBrains Mono', monospace; font-weight: 600; }
.report-item .vulns.found { color: var(--red); }
.report-item .vulns.none { color: var(--green); }
.hidden { display: none !important; }
/* ─── Responsive ─── */
@media (max-width: 768px) {
.sidebar { width: 60px; padding: 12px 8px; }
.sidebar .logo { font-size: 10px; padding: 8px; }
.sidebar .nav-btn span { display: none; }
.main { padding: 0; }
.content { padding: 16px; }
.input-group { flex-direction: column; }
}
</style>
</head>
<body>
<!-- ─── Sidebar ─── -->
<div class="sidebar">
<div class="logo">
ANALYZER<br>
<span>v2.0</span>
</div>
<div class="nav">
<button class="nav-btn active" onclick="showPanel('scan')">
<span class="icon"></span> <span>Scan</span>
</button>
<button class="nav-btn" onclick="showPanel('vectors')">
<span class="icon">🎯</span> <span>Vectors</span>
</button>
<button class="nav-btn" onclick="showPanel('reports')">
<span class="icon">📄</span> <span>Reports</span>
</button>
</div>
</div>
<!-- ─── Main ─── -->
<div class="main">
<div class="topbar">
<h1><span class="status-dot idle" id="statusDot"></span><span id="statusText">Ready</span></h1>
<div style="display:flex;gap:8px;align-items:center;">
<span style="font-size:12px;color:var(--text-dim)" id="timer">00:00</span>
</div>
</div>
<div class="content">
<!-- ═══ SCAN PANEL ═══ -->
<div id="panel-scan" class="">
<div class="panel">
<div class="panel-title">New Scan</div>
<div class="input-group">
<input type="text" id="targetInput" placeholder="https://target.com" spellcheck="false">
</div>
<div class="mode-selector">
<button class="mode-btn active" data-mode="quick" onclick="setMode(this)">⚡ Quick</button>
<button class="mode-btn" data-mode="deep" onclick="setMode(this)">🔍 Deep</button>
<button class="mode-btn" data-mode="custom" onclick="setMode(this)">🎯 Custom</button>
</div>
<button class="btn-primary" id="scanBtn" onclick="startScan()">▶ Launch Scan</button>
<button class="btn-primary btn-danger hidden" id="stopBtn" onclick="stopScan()">■ Stop</button>
</div>
<!-- Custom vector selector (hidden by default) -->
<div class="panel hidden" id="vectorSelector">
<div class="panel-title">Select Vectors</div>
<div class="vector-grid" id="vectorGrid"></div>
</div>
<!-- Terminal Output -->
<div class="terminal hidden" id="terminal">
<div class="terminal-header">
<div class="terminal-dots">
<div class="terminal-dot r"></div>
<div class="terminal-dot y"></div>
<div class="terminal-dot g"></div>
</div>
<span>analyzer@kali:~$</span>
</div>
<div class="terminal-body" id="terminalBody"></div>
</div>
</div>
<!-- ═══ VECTORS PANEL ═══ -->
<div id="panel-vectors" class="hidden">
<div class="panel">
<div class="panel-title">Attack Vectors — 21 Total</div>
<div class="vector-grid" id="vectorGridFull"></div>
</div>
</div>
<!-- ═══ REPORTS PANEL ═══ -->
<div id="panel-reports" class="hidden">
<div class="panel">
<div class="panel-title">Scan Reports</div>
<div class="report-list" id="reportList"></div>
</div>
</div>
</div>
</div>
<script>
// ─── State ───
let currentMode = 'quick';
let selectedVectors = [];
let currentScanId = null;
// ─── Panel Navigation ───
function showPanel(name) {
document.querySelectorAll('.content > div').forEach(d => d.classList.add('hidden'));
document.getElementById('panel-' + name).classList.remove('hidden');
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
if (event && event.target) {
const btn = event.target.closest('.nav-btn');
if (btn) btn.classList.add('active');
}
// Load panel content
if (name === 'vectors') loadVectorsFull();
if (name === 'reports') loadReports();
}
// ─── Mode Selection ───
function setMode(btn) {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
const vecSel = document.getElementById('vectorSelector');
if (currentMode === 'custom') {
vecSel.classList.remove('hidden');
loadVectorSelector();
} else {
vecSel.classList.add('hidden');
}
}
// ─── Vector Selector ───
async function loadVectorSelector() {
const res = await fetch('/api/vectors');
const vectors = await res.json();
const grid = document.getElementById('vectorGrid');
grid.innerHTML = '';
vectors.forEach(v => {
const card = document.createElement('div');
card.className = 'vector-card' + (selectedVectors.includes(v.num) ? ' selected' : '');
card.innerHTML = `
<div class="num">#${String(v.num).padStart(2,'0')}</div>
<div class="name">${v.name}</div>
<div class="sev sev-${v.severity.toLowerCase()}">${v.severity}</div>
`;
card.onclick = () => {
const idx = selectedVectors.indexOf(v.num);
if (idx >= 0) selectedVectors.splice(idx, 1);
else selectedVectors.push(v.num);
card.classList.toggle('selected');
};
grid.appendChild(card);
});
}
// ─── Vectors Full List ───
async function loadVectorsFull() {
const res = await fetch('/api/vectors');
const vectors = await res.json();
const grid = document.getElementById('vectorGridFull');
if (grid.children.length > 0) return; // already loaded
grid.innerHTML = '';
vectors.forEach(v => {
const card = document.createElement('div');
card.className = 'vector-card';
card.innerHTML = `
<div class="num">#${String(v.num).padStart(2,'0')}</div>
<div class="name">${v.name}</div>
<div class="sev sev-${v.severity.toLowerCase()}">${v.severity}</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">${v.desc}</div>
`;
grid.appendChild(card);
});
}
// ─── Reports List ───
async function loadReports() {
const res = await fetch('/api/reports');
const reports = await res.json();
const list = document.getElementById('reportList');
list.innerHTML = '';
if (reports.length === 0) {
list.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-dim)">No reports yet. Run a scan!</div>';
return;
}
reports.forEach(r => {
const item = document.createElement('div');
item.className = 'report-item';
const vulnClass = r.vulns > 0 ? 'found' : 'none';
item.innerHTML = `
<div>
<div class="target">${r.target}</div>
<div class="meta">${r.date} · ${(r.size/1024).toFixed(1)} KB</div>
</div>
<div class="vulns ${vulnClass}">${r.vulns > 0 ? '⚠ ' + r.vulns : '✅ 0'} vulns</div>
`;
item.onclick = () => window.open('/report/' + r.file, '_blank');
list.appendChild(item);
});
}
// ─── Terminal output ───
function escapeHtml(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function appendTerminal(text) {
const body = document.getElementById('terminalBody');
const line = document.createElement('div');
line.className = 'terminal-line';
// Sanitize then colorize
const safe = escapeHtml(text);
const html = safe
.replace(/💥.*/g, m => `<span class="find">${m}</span>`)
.replace(/✅.*/g, m => `<span class="ok">${m}</span>`)
.replace(/.*/g, m => `<span class="info">${m}</span>`)
.replace(/⚠️.*/g, m => `<span class="warn">${m}</span>`)
.replace(/🧠.*/g, m => `<span class="brain">${m}</span>`)
.replace(/➜.*/g, m => `<span class="dim">${m}</span>`)
.replace(/🎯.*/g, m => `<span class="accent">${m}</span>`)
.replace(/❌.*/g, m => `<span class="find">${m}</span>`);
line.innerHTML = html;
body.appendChild(line);
body.scrollTop = body.scrollHeight;
}
// ─── Timer ───
let timerInterval = null;
function startTimer() {
const start = Date.now();
timerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - start) / 1000);
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
const s = String(elapsed % 60).padStart(2, '0');
document.getElementById('timer').textContent = `${m}:${s}`;
}, 1000);
}
function stopTimer() {
if (timerInterval) clearInterval(timerInterval);
}
// ─── Scan ───
async function startScan() {
const target = document.getElementById('targetInput').value.trim();
if (!target) {
alert('Enter a target URL');
return;
}
// Show terminal
document.getElementById('terminal').classList.remove('hidden');
document.getElementById('terminalBody').innerHTML = '';
document.getElementById('scanBtn').classList.add('hidden');
document.getElementById('stopBtn').classList.remove('hidden');
// Status
document.getElementById('statusDot').className = 'status-dot running';
document.getElementById('statusText').textContent = 'Scanning...';
startTimer();
const payload = {
target: target,
mode: currentMode,
vectors: selectedVectors.join(',')
};
appendTerminal('🎯 Target: ' + target);
appendTerminal('⚡ Mode: ' + currentMode);
appendTerminal('───────────────');
try {
const res = await fetch('/api/scan', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
currentScanId = data.scan_id;
// Connect to SSE stream
const evtSource = new EventSource(`/api/scan/${data.scan_id}/stream`);
evtSource.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'output') {
appendTerminal(msg.data);
} else if (msg.type === 'complete') {
evtSource.close();
finishScan(msg.data);
} else if (msg.type === 'error') {
appendTerminal('❌ ' + msg.data);
evtSource.close();
finishScan({exit_code: -1});
}
};
evtSource.onerror = () => {
evtSource.close();
appendTerminal('⚠️ Connection lost. Scan may still be running.');
};
} catch (e) {
appendTerminal('❌ Failed to start scan: ' + e.message);
finishScan({});
}
}
function finishScan(data) {
stopTimer();
document.getElementById('statusDot').className = data.exit_code === 0 ? 'status-dot done' : 'status-dot error';
document.getElementById('statusText').textContent = data.exit_code === 0 ? 'Complete' : 'Failed';
document.getElementById('scanBtn').classList.remove('hidden');
document.getElementById('stopBtn').classList.add('hidden');
if (data.report) {
appendTerminal('');
appendTerminal('📄 Report: /report/' + data.report);
appendTerminal('✨ Scan complete in ' + (data.elapsed || '?'));
} else if (data.exit_code === 0) {
appendTerminal('✨ Scan complete');
}
loadReports();
}
async function stopScan() {
if (!currentScanId) return;
await fetch(`/api/scan/${currentScanId}/stop`, {method: 'POST'});
appendTerminal('⏹️ Scan stopped by user');
finishScan({exit_code: -1});
}
// ─── Init ───
loadVectorSelector();
loadReports(); // preload reports too
// Enter key starts scan
document.getElementById('targetInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') startScan();
});
</script>
</body>
</html>