الملفات
ghaymah-exam-Mohamed-Ashraf…/dashboard/script.js
2026-07-29 01:08:37 +00:00

362 أسطر
12 KiB
JavaScript

/* ==========================================================================
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;
}