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