add web GUI — dark hacker interface with real-time SSE output, vector picker, report viewer
This commit is contained in:
260
gui/server.py
Normal file
260
gui/server.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The Analyzer — Web GUI Server
|
||||
Run: python3 server.py [port]
|
||||
Opens a dark hacker-themed web interface at http://localhost:5000
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import queue
|
||||
import threading
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_from_directory, stream_with_context
|
||||
|
||||
# Paths
|
||||
HERE = Path(__file__).parent
|
||||
REPO = HERE.parent
|
||||
ANALYZER = REPO / "analyzer"
|
||||
REPORTS = REPO / "reports"
|
||||
VECTORS = REPO / "vectors"
|
||||
|
||||
app = Flask(__name__, template_folder=HERE / "templates", static_folder=HERE / "static")
|
||||
|
||||
# Active scan tracking
|
||||
active_scans = {}
|
||||
scan_history = []
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────
|
||||
|
||||
def get_vectors():
|
||||
"""Return list of available vectors with metadata."""
|
||||
vectors = []
|
||||
for f in sorted(VECTORS.glob("*.sh")):
|
||||
num = f.stem.split("-")[0]
|
||||
name = "-".join(f.stem.split("-")[1:])
|
||||
desc = ""
|
||||
sev = ""
|
||||
with open(f) as fh:
|
||||
for line in fh:
|
||||
if line.startswith("# Desc:"):
|
||||
desc = line.replace("# Desc:", "").strip()
|
||||
if line.startswith("# Severity:"):
|
||||
sev = line.replace("# Severity:", "").strip()
|
||||
vectors.append({
|
||||
"num": int(num),
|
||||
"name": name,
|
||||
"file": f.name,
|
||||
"desc": desc,
|
||||
"severity": sev
|
||||
})
|
||||
return vectors
|
||||
|
||||
def get_reports():
|
||||
"""Return list of completed reports."""
|
||||
reports = []
|
||||
for f in sorted(REPORTS.glob("*.md"), reverse=True):
|
||||
if f.name.startswith("."):
|
||||
continue
|
||||
size = f.stat().st_size
|
||||
target = "Unknown"
|
||||
vulns = 0
|
||||
date = ""
|
||||
with open(f) as fh:
|
||||
for line in fh:
|
||||
if line.startswith("**Target:**"):
|
||||
target = line.replace("**Target:**", "").strip()
|
||||
if line.startswith("**Date:**"):
|
||||
date = line.replace("**Date:**", "").strip()
|
||||
if line.startswith("### "):
|
||||
vulns += 1
|
||||
html_path = f.with_suffix(".html")
|
||||
has_html = html_path.exists()
|
||||
reports.append({
|
||||
"file": f.name,
|
||||
"target": target,
|
||||
"date": date,
|
||||
"vulns": vulns,
|
||||
"size": size,
|
||||
"html": has_html
|
||||
})
|
||||
return reports
|
||||
|
||||
# ─── SSE Scan Runner ──────────────────────────────────────
|
||||
|
||||
class ScanRunner:
|
||||
"""Runs the analyzer and streams output via SSE."""
|
||||
|
||||
def __init__(self, target, mode, vectors=None):
|
||||
self.target = target
|
||||
self.mode = mode
|
||||
self.vectors = vectors
|
||||
self.q = queue.Queue()
|
||||
self.process = None
|
||||
self.done = False
|
||||
self.scan_id = str(int(time.time()))
|
||||
self.start_time = time.time()
|
||||
|
||||
def run(self):
|
||||
"""Run the scan in a thread, pushing output to queue."""
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env["OLLAMA_HOST"] = env.get("OLLAMA_HOST", "http://10.30.20.110:11434")
|
||||
env["OLLAMA_MODEL"] = env.get("OLLAMA_MODEL", "granite4.1:8b")
|
||||
env["TERM"] = "xterm"
|
||||
|
||||
cmd = ["bash", str(ANALYZER), self.target, self.mode]
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
cwd=str(REPO)
|
||||
)
|
||||
|
||||
for line in iter(self.process.stdout.readline, ""):
|
||||
if line:
|
||||
self.q.put({"type": "output", "data": line.rstrip()})
|
||||
|
||||
self.process.wait()
|
||||
|
||||
# Find latest report
|
||||
latest_report = None
|
||||
reports = sorted(REPORTS.glob("*.md"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
for r in reports:
|
||||
if not r.name.startswith("."):
|
||||
latest_report = r.name
|
||||
break
|
||||
|
||||
elapsed = time.time() - self.start_time
|
||||
self.q.put({
|
||||
"type": "complete",
|
||||
"data": {
|
||||
"exit_code": self.process.returncode,
|
||||
"elapsed": f"{elapsed:.1f}s",
|
||||
"report": latest_report
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.q.put({"type": "error", "data": str(e)})
|
||||
finally:
|
||||
self.done = True
|
||||
|
||||
def stream(self):
|
||||
"""Generator for SSE streaming."""
|
||||
while not self.done or not self.q.empty():
|
||||
try:
|
||||
msg = self.q.get(timeout=0.5)
|
||||
yield f"data: {json.dumps(msg)}\n\n"
|
||||
except queue.Empty:
|
||||
yield ": keepalive\n\n"
|
||||
|
||||
# ─── Routes ───────────────────────────────────────────────
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
vectors = get_vectors()
|
||||
reports = get_reports()
|
||||
return render_template("index.html", vectors=vectors, reports=reports)
|
||||
|
||||
@app.route("/api/vectors")
|
||||
def api_vectors():
|
||||
return jsonify(get_vectors())
|
||||
|
||||
@app.route("/api/reports")
|
||||
def api_reports():
|
||||
return jsonify(get_reports())
|
||||
|
||||
@app.route("/api/scan", methods=["POST"])
|
||||
def start_scan():
|
||||
data = request.get_json()
|
||||
target = data.get("target", "").strip()
|
||||
mode = data.get("mode", "quick")
|
||||
vector_list = data.get("vectors", "")
|
||||
|
||||
if not target:
|
||||
return jsonify({"error": "Target URL required"}), 400
|
||||
|
||||
# Add https:// if missing
|
||||
if not target.startswith("http"):
|
||||
target = "https://" + target
|
||||
|
||||
runner = ScanRunner(target, mode, vector_list)
|
||||
scan_id = runner.scan_id
|
||||
active_scans[scan_id] = runner
|
||||
|
||||
thread = threading.Thread(target=runner.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
return jsonify({"scan_id": scan_id, "status": "started"})
|
||||
|
||||
@app.route("/api/scan/<scan_id>/stream")
|
||||
def scan_stream(scan_id):
|
||||
runner = active_scans.get(scan_id)
|
||||
if not runner:
|
||||
return jsonify({"error": "Scan not found"}), 404
|
||||
|
||||
return Response(
|
||||
stream_with_context(runner.stream()),
|
||||
mimetype="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/scan/<scan_id>/stop", methods=["POST"])
|
||||
def stop_scan(scan_id):
|
||||
runner = active_scans.get(scan_id)
|
||||
if runner and runner.process:
|
||||
runner.process.terminate()
|
||||
return jsonify({"status": "stopped"})
|
||||
return jsonify({"error": "Scan not found"}), 404
|
||||
|
||||
@app.route("/report/<filename>")
|
||||
def view_report(filename):
|
||||
md_path = REPORTS / filename
|
||||
html_path = md_path.with_suffix(".html")
|
||||
|
||||
if html_path.exists():
|
||||
return send_from_directory(str(REPORTS), html_path.name)
|
||||
|
||||
# Convert markdown to HTML on the fly
|
||||
if md_path.exists():
|
||||
content = md_path.read_text()
|
||||
import markdown
|
||||
html = markdown.markdown(content, extensions=["fenced_code", "tables"])
|
||||
return f"""<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8"><title>Analyzer Report</title>
|
||||
<style>body{{font-family:-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;max-width:800px;margin:40px auto;padding:20px;line-height:1.6}}
|
||||
h1{{color:#f97316}} h2{{color:#f1f5f9;border-bottom:1px solid #334155;padding-bottom:8px}}
|
||||
a{{color:#22d3ee}} code{{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:14px}}
|
||||
pre{{background:#1e293b;padding:16px;border-radius:8px;overflow-x:auto}}
|
||||
</style></head><body>{html}</body></html>"""
|
||||
|
||||
return "Report not found", 404
|
||||
|
||||
@app.route("/report/<filename>/raw")
|
||||
def raw_report(filename):
|
||||
md_path = REPORTS / filename
|
||||
if md_path.exists():
|
||||
return Response(md_path.read_text(), mimetype="text/plain")
|
||||
return "Not found", 404
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
|
||||
print(f"\n ╔══════════════════════════════════════╗")
|
||||
print(f" ║ THE ANALYZER — Web Interface ║")
|
||||
print(f" ║ ║")
|
||||
print(f" ║ http://localhost:{port} ║")
|
||||
print(f" ╚══════════════════════════════════════╝\n")
|
||||
app.run(host="0.0.0.0", port=port, debug=False, threaded=True)
|
||||
683
gui/templates/index.html
Normal file
683
gui/templates/index.html
Normal file
@@ -0,0 +1,683 @@
|
||||
<!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'));
|
||||
event.target.closest('.nav-btn').classList.add('active');
|
||||
}
|
||||
|
||||
// ─── 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();
|
||||
document.querySelector('[onclick*="vectors"]').onclick = () => { showPanel('vectors'); loadVectorsFull(); };
|
||||
document.querySelector('[onclick*="reports"]').onclick = () => { showPanel('reports'); loadReports(); };
|
||||
|
||||
// Enter key starts scan
|
||||
document.getElementById('targetInput').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') startScan();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user