Track last processed byte offset per log file so scans only read new entries, add --full flag to force a rescan, and persist state atomically.
186 lines
5.5 KiB
HTML
186 lines
5.5 KiB
HTML
{% extends "base.html" %} {% block title %}Scan — Intrusion Detection{% endblock
|
|
%} {% block content %}
|
|
<h1>Run a scan</h1>
|
|
<p class="muted">
|
|
Scans run in the background; only one at a time. You can leave this page and
|
|
come back.
|
|
</p>
|
|
|
|
<div class="panel">
|
|
<form id="scan-form" class="form-grid">
|
|
<label
|
|
>Type
|
|
<select id="kind">
|
|
<option value="consolidated">Consolidated (copyparty + npm)</option>
|
|
<option value="copyparty">copyparty only</option>
|
|
<option value="npm">npm only</option>
|
|
</select>
|
|
</label>
|
|
<label>Top IPs <input id="top" type="number" value="30" min="1" /></label>
|
|
<label
|
|
>Min denied (copyparty)
|
|
<input id="min_denied" type="number" value="1" min="0"
|
|
/></label>
|
|
<label
|
|
>Min errors (npm) <input id="min_errors" type="number" value="5" min="0"
|
|
/></label>
|
|
<label
|
|
>Error ratio (npm)
|
|
<input
|
|
id="error_ratio"
|
|
type="number"
|
|
step="0.05"
|
|
value="0.5"
|
|
min="0"
|
|
max="1"
|
|
/></label>
|
|
<label class="check"
|
|
><input id="update_firewall" type="checkbox" /> Update firewall after
|
|
scan</label
|
|
>
|
|
<label class="check"
|
|
><input id="dry_run" type="checkbox" /> Dry-run (preview, no
|
|
changes)</label
|
|
>
|
|
<label class="check"
|
|
><input id="full" type="checkbox" /> Full scan (ignore incremental
|
|
state)</label
|
|
>
|
|
</form>
|
|
<div class="panel-actions">
|
|
<button class="btn primary" id="run-btn">Run scan</button>
|
|
<span class="muted" id="busy-hint"></span>
|
|
</div>
|
|
<div id="job-status" class="job-status"></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-head"><h2>Recent jobs</h2></div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Job</th>
|
|
<th>Kind</th>
|
|
<th>State</th>
|
|
<th>Started</th>
|
|
<th>Finished</th>
|
|
<th class="num">Blocked</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="jobs"></tbody>
|
|
</table>
|
|
</div>
|
|
{% endblock %} {% block scripts %}
|
|
<script>
|
|
let pollTimer = null;
|
|
|
|
const form = document.getElementById("scan-form");
|
|
const runBtn = document.getElementById("run-btn");
|
|
const hint = document.getElementById("busy-hint");
|
|
|
|
async function runScan() {
|
|
const kind = document.getElementById("kind").value;
|
|
const params = {
|
|
top: parseInt(document.getElementById("top").value, 10) || 30,
|
|
min_denied:
|
|
parseInt(document.getElementById("min_denied").value, 10) || 1,
|
|
min_errors:
|
|
parseInt(document.getElementById("min_errors").value, 10) || 5,
|
|
error_ratio:
|
|
parseFloat(document.getElementById("error_ratio").value) || 0.5,
|
|
update_firewall: document.getElementById("update_firewall").checked,
|
|
dry_run: document.getElementById("dry_run").checked,
|
|
full: document.getElementById("full").checked,
|
|
quiet: true,
|
|
};
|
|
runBtn.disabled = true;
|
|
try {
|
|
const job = await startScan(kind, params);
|
|
setStatus(`Job ${job.job_id} started (${job.state}).`, "ok");
|
|
poll(job.job_id);
|
|
} catch (e) {
|
|
setStatus(e.message, "error");
|
|
runBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function setStatus(msg, type) {
|
|
const box = document.getElementById("job-status");
|
|
box.textContent = msg;
|
|
box.style.color =
|
|
type === "error"
|
|
? "var(--danger)"
|
|
: type === "ok"
|
|
? "var(--ok)"
|
|
: "inherit";
|
|
}
|
|
|
|
async function poll(jobId) {
|
|
clearTimeout(pollTimer);
|
|
let job;
|
|
try {
|
|
job = await api(`/api/scan/${jobId}`);
|
|
} catch (e) {
|
|
setStatus(e.message, "error");
|
|
runBtn.disabled = false;
|
|
return;
|
|
}
|
|
const r = job.result || {};
|
|
setStatus(
|
|
`[${job.state}] ${job.kind} — started ${fmtTime(job.started)}` +
|
|
(job.finished ? `, finished ${fmtTime(job.finished)}` : "") +
|
|
(r.blocked_targets != null
|
|
? `, blocked targets: ${r.blocked_targets}`
|
|
: "") +
|
|
(job.error ? `, error: ${job.error}` : ""),
|
|
job.state === "error" ? "error" : job.state === "done" ? "ok" : "",
|
|
);
|
|
if (job.state === "running" || job.state === "queued") {
|
|
pollTimer = setTimeout(() => poll(jobId), 2000);
|
|
} else {
|
|
runBtn.disabled = false;
|
|
refreshJobs();
|
|
const running = (await api("/api/scan/status")).running;
|
|
hint.textContent = running ? "" : "";
|
|
}
|
|
}
|
|
|
|
async function refreshJobs() {
|
|
try {
|
|
const s = await api("/api/scan/status");
|
|
hint.textContent = s.running ? "A scan is currently running…" : "";
|
|
const body = document.getElementById("jobs");
|
|
body.innerHTML = "";
|
|
const jobs = s.latest_scans || [];
|
|
if (!jobs.length) {
|
|
body.innerHTML =
|
|
'<tr><td colspan="6" class="muted">No jobs yet.</td></tr>';
|
|
return;
|
|
}
|
|
for (const j of jobs) {
|
|
const r = j.result || {};
|
|
body.append(
|
|
el(
|
|
"tr",
|
|
el("td", { class: "mono", text: j.job_id }),
|
|
el("td", { text: j.kind }),
|
|
el("td", {}, badge(j.state)),
|
|
el("td", { text: fmtTime(j.started) }),
|
|
el("td", { text: fmtTime(j.finished) }),
|
|
el("td", { class: "num", text: r.blocked_targets ?? "—" }),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
toast(e.message, "error");
|
|
}
|
|
}
|
|
|
|
runBtn.addEventListener("click", runScan);
|
|
refreshJobs();
|
|
// /api/scan/status is only polled every 15 s; the active job's own
|
|
// 2 s poll keeps live progress without hammering the status endpoint.
|
|
setInterval(refreshJobs, 15000);
|
|
</script>
|
|
{% endblock %}
|