feat: browser dashboard with live scan view, results, JSON export

This commit is contained in:
InfoLeak
2026-06-21 18:51:35 +02:00
parent bfeb765610
commit e3d483422d
3 changed files with 386 additions and 1 deletions
+145
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── 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
View File
@@ -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>
+172
View File
@@ -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; }