🎉 Initial commit: CrowdSec notification service
All checks were successful
🚀 Docker Build and Push / build-and-push (push) Successful in 48s
All checks were successful
🚀 Docker Build and Push / build-and-push (push) Successful in 48s
This commit is contained in:
35
.gitea/workflows/docker.yaml
Normal file
35
.gitea/workflows/docker.yaml
Normal file
@@ -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
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.DS_Store
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -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"]
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -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
|
||||
88
app.py
Normal file
88
app.py
Normal file
@@ -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)
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask>=2.0
|
||||
requests>=2.25
|
||||
gunicorn>=20.0
|
||||
Reference in New Issue
Block a user