Delete dashboard/script.js
هذا الالتزام موجود في:
@@ -1,361 +0,0 @@
|
|||||||
/* ==========================================================================
|
|
||||||
SIEM Threat Console — dashboard logic
|
|
||||||
Loads siem_report.json (falls back to embedded data.js so the dashboard
|
|
||||||
still works when opened directly via file://), then renders every panel.
|
|
||||||
|
|
||||||
Security note: alert descriptions/evidence are LOG DATA -- they may
|
|
||||||
literally contain attacker-supplied strings like "<script>...". Every
|
|
||||||
place that injects log-derived text into the DOM uses textContent /
|
|
||||||
createElement, never innerHTML, so the console itself can't be XSS'd by
|
|
||||||
the very payloads it's reporting on.
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
const SEVERITY_ORDER = ["critical", "high", "medium", "low"];
|
|
||||||
const SEVERITY_COLOR = {
|
|
||||||
critical: "#FF3B4E",
|
|
||||||
high: "#FF8C42",
|
|
||||||
medium: "#FFD23F",
|
|
||||||
low: "#3FA7FF",
|
|
||||||
};
|
|
||||||
const SEVERITY_CLASS = { critical: "crit", high: "high", medium: "med", low: "low" };
|
|
||||||
|
|
||||||
let REPORT = null;
|
|
||||||
let expandedRowId = null;
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
loadData();
|
|
||||||
document.getElementById("refresh-btn").addEventListener("click", loadData);
|
|
||||||
document.getElementById("search-input").addEventListener("input", renderTable);
|
|
||||||
document.getElementById("severity-filter").addEventListener("change", renderTable);
|
|
||||||
document.getElementById("endpoint-filter").addEventListener("change", renderTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
setSourceNote("loading…");
|
|
||||||
try {
|
|
||||||
const res = await fetch(`siem_report.json?_=${Date.now()}`, { cache: "no-store" });
|
|
||||||
if (!res.ok) throw new Error("fetch failed");
|
|
||||||
REPORT = await res.json();
|
|
||||||
setSourceNote("live: siem_report.json");
|
|
||||||
} catch (err) {
|
|
||||||
if (typeof SIEM_DATA !== "undefined") {
|
|
||||||
REPORT = SIEM_DATA;
|
|
||||||
setSourceNote("embedded data.js (serve over http:// to enable live fetch)");
|
|
||||||
} else {
|
|
||||||
setSourceNote("no data found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSourceNote(text) {
|
|
||||||
document.getElementById("data-source-note").textContent = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAll() {
|
|
||||||
renderHeader();
|
|
||||||
renderStatCards();
|
|
||||||
renderRadar();
|
|
||||||
renderSeverityMeter();
|
|
||||||
renderIpList();
|
|
||||||
populateEndpointFilter();
|
|
||||||
renderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- header */
|
|
||||||
|
|
||||||
function renderHeader() {
|
|
||||||
const gen = new Date(REPORT.generated_at);
|
|
||||||
document.getElementById("generated-at").textContent = isNaN(gen)
|
|
||||||
? REPORT.generated_at
|
|
||||||
: gen.toLocaleString();
|
|
||||||
document.getElementById("endpoint-count").textContent = REPORT.meta.endpoints.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------ stat cards */
|
|
||||||
|
|
||||||
function renderStatCards() {
|
|
||||||
const s = REPORT.stats;
|
|
||||||
const cards = [
|
|
||||||
{ label: "Total Alerts", value: s.total_alerts, cls: "neutral" },
|
|
||||||
{ label: "Critical", value: s.by_severity.critical, cls: "crit" },
|
|
||||||
{ label: "High", value: s.by_severity.high, cls: "high" },
|
|
||||||
{ label: "Medium", value: s.by_severity.medium, cls: "med" },
|
|
||||||
{ label: "Low", value: s.by_severity.low, cls: "low" },
|
|
||||||
{ label: "Malicious IPs", value: s.unique_malicious_ips, cls: "neutral" },
|
|
||||||
];
|
|
||||||
const grid = document.getElementById("stat-grid");
|
|
||||||
grid.textContent = "";
|
|
||||||
cards.forEach((c) => {
|
|
||||||
const card = el("div", { class: `stat-card ${c.cls}` });
|
|
||||||
card.appendChild(el("p", { class: "label", text: c.label }));
|
|
||||||
card.appendChild(el("p", { class: "num", text: String(c.value) }));
|
|
||||||
grid.appendChild(card);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------- IP hashing */
|
|
||||||
|
|
||||||
function hashIpToAngle(ip) {
|
|
||||||
let h = 0;
|
|
||||||
for (let i = 0; i < ip.length; i++) h = (h * 31 + ip.charCodeAt(i)) >>> 0;
|
|
||||||
return h % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
function topSeverityOf(ipInfo) {
|
|
||||||
return SEVERITY_ORDER.find((s) => ipInfo.severity_counts[s] > 0) || "low";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ radar */
|
|
||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
||||||
function svgEl(tag, attrs) {
|
|
||||||
const node = document.createElementNS(SVG_NS, tag);
|
|
||||||
Object.entries(attrs || {}).forEach(([k, v]) => node.setAttribute(k, v));
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRadar() {
|
|
||||||
const svg = document.getElementById("radar-svg");
|
|
||||||
svg.textContent = "";
|
|
||||||
const cx = 150, cy = 150;
|
|
||||||
const rings = [40, 75, 110, 140];
|
|
||||||
|
|
||||||
rings.forEach((r) => {
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx, cy, r, fill: "none", stroke: "#1A2432", "stroke-width": 1,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
// crosshairs
|
|
||||||
[0, 90, 180, 270].forEach((deg) => {
|
|
||||||
const rad = (deg * Math.PI) / 180;
|
|
||||||
svg.appendChild(svgEl("line", {
|
|
||||||
x1: cx, y1: cy,
|
|
||||||
x2: cx + 140 * Math.cos(rad), y2: cy + 140 * Math.sin(rad),
|
|
||||||
stroke: "#161F2B", "stroke-width": 1,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// sweep
|
|
||||||
const sweep = svgEl("g", { class: "radar-sweep" });
|
|
||||||
sweep.appendChild(svgEl("line", {
|
|
||||||
x1: cx, y1: cy, x2: cx, y2: cy - 140, stroke: "#3FA7FF", "stroke-width": 1.5, opacity: 0.7,
|
|
||||||
}));
|
|
||||||
const wedge = svgEl("path", {
|
|
||||||
d: `M ${cx} ${cy} L ${cx} ${cy - 140} A 140 140 0 0 1 ${cx + 140 * Math.sin((35 * Math.PI) / 180)} ${cy - 140 * Math.cos((35 * Math.PI) / 180)} Z`,
|
|
||||||
fill: "#3FA7FF", opacity: 0.08,
|
|
||||||
});
|
|
||||||
sweep.appendChild(wedge);
|
|
||||||
svg.appendChild(sweep);
|
|
||||||
|
|
||||||
const ips = REPORT.malicious_ips.slice(0, 8);
|
|
||||||
const maxScore = ips.length ? ips[0].threat_score : 1;
|
|
||||||
|
|
||||||
ips.forEach((info) => {
|
|
||||||
const angle = hashIpToAngle(info.ip);
|
|
||||||
const rad = (angle * Math.PI) / 180;
|
|
||||||
const norm = maxScore > 0 ? info.threat_score / maxScore : 0;
|
|
||||||
const radius = 128 - norm * 95; // higher score -> nearer centre
|
|
||||||
const x = cx + radius * Math.cos(rad);
|
|
||||||
const y = cy + radius * Math.sin(rad);
|
|
||||||
const sev = topSeverityOf(info);
|
|
||||||
const color = SEVERITY_COLOR[sev];
|
|
||||||
|
|
||||||
if (sev === "critical") {
|
|
||||||
svg.appendChild(svgEl("circle", {
|
|
||||||
cx: x, cy: y, r: 10, fill: "none", stroke: color, "stroke-width": 1.2,
|
|
||||||
class: "radar-ping",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
svg.appendChild(svgEl("circle", { cx: x, cy: y, r: 4.5, fill: color }));
|
|
||||||
|
|
||||||
const label = svgEl("text", {
|
|
||||||
x: x + (Math.cos(rad) >= 0 ? 8 : -8),
|
|
||||||
y: y + 3,
|
|
||||||
fill: "#8592A3",
|
|
||||||
"font-family": "JetBrains Mono, monospace",
|
|
||||||
"font-size": 9,
|
|
||||||
"text-anchor": Math.cos(rad) >= 0 ? "start" : "end",
|
|
||||||
});
|
|
||||||
label.textContent = info.ip;
|
|
||||||
svg.appendChild(label);
|
|
||||||
});
|
|
||||||
|
|
||||||
const legend = document.getElementById("radar-legend");
|
|
||||||
legend.textContent = "";
|
|
||||||
const present = new Set(ips.map(topSeverityOf));
|
|
||||||
SEVERITY_ORDER.filter((s) => present.has(s)).forEach((s) => {
|
|
||||||
const span = el("span");
|
|
||||||
const dot = el("span", { class: "legend-dot" });
|
|
||||||
dot.style.background = SEVERITY_COLOR[s];
|
|
||||||
span.appendChild(dot);
|
|
||||||
span.appendChild(document.createTextNode(s));
|
|
||||||
legend.appendChild(span);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------- sev meter */
|
|
||||||
|
|
||||||
function renderSeverityMeter() {
|
|
||||||
const bar = document.getElementById("sev-meter-bar");
|
|
||||||
const labels = document.getElementById("sev-meter-labels");
|
|
||||||
bar.textContent = "";
|
|
||||||
labels.textContent = "";
|
|
||||||
const s = REPORT.stats.by_severity;
|
|
||||||
const total = REPORT.stats.total_alerts;
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
bar.appendChild(el("div", { class: "seg none" }));
|
|
||||||
} else {
|
|
||||||
SEVERITY_ORDER.forEach((sev) => {
|
|
||||||
const count = s[sev];
|
|
||||||
if (count > 0) {
|
|
||||||
const seg = el("div", { class: `seg ${SEVERITY_CLASS[sev]}` });
|
|
||||||
seg.style.flexGrow = String(count);
|
|
||||||
bar.appendChild(seg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
SEVERITY_ORDER.forEach((sev) => {
|
|
||||||
const item = el("span");
|
|
||||||
const dot = el("span", { class: "legend-dot" });
|
|
||||||
dot.style.background = SEVERITY_COLOR[sev];
|
|
||||||
item.appendChild(dot);
|
|
||||||
item.appendChild(document.createTextNode(`${sev} · ${s[sev]}`));
|
|
||||||
labels.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------- IP list */
|
|
||||||
|
|
||||||
function renderIpList() {
|
|
||||||
const list = document.getElementById("ip-list");
|
|
||||||
list.textContent = "";
|
|
||||||
const ips = REPORT.malicious_ips;
|
|
||||||
document.getElementById("ip-count-hint").textContent = `${ips.length} tracked`;
|
|
||||||
|
|
||||||
if (ips.length === 0) {
|
|
||||||
list.appendChild(el("li", { text: "No malicious IPs detected." }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const maxScore = ips[0].threat_score || 1;
|
|
||||||
ips.slice(0, 10).forEach((info, idx) => {
|
|
||||||
const row = el("li", { class: "ip-row" });
|
|
||||||
row.appendChild(el("span", { class: "ip-rank", text: String(idx + 1) }));
|
|
||||||
row.appendChild(el("span", { class: "ip-addr", text: info.ip }));
|
|
||||||
|
|
||||||
const track = el("span", { class: "ip-bar-track" });
|
|
||||||
const fill = el("span", { class: "ip-bar-fill" });
|
|
||||||
fill.style.width = `${Math.max(6, (info.threat_score / maxScore) * 100)}%`;
|
|
||||||
fill.style.background = SEVERITY_COLOR[topSeverityOf(info)];
|
|
||||||
track.appendChild(fill);
|
|
||||||
row.appendChild(track);
|
|
||||||
|
|
||||||
row.appendChild(el("span", { class: "ip-score", text: String(info.threat_score) }));
|
|
||||||
list.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------- filters */
|
|
||||||
|
|
||||||
function populateEndpointFilter() {
|
|
||||||
const select = document.getElementById("endpoint-filter");
|
|
||||||
const current = select.value;
|
|
||||||
const endpoints = Array.from(new Set(REPORT.alerts.map((a) => a.endpoint))).sort();
|
|
||||||
select.textContent = "";
|
|
||||||
select.appendChild(el("option", { value: "", text: "All endpoints" }));
|
|
||||||
endpoints.forEach((ep) => select.appendChild(el("option", { value: ep, text: ep })));
|
|
||||||
select.value = current || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ table */
|
|
||||||
|
|
||||||
function renderTable() {
|
|
||||||
const tbody = document.getElementById("alerts-tbody");
|
|
||||||
tbody.textContent = "";
|
|
||||||
|
|
||||||
const q = document.getElementById("search-input").value.trim().toLowerCase();
|
|
||||||
const sevFilter = document.getElementById("severity-filter").value;
|
|
||||||
const epFilter = document.getElementById("endpoint-filter").value;
|
|
||||||
|
|
||||||
const filtered = REPORT.alerts.filter((a) => {
|
|
||||||
if (sevFilter && a.severity !== sevFilter) return false;
|
|
||||||
if (epFilter && a.endpoint !== epFilter) return false;
|
|
||||||
if (q) {
|
|
||||||
const hay = `${a.src_ip} ${a.alert_type} ${a.description} ${a.endpoint}`.toLowerCase();
|
|
||||||
if (!hay.includes(q)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("alert-count-hint").textContent =
|
|
||||||
`${filtered.length} / ${REPORT.alerts.length} alerts`;
|
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
const tr = el("tr", { class: "empty-row" });
|
|
||||||
const td = el("td", { text: "No alerts match your filters." });
|
|
||||||
td.colSpan = 6;
|
|
||||||
tr.appendChild(td);
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.forEach((a) => {
|
|
||||||
const tr = el("tr", { class: a.id === expandedRowId ? "expanded" : "" });
|
|
||||||
tr.appendChild(el("td", { class: "ts", text: formatTs(a.timestamp) }));
|
|
||||||
|
|
||||||
const sevTd = el("td");
|
|
||||||
sevTd.appendChild(el("span", { class: `badge ${a.severity}`, text: a.severity }));
|
|
||||||
tr.appendChild(sevTd);
|
|
||||||
|
|
||||||
const typeTd = el("td");
|
|
||||||
typeTd.appendChild(el("span", { class: "type-tag", text: a.alert_type }));
|
|
||||||
tr.appendChild(typeTd);
|
|
||||||
|
|
||||||
tr.appendChild(el("td", { class: "ip", text: a.src_ip }));
|
|
||||||
tr.appendChild(el("td", { class: "endpoint", text: a.endpoint }));
|
|
||||||
tr.appendChild(el("td", { class: "desc", text: a.description }));
|
|
||||||
|
|
||||||
tr.addEventListener("click", () => {
|
|
||||||
expandedRowId = expandedRowId === a.id ? null : a.id;
|
|
||||||
renderTable();
|
|
||||||
});
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
|
|
||||||
if (expandedRowId === a.id) {
|
|
||||||
const evTr = el("tr", { class: "evidence-row" });
|
|
||||||
const evTd = el("td");
|
|
||||||
evTd.colSpan = 6;
|
|
||||||
const box = el("div", { class: "evidence-box" });
|
|
||||||
if (a.evidence && a.evidence.length) {
|
|
||||||
box.textContent = a.evidence.join("\n");
|
|
||||||
} else {
|
|
||||||
box.textContent = "No raw log lines attached to this alert (derived/correlated finding).";
|
|
||||||
}
|
|
||||||
evTd.appendChild(box);
|
|
||||||
evTr.appendChild(evTd);
|
|
||||||
tbody.appendChild(evTr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTs(iso) {
|
|
||||||
const d = new Date(iso);
|
|
||||||
if (isNaN(d)) return iso;
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} `
|
|
||||||
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ util */
|
|
||||||
|
|
||||||
function el(tag, opts) {
|
|
||||||
const node = document.createElement(tag);
|
|
||||||
opts = opts || {};
|
|
||||||
if (opts.class) node.className = opts.class;
|
|
||||||
if (opts.text !== undefined) node.textContent = opts.text;
|
|
||||||
if (opts.value !== undefined) node.value = opts.value;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم