Bannis : résolution AS via reverse-index RIPE Stat local + fallback ipinfo.io

- CIDRs bannis via "Ban AS" : résolus depuis as_cache/*.json (0 appel API)
  + nom/pays récupérés en 1 seule requête PostgreSQL (DISTINCT ON asn)
- IPs/CIDRs bannis individuellement : _batch_lookup_ip_asn avec fallback
  ipinfo.io (résout les cas où ip-api.com retourne vide, ex: 103.51.13.0)
- Entrées avec asn='' exclues du cache (AND asn != '') → re-tentée à chaque fois

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alpinux 2026-05-06 15:10:26 +02:00
parent e0a3dd42f4
commit d272b3e8b7

View file

@ -1215,17 +1215,70 @@ def errors_banned_groups():
banned_ips, banned_nets = _get_banned_ips()
all_banned = sorted(banned_ips) + sorted(str(n) for n in banned_nets)
rep_ips = [b.split("/")[0] for b in all_banned]
asn_map = _batch_lookup_ip_asn(rep_ips)
# ── Étape 1 : reverse-index depuis les fichiers RIPE Stat locaux ──
# Les CIDRs bannis via "Ban AS" sont tous dans as_cache/AS*.json → zéro appel API
prefix_to_asn: dict = {}
if _AS_CACHE_DIR.exists():
for cf in _AS_CACHE_DIR.glob("AS*.json"):
try:
d = json.loads(cf.read_text())
asn = str(d.get("asn", cf.stem[2:]))
for p in d.get("prefixes", []):
prefix_to_asn[p] = asn
except Exception:
pass
# ── Étape 2 : classer chaque entrée : connue (prefix cache) ou à chercher ──
entry_asn: dict = {} # entry → asn string
known_asns: set = set()
needs_api: list = [] # entries non trouvées dans prefix cache
for entry in all_banned:
if entry in prefix_to_asn:
asn = prefix_to_asn[entry]
entry_asn[entry] = asn
known_asns.add(asn)
else:
needs_api.append(entry)
# ── Étape 3 : nom/pays des AS connus — 1 seule requête PostgreSQL ──
asn_details: dict = {} # asn → {name, country}
if known_asns:
with _pg() as conn:
if conn:
try:
with conn.cursor() as cur:
cur.execute(
"SELECT DISTINCT ON (asn) asn, name, country FROM ip_asn_cache "
"WHERE asn = ANY(%s) ORDER BY asn, fetched_at DESC",
(list(known_asns),)
)
for row in cur.fetchall():
asn_details[row[0]] = {"name": row[1], "country": row[2]}
except Exception:
try: conn.rollback()
except: pass
# ── Étape 4 : lookup API pour les IPs/CIDRs bannis individuellement ──
if needs_api:
rep_ips = [e.split("/")[0] for e in needs_api]
ip_map = _batch_lookup_ip_asn(rep_ips)
for entry, rep_ip in zip(needs_api, rep_ips):
info = ip_map.get(rep_ip, {})
entry_asn[entry] = info.get("asn") or "?"
if info.get("asn"):
known_asns.add(info["asn"])
asn_details.setdefault(info["asn"], {"name": info.get("name", ""), "country": info.get("country", "")})
# ── Étape 5 : construire les groupes ──
by_asn: dict = {}
for b_entry, rep_ip in zip(all_banned, rep_ips):
info = asn_map.get(rep_ip, {})
key = info.get("asn") or "?"
if key not in by_asn:
by_asn[key] = {"asn": info.get("asn", ""), "name": info.get("name", ""),
"country": info.get("country", ""), "entries": []}
by_asn[key]["entries"].append(b_entry)
for entry in all_banned:
asn = entry_asn.get(entry, "?")
det = asn_details.get(asn, {})
if asn not in by_asn:
by_asn[asn] = {"asn": asn if asn != "?" else "", "name": det.get("name", ""),
"country": det.get("country", ""), "entries": []}
by_asn[asn]["entries"].append(entry)
groups = sorted(by_asn.values(), key=lambda g: len(g["entries"]), reverse=True)
return jsonify({"groups": groups, "total": len(all_banned)})