- Utilisation de 2 requêtes séparées + fusion en Python - 1ère requête: ml_detected_anomalies pour les détections récentes - 2ème requête: view_dashboard_entities avec IN clause pour les user-agents - La clause IN permet d'utiliser l'index ClickHouse (splitByChar ne l'utilise pas) - PREWHERE optimise les performances de requête Problème résolu: - unique_ua était toujours à 0 car la jointure LEFT JOIN ne fonctionnait pas - La solution avec IN clause fonctionne car elle utilise l'index sur entity_value Testé avec 141.98.11.0/24: - 5 IPs, 8 détections, 65 user-agents uniques - 141.98.11.209: 68 user-agents différents Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
301 lines
8.3 KiB
Markdown
301 lines
8.3 KiB
Markdown
# 🔍 Configuration de `ml_detected_anomalies`
|
|
|
|
## 📊 Structure de la table
|
|
|
|
**Requête:** `SHOW CREATE TABLE mabase_prod.ml_detected_anomalies`
|
|
|
|
```sql
|
|
CREATE TABLE mabase_prod.ml_detected_anomalies
|
|
(
|
|
`detected_at` DateTime,
|
|
`src_ip` IPv6,
|
|
`ja4` String,
|
|
`host` String,
|
|
`bot_name` String,
|
|
`anomaly_score` Float32,
|
|
`threat_level` String,
|
|
`model_name` String,
|
|
`recurrence` UInt32,
|
|
`asn_number` String,
|
|
`asn_org` String,
|
|
`asn_detail` String,
|
|
`asn_domain` String,
|
|
`country_code` String,
|
|
`asn_label` String,
|
|
`hits` UInt64,
|
|
`hit_velocity` Float32,
|
|
`fuzzing_index` Float32,
|
|
`post_ratio` Float32,
|
|
`port_exhaustion_ratio` Float32,
|
|
`max_keepalives` UInt32,
|
|
`orphan_ratio` Float32,
|
|
`tcp_jitter_variance` Float32,
|
|
`tcp_shared_count` UInt32,
|
|
`true_window_size` UInt64,
|
|
`window_mss_ratio` Float32,
|
|
`alpn_http_mismatch` UInt8,
|
|
`is_alpn_missing` UInt8,
|
|
`sni_host_mismatch` UInt8,
|
|
`header_count` UInt16,
|
|
`has_accept_language` UInt8,
|
|
`has_cookie` UInt8,
|
|
`has_referer` UInt8,
|
|
`modern_browser_score` UInt8,
|
|
`is_headless` UInt8,
|
|
`ua_ch_mismatch` UInt8,
|
|
`header_order_shared_count` UInt32,
|
|
`ip_id_zero_ratio` Float32,
|
|
`request_size_variance` Float32,
|
|
`multiplexing_efficiency` Float32,
|
|
`mss_mobile_mismatch` UInt8,
|
|
`correlated` UInt8,
|
|
`reason` String,
|
|
`asset_ratio` Float32,
|
|
`direct_access_ratio` Float32,
|
|
`is_ua_rotating` UInt8,
|
|
`distinct_ja4_count` UInt32,
|
|
`src_port_density` Float32,
|
|
`ja4_asn_concentration` Float32,
|
|
`ja4_country_concentration` Float32,
|
|
`is_rare_ja4` UInt8,
|
|
`header_order_confidence` Float32,
|
|
`distinct_header_orders` UInt32,
|
|
`temporal_entropy` Float32,
|
|
`path_diversity_ratio` Float32,
|
|
`url_depth_variance` Float32,
|
|
`anomalous_payload_ratio` Float32
|
|
)
|
|
ENGINE = ReplacingMergeTree(detected_at)
|
|
ORDER BY src_ip
|
|
TTL detected_at + toIntervalDay(30)
|
|
SETTINGS index_granularity = 8192
|
|
```
|
|
|
|
---
|
|
|
|
## ⚙️ Configuration détaillée
|
|
|
|
### 1. **Moteur de stockage**
|
|
```
|
|
ENGINE = ReplacingMergeTree(detected_at)
|
|
```
|
|
- **Type:** `ReplacingMergeTree`
|
|
- **Version column:** `detected_at`
|
|
- **Comportement:** Garde la dernière version des lignes dupliquées lors des merges
|
|
|
|
### 2. **Clé de tri (ORDER BY)**
|
|
```
|
|
ORDER BY src_ip
|
|
```
|
|
- **Clé primaire:** `src_ip` (IPv6)
|
|
- **Optimisation:** Les requêtes par IP sont très rapides
|
|
- **Impact:** Les requêtes par date (`detected_at`) nécessitent un scan complet
|
|
|
|
### 3. **Politique de rétention (TTL)**
|
|
```
|
|
TTL detected_at + toIntervalDay(30)
|
|
```
|
|
- **Durée actuelle:** **30 jours**
|
|
- **Comportement:** Les lignes sont supprimées 30 jours après `detected_at`
|
|
- **Application:** Automatique pendant les opérations de merge
|
|
|
|
### 4. **Partitionnement**
|
|
```
|
|
-- Aucun partitionnement explicite
|
|
```
|
|
- **Statut:** **Non partitionnée** (tuple())
|
|
- **Impact:** Toutes les données dans une seule partition
|
|
- **Conséquence:**
|
|
- ✅ Requêtes plus simples
|
|
- ❌ OPTIMIZE FINAL plus lent sur grandes tables
|
|
- ❌ Impossible de DROPper une partition ancienne
|
|
|
|
### 5. **Index**
|
|
```
|
|
SETTINGS index_granularity = 8192
|
|
```
|
|
- **Granularité:** 8192 lignes par marque d'index
|
|
- **Standard:** Valeur par défaut de ClickHouse
|
|
|
|
---
|
|
|
|
## 📈 Statistiques actuelles
|
|
|
|
**Requête:** `SELECT count(), min(detected_at), max(detected_at) FROM ml_detected_anomalies`
|
|
|
|
| Métrique | Valeur |
|
|
|----------|--------|
|
|
| **Total lignes** | 57,338 |
|
|
| **Donnée la plus ancienne** | 2026-03-13 20:30:19 |
|
|
| **Donnée la plus récente** | 2026-03-15 17:57:10 |
|
|
| **Période couverte** | ~2 jours |
|
|
| **TTL actuel** | 30 jours |
|
|
|
|
---
|
|
|
|
## 🔍 Analyse du problème: 212.30.36.0/24
|
|
|
|
### Incident dans `api/incidents/clusters`
|
|
```json
|
|
{
|
|
"subnet": "212.30.36.0/24",
|
|
"unique_ips": 10,
|
|
"total_detections": 10,
|
|
"first_seen": "2026-03-15T03:55:28",
|
|
"last_seen": "2026-03-15T03:55:28"
|
|
}
|
|
```
|
|
|
|
### Données dans `ml_detected_anomalies`
|
|
- **Âge:** ~15 heures (bien dans les 30 jours)
|
|
- **Statut:** **Devrait être présent** ✅
|
|
|
|
### Pourquoi "Subnet non trouvé" ?
|
|
|
|
**Hypothèses:**
|
|
|
|
1. **IPv6 vs IPv4** ⚠️
|
|
- La table stocke `src_ip` en **IPv6**
|
|
- Les IPs IPv4 sont stockées comme `::ffff:x.x.x.x`
|
|
- Notre requête utilise `replaceRegexpAll(toString(src_ip), '^::ffff:', '')`
|
|
- **Vérifier:** Est-ce que le nettoyage IPv4 fonctionne correctement ?
|
|
|
|
2. **ReplacingMergeTree** ⚠️
|
|
- Les lignes marquées pour suppression peuvent encore être visibles
|
|
- **Vérifier:** Y a-t-il des lignes dupliquées avec `detected_at` différents ?
|
|
|
|
3. **Données réellement absentes** ❌
|
|
- Les 10 détections de `212.30.36.0/24` ont été supprimées
|
|
- **Cause possible:** Bug dans bot_detector_ai ou nettoyage prématuré
|
|
|
|
---
|
|
|
|
## 🧪 Tests de diagnostic
|
|
|
|
### Test 1: Vérifier format IPv4
|
|
|
|
```sql
|
|
SELECT
|
|
src_ip,
|
|
toString(src_ip) AS ip_string,
|
|
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip
|
|
FROM mabase_prod.ml_detected_anomalies
|
|
WHERE detected_at >= now() - INTERVAL 1 HOUR
|
|
LIMIT 10;
|
|
```
|
|
|
|
### Test 2: Chercher le subnet spécifique
|
|
|
|
```sql
|
|
SELECT
|
|
count(),
|
|
min(detected_at),
|
|
max(detected_at)
|
|
FROM mabase_prod.ml_detected_anomalies
|
|
WHERE
|
|
detected_at >= now() - INTERVAL 30 DAY
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36';
|
|
```
|
|
|
|
### Test 3: Vérifier les IPs du subnet
|
|
|
|
```sql
|
|
SELECT
|
|
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip,
|
|
count() AS detections,
|
|
min(detected_at) AS first_seen,
|
|
max(detected_at) AS last_seen
|
|
FROM mabase_prod.ml_detected_anomalies
|
|
WHERE
|
|
detected_at >= now() - INTERVAL 30 DAY
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36'
|
|
GROUP BY clean_ip
|
|
ORDER BY detections DESC
|
|
LIMIT 20;
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Recommandations
|
|
|
|
### 1. **Augmenter la rétention** (déjà documenté)
|
|
|
|
```sql
|
|
-- Passer de 30 à 90 jours
|
|
ALTER TABLE mabase_prod.ml_detected_anomalies
|
|
MODIFY TTL detected_at + INTERVAL 90 DAY;
|
|
```
|
|
|
|
### 2. **Ajouter le partitionnement** (optionnel)
|
|
|
|
```sql
|
|
-- Recréer la table avec partitionnement mensuel
|
|
CREATE TABLE mabase_prod.ml_detected_anomalies_new
|
|
(
|
|
-- ... mêmes colonnes ...
|
|
)
|
|
ENGINE = ReplacingMergeTree(detected_at)
|
|
PARTITION BY toYYYYMM(detected_at) -- Partition par mois
|
|
ORDER BY src_ip
|
|
TTL detected_at + INTERVAL 90 DAY
|
|
SETTINGS index_granularity = 8192;
|
|
|
|
-- Migrer les données
|
|
INSERT INTO ml_detected_anomalies_new SELECT * FROM ml_detected_anomalies;
|
|
|
|
-- Renommer
|
|
RENAME TABLE ml_detected_anomalies TO ml_detected_anomalies_old,
|
|
ml_detected_anomalies_new TO ml_detected_anomalies;
|
|
|
|
-- Drop l'ancienne table après vérification
|
|
DROP TABLE ml_detected_anomalies_old;
|
|
```
|
|
|
|
### 3. **Ajouter un index sur detected_at** (optionnel)
|
|
|
|
```sql
|
|
-- Ajouter un index secondaire pour les requêtes temporelles
|
|
ALTER TABLE mabase_prod.ml_detected_anomalies
|
|
ADD INDEX idx_detected_at detected_at TYPE minmax GRANULARITY 8192;
|
|
```
|
|
|
|
### 4. **Corriger le bug 212.30.36.0/24**
|
|
|
|
**Action immédiate:**
|
|
|
|
```sql
|
|
-- Vérifier si les données existent
|
|
SELECT count()
|
|
FROM mabase_prod.ml_detected_anomalies
|
|
WHERE
|
|
detected_at >= toDateTime('2026-03-15 03:00:00')
|
|
AND detected_at <= toDateTime('2026-03-15 05:00:00')
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
|
|
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36';
|
|
```
|
|
|
|
**Si count = 0:** Les données ont été supprimées prématurément (bug bot_detector_ai)
|
|
|
|
**Si count > 0:** Il y a un bug dans la requête SQL de l'API subnet
|
|
|
|
---
|
|
|
|
## 📚 Fichiers à modifier
|
|
|
|
| Fichier | Modification | Statut |
|
|
|---------|--------------|--------|
|
|
| `deploy_dashboard_entities_view.sql` | TTL: 30 → 90 jours | ✅ Fait |
|
|
| `deploy_user_agents_view.sql` | TTL: 7 → 90 jours | ✅ Fait |
|
|
| `update_retention_policy.sql` | Script d'application | ✅ Créé |
|
|
| `ml_detected_anomalies` | TTL: 30 → 90 jours | ⏳ À appliquer |
|
|
|
|
---
|
|
|
|
**Dernière mise à jour:** 2026-03-15
|
|
**Version:** 1.0
|