From d272b3e8b745c0cd845f022439d07c3aea5dbc93 Mon Sep 17 00:00:00 2001 From: Alpinux Date: Wed, 6 May 2026 15:10:26 +0200 Subject: [PATCH] =?UTF-8?q?Bannis=20:=20r=C3=A9solution=20AS=20via=20rever?= =?UTF-8?q?se-index=20RIPE=20Stat=20local=20+=20fallback=20ipinfo.io?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/app.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/app/app.py b/app/app.py index 8071d15..48811c4 100644 --- a/app/app.py +++ b/app/app.py @@ -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)})