Merge pull request #201 from outis1one/claude/asterisk-digital-ocean-w22kk8
Show carrier/ASN and country in the CrowdSec bans table
This commit is contained in:
@@ -426,6 +426,12 @@ def get_decisions():
|
|||||||
return []
|
return []
|
||||||
decisions = []
|
decisions = []
|
||||||
for alert in data or []:
|
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 []:
|
for d in alert.get("decisions") or []:
|
||||||
decisions.append({
|
decisions.append({
|
||||||
"id": d.get("id"),
|
"id": d.get("id"),
|
||||||
@@ -433,6 +439,9 @@ def get_decisions():
|
|||||||
"scenario": d.get("scenario"),
|
"scenario": d.get("scenario"),
|
||||||
"duration": d.get("duration"),
|
"duration": d.get("duration"),
|
||||||
"origin": d.get("origin"),
|
"origin": d.get("origin"),
|
||||||
|
"as_number": source.get("as_number", ""),
|
||||||
|
"as_name": source.get("as_name", ""),
|
||||||
|
"country": source.get("cn", ""),
|
||||||
})
|
})
|
||||||
return decisions
|
return decisions
|
||||||
|
|
||||||
@@ -444,7 +453,11 @@ def delete_decision(decision_id):
|
|||||||
return ok, (err or out or ("deleted" if ok else "failed"))
|
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()
|
asns = set()
|
||||||
for path in ASN_SCENARIO_FILES:
|
for path in ASN_SCENARIO_FILES:
|
||||||
try:
|
try:
|
||||||
@@ -458,7 +471,8 @@ def get_asn_exempt():
|
|||||||
tok = tok.strip().strip("'").strip('"')
|
tok = tok.strip().strip("'").strip('"')
|
||||||
if tok:
|
if tok:
|
||||||
asns.add(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):
|
def set_asn_exempt(asn_list):
|
||||||
@@ -530,7 +544,7 @@ INDEX_HTML = """<!doctype html>
|
|||||||
<div id="tab-crowdsec" style="display:none">
|
<div id="tab-crowdsec" style="display:none">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="margin-top:0">Active bans</h3>
|
<h3 style="margin-top:0">Active bans</h3>
|
||||||
<table id="dec-table"><thead><tr><th>IP/Range</th><th>Scenario</th><th>Duration</th><th>Origin</th><th></th></tr></thead><tbody></tbody></table>
|
<table id="dec-table"><thead><tr><th>IP/Range</th><th>Scenario</th><th>Network / Carrier</th><th>Country</th><th>Duration</th><th>Origin</th><th></th></tr></thead><tbody></tbody></table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="margin-top:0">Asterisk brute-force ASN exemptions</h3>
|
<h3 style="margin-top:0">Asterisk brute-force ASN exemptions</h3>
|
||||||
@@ -539,6 +553,7 @@ INDEX_HTML = """<!doctype html>
|
|||||||
<input type="text" id="asn-input" placeholder="e.g. 21928, 14593">
|
<input type="text" id="asn-input" placeholder="e.g. 21928, 14593">
|
||||||
<button class="action" id="asn-save">Save</button>
|
<button class="action" id="asn-save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="asn-list" class="muted" style="margin-top:0.5rem"></div>
|
||||||
<div id="msg"></div>
|
<div id="msg"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,17 +583,24 @@ async function loadSecurity() {
|
|||||||
</tr>`).join("") || "<tr><td colspan=5 class=muted>No events found.</td></tr>";
|
</tr>`).join("") || "<tr><td colspan=5 class=muted>No events found.</td></tr>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastDecisions = [];
|
||||||
|
|
||||||
async function loadDecisions() {
|
async function loadDecisions() {
|
||||||
const res = await fetch("/api/decisions");
|
const res = await fetch("/api/decisions");
|
||||||
const decisions = await res.json();
|
lastDecisions = await res.json();
|
||||||
const tbody = document.querySelector("#dec-table tbody");
|
const tbody = document.querySelector("#dec-table tbody");
|
||||||
tbody.innerHTML = decisions.map(d => `<tr>
|
tbody.innerHTML = lastDecisions.map(d => `<tr>
|
||||||
<td>${esc(d.value)}</td>
|
<td>${esc(d.value)}</td>
|
||||||
<td>${esc(d.scenario)}</td>
|
<td>${esc(d.scenario)}</td>
|
||||||
|
<td>${d.as_number ? esc(d.as_number) + (d.as_name ? " — " + esc(d.as_name) : "") : ""}</td>
|
||||||
|
<td>${esc(d.country)}</td>
|
||||||
<td>${esc(d.duration)}</td>
|
<td>${esc(d.duration)}</td>
|
||||||
<td>${esc(d.origin)}</td>
|
<td>${esc(d.origin)}</td>
|
||||||
<td><button class="action" onclick="unban(${d.id})">Unban</button></td>
|
<td>
|
||||||
</tr>`).join("") || "<tr><td colspan=5 class=muted>No active bans.</td></tr>";
|
<button class="action" onclick="unban(${d.id})">Unban</button>
|
||||||
|
${d.as_number ? `<button class="action" onclick="exemptAsn('${esc(d.as_number)}')">Exempt ASN</button>` : ""}
|
||||||
|
</td>
|
||||||
|
</tr>`).join("") || "<tr><td colspan=7 class=muted>No active bans.</td></tr>";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unban(id) {
|
async function unban(id) {
|
||||||
@@ -589,10 +611,23 @@ async function unban(id) {
|
|||||||
loadDecisions();
|
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() {
|
async function loadAsnExempt() {
|
||||||
const res = await fetch("/api/asn-exempt");
|
const res = await fetch("/api/asn-exempt");
|
||||||
const data = await res.json();
|
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 () => {
|
document.getElementById("asn-save").addEventListener("click", async () => {
|
||||||
@@ -646,7 +681,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif self.path == "/api/decisions":
|
elif self.path == "/api/decisions":
|
||||||
self._json(get_decisions())
|
self._json(get_decisions())
|
||||||
elif self.path == "/api/asn-exempt":
|
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:
|
else:
|
||||||
self._json({"error": "not found"}, 404)
|
self._json({"error": "not found"}, 404)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user