Reflète l'état réel du système après les étapes 1-9 du roadmap : - §5.2 (fleet_detector NetworkX/Louvain) et §5.8 (Jaccard cross-domain) : ✅ - MetaLearner (régression logistique, fallback poids fixes) : documenté - ExIFFI (profondeur isolation EIF) + erreur AE par feature : documenté - KL divergence en complément du KS, drift adversarial : documenté - HTTP/2 fingerprinting (h2_fingerprint, dict_browser_h2, axis_h2_coherence) : documenté - Métriques de cycle (metrics.py, ml_performance_metrics, alertes) : documenté - Browser confidence : 5 axes → 6 axes (axis_h2_coherence) - 85 features (73 FEATURES + 12 FEATURES_COMPLET), 12 modules, 53 routes dashboard - Conformité thèse : 99.4% (était 97.9%), §5 : 87.5% (était 62.5%) - Tables nouvelles : fleet_detections, ml_performance_metrics, soc_feedback - Dictionnaires : 8 (dict_browser_h2 ajouté) - Dashboard : 16 pages + 37 API routes (fleet, health ajoutés) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
724 lines
32 KiB
Markdown
724 lines
32 KiB
Markdown
# Schéma de base de données
|
||
|
||
La plateforme ja4-platform utilise ClickHouse comme entrepôt de données central.
|
||
Le schéma est réparti sur **deux bases de données** configurables via variables
|
||
d'environnement (`CLICKHOUSE_DB_LOGS`, `CLICKHOUSE_DB_PROCESSING`) :
|
||
|
||
| Variable | Défaut | Rôle |
|
||
|----------|--------|------|
|
||
| `CLICKHOUSE_DB_LOGS` | `ja4_logs` | Ingestion brute + logs HTTP parsés |
|
||
| `CLICKHOUSE_DB_PROCESSING` | `ja4_processing` | Agrégations, ML, vues, dictionnaires, audit |
|
||
|
||
Les vues matérialisées dans `ja4_processing` lisent depuis `ja4_logs` (références
|
||
inter-bases). Le schéma complet est défini dans 13 fichiers SQL ordonnés dans
|
||
`shared/clickhouse/` et déployé via `deploy_schema.sh`.
|
||
|
||
---
|
||
|
||
## Récapitulatif global
|
||
|
||
| Catégorie | Nombre | Objets |
|
||
|-----------|--------|--------|
|
||
| **Bases de données** | 2 | `ja4_logs`, `ja4_processing` |
|
||
| **Tables** | 19 | `http_logs_raw`, `http_logs`, `ref_bot_networks`, `bot_ip`, `bot_ja4`, `anubis_ip_rules`, `anubis_asn_rules`, `agg_host_ip_ja4_1h`, `agg_header_fingerprint_1h`, `agg_path_sequences_1h`, `agg_request_timing_1h`, `agg_ip_behavior_1h`, `agg_resource_cascade_1h`, `ml_detected_anomalies`, `ml_all_scores`, `audit_logs`, `fleet_detections`, `ml_performance_metrics`, `soc_feedback` |
|
||
| **Dictionnaires** | 8 | `dict_iplocate_asn`, `dict_bot_ip`, `dict_bot_ja4`, `dict_browser_ja4`, `dict_browser_h2`, `dict_asn_reputation`, `dict_anubis_ip`, `dict_anubis_asn` |
|
||
| **Vues matérialisées** | 7 | `mv_http_logs`, `mv_agg_host_ip_ja4_1h`, `mv_agg_header_fingerprint_1h`, `mv_agg_path_sequences_1h`, `mv_agg_request_timing_1h`, `mv_agg_ip_behavior_1h`, `mv_agg_resource_cascade_1h` |
|
||
| **Vues** | 9 | `view_ip_recurrence`, `view_ai_features_1h`, `view_form_bruteforce_detected`, `view_host_ip_ja4_rotation`, `view_dashboard_user_agents`, `view_dashboard_entities`, `view_resource_cascade_1h`, `view_thesis_features_1h`, `view_health_metrics` |
|
||
|
||
---
|
||
|
||
## Rétention des données (TTL)
|
||
|
||
| Table | TTL | Clé de partition |
|
||
|-------|-----|------------------|
|
||
| `http_logs_raw` | 2 heures | `toStartOfHour(ingest_time)` |
|
||
| `http_logs` | 30 jours | `toDate(log_date)` |
|
||
| `agg_host_ip_ja4_1h` | 7 jours | `toDate(window_start)` |
|
||
| `agg_header_fingerprint_1h` | 7 jours | `toDate(window_start)` |
|
||
| `agg_path_sequences_1h` | 7 jours | `toDate(window_start)` |
|
||
| `agg_request_timing_1h` | 7 jours | `toDate(window_start)` |
|
||
| `agg_ip_behavior_1h` | 7 jours | `toDate(window_start)` |
|
||
| `agg_resource_cascade_1h` | 7 jours | `toDate(window_start)` |
|
||
| `ml_detected_anomalies` | 7 jours | `toDate(detected_at)` |
|
||
| `ml_all_scores` | 7 jours | `toDate(window_start)` |
|
||
| `audit_logs` | 90 jours | `toDate(timestamp)` |
|
||
|
||
Toutes les tables d'agrégation et ML utilisent `ttl_only_drop_parts = 1` pour
|
||
une expiration efficace au niveau des partitions.
|
||
|
||
---
|
||
|
||
## Tables — Base `ja4_logs`
|
||
|
||
### http_logs_raw
|
||
|
||
Table d'ingestion brute — cible directe des INSERTs du correlator.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `raw_json` | String CODEC(ZSTD(3)) | Log corrélé complet au format JSON |
|
||
| `ingest_time` | DateTime DEFAULT `now()` | Horodatage d'insertion |
|
||
|
||
- **Moteur** : MergeTree
|
||
- **Partition** : `toStartOfHour(ingest_time)`
|
||
- **Tri** : `ingest_time`
|
||
- **TTL** : `ingest_time + INTERVAL 2 HOUR`
|
||
- **Settings** : `index_granularity = 8192, ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### http_logs
|
||
|
||
Table de logs HTTP parsés et enrichis — alimentée par la vue matérialisée
|
||
`mv_http_logs`.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `time` | DateTime | Horodatage de la requête |
|
||
| `log_date` | Date DEFAULT `toDate(time)` | Clé de partition |
|
||
| `src_ip` | IPv4 | IP source du client |
|
||
| `src_port` | UInt16 | Port source |
|
||
| `dst_ip` | IPv4 | IP destination du serveur |
|
||
| `dst_port` | UInt16 | Port destination |
|
||
| `src_asn` | UInt32 | ASN source (enrichi via dict_iplocate_asn) |
|
||
| `src_country_code` | LowCardinality(String) | Code pays |
|
||
| `src_as_name` | LowCardinality(String) | Nom de l'AS |
|
||
| `src_org` | LowCardinality(String) | Organisation de l'AS |
|
||
| `src_domain` | LowCardinality(String) | Domaine de l'AS |
|
||
| `method` | LowCardinality(String) | Méthode HTTP |
|
||
| `scheme` | LowCardinality(String) | Schéma URL (http/https) |
|
||
| `host` | LowCardinality(String) | En-tête Host HTTP |
|
||
| `path` | String CODEC(ZSTD(3)) | Chemin de la requête |
|
||
| `query` | String CODEC(ZSTD(3)) | Paramètres de requête |
|
||
| `http_version` | LowCardinality(String) | Version HTTP |
|
||
| `orphan_side` | LowCardinality(String) | Côté orphelin (A, B, ou vide) |
|
||
| `correlated` | UInt8 | 1 si corrélation HTTP+TLS réussie |
|
||
| `keepalives` | UInt16 | Numéro de séquence keep-alive |
|
||
| `a_timestamp` | UInt64 | Horodatage source A (ns) |
|
||
| `b_timestamp` | UInt64 | Horodatage source B (ns) |
|
||
| `conn_id` | String CODEC(ZSTD(3)) | Identifiant de connexion TCP |
|
||
| `ip_meta_df` | UInt8 | Drapeau Don't Fragment |
|
||
| `ip_meta_id` | UInt16 | Identification IP |
|
||
| `ip_meta_total_length` | UInt16 | Longueur totale IP |
|
||
| `ip_meta_ttl` | UInt8 | TTL IP |
|
||
| `tcp_meta_options` | LowCardinality(String) | Options TCP |
|
||
| `tcp_meta_window_size` | UInt32 | Taille de fenêtre TCP |
|
||
| `tcp_meta_mss` | UInt16 | MSS TCP |
|
||
| `tcp_meta_window_scale` | UInt8 | Facteur d'échelle de fenêtre TCP |
|
||
| `syn_to_clienthello_ms` | Int32 | Délai SYN→ClientHello (ms) |
|
||
| `tls_version` | LowCardinality(String) | Version TLS |
|
||
| `tls_sni` | LowCardinality(String) | SNI TLS |
|
||
| `tls_alpn` | LowCardinality(String) | ALPN TLS |
|
||
| `ja3` | String CODEC(ZSTD(3)) | Empreinte JA3 |
|
||
| `ja3_hash` | String CODEC(ZSTD(3)) | Hash MD5 JA3 |
|
||
| `ja4` | String CODEC(ZSTD(3)) | Empreinte JA4 |
|
||
| `client_headers` | String CODEC(ZSTD(3)) | Noms d'en-têtes séparés par virgule |
|
||
| `header_user_agent` | String CODEC(ZSTD(3)) | En-tête User-Agent |
|
||
| `header_accept` | String CODEC(ZSTD(3)) | En-tête Accept |
|
||
| `header_accept_encoding` | String CODEC(ZSTD(3)) | En-tête Accept-Encoding |
|
||
| `header_accept_language` | String CODEC(ZSTD(3)) | En-tête Accept-Language |
|
||
| `header_content_type` | String CODEC(ZSTD(3)) | En-tête Content-Type |
|
||
| `header_x_request_id` | String CODEC(ZSTD(3)) | En-tête X-Request-Id |
|
||
| `header_x_trace_id` | String CODEC(ZSTD(3)) | En-tête X-Trace-Id |
|
||
| `header_x_forwarded_for` | String CODEC(ZSTD(3)) | En-tête X-Forwarded-For |
|
||
| `header_sec_ch_ua` | String CODEC(ZSTD(3)) | En-tête Sec-CH-UA |
|
||
| `header_sec_ch_ua_mobile` | String CODEC(ZSTD(3)) | En-tête Sec-CH-UA-Mobile |
|
||
| `header_sec_ch_ua_platform` | String CODEC(ZSTD(3)) | En-tête Sec-CH-UA-Platform |
|
||
| `header_sec_fetch_dest` | String CODEC(ZSTD(3)) | En-tête Sec-Fetch-Dest |
|
||
| `header_sec_fetch_mode` | String CODEC(ZSTD(3)) | En-tête Sec-Fetch-Mode |
|
||
| `header_sec_fetch_site` | String CODEC(ZSTD(3)) | En-tête Sec-Fetch-Site |
|
||
| `anubis_bot_name` | LowCardinality(String) DEFAULT `''` | Nom du bot détecté par Anubis |
|
||
| `anubis_bot_action` | LowCardinality(String) DEFAULT `''` | Action Anubis |
|
||
| `anubis_bot_category` | LowCardinality(String) DEFAULT `''` | Catégorie Anubis |
|
||
| `h2_fingerprint` | String DEFAULT `''` | Fingerprint HTTP/2 au format Akamai (SETTINGS\|WU\|PRIORITY\|PSEUDO) |
|
||
| `h2_settings_fp` | String DEFAULT `''` | Valeurs brutes des paramètres SETTINGS HTTP/2 |
|
||
| `h2_window_update` | UInt32 DEFAULT 0 | Valeur WINDOW_UPDATE initial du client HTTP/2 |
|
||
| `h2_pseudo_order` | LowCardinality(String) DEFAULT `''` | Ordre des pseudo-headers (`:method:authority:scheme:path`) |
|
||
|
||
Index de saut de données :
|
||
|
||
| Index | Type | Granularité |
|
||
|-------|------|-------------|
|
||
| `idx_src_ip` | bloom_filter(0.01) | 4 |
|
||
| `idx_ja4` | bloom_filter(0.01) | 4 |
|
||
|
||
- **Moteur** : MergeTree
|
||
- **Partition** : `toDate(log_date)`
|
||
- **Tri** : `(time, src_ip, dst_ip, ja4)`
|
||
- **TTL** : `log_date + INTERVAL 30 DAY`
|
||
- **Settings** : `index_granularity = 8192, ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
## Tables — Base `ja4_processing`
|
||
|
||
### Tables Anubis
|
||
|
||
Tables de règles pour la détection de crawlers Anubis.
|
||
|
||
| Table | Clé de tri | Colonnes | Moteur |
|
||
|-------|-----------|----------|--------|
|
||
| `anubis_ip_rules` | `prefix` (String) | `bot_name`, `action`, `rule_id` (UInt64), `has_ua` (UInt8), `category` | ReplacingMergeTree |
|
||
| `anubis_asn_rules` | `asn` (UInt32) | `bot_name`, `action`, `category` | ReplacingMergeTree |
|
||
|
||
> **Note** : les tables `anubis_ua_rules` et `anubis_country_rules` ainsi que les
|
||
> dictionnaires `dict_anubis_ua` et `dict_anubis_country` ont été supprimés.
|
||
> L'enrichissement Anubis repose désormais sur deux niveaux : IP/CIDR → ASN.
|
||
|
||
---
|
||
|
||
### ref_bot_networks
|
||
|
||
Table de référence des réseaux de bots connus (CIDR).
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `network` | String | Réseau CIDR |
|
||
| `bot_name` | LowCardinality(String) | Nom du bot |
|
||
| `is_legitimate` | UInt8 | 1 = bot légitime |
|
||
| `last_update` | DateTime | Dernière mise à jour |
|
||
|
||
- **Moteur** : ReplacingMergeTree(last_update)
|
||
- **Tri** : `(network, bot_name)`
|
||
|
||
---
|
||
|
||
### bot_ip / bot_ja4
|
||
|
||
Tables fichier CSV pour la recherche rapide de bots.
|
||
|
||
| Table | Colonne | Moteur |
|
||
|-------|---------|--------|
|
||
| `bot_ip` | `ip` (String) | File(CSV, `'bot_ip.csv'`) |
|
||
| `bot_ja4` | `ja4` (String) | File(CSV, `'bot_ja4.csv'`) |
|
||
|
||
---
|
||
|
||
### agg_host_ip_ja4_1h
|
||
|
||
Agrégation comportementale par `(src_ip, ja4, host)` par heure. Utilise des
|
||
colonnes `SimpleAggregateFunction` et `AggregateFunction` pour l'agrégation
|
||
incrémentale.
|
||
|
||
**Colonnes clés** : `window_start` (DateTime), `src_ip` (IPv6), `ja4` (String),
|
||
`host` (String), `src_asn` (UInt32).
|
||
|
||
**Colonnes d'agrégation (~50)** :
|
||
|
||
| Catégorie | Colonnes |
|
||
|-----------|----------|
|
||
| Compteurs (SimpleAggregateFunction sum) | `hits`, `count_post`, `orphan_count`, `ip_id_zero_count`, `mss_1460_count`, `count_assets`, `count_no_referer`, `tls12_count`, `count_head`, `count_no_sec_fetch`, `count_generic_accept`, `count_http10`, `count_no_wscale`, `count_correlated`, `count_no_accept_enc`, `count_http_scheme`, `count_xff`, `count_unusual_ct`, `count_non_std_port`, `count_login_post` |
|
||
| Valeurs uniques (AggregateFunction uniq) | `uniq_paths`, `uniq_query_params`, `unique_src_ports`, `unique_conn_id`, `uniq_ua`, `uniq_ja3` |
|
||
| Variances (AggregateFunction varPop) | `tcp_jitter_variance`, `total_ip_length_var`, `url_depth_variance`, `ip_df_var` |
|
||
| Moyennes (AggregateFunction avg/avgIf) | `avg_syn_ms`, `avg_ttl` |
|
||
| Variance conditionnelle (AggregateFunction varPopIf) | `ttl_var` |
|
||
|
||
**Projection** : `proj_by_ip` → ORDER BY `(src_ip, window_start, ja4, host)`
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip, ja4, host)`
|
||
- **TTL** : `window_start + INTERVAL 7 DAY` (partition `toDate(window_start)`)
|
||
- **Settings** : `deduplicate_merge_projection_mode = 'drop'`
|
||
|
||
---
|
||
|
||
### agg_header_fingerprint_1h
|
||
|
||
Agrégation d'empreinte d'en-têtes par `(src_ip)` par heure.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `window_start` | DateTime | Début de la fenêtre horaire |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `header_order_hash` | SimpleAggregateFunction(any, String) | Hash de l'ordre des en-têtes |
|
||
| `header_count` | SimpleAggregateFunction(max, UInt16) | Nombre max d'en-têtes |
|
||
| `has_accept_language` | SimpleAggregateFunction(max, UInt8) | Présence Accept-Language |
|
||
| `has_cookie` | SimpleAggregateFunction(max, UInt8) | Présence Cookie |
|
||
| `has_referer` | SimpleAggregateFunction(max, UInt8) | Présence Referer |
|
||
| `modern_browser_score` | SimpleAggregateFunction(max, UInt8) | Score de conformité navigateur |
|
||
| `ua_ch_mismatch` | SimpleAggregateFunction(max, UInt8) | Incohérence UA/Client Hints |
|
||
| `sec_ch_mobile_mismatch` | SimpleAggregateFunction(max, UInt8) | Incohérence Sec-CH-UA-Mobile |
|
||
| `sec_fetch_mode` | SimpleAggregateFunction(any, String) | Valeur Sec-Fetch-Mode |
|
||
| `sec_fetch_dest` | SimpleAggregateFunction(any, String) | Valeur Sec-Fetch-Dest |
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip)`
|
||
- **TTL** : `window_start + INTERVAL 7 DAY` (partition `toDate(window_start)`)
|
||
|
||
---
|
||
|
||
### agg_path_sequences_1h (thèse §5.1)
|
||
|
||
Entropie des séquences de chemins — transitions de Markov sur les chemins normalisés.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `window_start` | DateTime | Début de la fenêtre |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4` | LowCardinality(String) | Empreinte JA4 |
|
||
| `host` | LowCardinality(String) | Hôte cible |
|
||
| `path_sequence` | AggregateFunction(groupArray(100), Tuple(UInt32, String)) | Séquence ordonnée (timestamp, chemin) |
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip, ja4, host)`
|
||
- **Partition** : `toDate(window_start)` — **TTL** : 7 jours
|
||
- **Settings** : `ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### agg_request_timing_1h (thèse §5.3)
|
||
|
||
Cadence des requêtes — analyse du coefficient de variation et des bursts.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `window_start` | DateTime | Début de la fenêtre |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4` | LowCardinality(String) | Empreinte JA4 |
|
||
| `host` | LowCardinality(String) | Hôte cible |
|
||
| `request_times` | AggregateFunction(groupArrayIf(500), UInt64, UInt8) | Horodatages des requêtes (filtré a_timestamp > 0) |
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip, ja4, host)`
|
||
- **Partition** : `toDate(window_start)` — **TTL** : 7 jours
|
||
- **Settings** : `ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### agg_ip_behavior_1h (thèse §5.5 / §5.8)
|
||
|
||
Dérive JA4 et comportement inter-domaines par IP.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `window_start` | DateTime | Début de la fenêtre |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4_sequence` | AggregateFunction(groupArray(200), Tuple(UInt32, String)) | Séquence temporelle (timestamp, ja4) |
|
||
| `host_hits_keys` | AggregateFunction(sumMap, Array(String), Array(UInt64)) | Distribution hôte → hits |
|
||
| `host_count` | AggregateFunction(uniq, String) | Nombre d'hôtes distincts |
|
||
| `total_hits` | SimpleAggregateFunction(sum, UInt64) | Requêtes totales |
|
||
| `first_seen` | SimpleAggregateFunction(min, DateTime) | Première observation |
|
||
| `last_seen` | SimpleAggregateFunction(max, DateTime) | Dernière observation |
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip)`
|
||
- **Partition** : `toDate(window_start)` — **TTL** : 7 jours
|
||
- **Settings** : `ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### agg_resource_cascade_1h (thèse §5.4)
|
||
|
||
Arbre de dépendances de chargement de ressources.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `window_start` | DateTime | Début de la fenêtre |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4` | LowCardinality(String) | Empreinte JA4 |
|
||
| `host` | LowCardinality(String) | Hôte cible |
|
||
| `resource_loads` | AggregateFunction(groupArray(200), Tuple(UInt32, UInt8)) | Chargements (timestamp, is_asset) |
|
||
|
||
- **Moteur** : AggregatingMergeTree
|
||
- **Tri** : `(window_start, src_ip, ja4, host)`
|
||
- **Partition** : `toDate(window_start)` — **TTL** : 7 jours
|
||
- **Settings** : `ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### ml_detected_anomalies
|
||
|
||
Détections d'anomalies au-dessus du seuil de menace.
|
||
|
||
**Colonnes principales** :
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `detected_at` | DateTime | Horodatage de la détection |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4` | String | Empreinte JA4 |
|
||
| `host` | String | Hôte cible |
|
||
| `bot_name` | String | Nom du bot identifié |
|
||
| `browser_family` | LowCardinality(String) DEFAULT `''` | Famille de navigateur |
|
||
| `anomaly_score` | Float32 | Score d'anomalie normalisé |
|
||
| `raw_anomaly_score` | Float32 DEFAULT `0` | Score brut avant normalisation |
|
||
| `threat_level` | String | Niveau de menace (CRITICAL, HIGH, MEDIUM, LOW) |
|
||
| `model_name` | String | Nom du modèle (Complet, Applicatif, etc.) |
|
||
| `recurrence` | UInt32 | Nombre de détections précédentes |
|
||
| `campaign_id` | Int32 DEFAULT `-1` | Identifiant de campagne HDBSCAN |
|
||
| `reason` | String | Explication de la détection |
|
||
|
||
**Colonnes de contexte réseau** : `asn_number`, `asn_org`, `asn_detail`,
|
||
`asn_domain`, `country_code`, `asn_label` (tous String).
|
||
|
||
**Colonnes de features ML (~30)** : `hits`, `hit_velocity`, `fuzzing_index`,
|
||
`post_ratio`, `port_exhaustion_ratio`, `max_keepalives`, `orphan_ratio`,
|
||
`tcp_jitter_variance`, `tcp_shared_count`, `true_window_size`, `window_mss_ratio`,
|
||
`alpn_http_mismatch`, `is_alpn_missing`, `sni_host_mismatch`, `header_count`,
|
||
`has_accept_language`, `has_cookie`, `has_referer`, `modern_browser_score`,
|
||
`is_headless`, `ua_ch_mismatch`, `header_order_shared_count`, `ip_id_zero_ratio`,
|
||
`request_size_variance`, `multiplexing_efficiency`, `mss_mobile_mismatch`,
|
||
`correlated`, `asset_ratio`, `direct_access_ratio`, `is_ua_rotating`,
|
||
`distinct_ja4_count`, `src_port_density`, `ja4_asn_concentration`,
|
||
`ja4_country_concentration`, `is_rare_ja4`, `header_order_confidence`,
|
||
`distinct_header_orders`, `temporal_entropy`, `path_diversity_ratio`,
|
||
`url_depth_variance`, `anomalous_payload_ratio`.
|
||
|
||
**Colonnes Anubis** : `anubis_bot_name`, `anubis_bot_action`, `anubis_bot_category`
|
||
(LowCardinality(String) DEFAULT `''`).
|
||
|
||
**Index de saut** :
|
||
|
||
| Index | Type | Granularité |
|
||
|-------|------|-------------|
|
||
| `idx_detected_at` | minmax | 4 |
|
||
| `idx_threat_level` | set(8) | 4 |
|
||
| `idx_bot_name` | bloom_filter() | 4 |
|
||
|
||
- **Moteur** : ReplacingMergeTree(detected_at)
|
||
- **Partition** : `toYYYYMMDD(detected_at)`
|
||
- **Tri** : `(src_ip)`
|
||
- **TTL** : `detected_at + INTERVAL 7 DAY`
|
||
- **Settings** : `index_granularity = 8192, ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### ml_all_scores
|
||
|
||
Toutes les classifications ML (sans filtre de seuil) pour l'observabilité.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `detected_at` | DateTime | Horodatage de la détection |
|
||
| `window_start` | DateTime | Début de la fenêtre d'analyse |
|
||
| `src_ip` | IPv6 | IP source |
|
||
| `ja4` | String | Empreinte JA4 |
|
||
| `host` | String | Hôte cible |
|
||
| `bot_name` | String | Nom du bot |
|
||
| `browser_family` | LowCardinality(String) DEFAULT `''` | Famille de navigateur |
|
||
| `anomaly_score` | Float32 | Score final |
|
||
| `raw_anomaly_score` | Float32 | Score brut |
|
||
| `threat_level` | String | Niveau de menace |
|
||
| `model_name` | String | Nom du modèle |
|
||
| `correlated` | UInt8 | 1 si trafic corrélé |
|
||
| `asn_number` | String | Numéro ASN |
|
||
| `asn_org` | String | Organisation ASN |
|
||
| `country_code` | String | Code pays |
|
||
| `asn_label` | String | Label de réputation ASN |
|
||
| `hits` | UInt64 | Nombre de requêtes |
|
||
| `hit_velocity` | Float32 | Vélocité des hits |
|
||
| `fuzzing_index` | Float32 | Indice de fuzzing |
|
||
| `post_ratio` | Float32 | Ratio de requêtes POST |
|
||
| `campaign_id` | Int32 | Identifiant de campagne |
|
||
| `ae_recon_error` | Float32 DEFAULT `0` | Erreur de reconstruction autoencoder |
|
||
| `xgb_prob` | Float32 DEFAULT `0` | Probabilité XGBoost supervisé |
|
||
| `anubis_bot_name` | LowCardinality(String) DEFAULT `''` | Nom du bot Anubis |
|
||
| `anubis_bot_action` | LowCardinality(String) DEFAULT `''` | Action Anubis |
|
||
| `anubis_bot_category` | LowCardinality(String) DEFAULT `''` | Catégorie Anubis |
|
||
|
||
**Index de saut** :
|
||
|
||
| Index | Type | Granularité |
|
||
|-------|------|-------------|
|
||
| `idx_detected_at` | minmax | 4 |
|
||
| `idx_threat_level` | set(8) | 4 |
|
||
|
||
- **Moteur** : ReplacingMergeTree(detected_at)
|
||
- **Partition** : `toYYYYMMDD(window_start)`
|
||
- **Tri** : `(window_start, src_ip, ja4, host, model_name)`
|
||
- **TTL** : `window_start + INTERVAL 7 DAY`
|
||
- **Settings** : `index_granularity = 8192, ttl_only_drop_parts = 1`
|
||
|
||
---
|
||
|
||
### fleet_detections
|
||
|
||
Résultats de détection de flottes de bots coordonnées via analyse de graphe bipartite JA4×ASN (module `fleet.py`).
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `src_ip` | IPv6 | Adresse IP membre de la flotte |
|
||
| `campaign_id` | Int32 | Identifiant de la communauté détectée |
|
||
| `fleet_score` | Float32 | Score de flotte : `taille × densité / log(nb_ASN)` |
|
||
| `ja4_list` | Array(String) | Empreintes JA4 partagées dans la communauté |
|
||
| `asn_list` | Array(UInt32) | ASNs impliqués dans la communauté |
|
||
| `member_count` | UInt32 | Nombre total d'IPs dans la communauté |
|
||
| `window_start` | DateTime | Début de la fenêtre de détection |
|
||
| `detected_at` | DateTime | Horodatage d'insertion |
|
||
|
||
- **Moteur** : ReplacingMergeTree(detected_at)
|
||
- **Partition** : `toDate(window_start)`
|
||
- **Tri** : `(window_start, src_ip, campaign_id)`
|
||
- **TTL** : `window_start + INTERVAL 7 DAY`
|
||
|
||
---
|
||
|
||
### ml_performance_metrics
|
||
|
||
Métriques de performance du pipeline ML par cycle de détection (module `metrics.py`).
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `model_name` | LowCardinality(String) | Nom du modèle (`complet`, `applicatif`) |
|
||
| `cycle_id` | String | Identifiant unique du cycle (timestamp ISO) |
|
||
| `anomaly_rate` | Float32 | Taux d'anomalies détectées (0.0–1.0) |
|
||
| `known_bot_rate` | Float32 | Taux de KNOWN_BOT dans le cycle |
|
||
| `legit_browser_rate` | Float32 | Taux de LEGITIMATE_BROWSER dans le cycle |
|
||
| `drift_rate` | Float32 | Fraction de features en drift (KS ou KL) |
|
||
| `corr_rate` | Float32 | Taux de sessions corrélées (correlated=1) |
|
||
| `cycle_latency_s` | Float32 | Durée totale du cycle d'inférence en secondes |
|
||
| `alert_flags` | Array(String) | Liste des alertes déclenchées dans ce cycle |
|
||
| `recorded_at` | DateTime | Horodatage d'enregistrement |
|
||
|
||
- **Moteur** : MergeTree
|
||
- **Partition** : `toDate(recorded_at)`
|
||
- **Tri** : `(recorded_at, model_name)`
|
||
- **TTL** : `recorded_at + INTERVAL 90 DAY`
|
||
|
||
---
|
||
|
||
### soc_feedback
|
||
|
||
Feedback des analystes SOC pour l'entraînement supervisé XGBoost et le MetaLearner.
|
||
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| `src_ip` | IPv6 | Adresse IP classifiée |
|
||
| `label` | LowCardinality(String) | Classification : `bot`, `legitimate`, `suspicious` |
|
||
| `analyst` | String | Identifiant de l'analyste |
|
||
| `confidence` | Float32 | Confiance déclarée (0.0–1.0) |
|
||
| `notes` | String | Notes libres de l'analyste |
|
||
| `created_at` | DateTime | Horodatage de la classification |
|
||
|
||
- **Moteur** : ReplacingMergeTree(created_at)
|
||
- **Partition** : `toDate(created_at)`
|
||
- **Tri** : `(src_ip, created_at)`
|
||
|
||
---
|
||
|
||
### audit_logs
|
||
|
||
Journal d'audit SOC pour le suivi de l'activité du dashboard.
|
||
|
||
| Colonne | Type | Défaut | Description |
|
||
|---------|------|--------|-------------|
|
||
| `timestamp` | DateTime | `now()` | Horodatage de l'événement |
|
||
| `user_name` | LowCardinality(String) | `'soc_user'` | Nom de l'analyste |
|
||
| `action` | LowCardinality(String) | — | Action effectuée |
|
||
| `entity_type` | LowCardinality(String) | `''` | Type d'entité (ip, ja4, etc.) |
|
||
| `entity_id` | String | `''` | Identifiant de l'entité |
|
||
| `entity_count` | UInt32 | `0` | Nombre d'entités |
|
||
| `details` | String CODEC(ZSTD(3)) | `''` | Détails en JSON |
|
||
| `client_ip` | String | `''` | IP du client analyste |
|
||
|
||
- **Moteur** : MergeTree
|
||
- **Partition** : `toDate(timestamp)`
|
||
- **Tri** : `(timestamp, user_name, action)`
|
||
- **TTL** : `toDate(timestamp) + INTERVAL 90 DAY`
|
||
- **Settings** : `index_granularity = 8192`
|
||
|
||
---
|
||
|
||
## Vues matérialisées
|
||
|
||
### mv_http_logs (`ja4_logs`)
|
||
|
||
- **Source** : `ja4_logs.http_logs_raw`
|
||
- **Cible** : `ja4_logs.http_logs`
|
||
- **Transformation** : Parse le champ `raw_json` via les fonctions `JSONExtract*`.
|
||
Enrichit avec les données ASN depuis `dict_iplocate_asn` et la détection de bots
|
||
Anubis via `dict_anubis_ip` + `dict_anubis_asn` avec cascade de priorité
|
||
COALESCE : IP/CIDR → ASN.
|
||
|
||
### mv_agg_host_ip_ja4_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_host_ip_ja4_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip, ja4, host, src_asn)`.
|
||
Calcule ~50 features comportementales : compteurs de hits, ratios POST,
|
||
unicité des chemins/paramètres, jitter TCP, timing SYN, keep-alives,
|
||
compteurs d'orphelins, rotation UA, variance des métadonnées IP, etc.
|
||
|
||
### mv_agg_header_fingerprint_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_header_fingerprint_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip)`.
|
||
Calcule le hash d'ordre des en-têtes, le nombre d'en-têtes, le score de
|
||
conformité navigateur (Sec-CH-UA = 100, UA seul = 50), l'incohérence
|
||
UA↔Sec-CH-UA-Platform et Sec-CH-UA-Mobile.
|
||
|
||
### mv_agg_path_sequences_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_path_sequences_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip, ja4, host)`.
|
||
Stocke `groupArrayState(100)(tuple(timestamp, path))`.
|
||
|
||
### mv_agg_request_timing_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_request_timing_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip, ja4, host)`.
|
||
Stocke `groupArrayIfState(500)(a_timestamp, a_timestamp > 0)`.
|
||
|
||
### mv_agg_ip_behavior_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_ip_behavior_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip)`.
|
||
Stocke la séquence JA4, la distribution sumMap hôte→hits, le compteur
|
||
d'hôtes uniques, les hits totaux et les bornes temporelles.
|
||
|
||
### mv_agg_resource_cascade_1h (`ja4_processing`)
|
||
|
||
- **Source** : `ja4_logs.http_logs`
|
||
- **Cible** : `ja4_processing.agg_resource_cascade_1h`
|
||
- **Transformation** : GROUP BY `(toStartOfHour(time), src_ip, ja4, host)`.
|
||
Stocke `groupArrayState(200)(tuple(timestamp, is_asset))` où is_asset est
|
||
déterminé par correspondance regex sur les extensions de fichiers statiques.
|
||
|
||
---
|
||
|
||
## Dictionnaires
|
||
|
||
### Dictionnaires basés sur fichier CSV
|
||
|
||
Tous les fichiers doivent être placés dans `/var/lib/clickhouse/user_files/`.
|
||
|
||
| Dictionnaire | Fichier CSV | Clé | Layout | Attributs | Lifetime | Entrées approx. |
|
||
|--------------|------------|-----|--------|-----------|----------|------------------|
|
||
| `dict_iplocate_asn` | `iplocate-ip-to-asn.csv` | `network` (String) | IP_TRIE | `asn` (UInt32), `country_code`, `name` | 3600–7200 s | ~714K |
|
||
| `dict_bot_ip` | `bot_ip.csv` | `prefix` (String) | IP_TRIE | `bot_name` (String) | 300 s | ~3,5K CIDR |
|
||
| `dict_bot_ja4` | `bot_ja4.csv` | `ja4` (String) | COMPLEX_KEY_HASHED | `bot_name` (String) | 300 s | ~31 |
|
||
| `dict_browser_ja4` | `browser_ja4.csv` | `ja4` (String) | COMPLEX_KEY_HASHED | `browser_family`, `tls_library`, `context` | 300 s | ~1,2K |
|
||
| `dict_browser_h2` | `browser_h2.csv` | `h2_fingerprint` (String) | COMPLEX_KEY_HASHED | `browser_family`, `h2_known` (UInt8) | 300 s | ~50 |
|
||
| `dict_asn_reputation` | `asn_reputation.csv` | `src_asn` (UInt64) | HASHED | `label` (String) | 300 s | ~82K |
|
||
|
||
### Dictionnaires basés sur ClickHouse
|
||
|
||
| Dictionnaire | Table source | Clé | Layout | Attributs | Lifetime |
|
||
|--------------|-------------|-----|--------|-----------|----------|
|
||
| `dict_anubis_ip` | `ja4_processing.anubis_ip_rules` | `prefix` (String) | IP_TRIE | `bot_name`, `action`, `rule_id` (UInt64), `has_ua` (UInt8), `category` | 300–600 s |
|
||
| `dict_anubis_asn` | `ja4_processing.anubis_asn_rules` | `asn` (UInt32) | FLAT | `bot_name`, `action`, `category` | 300–600 s |
|
||
|
||
> **Note** : les dictionnaires Anubis basés sur ClickHouse nécessitent que les
|
||
> identifiants de connexion soient configurés dans les fichiers SQL (mot de passe
|
||
> par défaut `CHANGE_ME` à remplacer avant la mise en production).
|
||
|
||
---
|
||
|
||
## Vues
|
||
|
||
### view_ai_features_1h
|
||
|
||
Calcule ~85 features ML par `(src_ip, ja4, host)` sur les dernières 24 heures
|
||
en joignant `agg_host_ip_ja4_1h` et `agg_header_fingerprint_1h`.
|
||
|
||
| Catégorie | Features |
|
||
|-----------|----------|
|
||
| Comportementales | `hits`, `hit_velocity`, `fuzzing_index`, `post_ratio`, `orphan_ratio`, `asset_ratio`, `direct_access_ratio` |
|
||
| Connexion | `max_keepalives`, `multiplexing_efficiency`, `port_exhaustion_ratio`, `src_port_density` |
|
||
| Navigateur | `modern_browser_score`, `ua_ch_mismatch`, `header_order_shared_count`, `is_headless` |
|
||
| TLS | `alpn_http_mismatch`, `is_alpn_missing`, `sni_host_mismatch` |
|
||
| L4 | `tcp_jitter_variance`, `avg_ttl`, `ttl_std`, `syn_timing_cv`, `window_mss_ratio` |
|
||
| Réputation | `bot_name` (dict_bot_ip / dict_bot_ja4), `browser_family` (dict_browser_ja4), `asn_label` (dict_asn_reputation), `anubis_bot_name`/`action`/`category` (dict_anubis_ip / dict_anubis_asn) |
|
||
| Statistiques | `temporal_entropy`, `ja3_diversity_ratio`, `ja4_asn_concentration`, `ja4_country_concentration` |
|
||
| P1 | `has_xff`, `unusual_content_type_ratio`, `non_standard_port_ratio`, `login_post_concentration` |
|
||
|
||
Utilise des fonctions de fenêtrage (`sum() OVER`, `count() OVER`, `uniqExact() OVER`)
|
||
pour les features de concentration et de partage TCP.
|
||
|
||
### view_ip_recurrence
|
||
|
||
Agrège les données de récurrence depuis `ml_detected_anomalies` (30 derniers jours) :
|
||
|
||
```sql
|
||
SELECT
|
||
src_ip,
|
||
count() AS recurrence,
|
||
min(detected_at) AS first_seen,
|
||
max(detected_at) AS last_seen,
|
||
max(anomaly_score) AS worst_score,
|
||
argMax(threat_level, anomaly_score) AS worst_threat_level
|
||
FROM ja4_processing.ml_detected_anomalies
|
||
WHERE detected_at >= now() - INTERVAL 30 DAY
|
||
GROUP BY src_ip;
|
||
```
|
||
|
||
### view_form_bruteforce_detected
|
||
|
||
Détection de force brute sur les formulaires. Source : `agg_host_ip_ja4_1h`
|
||
(dernières 24h). Filtre les combinaisons `(src_ip, host)` ayant
|
||
`count_post >= 10`. Retourne `src_ip`, `host`, `ja4` (argMax par hits),
|
||
`hits`, `query_params_count`.
|
||
|
||
### view_host_ip_ja4_rotation
|
||
|
||
Détection de rotation d'empreintes JA4 par IP. Source : `agg_host_ip_ja4_1h`
|
||
(dernières 24h, ja4 ≠ ''). Filtre les IP ayant `distinct_ja4_count >= 2`.
|
||
Retourne `src_ip`, `distinct_ja4_count`, `total_hits`, `first_seen`, `last_seen`.
|
||
|
||
### view_dashboard_user_agents
|
||
|
||
Agrégation des User-Agents pour le dashboard. Source : `ja4_logs.http_logs`
|
||
(7 derniers jours). GROUP BY `(src_ip, ja4, toStartOfHour(time), log_date)`.
|
||
Retourne `src_ip` (normalisé IPv4), `ja4`, `hour`, `log_date`,
|
||
`user_agents` (groupUniqArray(100)), `requests`.
|
||
|
||
### view_dashboard_entities
|
||
|
||
Vue d'entités du dashboard. Source : `ja4_logs.http_logs` (7 derniers jours).
|
||
Structure `UNION ALL` de 5 branches — une par type d'entité : `ip`, `ja4`,
|
||
`country`, `asn`, `host`. Retourne `entity_type`, `entity_value`, `src_ip`,
|
||
`ja4`, `host`, `log_date`, `client_headers`, `asns`, `countries`, `user_agents`.
|
||
|
||
### view_resource_cascade_1h (thèse §5.4)
|
||
|
||
Analyse de cascade de chargement de ressources. Source : `agg_resource_cascade_1h`
|
||
(dernières 24h). Sépare les chargements en documents (is_asset=0) et assets
|
||
(is_asset=1). Calcule `doc_count`, `asset_count`, `root_to_first_asset_delay`,
|
||
`asset_load_stddev` (σ des timestamps d'assets — mesure de simultanéité).
|
||
|
||
### view_thesis_features_1h (thèse §5)
|
||
|
||
Vue unifiée des features avancées de détection de la thèse. Joint (via CTEs
|
||
sur les 24 dernières heures) :
|
||
|
||
| Source | Features calculées |
|
||
|--------|-------------------|
|
||
| `agg_path_sequences_1h` (§5.1) | `path_transition_entropy` (entropie de Shannon normalisée des transitions Markov-1) |
|
||
| `agg_request_timing_1h` (§5.3) | `cadence_cv`, `burst_ratio` (Δt<100ms), `pause_ratio` (Δt>5s), `lag1_autocorrelation`, `benford_deviation` (χ² vs loi de Benford) |
|
||
| `agg_ip_behavior_1h` (§5.5/§5.8) | `ja4_drift_ratio`, `ja4_distinct_in_session`, `host_diversity`, `host_sweep_speed`, `host_coverage_uniformity` |
|
||
| `view_resource_cascade_1h` (§5.4) | `doc_count`, `asset_count`, `root_to_first_asset_delay`, `asset_load_stddev` |
|
||
|
||
**Clés de jointure** : `(window_start, src_ip, ja4, host)` pour §5.1/§5.3/§5.4 ;
|
||
`(window_start, src_ip)` pour §5.5/§5.8.
|
||
|
||
---
|
||
|
||
## Index de performance (10_perf_indexes.sql)
|
||
|
||
Migration idempotente ajoutant des index secondaires et projections aux tables
|
||
existantes (les installations fraîches les ont déjà dans 04/05/06) :
|
||
|
||
| Table | Index / Projection ajouté |
|
||
|-------|--------------------------|
|
||
| `ml_detected_anomalies` | `idx_detected_at` (minmax), `idx_threat_level` (set(8)), `idx_bot_name` (bloom_filter) |
|
||
| `ml_all_scores` | `idx_detected_at` (minmax), `idx_threat_level` (set(8)) |
|
||
| `http_logs` | `idx_src_ip` (bloom_filter(0.01)), `idx_ja4` (bloom_filter(0.01)) |
|
||
| `agg_host_ip_ja4_1h` | `proj_by_ip` (projection ORDER BY src_ip, window_start, ja4, host) |
|
||
|
||
---
|
||
|
||
## Comptes utilisateurs
|
||
|
||
| Utilisateur | Permissions | Usage |
|
||
|-------------|------------|-------|
|
||
| `data_writer` | INSERT + SELECT sur `ja4_logs.http_logs_raw` | Service correlator |
|
||
| `analyst` | SELECT sur `ja4_logs.http_logs`, `ja4_processing.ml_detected_anomalies`, `ja4_processing.ml_all_scores`, `ja4_processing.view_ai_features_1h`, `ja4_processing.view_ip_recurrence`, `ja4_processing.audit_logs` | Dashboard / analystes SOC |
|
||
|
||
> **Sécurité** : les mots de passe par défaut sont `ChangeMe`. Remplacer par des
|
||
> mots de passe forts avant la mise en production. Stocker les identifiants dans
|
||
> un gestionnaire de secrets.
|