feat: browser dashboard with live scan view, results, JSON export
This commit is contained in:
+145
@@ -0,0 +1,145 @@
|
||||
const API = 'http://localhost:8000';
|
||||
|
||||
const views = {
|
||||
starter: document.getElementById('view-starter'),
|
||||
scanning: document.getElementById('view-scanning'),
|
||||
results: document.getElementById('view-results'),
|
||||
};
|
||||
|
||||
function showView(name) {
|
||||
Object.values(views).forEach(v => v.classList.add('hidden'));
|
||||
views[name].classList.remove('hidden');
|
||||
}
|
||||
|
||||
// ── Findings rendering ────────────────────────────────────────────────────────
|
||||
|
||||
function renderFinding(f) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'finding';
|
||||
el.dataset.severity = f.severity;
|
||||
el.innerHTML = `
|
||||
<div class="finding-header">
|
||||
<span class="badge badge-${f.severity}">${f.severity}</span>
|
||||
<span class="finding-type">${f.type.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
<div class="finding-url">${f.url}</div>
|
||||
<div class="finding-evidence">${escapeHtml(f.evidence)}</div>
|
||||
`;
|
||||
el.addEventListener('click', () => el.classList.toggle('open'));
|
||||
return el;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// ── Scan flow ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let currentScanId = null;
|
||||
let pollTimer = null;
|
||||
|
||||
async function startScan(url, modules) {
|
||||
const res = await fetch(`${API}/scan`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url, modules }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const { scan_id } = await res.json();
|
||||
return scan_id;
|
||||
}
|
||||
|
||||
function startPolling(scanId) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => pollScan(scanId), 2000);
|
||||
}
|
||||
|
||||
async function pollScan(scanId) {
|
||||
const res = await fetch(`${API}/scan/${scanId}`);
|
||||
if (!res.ok) return;
|
||||
const job = await res.json();
|
||||
|
||||
updateLiveView(job);
|
||||
|
||||
if (job.status === 'completed' || job.status === 'error') {
|
||||
clearInterval(pollTimer);
|
||||
showResults(job);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLiveView(job) {
|
||||
const fill = document.getElementById('progress-fill');
|
||||
const text = document.getElementById('progress-text');
|
||||
const list = document.getElementById('live-findings');
|
||||
|
||||
if (job.total > 0) {
|
||||
const pct = Math.round((job.progress / job.total) * 100);
|
||||
fill.style.width = `${pct}%`;
|
||||
text.textContent = `${job.progress} / ${job.total} paths checked`;
|
||||
} else {
|
||||
fill.style.width = '100%';
|
||||
text.textContent = job.status === 'running' ? 'Scanning…' : job.status;
|
||||
}
|
||||
|
||||
const existing = list.children.length;
|
||||
job.findings.slice(existing).forEach(f => {
|
||||
list.prepend(renderFinding(f));
|
||||
});
|
||||
}
|
||||
|
||||
function showResults(job) {
|
||||
const summary = document.getElementById('results-summary');
|
||||
const list = document.getElementById('results-findings');
|
||||
|
||||
summary.textContent = `${job.findings.length} finding${job.findings.length !== 1 ? 's' : ''} — ${job.target_url}`;
|
||||
list.innerHTML = '';
|
||||
|
||||
const sorted = [...job.findings].sort((a, b) => {
|
||||
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
||||
return (order[a.severity] ?? 5) - (order[b.severity] ?? 5);
|
||||
});
|
||||
|
||||
sorted.forEach(f => list.appendChild(renderFinding(f)));
|
||||
showView('results');
|
||||
}
|
||||
|
||||
// ── Event listeners ───────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('scan-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const url = document.getElementById('url-input').value;
|
||||
const modules = [...document.querySelectorAll('input[name="modules"]:checked')]
|
||||
.map(el => el.value);
|
||||
|
||||
document.getElementById('scan-target').textContent = `Scanning ${url}`;
|
||||
document.getElementById('progress-fill').style.width = '0%';
|
||||
document.getElementById('progress-text').textContent = 'Starting…';
|
||||
document.getElementById('live-findings').innerHTML = '';
|
||||
showView('scanning');
|
||||
|
||||
try {
|
||||
currentScanId = await startScan(url, modules);
|
||||
startPolling(currentScanId);
|
||||
} catch (err) {
|
||||
alert(`Failed to start scan: ${err.message}`);
|
||||
showView('starter');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('new-scan-btn').addEventListener('click', () => {
|
||||
clearInterval(pollTimer);
|
||||
showView('starter');
|
||||
});
|
||||
|
||||
document.getElementById('export-btn').addEventListener('click', () => {
|
||||
if (currentScanId) {
|
||||
window.location.href = `${API}/scan/${currentScanId}/export`;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('filter-severity').addEventListener('change', (e) => {
|
||||
const value = e.target.value;
|
||||
document.querySelectorAll('#results-findings .finding').forEach(el => {
|
||||
el.style.display = (!value || el.dataset.severity === value) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
+69
-1
@@ -1 +1,69 @@
|
||||
<!DOCTYPE html><html><body>loading...</body></html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InfoLeak Scanner</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>InfoLeak Scanner</h1>
|
||||
<p class="subtitle">Web Application Information Leak Detector</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- View: Starter -->
|
||||
<section id="view-starter">
|
||||
<form id="scan-form">
|
||||
<div class="field">
|
||||
<label for="url-input">Target URL</label>
|
||||
<input id="url-input" type="url" placeholder="https://example.com" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Modules</label>
|
||||
<div class="checkboxes">
|
||||
<label><input type="checkbox" name="modules" value="paths" checked /> Path Prober</label>
|
||||
<label><input type="checkbox" name="modules" value="headers" checked /> Header Analyzer</label>
|
||||
<label><input type="checkbox" name="modules" value="secrets" checked /> Response Inspector</label>
|
||||
<label><input type="checkbox" name="modules" value="directory" checked /> Directory Listing</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Start Scan</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- View: Scanning -->
|
||||
<section id="view-scanning" class="hidden">
|
||||
<div class="scan-status">
|
||||
<span id="scan-target"></span>
|
||||
<div class="progress-bar"><div id="progress-fill"></div></div>
|
||||
<span id="progress-text">Initializing...</span>
|
||||
</div>
|
||||
<div id="live-findings" class="findings-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- View: Results -->
|
||||
<section id="view-results" class="hidden">
|
||||
<div class="results-header">
|
||||
<span id="results-summary"></span>
|
||||
<div class="results-actions">
|
||||
<select id="filter-severity">
|
||||
<option value="">All severities</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="info">Info</option>
|
||||
</select>
|
||||
<button id="export-btn">Export JSON</button>
|
||||
<button id="new-scan-btn">New Scan</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="results-findings" class="findings-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--border: #2a2d3a;
|
||||
--text: #e2e8f0;
|
||||
--muted: #6b7280;
|
||||
--accent: #6366f1;
|
||||
--critical: #ef4444;
|
||||
--high: #f97316;
|
||||
--medium: #eab308;
|
||||
--low: #3b82f6;
|
||||
--info: #6b7280;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.subtitle { color: var(--muted); margin-top: 0.25rem; font-size: 0.9rem; }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Starter form */
|
||||
#scan-form {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.field { margin-bottom: 1.25rem; }
|
||||
.field label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--muted); }
|
||||
|
||||
input[type="url"] {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type="url"]:focus { outline: none; border-color: var(--accent); }
|
||||
|
||||
.checkboxes { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.checkboxes label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.875rem; cursor: pointer; }
|
||||
|
||||
button {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
button:hover { opacity: 0.85; }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* Scanning view */
|
||||
.scan-status {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
#scan-target { font-size: 0.875rem; color: var(--muted); display: block; margin-bottom: 0.75rem; }
|
||||
|
||||
.progress-bar {
|
||||
background: var(--border);
|
||||
border-radius: 999px;
|
||||
height: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#progress-fill {
|
||||
background: var(--accent);
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#progress-text { font-size: 0.8rem; color: var(--muted); }
|
||||
|
||||
/* Results header */
|
||||
.results-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
#results-summary { font-weight: 600; }
|
||||
|
||||
.results-actions { display: flex; gap: 0.5rem; align-items: center; }
|
||||
|
||||
select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#new-scan-btn { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
|
||||
|
||||
/* Findings */
|
||||
.findings-list { display: flex; flex-direction: column; gap: 0.5rem; max-width: 900px; }
|
||||
|
||||
.finding {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.finding[data-severity="critical"] { border-left-color: var(--critical); }
|
||||
.finding[data-severity="high"] { border-left-color: var(--high); }
|
||||
.finding[data-severity="medium"] { border-left-color: var(--medium); }
|
||||
.finding[data-severity="low"] { border-left-color: var(--low); }
|
||||
.finding[data-severity="info"] { border-left-color: var(--info); }
|
||||
|
||||
.finding-header { display: flex; align-items: center; gap: 0.75rem; }
|
||||
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.badge-critical { background: var(--critical); color: white; }
|
||||
.badge-high { background: var(--high); color: white; }
|
||||
.badge-medium { background: var(--medium); color: #1a1a1a; }
|
||||
.badge-low { background: var(--low); color: white; }
|
||||
.badge-info { background: var(--surface); border: 1px solid var(--border); color: var(--muted); }
|
||||
|
||||
.finding-type { font-size: 0.875rem; font-weight: 500; }
|
||||
.finding-url { font-size: 0.8rem; color: var(--muted); word-break: break-all; }
|
||||
.finding-evidence { margin-top: 0.5rem; font-size: 0.8rem; color: var(--muted); font-family: monospace; background: var(--bg); padding: 0.4rem 0.6rem; border-radius: 0.25rem; display: none; word-break: break-all; }
|
||||
.finding.open .finding-evidence { display: block; }
|
||||
|
||||
Reference in New Issue
Block a user