docs: add standardized comments to all services (Python, Go, Bash)

- Add docs/commenting-standard.md defining per-language comment standards
  (Go godoc, Python PEP-257, C Doxygen, Bash header blocks, SQL banners)

- services/dashboard: 100% docstring coverage (100/100 functions)
  - All FastAPI route handlers, helpers, classes, and models documented
  - Language: French (project convention)

- services/bot-detector: 100% docstring coverage (53/53 symbols)
  - bot_detector.py: 14 functions + module docstring
  - anubis/fetch_rules.py: 9 functions

- shared/python/ja4_common: full docstrings on ClickHouseClient (7 methods)
  and ClickHouseSettings class

- services/correlator: 24 godoc comments added across 6 Go files
  - correlation_service.go: 10 private helpers
  - unixsocket/source.go: 6 parsing/socket helpers
  - correlated_log.go: 4 field extraction helpers
  - orchestrator.go, logger.go, main.go: 4 comments

- services/correlator/scripts/audit-architecture.sh: standardized header block

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-07 21:32:29 +02:00
parent 12d60975da
commit 3dfeba860b
22 changed files with 388 additions and 10 deletions

View File

@ -1,3 +1,13 @@
"""Détecteur de bots par apprentissage automatique semi-supervisé (IsolationForest).
Ce module implémente le cycle de détection IA du service bot_detector :
- chargement et retraining automatique du modèle IsolationForest,
- scoring, normalisation et classification du trafic (fenêtre 1h / 24h),
- intégration des règles Anubis (ALLOW / DENY / WEIGH),
- clustering comportemental DBSCAN, déduplication inter-cycles,
- explainabilité SHAP, détection de dérive conceptuelle,
- écriture des résultats dans ClickHouse (ml_detected_anomalies, ml_all_scores).
"""
import time
import os
import json
@ -30,6 +40,10 @@ warnings.filterwarnings('ignore')
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════
def _require_float(name, default, lo=None, hi=None):
"""Lit une variable d'environnement comme flottant et valide la plage si spécifiée.
Lève SystemExit si la valeur est non numérique ou hors plage (lo, hi) exclusive.
"""
raw = os.getenv(name, str(default))
try:
v = float(raw)
@ -119,9 +133,15 @@ logger.addHandler(_file_handler)
# Wrapper court pour homogénéiser les appels de logging (évite d'importer logger partout).
def log_info(message: str):
"""Enregistre un message de niveau INFO dans le logger du service."""
logger.info(message)
def log_decision(event: str, cycle_id: str, model: str = '', row: dict = None):
"""Enregistre un événement de décision IA au format JSONL dans le fichier de log rotatif.
Chaque ligne contient l'horodatage, le cycle_id, l'événement, le modèle,
la contamination, le seuil et les données supplémentaires de ``row``.
"""
entry = {
'ts': datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
'cycle_id': cycle_id,
@ -136,6 +156,7 @@ def log_decision(event: str, cycle_id: str, model: str = '', row: dict = None):
_file_handler.stream.flush()
def _append_training_history(entry: dict):
"""Ajoute une entrée de métadonnées d'entraînement au fichier d'historique JSONL."""
with open(TRAINING_HISTORY_FILE, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False, default=str) + '\n')
@ -143,6 +164,7 @@ def _append_training_history(entry: dict):
# ARRÊT PROPRE ET HEALTH CHECK
# ═══════════════════════════════════════════════════════════════════════════════
def _shutdown(sig, frame):
"""Gestionnaire de signal SIGTERM/SIGINT : journalise l'arrêt et quitte proprement."""
log_info(f"Signal {sig} reçu — arrêt propre.")
log_decision('SERVICE_STOP', 'shutdown', '', {'signal': sig})
sys.exit(0)
@ -152,12 +174,20 @@ signal.signal(signal.SIGINT, _shutdown)
_service_healthy = True
class _HealthHandler(BaseHTTPRequestHandler):
"""Gestionnaire HTTP minimal pour le point de santé du service.
Répond 200/OK si le service est sain, 503/DEGRADED dans le cas contraire.
"""
def do_GET(self):
"""Répond à la requête GET : renvoie 200 OK ou 503 DEGRADED selon l'état du service."""
code = 200 if _service_healthy else 503
self.send_response(code)
self.end_headers()
self.wfile.write(b'OK' if _service_healthy else b'DEGRADED')
def log_message(self, *args): pass
def log_message(self, *args):
"""Supprime les logs HTTP internes pour ne pas polluer la sortie standard."""
pass
threading.Thread(
target=lambda: HTTPServer(('', HEALTH_PORT), _HealthHandler).serve_forever(),
@ -174,7 +204,10 @@ def get_client():
return _ja4_get_client().connect()
def score_to_threat_level(score: float) -> str:
# Seuils : CRITICAL < -0.30 | HIGH < -0.15 | MEDIUM < -0.05 | LOW < 0 | NORMAL ≥ 0
"""Convertit un score d'anomalie brut IsolationForest en niveau de menace textuel.
Seuils : CRITICAL < 0.30 | HIGH < 0.15 | MEDIUM < 0.05 | LOW < 0 | NORMAL ≥ 0.
"""
if score < -0.30: return 'CRITICAL'
if score < -0.15: return 'HIGH'
if score < -0.05: return 'MEDIUM'
@ -185,9 +218,11 @@ def score_to_threat_level(score: float) -> str:
# GESTION DES MODÈLES
# ═══════════════════════════════════════════════════════════════════════════════
def _current_pointer_path(name: str) -> str:
"""Retourne le chemin du fichier pointeur vers la version courante du modèle ``name``."""
return os.path.join(MODEL_DIR, f'model_{name}.current')
def _get_current_version(name: str):
"""Lit le fichier pointeur et retourne (chemin_modèle, métadonnées) ou (None, None) si absent."""
pointer = _current_pointer_path(name)
if not os.path.exists(pointer): return None, None
with open(pointer) as f: version_id = f.read().strip()
@ -198,6 +233,7 @@ def _get_current_version(name: str):
return model_path, meta
def _purge_old_versions(name: str):
"""Supprime les versions excédentaires du modèle ``name`` en ne conservant que MODEL_HISTORY_COUNT fichiers."""
pattern = os.path.join(MODEL_DIR, f'model_{name}_*.joblib')
versions = sorted(glob.glob(pattern))
to_delete = versions[:-MODEL_HISTORY_COUNT] if len(versions) > MODEL_HISTORY_COUNT else []
@ -209,6 +245,15 @@ def _purge_old_versions(name: str):
log_info(f"[{name}] Version purgée : {version_id} (limite={MODEL_HISTORY_COUNT})")
def load_or_train_model(name: str, human_baseline: pd.DataFrame, features: list, cycle_id: str):
"""Charge le modèle IsolationForest existant ou en entraîne un nouveau si nécessaire.
Réutilise le modèle si son âge est inférieur à RETRAIN_INTERVAL_H et si aucune
dérive conceptuelle significative n'est détectée (A1). En cas d'expiration ou de
dérive, entraîne un nouveau modèle sur ``human_baseline``, le sérialise sur disque,
met à jour le fichier pointeur et purge les anciennes versions.
Retourne l'objet IsolationForest entraîné ou rechargé.
"""
model_path, meta = _get_current_version(name)
if model_path and meta:
trained_at = datetime.fromisoformat(meta['trained_at'])
@ -475,7 +520,15 @@ def _cluster_anomalies(anomalies: pd.DataFrame, features: list) -> pd.DataFrame:
# ANALYSE SEMI-SUPERVISÉE
# ═══════════════════════════════════════════════════════════════════════════════
def run_semi_supervised_logic(df, features, name, cycle_id, recurrence_map):
# ── Trifurcation du trafic selon bot_name et Anubis ─────────────────────
"""Applique le pipeline de détection semi-supervisée sur un sous-ensemble du trafic.
Trifurque le trafic en bots connus, bots Anubis ALLOW et trafic inconnu,
entraîne ou charge le modèle IsolationForest sur la baseline humaine,
score le trafic inconnu, applique les améliorations A2/A4/A6/A8,
et retourne (threats, all_scored) sous forme de DataFrames.
Effets de bord : écriture dans les logs de décision via log_decision.
"""
# 1. Bots connus (dict_bot_ip / dict_bot_ja4) → exclus du scoring IF
known_bots = df[df['bot_name'] != ''].copy()
rest = df[df['bot_name'] == ''].copy()
@ -668,6 +721,7 @@ def _filter_recent_detections(client, all_anom: pd.DataFrame) -> pd.DataFrame:
return all_anom
recent_map = dict(zip(recent_df['src_ip'], recent_df['best_score']))
def _should_insert(row):
"""Détermine si une anomalie doit être réinsérée selon l'évolution du score."""
prev = recent_map.get(row['src_ip'])
if prev is None:
return True
@ -712,7 +766,13 @@ def _preprocess_df(df: pd.DataFrame) -> pd.DataFrame:
# ═══════════════════════════════════════════════════════════════════════════════
_consecutive_failures = 0
def fetch_and_analyze():
global _service_healthy, _consecutive_failures
"""Exécute un cycle complet de détection : requête ClickHouse, scoring et insertion des résultats.
Récupère le trafic depuis la vue view_ai_features_1h (et optionnellement view_ai_features_24h),
applique run_semi_supervised_logic sur les deux modèles (Complet / Applicatif),
insère les scores dans ml_all_scores et les anomalies dans ml_detected_anomalies.
Met à jour _service_healthy et _consecutive_failures en cas d'échec de requête.
"""
cycle_id = datetime.now().strftime('%Y%m%d_%H%M%S')
log_info('=== Lancement cycle IA ===')