diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index ba05155..e4c9371 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -426,6 +426,12 @@ def get_decisions(): return [] decisions = [] for alert in data or []: + # AS number/name and country live on the parent alert's "source" + # object, not on the individual decision — confirmed against real + # output (source.as_number, source.as_name, source.cn) rather than + # guessed, after getting evt.Enriched.ASNNumber's type wrong earlier + # tonight for the same underlying data. + source = alert.get("source") or {} for d in alert.get("decisions") or []: decisions.append({ "id": d.get("id"), @@ -433,6 +439,9 @@ def get_decisions(): "scenario": d.get("scenario"), "duration": d.get("duration"), "origin": d.get("origin"), + "as_number": source.get("as_number", ""), + "as_name": source.get("as_name", ""), + "country": source.get("cn", ""), }) return decisions @@ -444,7 +453,11 @@ def delete_decision(decision_id): return ok, (err or out or ("deleted" if ok else "failed")) -def get_asn_exempt(): +def get_asn_exempt(known_names=None): + """known_names: optional {asn: as_name} lookup, built from current + decisions, to label already-exempt ASNs that aren't actively generating + bans right now (and so wouldn't otherwise have a name available).""" + known_names = known_names or {} asns = set() for path in ASN_SCENARIO_FILES: try: @@ -458,7 +471,8 @@ def get_asn_exempt(): tok = tok.strip().strip("'").strip('"') if tok: asns.add(tok) - return sorted(asns, key=lambda x: int(x) if x.isdigit() else 0) + ordered = sorted(asns, key=lambda x: int(x) if x.isdigit() else 0) + return [{"asn": a, "name": known_names.get(a, "")} for a in ordered] def set_asn_exempt(asn_list): @@ -530,7 +544,7 @@ INDEX_HTML = """ @@ -568,17 +583,24 @@ async function loadSecurity() { `).join("") || "No events found."; } +let lastDecisions = []; + async function loadDecisions() { const res = await fetch("/api/decisions"); - const decisions = await res.json(); + lastDecisions = await res.json(); const tbody = document.querySelector("#dec-table tbody"); - tbody.innerHTML = decisions.map(d => ` + tbody.innerHTML = lastDecisions.map(d => ` ${esc(d.value)} ${esc(d.scenario)} + ${d.as_number ? esc(d.as_number) + (d.as_name ? " — " + esc(d.as_name) : "") : ""} + ${esc(d.country)} ${esc(d.duration)} ${esc(d.origin)} - - `).join("") || "No active bans."; + + + ${d.as_number ? `` : ""} + + `).join("") || "No active bans."; } async function unban(id) { @@ -589,10 +611,23 @@ async function unban(id) { loadDecisions(); } +async function exemptAsn(asn) { + const current = document.getElementById("asn-input").value.split(",").map(s => s.trim()).filter(Boolean); + if (current.includes(asn)) { alert("ASN " + asn + " is already exempt."); return; } + if (!confirm("Add ASN " + asn + " to the Asterisk brute-force exemption list? This only affects Asterisk auth-failure detection — SSH/web/geo protection is unaffected.")) return; + current.push(asn); + document.getElementById("asn-input").value = current.join(", "); + document.getElementById("asn-save").click(); +} + async function loadAsnExempt() { const res = await fetch("/api/asn-exempt"); const data = await res.json(); - document.getElementById("asn-input").value = (data.asns || []).join(", "); + const asns = data.asns || []; + document.getElementById("asn-input").value = asns.map(a => a.asn).join(", "); + document.getElementById("asn-list").textContent = asns.length + ? "Currently exempt: " + asns.map(a => a.asn + (a.name ? " (" + a.name + ")" : "")).join(", ") + : "No ASNs currently exempted."; } document.getElementById("asn-save").addEventListener("click", async () => { @@ -646,7 +681,9 @@ class Handler(BaseHTTPRequestHandler): elif self.path == "/api/decisions": self._json(get_decisions()) elif self.path == "/api/asn-exempt": - self._json({"asns": get_asn_exempt()}) + decisions = get_decisions() + known_names = {d["as_number"]: d["as_name"] for d in decisions if d.get("as_number")} + self._json({"asns": get_asn_exempt(known_names)}) else: self._json({"error": "not found"}, 404)