feat(reports): store and browse report artifacts
This commit is contained in:
1 parent
5967a43eb5
commit
6f2e479958
13 files changed
+875
-93
No files matched your search
+124
-76
@@ -1,76 +1,124 @@
|
||||
{% extends "base.html" %} {% block title %}Reports — Intrusion Detection{%
|
||||
endblock %} {% block content %}
|
||||
<h1>Reports</h1>
|
||||
<p class="muted">Browse the timestamped reports written by each scanner.</p>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-actions" style="margin-top: 0">
|
||||
<label style="flex-direction: row; align-items: center; gap: 0.5rem"
|
||||
>Source
|
||||
<select id="source">
|
||||
<option value="copyparty">copyparty</option>
|
||||
<option value="npm">npm</option>
|
||||
<option value="app">app</option>
|
||||
<option value="mikrotik">mikrotik</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn" id="refresh-btn">Refresh</button>
|
||||
</div>
|
||||
<div class="list" id="files"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head"><h2 id="content-title">Report content</h2></div>
|
||||
<pre class="content" id="content">
|
||||
Select a report on the left to view it.</pre
|
||||
>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
async function loadFiles() {
|
||||
const source = document.getElementById("source").value;
|
||||
const list = document.getElementById("files");
|
||||
list.innerHTML = '<div class="muted">Loading…</div>';
|
||||
try {
|
||||
const data = await api(`/api/reports/${source}`);
|
||||
list.innerHTML = "";
|
||||
const files = data.files || [];
|
||||
if (!files.length) {
|
||||
list.innerHTML = '<div class="muted">No reports in this source.</div>';
|
||||
return;
|
||||
}
|
||||
for (const name of files) {
|
||||
list.append(
|
||||
el("a", {
|
||||
href: "#",
|
||||
text: name,
|
||||
onclick: (ev) => {
|
||||
ev.preventDefault();
|
||||
openReport(name);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
list.innerHTML = `<div class="muted">${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function openReport(name) {
|
||||
const source = document.getElementById("source").value;
|
||||
document.getElementById("content-title").textContent = name;
|
||||
const pre = document.getElementById("content");
|
||||
pre.textContent = "Loading…";
|
||||
try {
|
||||
const data = await api(`/api/reports/${source}/${name}`);
|
||||
pre.textContent = data.content;
|
||||
} catch (e) {
|
||||
pre.textContent = `Error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", loadFiles);
|
||||
document.getElementById("source").addEventListener("change", loadFiles);
|
||||
loadFiles();
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% extends "base.html" %} {% block title %}Reports — Intrusion Detection{% endblock %} {% block content %}
|
||||
<h1>Reports</h1>
|
||||
<p class="muted">Browse the timestamped reports written by each scanner.</p>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-actions" style="margin-top: 0">
|
||||
<label style="flex-direction: row; align-items: center; gap: 0.5rem"
|
||||
>Source
|
||||
<select id="source">
|
||||
<option value="copyparty">copyparty</option>
|
||||
<option value="npm">npm</option>
|
||||
<option value="app">app</option>
|
||||
<option value="mikrotik">mikrotik</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn" id="refresh-btn">Refresh</button>
|
||||
</div>
|
||||
<div class="list" id="files"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head"><h2 id="content-title">Report content</h2></div>
|
||||
<pre class="content" id="content">Select a report on the left to view it.</pre>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
let nextCursor = null;
|
||||
|
||||
function addReportLink(list, label, onClick) {
|
||||
const link = document.createElement("a");
|
||||
link.href = "#";
|
||||
link.textContent = label;
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
list.append(link);
|
||||
}
|
||||
|
||||
async function loadFiles(append = false) {
|
||||
const source = document.getElementById("source").value;
|
||||
const list = document.getElementById("files");
|
||||
if (!append) {
|
||||
nextCursor = null;
|
||||
list.innerHTML = '<div class="muted">Loading…</div>';
|
||||
} else {
|
||||
document.getElementById("load-more")?.remove();
|
||||
}
|
||||
|
||||
let importedPaths = new Set();
|
||||
try {
|
||||
const query = new URLSearchParams({ source, limit: "50" });
|
||||
if (append && nextCursor) query.set("cursor", nextCursor);
|
||||
const data = await api(`/api/reports?${query}`);
|
||||
if (!append) list.innerHTML = "";
|
||||
for (const artifact of data.items || []) {
|
||||
importedPaths.add(artifact.original_path);
|
||||
addReportLink(list, `${artifact.generated_at} | ${artifact.report_type} | ${artifact.original_path}`, () =>
|
||||
openArtifact(artifact.id),
|
||||
);
|
||||
}
|
||||
nextCursor = data.next_cursor;
|
||||
} catch (e) {
|
||||
if (append) {
|
||||
list.append(el("div", { className: "muted", text: e.message }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!append) {
|
||||
try {
|
||||
const data = await api(`/api/reports/${source}`);
|
||||
for (const name of data.files || []) {
|
||||
if (importedPaths.has(`${source}/${name}`)) continue;
|
||||
addReportLink(list, `${name} | legacy file`, () => openReport(name));
|
||||
}
|
||||
} catch (e) {
|
||||
if (!list.childElementCount) list.append(el("div", { className: "muted", text: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
if (nextCursor) {
|
||||
const button = document.createElement("button");
|
||||
button.id = "load-more";
|
||||
button.className = "btn";
|
||||
button.textContent = "Load more";
|
||||
button.addEventListener("click", () => loadFiles(true));
|
||||
list.append(button);
|
||||
}
|
||||
if (!list.childElementCount) list.innerHTML = '<div class="muted">No reports in this source.</div>';
|
||||
}
|
||||
|
||||
async function openArtifact(id) {
|
||||
document.getElementById("content-title").textContent = "Database report";
|
||||
const pre = document.getElementById("content");
|
||||
pre.textContent = "Loading…";
|
||||
try {
|
||||
const data = await api(`/api/report-artifacts/${id}`);
|
||||
document.getElementById("content-title").textContent = data.original_path;
|
||||
const payload = data.payload || {};
|
||||
pre.textContent = payload.format === "text" ? payload.content || "" : JSON.stringify(payload, null, 2);
|
||||
} catch (e) {
|
||||
pre.textContent = `Error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function openReport(name) {
|
||||
const source = document.getElementById("source").value;
|
||||
document.getElementById("content-title").textContent = name;
|
||||
const pre = document.getElementById("content");
|
||||
pre.textContent = "Loading…";
|
||||
try {
|
||||
const data = await api(`/api/reports/${source}/${name}`);
|
||||
pre.textContent = data.content;
|
||||
} catch (e) {
|
||||
pre.textContent = `Error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", () => loadFiles());
|
||||
document.getElementById("source").addEventListener("change", () => loadFiles());
|
||||
loadFiles();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user