From eef8c3835e27e05a307c5958ae2ac1ef5cd73503 Mon Sep 17 00:00:00 2001 From: Maxime Killinger Date: Wed, 31 Dec 2025 18:09:20 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Initial=20commit:=20CrowdSec=20n?= =?UTF-8?q?otification=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker.yaml | 35 ++++++++++++++ .gitignore | 13 ++++++ Dockerfile | 12 +++++ README.md | 51 +++++++++++++++++++++ app.py | 88 ++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ 6 files changed, 202 insertions(+) create mode 100644 .gitea/workflows/docker.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml new file mode 100644 index 0000000..46073f2 --- /dev/null +++ b/.gitea/workflows/docker.yaml @@ -0,0 +1,35 @@ +name: 🚀 Docker Build and Push + +on: [push] + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout code + uses: https://github.com/actions/checkout@v4 + + - name: 🛠️ Set up Docker Buildx + uses: https://github.com/docker/setup-buildx-action@v3 + + - name: 🔐 Login to Gitea Registry + uses: https://github.com/docker/login-action@v3 + with: + registry: gitea.killinger.fr + username: maxime.killinger + password: ${{ secrets.DOCKER_TOKEN }} + + - name: 📦 Build and push Docker image + uses: https://github.com/docker/build-push-action@v5 + with: + context: . + push: true + tags: | + gitea.killinger.fr/maxime.killinger/crowdsec-notify:${{ github.ref_name == 'main' && 'latest' || github.ref_name }} + + - name: 🔔 Trigger Watchtower + if: github.ref == 'refs/heads/main' + env: + TOKEN: ${{ secrets.WATCHTOWER_TOKEN }} + run: | + curl -X GET -H "Authorization: Bearer $TOKEN" http://192.168.1.118:3026/v1/update diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9854d56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.env +.venv +venv/ +ENV/ +.idea/ +.vscode/ +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..92598b1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-alpine + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 5000 + +CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1427d2 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# CrowdSec Notify + +Service de notification intelligent pour CrowdSec qui regroupe les alertes par IP. + +## Fonctionnalités + +- 📊 Regroupe les alertes par IP (évite les doublons) +- 🌍 Affiche le nom complet du pays +- 🔗 Inclut un lien WHOIS pour chaque IP +- 📱 Envoie les notifications via Apprise (Telegram) + +## Déploiement + +L'image Docker est automatiquement construite et poussée sur `gitea.killinger.fr/services/crowdsec-notify:latest` + +### Variables d'environnement + +| Variable | Description | Défaut | +|----------|-------------|--------| +| `APPRISE_URL` | URL du endpoint Apprise | `http://192.168.1.118:8000/notify/telegram_alerts` | + +### Docker Run + +```bash +docker run -d \ + --name crowdsec-notify \ + --restart unless-stopped \ + -p 5000:5000 \ + gitea.killinger.fr/services/crowdsec-notify:latest +``` + +## Configuration CrowdSec + +Dans `/etc/crowdsec/notifications/http.yaml` : + +```yaml +type: http +name: apprise_alert +group_wait: 5m +group_threshold: 10 +format: | + {{ . | toJson }} +url: http://crowdsec-notify:5000/crowdsec +method: POST +headers: + Content-Type: application/json +``` + +## Auteur + +Maxime Killinger diff --git a/app.py b/app.py new file mode 100644 index 0000000..d3b870b --- /dev/null +++ b/app.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +CrowdSec Notification Webhook +Groups alerts by IP and sends formatted notifications via Apprise +Author: Maxime Killinger +""" + +import os +from flask import Flask, request, jsonify +import requests + +app = Flask(__name__) + +APPRISE_URL = os.environ.get("APPRISE_URL", "http://192.168.1.118:8000/notify/telegram_alerts") + +# Country codes to full names +COUNTRIES = { + "CN": "Chine", "RU": "Russie", "US": "États-Unis", "FR": "France", + "DE": "Allemagne", "GB": "Royaume-Uni", "NL": "Pays-Bas", "BR": "Brésil", + "IN": "Inde", "KR": "Corée du Sud", "JP": "Japon", "VN": "Vietnam", + "ID": "Indonésie", "TW": "Taïwan", "HK": "Hong Kong", "SG": "Singapour", + "TH": "Thaïlande", "PH": "Philippines", "MY": "Malaisie", "PK": "Pakistan", + "BD": "Bangladesh", "IR": "Iran", "TR": "Turquie", "UA": "Ukraine", + "PL": "Pologne", "RO": "Roumanie", "IT": "Italie", "ES": "Espagne", + "AR": "Argentine", "MX": "Mexique", "CO": "Colombie", "CL": "Chili", + "ZA": "Afrique du Sud", "EG": "Égypte", "NG": "Nigeria", "KE": "Kenya", + "AU": "Australie", "NZ": "Nouvelle-Zélande", "CA": "Canada", "SE": "Suède", + "NO": "Norvège", "FI": "Finlande", "DK": "Danemark", "BE": "Belgique", + "CH": "Suisse", "AT": "Autriche", "CZ": "Tchéquie", "HU": "Hongrie", + "BG": "Bulgarie", "GR": "Grèce", "PT": "Portugal", "IE": "Irlande", +} + +def get_country_name(code): + if not code: + return "Inconnu" + return COUNTRIES.get(code.upper(), code) + +def format_scenario(scenario): + if not scenario: + return "unknown" + return scenario.replace("crowdsecurity/", "").replace("_", " ") + +@app.route('/crowdsec', methods=['POST']) +def handle_crowdsec(): + try: + alerts = request.json + if not alerts: + return jsonify({"status": "no data"}), 200 + + # Group by IP + ip_groups = {} + for alert in alerts: + ip = alert.get("Source", {}).get("IP", "unknown") + country = alert.get("Source", {}).get("Cn", "") + scenario = alert.get("Scenario", "unknown") + + if ip not in ip_groups: + ip_groups[ip] = {"country": country, "scenarios": []} + ip_groups[ip]["scenarios"].append(format_scenario(scenario)) + + # Format message + num_ips = len(ip_groups) + lines = [] + + for ip, data in ip_groups.items(): + country_name = get_country_name(data["country"]) + whois_link = f"https://who.is/whois-ip/ip-address/{ip}" + lines.append(f"🚫 {ip} ({country_name})") + lines.append(f" 🔗 {whois_link}") + for scenario in data["scenarios"]: + lines.append(f" └ {scenario}") + lines.append("") + + body = "\n".join(lines).strip() + title = f"🛡️ CrowdSec - {num_ips} IP{'s' if num_ips > 1 else ''} bannie{'s' if num_ips > 1 else ''}" + + requests.post(APPRISE_URL, json={"title": title, "body": body}, timeout=10) + return jsonify({"status": "sent", "ips": num_ips}), 200 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + +@app.route('/health', methods=['GET']) +def health(): + return jsonify({"status": "ok"}), 200 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..482c80a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask>=2.0 +requests>=2.25 +gunicorn>=20.0