diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index cab8096..9802e56 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog — Alpinux Static +## [1.7.0] — 2026-05-06 + +### Ajouté +- Page **Bannis** (`/errors/banned/`) : liste tous les bannissements fail2ban (`global-blacklist`) groupés par AS, avec nom de l'opérateur et pays +- Débloquer une IP/CIDR individuelle ou tout un AS d'un seul clic +- Filtre de recherche dynamique par IP, CIDR ou nom d'AS +- Mise à jour en temps réel des lignes après déblocage (pas de rechargement) +- Sudoers `static-cdn` : ajout de la permission `unbanip` + +--- + ## [1.6.1] — 2026-05-06 ### Modifié diff --git a/app/VERSION b/app/VERSION index 9c6d629..bd8bf88 100644 --- a/app/VERSION +++ b/app/VERSION @@ -1 +1 @@ -1.6.1 +1.7.0 diff --git a/app/app.py b/app/app.py index d44703b..8b16403 100644 --- a/app/app.py +++ b/app/app.py @@ -1138,6 +1138,62 @@ def errors_ban(): return jsonify({"error": str(e)}), 500 +@app.route("/errors/banned/") +def errors_banned(): + redir = _require_admin() + if redir: + return redir + banned_ips, banned_nets = _get_banned_ips() + all_entries = sorted(banned_ips) + sorted(str(n) for n in banned_nets) + + by_asn: dict = {} + for entry in all_entries: + rep_ip = entry.split("/")[0] + info = _lookup_ip_asn(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(entry) + + groups = sorted(by_asn.values(), key=lambda g: len(g["entries"]), reverse=True) + return render_template("banned.html", groups=groups, total=len(all_entries)) + + +@app.route("/errors/unban", methods=["POST"]) +def errors_unban(): + global _BANNED_CACHE_TS + redir = _require_admin() + if redir: + return redir + entries = request.form.getlist("entries") or [request.form.get("entry", "").strip()] + entries = [e.strip() for e in entries if e.strip()] + if not entries: + return jsonify({"error": "Aucune cible spécifiée"}), 400 + for e in entries: + if not re.match(r"^[\d\.a-fA-F:/]+$", e): + return jsonify({"error": f"Entrée invalide : {e}"}), 400 + jail = "global-blacklist" + unbanned, errors = [], [] + for entry in entries: + try: + r = subprocess.run( + ["/usr/bin/sudo", "/usr/bin/fail2ban-client", "set", jail, "unbanip", entry], + capture_output=True, text=True, timeout=10 + ) + (unbanned if r.returncode == 0 else errors).append(entry) + except Exception as ex: + errors.append(f"{entry}: {ex}") + _BANNED_CACHE_TS = 0 + if errors and not unbanned: + return jsonify({"error": f"Erreur sur : {', '.join(errors)}"}), 500 + return jsonify({"ok": True, "unbanned": unbanned, "errors": errors, "count": len(unbanned)}) + + # ── Corbeille ──────────────────────────────────────────────────────── @app.route("/trash") diff --git a/app/static/app.css b/app/static/app.css index 6346a1b..b84898f 100644 --- a/app/static/app.css +++ b/app/static/app.css @@ -381,3 +381,13 @@ footer { background: var(--blue-dark); color: rgba(255,255,255,.6); margin-top: .err-ignored-chip { display: inline-flex; align-items: center; gap: .35rem; background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 20px; padding: .2rem .6rem .2rem .8rem; font-size: .82rem; font-family: monospace; } .err-ignored-remove { background: none; border: none; cursor: pointer; font-size: .75rem; color: #9ca3af; line-height: 1; padding: 0; transition: color .15s; } .err-ignored-remove:hover { color: #ef4444; } +.banned-table .col-asn { width: 7rem; } +.banned-table .col-country { width: 4rem; text-align: center; } +.banned-as-row td { background: var(--blue-light); border-top: 2px solid var(--border); } +.banned-as-cell { padding: .5rem .75rem; } +.banned-as-label { font-size: .9rem; } +.banned-country { color: var(--muted); font-size: .8rem; } +.banned-entry-row td { padding-left: 2rem; } +.btn-unban { font-size: .75rem; padding: .2rem .5rem; background: #f0fdf4; color: #15803d; border: 1px solid #86efac; border-radius: 5px; cursor: pointer; } +.btn-unban:hover:not(:disabled) { filter: brightness(.92); } +.btn-unban-as { background: #fefce8; color: #854d0e; border-color: #fde68a; } diff --git a/app/templates/banned.html b/app/templates/banned.html new file mode 100644 index 0000000..838cc99 --- /dev/null +++ b/app/templates/banned.html @@ -0,0 +1,161 @@ +{% extends "base.html" %} +{% block title %}Bannissements{% endblock %} + +{% block content %} +
+
+

Bannissements {{ total }} entrée{{ 's' if total != 1 }}

+ +
+ + {% if not groups %} +

Aucune IP bannie dans global-blacklist.

+ {% else %} + +
+ + +
+ + + + + + + + + + + + + {% for group in groups %} + + + + + {% for entry in group.entries %} + + + + + {% endfor %} + {% endfor %} + +
IP / CIDRASOpérateurPays
+ + {% if group.asn %}AS{{ group.asn }}{% else %}AS inconnu{% endif %} + {% if group.name %} · {{ group.name }}{% endif %} + {% if group.country %} [{{ group.country }}]{% endif %} + + {{ group.entries|length }} entrée{{ 's' if group.entries|length != 1 }} + + +
{{ entry }} + +
+ {% endif %} +
+ + +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 3c0545b..348b273 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -25,6 +25,8 @@ {% if request.endpoint in ('stats', 'stats_report') %}class="active"{% endif %}>Statistiques Erreurs + Bannis Corbeille{% if trash_count %}{{ trash_count }}{% endif %}