Files
dashboard/ML_DETECTED_ANOMALIES_CONFIG.md
SOC Analyst ee2b24b277 fix: Subnet investigation - Récupération des user-agents depuis view_dashboard_entities
- 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>
2026-03-15 19:41:48 +01:00

8.3 KiB

🔍 Configuration de ml_detected_anomalies

📊 Structure de la table

Requête: SHOW CREATE TABLE mabase_prod.ml_detected_anomalies

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

{
  "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

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

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

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é)

-- Passer de 30 à 90 jours
ALTER TABLE mabase_prod.ml_detected_anomalies 
MODIFY TTL detected_at + INTERVAL 90 DAY;

2. Ajouter le partitionnement (optionnel)

-- 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)

-- 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:

-- 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