diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..5d1352b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,114 @@ +# Copilot Instructions — Bot Detector Dashboard + +## Architecture Overview + +This is a **SOC (Security Operations Center) dashboard** for visualizing bot detections from an upstream `bot_detector_ai` service. It is a **single-service, full-stack app**: the FastAPI backend serves the built React frontend as static files *and* exposes a REST API, all on port 8000. There is no separate frontend server in production and **no authentication**. + +**Data source:** ClickHouse database (`mabase_prod`), primarily the `ml_detected_anomalies` table and the `view_dashboard_entities` view. + +``` +dashboard/ +├── backend/ # Python 3.11 + FastAPI — REST API + static file serving +│ ├── main.py # App entry point: CORS, router registration, SPA catch-all +│ ├── config.py # pydantic-settings Settings, reads .env +│ ├── database.py # ClickHouseClient singleton (db) +│ ├── models.py # All Pydantic v2 response models +│ ├── routes/ # One module per domain: metrics, detections, variability, +│ │ # attributes, analysis, entities, incidents, audit, reputation +│ └── services/ +│ └── reputation_ip.py # Async httpx → ip-api.com + ipinfo.io (no API keys) +└── frontend/ # React 18 + TypeScript 5 + Vite 5 + Tailwind CSS 3 + └── src/ + ├── App.tsx # BrowserRouter + Sidebar + TopHeader + all Routes + ├── ThemeContext.tsx # dark/light/auto, persisted to localStorage (key: soc_theme) + ├── api/client.ts # Axios instance (baseURL: /api) + all TS interfaces + ├── components/ # One component per route view + shared panels + ui/ + ├── hooks/ # useMetrics, useDetections, useVariability (polling wrappers) + └── utils/STIXExporter.ts +``` + +## Dev Commands + +```bash +# Backend (run from repo root) +pip install -r requirements.txt +python -m uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000 + +# Frontend (separate terminal) +cd frontend && npm install +npm run dev # :3000 with HMR, proxies /api → localhost:8000 +npm run build # tsc type-check + vite build → frontend/dist/ +npm run preview # preview the production build + +# Docker (production) +docker compose up -d dashboard_web +docker compose build dashboard_web && docker compose up -d dashboard_web +docker compose logs -f dashboard_web +``` + +There is no test suite or linter configured (no pytest, vitest, ESLint, Black, etc.). + +```bash +# Manual smoke tests +curl http://localhost:8000/health +curl http://localhost:8000/api/metrics | jq '.summary' +curl "http://localhost:8000/api/detections?page=1&page_size=5" | jq '.items | length' +``` + +## Key Conventions + +### Backend + +- **All routes are raw SQL** — no ORM. Results are accessed by positional index: `result.result_rows[0][n]`. Column order is determined by the `SELECT` statement. +- **Query parameters** use `%(name)s` dict syntax: `db.query(sql, {"param": value})`. +- **Every router module** defines `router = APIRouter(prefix="/api/", tags=["..."])` and is registered in `main.py` via `app.include_router(...)`. +- **SPA catch-all** (`/{full_path:path}`) **must remain the last registered route** in `main.py`. New routers must be added with `app.include_router()` before it. +- **IPv4 IPs** are stored as IPv6-mapped (`::ffff:x.x.x.x`) in `src_ip`; queries normalize with `replaceRegexpAll(toString(src_ip), '^::ffff:', '')`. +- **NULL guards** — all row fields are coalesced: `row[n] or ""`, `row[n] or 0`, `row[n] or "LOW"`. +- **`anomaly_score`** can be negative in the DB; always normalize with `abs()` for display. +- **`analysis.py`** stores SOC classifications in a `classifications` ClickHouse table. The `audit_logs` table is optional — routes silently return empty results if absent. + +### Frontend + +- **API calls** use the axios instance from `src/api/client.ts` (baseURL `/api`) or direct `fetch('/api/...')`. There is **no global state manager** — components use `useState`/`useEffect` or custom hooks directly. +- **TypeScript interfaces** in `client.ts` mirror the Pydantic models in `backend/models.py`. Both must be kept in sync when changing data shapes. +- **Tailwind uses semantic CSS-variable tokens** — always use `bg-background`, `bg-background-secondary`, `bg-background-card`, `text-text-primary`, `text-text-secondary`, `text-text-disabled`, `bg-accent-primary`, `threat-critical/high/medium/low` rather than raw Tailwind color classes (e.g., `slate-800`). This ensures dark/light theme compatibility. +- **Threat level taxonomy**: `CRITICAL` > `HIGH` > `MEDIUM` > `LOW` — always uppercase strings; colors: red / orange / yellow / green. +- **URL encoding**: entity values with special characters (JA4 fingerprints, subnets) are `encodeURIComponent`-encoded. Subnets use `_24` in place of `/24` (e.g., `/entities/subnet/141.98.11.0_24`). +- **Recent investigations** are stored in `localStorage` under `soc_recent_investigations` (max 8). Tracked by `RouteTracker` component. Only types `ip`, `ja4`, `subnet` are tracked. +- **Auto-refresh**: metrics every 30 s, incidents every 60 s. +- **French UI text** — all user-facing strings and log messages are in French; code identifiers are in English. + +### Frontend → Backend in Dev vs Production + +- **Dev**: Vite dev server on `:3000` proxies `/api/*` to `http://localhost:8000` (see `vite.config.ts`). +- **Production**: React SPA is served by FastAPI from `frontend/dist/`. API calls hit the same origin at `:8000` — no proxy needed. + +### Docker + +- Single service using `network_mode: "host"` — no port mapping; the container shares the host network stack. +- Multi-stage Dockerfile: `node:20-alpine` builds the frontend → `python:3.11-slim` installs deps → final image copies both. + +## Environment Variables (`.env`) + +| Variable | Default | Description | +|---|---|---| +| `CLICKHOUSE_HOST` | `clickhouse` | ClickHouse hostname | +| `CLICKHOUSE_PORT` | `8123` | ClickHouse HTTP port (set in code) | +| `CLICKHOUSE_DB` | `mabase_prod` | Database name | +| `CLICKHOUSE_USER` | `admin` | | +| `CLICKHOUSE_PASSWORD` | `` | | +| `API_HOST` | `0.0.0.0` | Uvicorn bind host | +| `API_PORT` | `8000` | Uvicorn bind port | +| `CORS_ORIGINS` | `["http://localhost:3000", ...]` | Allowed origins | + +> ⚠️ The `.env` file contains real credentials — never commit it to public repos. + +## ClickHouse Tables + +| Table / View | Used by | +|---|---| +| `ml_detected_anomalies` | Primary source for detections, metrics, variability, analysis | +| `view_dashboard_entities` | User agents, client headers, paths, query params (entities routes) | +| `classifications` | SOC analyst classifications (created by `analysis.py`) | +| `mabase_prod.audit_logs` | Audit trail (optional — missing table is handled silently) | diff --git a/AUDIT_SOC_DASHBOARD.md b/AUDIT_SOC_DASHBOARD.md new file mode 100644 index 0000000..c8c9b01 --- /dev/null +++ b/AUDIT_SOC_DASHBOARD.md @@ -0,0 +1,203 @@ +# Audit SOC du dashboard + +## Résumé exécutif + +Le dashboard est riche fonctionnellement (incidents, investigation IP/JA4, threat intel), mais **pas prêt pour un usage SOC en production** sans durcissement. + +Points majeurs : + +- **Sécurité d’accès insuffisante** : pas d’authentification/RBAC. +- **Navigation incohérente** : plusieurs liens pointent vers des routes inexistantes. +- **Traçabilité/audit partielle** : journalisation contournable et parfois “success” même en échec. +- **Organisation UX perfectible** pour un triage SOC rapide (priorisation, workflow, “next actions”). + + +## Périmètre audité + +- Frontend React (`frontend/src/App.tsx` + composants de navigation et investigation). +- Backend FastAPI (`backend/main.py` + routes `incidents`, `audit`, `entities`, `analysis`, `detections`, `reputation`). +- Documentation projet (`README.md`). + + +## Cartographie des pages et navigation + +### Routes front déclarées + +- `/` → `IncidentsView` +- `/threat-intel` → `ThreatIntelView` +- `/detections` → `DetectionsList` +- `/detections/:type/:value` → `DetailsView` +- `/investigation/:ip` → `InvestigationView` +- `/investigation/ja4/:ja4` → `JA4InvestigationView` +- `/entities/subnet/:subnet` → `SubnetInvestigation` +- `/entities/:type/:value` → `EntityInvestigationView` +- `/tools/correlation-graph/:ip` → `CorrelationGraph` +- `/tools/timeline/:ip?` → `InteractiveTimeline` + +### Graphe de navigation (pages) + +```mermaid +flowchart LR + A["/ (Incidents)"] --> B["/investigation/:ip"] + A --> C["/entities/subnet/:subnet"] + A --> X["/bulk-classify?ips=... (route absente)"] + A --> T["/threat-intel"] + + D["/detections"] --> E["/detections/:type/:value"] + D --> B + E --> B + E --> F["/investigation/ja4/:ja4"] + + C --> B + C --> G["/entities/ip/:ip"] + G --> B + G --> F + F --> B + + B --> H["/tools/correlation-graph/:ip"] + B --> I["/tools/timeline/:ip?"] + + Q["QuickSearch (global + local)"] --> Y["/investigate/... (route absente)"] + Q --> Z["/incidents?threat_level=CRITICAL (route absente)"] +``` + +### Incohérences de navigation identifiées + +- `QuickSearch` navigue vers `/investigate/...` et `/incidents...` mais ces routes n’existent pas. +- `IncidentsView` envoie vers `/bulk-classify?...` sans route déclarée. +- `DetectionsList` utilise `window.location.href` (rechargement complet) au lieu du router. +- Navigation top-level limitée à 2 entrées (“Incidents”, “Threat Intel”), alors que “Détections” est une vue centrale SOC. +- Usage de `window.location.pathname` dans `App.tsx` pour récupérer `:ip` sur certaines routes outils (fragile, non idiomatique React Router). + + +## Constat sécurité / robustesse (usage SOC) + +## Critique + +- **Absence d’authentification et de RBAC** (confirmé aussi dans le README “usage local”). + - Impact SOC : impossible d’attribuer correctement les actions analyste, risque d’accès non maîtrisé. + +- **Injection potentielle dans `entities.py`** : + - Construction d’un `IN (...)` SQL par concaténation de valeurs (`ip_values`), non paramétrée. + - Impact : surface d’injection côté backend. + +- **Audit log non fiable** : + - `/api/audit/logs` accepte un `user` fourni par la requête (default `soc_user`). + - En cas d’échec d’insert audit, le code retourne quand même `status: success`. + - Impact : non-répudiation faible, traçabilité compromise. + +## Élevé + +- **Rate limiting non appliqué** : + - Variable `RATE_LIMIT_PER_MINUTE` existe mais pas de middleware effectif. + - Impact : exposition aux abus/DoS et scraping massif. + +- **Fuite d’erreurs internes** : + - Plusieurs endpoints retournent `detail=f"Erreur: {str(e)}"`. + - Impact : divulgation d’informations techniques. + +## Moyen + +- **Dépendance externe réputation IP** (`ip-api` en HTTP + `ipinfo`) sans contrôle de résilience avancé (fallback opérationnel limité). +- **Composants avec `console.error`/`console.log`** en production front. +- **Endpoints incidents partiellement “mockés”** (`Implementation en cours`) pouvant tromper l’analyste. + + +## Format des pages : ce qu’il faut améliorer + +## 1) Priorisation SOC visuelle + +- Uniformiser les conventions de sévérité (couleur, wording, position). +- Ajouter un bandeau “Incidents nécessitant action immédiate” en haut de `/`. +- Afficher systématiquement : **niveau, confiance, impact, dernière activité, action recommandée**. + +## 2) Densité et lisibilité + +- Réduire l’usage d’emojis non essentiels dans les zones de décision. +- Passer les tableaux volumineux en mode “triage” : + - colonnes par défaut minimales, + - tri par criticité/recence, + - tags compacts avec tooltip. + +## 3) Workflow analyste explicite + +- Introduire des CTA standardisés : + - `Investiguer`, `Escalader`, `Classer`, `Créer IOC`, `Exporter`. +- Ajouter une timeline d’actions SOC (qui a fait quoi, quand, pourquoi) directement sur les vues incident/investigation. + +## 4) Accessibilité opérationnelle + +- Raccourcis clavier cohérents (navigation, filtres, next incident). +- État vide explicite + actions suggérées. +- Breadcrumb homogène entre toutes les vues. + + +## Organisation de l’information : recommandations + +## IA) Repenser l’IA de navigation (menu) + +Proposition de structure : + +- **Triage** + - Incidents (par défaut) + - Détections +- **Investigation** + - Recherche entité + - Vue IP + - Vue JA4 + - Subnet +- **Knowledge** + - Threat Intel + - Tags/Patterns +- **Administration** + - Audit logs + - Santé plateforme + +## IB) Normaliser les routes + +- Remplacer les routes mortes (`/investigate`, `/incidents`, `/bulk-classify` non déclaré) par des routes existantes ou les implémenter. +- Éviter `window.location.*` dans les composants routés. +- Centraliser les chemins dans un module unique (ex: `routes.ts`) pour éviter les divergences. + +## IC) Standardiser le modèle de page + +Chaque page SOC devrait avoir la même ossature : + +1. Contexte (titre + périmètre + horodatage). +2. KPIs critiques. +3. Tableau principal de triage. +4. Panneau actions. +5. Journal d’activité lié à la page. + + +## Plan d’amélioration priorisé + +## Phase 1 (bloquant prod SOC) + +- Ajouter auth SSO/OIDC + RBAC (viewer/analyst/admin). +- Corriger routes mortes et navigation cassée. +- Corriger requête SQL non paramétrée dans `entities.py`. +- Fiabiliser audit log (identité dérivée de l’auth, échec explicite si log non écrit). + +## Phase 2 (fiabilité) + +- Mettre en place rate limiting effectif. +- Assainir gestion d’erreurs (messages utilisateurs + logs serveurs structurés). +- Retirer `window.location.href` et unifier navigation SPA. + +## Phase 3 (UX SOC) + +- Refonte “triage-first” des écrans (priorité, next action, temps de traitement). +- Uniformiser design tokens et hiérarchie visuelle. +- Ajouter vues “queue analyste” et “handover” (passation de quart). + + +## Verdict + +Le socle est prometteur pour l’investigation technique, mais pour un SOC opérationnel il faut d’abord : + +1. **Sécuriser l’accès et la traçabilité**. +2. **Fiabiliser la navigation et les routes**. +3. **Recentrer les pages sur le flux de triage SOC**. + +Sans ces corrections, le risque principal est une **dette opérationnelle** (temps perdu en triage) et une **dette de conformité** (auditabilité insuffisante). diff --git a/MCP_TESTS_REPORT.md b/MCP_TESTS_REPORT.md deleted file mode 100644 index cd1063f..0000000 --- a/MCP_TESTS_REPORT.md +++ /dev/null @@ -1,309 +0,0 @@ -# 🧪 Rapport de Tests Interface - MCP Browser - -**Date:** 2026-03-14 -**Version:** 1.5.0 (Dashboard + Graph + IPv4 Fix) -**Testeur:** MCP Browser -**Statut:** ✅ **TOUS LES TESTS PASSÉS** - ---- - -## 📊 RÉSULTATS DES TESTS - -| Test | Résultat | Détails | -|------|----------|---------| -| **Connexion MCP** | ✅ PASSÉ | http://192.168.1.2:8000 | -| **Page Dashboard** | ✅ PASSÉ | Incidents affichés | -| **Page Threat Intel** | ✅ PASSÉ | Classifications visibles | -| **Navigation** | ✅ PASSÉ | Liens fonctionnels | -| **QuickSearch** | ✅ PASSÉ | Barre de recherche présente | -| **Incidents** | ✅ PASSÉ | 20 incidents affichés | -| **Top Menaces** | ✅ PASSÉ | Top 10 affiché | -| **Boutons Action** | ✅ PASSÉ | Investiguer, Classifier, Export | - ---- - -## ✅ TESTS DÉTAILLÉS - -### 1. Connexion MCP Browser -**URL:** `http://192.168.1.2:8000` -**Résultat:** ✅ **CONNECTÉ** - -``` -Page URL: http://192.168.1.2:8000/ -Page Title: Bot Detector Dashboard -``` - ---- - -### 2. Page Dashboard (/) - -**Éléments vérifiés:** -- ✅ Titre: "SOC Dashboard" -- ✅ QuickSearch présent (Cmd+K) -- ✅ Navigation: Incidents + Threat Intel -- ✅ Metrics: CRITICAL (0), HIGH (0), MEDIUM (4,677), TOTAL (27,145) -- ✅ 20 incidents clusterisés affichés -- ✅ Top Menaces Actives (10 lignes) - -**Structure:** -``` -┌─────────────────────────────────────────────────────────┐ -│ SOC Dashboard [🔍 QuickSearch] │ -│ [Incidents] [Threat Intel] │ -├─────────────────────────────────────────────────────────┤ -│ [CRITICAL: 0] [HIGH: 0] [MEDIUM: 4,677] [TOTAL: 27K] │ -├─────────────────────────────────────────────────────────┤ -│ Incidents Prioritaires (20) │ -│ ☐ INC-001 176.65.132.0/24 Score:18 🇩🇪 DE │ -│ ├─ IPs: 1 Détections: 3 ASN: AS51396 │ -│ ├─ JA4: t13d1812h1_... │ -│ └─ [Investiguer] [Voir détails] [Classifier] │ -│ ☐ INC-002 108.131.244.0/24 Score:16 🇺🇸 US │ -│ ... (18 autres) │ -├─────────────────────────────────────────────────────────┤ -│ Top Menaces Actives │ -│ # Entité Type Score Pays ASN Hits/s │ -│ 1 176.65.132.0 IP 18 🇩🇪 AS51396 0 │ -│ ... (9 autres) │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -### 3. Page Threat Intel (/threat-intel) - -**Éléments vérifiés:** -- ✅ Titre: "📚 Threat Intelligence" -- ✅ Metrics: Malicious (0), Suspicious (0), Légitime (0), Total (2) -- ✅ Filtres: Recherche, Label, Tags -- ✅ Tags populaires: ja4-rotation (1), ua-rotation (2), distributed (1) -- ✅ Tableau des classifications (2 lignes) - -**Classifications affichées:** -``` -┌─────────────────────────────────────────────────────────┐ -│ 📋 Classifications Récentes │ -│ Date │ Entité │ Label │ Tags │ -│ 14/03 18:09│ t13d2013h2_... │ ✅ LEGIT. │ ja4-... │ -│ 14/03 18:01│ t13d190900_... │ ❌ MALIC. │ ua-... │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -### 4. Navigation - -**Liens testés:** -``` -✅ / → Dashboard (Incidents) -✅ /threat-intel → Threat Intelligence -✅ Incidents → Link actif sur page / -✅ Threat Intel → Link actif sur page /threat-intel -``` - ---- - -### 5. QuickSearch - -**Éléments présents:** -- ✅ Icône: 🔍 -- ✅ Placeholder: "Rechercher IP, JA4, ASN, Host... (Cmd+K)" -- ✅ Raccourci: ⌘ K affiché -- ✅ Input textbox fonctionnel - ---- - -### 6. Incidents Clusterisés - -**Données affichées (20 incidents):** -- ✅ Checkbox de sélection -- ✅ Sévérité: LOW (tous) -- ✅ ID: INC-20260314-001 à 020 -- ✅ Subnet: ex: 176.65.132.0/24 -- ✅ Score de risque: 13-20/100 -- ✅ IPs: 1-2 -- ✅ Détections: 2-6 -- ✅ Pays: 🇩🇪 DE, 🇺🇸 US, 🇫🇷 FR, 🇨🇳 CN, etc. -- ✅ ASN: AS51396, AS16509, etc. -- ✅ Tendance: ↑ 23% -- ✅ JA4 Principal: affiché - -**Boutons d'action:** -- ✅ Investiguer → /investigation/:ip -- ✅ Voir détails → /entities/ip/:ip -- ✅ Classifier → /bulk-classify -- ✅ Export STIX → Télécharge JSON - ---- - -### 7. Top Menaces Actives - -**Tableau (10 lignes):** -``` -# Entité Type Score Pays ASN Hits/s Tendance -1 176.65.132.0 IP 18 🇩🇪 AS51396 0 ↑ 23% -2 108.131.244.0 IP 16 🇺🇸 AS16509 0 ↑ 23% -3 46.4.81.0 IP 15 🇩🇪 AS24940 0 ↑ 23% -4 162.55.94.0 IP 15 🇩🇪 AS24940 0 ↑ 23% -5 54.72.53.0 IP 15 🇺🇸 AS16509 0 ↑ 23% -6 92.184.121.0 IP 15 🇫🇷 AS3215 0 ↑ 23% -7 216.9.225.0 IP 20 🇺🇸 AS44382 0 ↑ 23% -8 72.50.5.0 IP 19 🇵🇷 AS10396 0 ↑ 23% -9 77.83.36.0 IP 14 🇩🇪 AS214403 0 ↑ 23% -10 18.201.206.0 IP 14 🇺🇸 AS16509 0 ↑ 23% -``` - -**Colonnes vérifiées:** -- ✅ # (rang) -- ✅ Entité (IP sans ::ffff:) -- ✅ Type (IP) -- ✅ Score (badge couleur) -- ✅ Pays (drapeau + code) -- ✅ ASN -- ✅ Hits/s -- ✅ Tendance (↑ ↓ →) - ---- - -## 🎯 FONCTIONNALITÉS TESTÉES - -### Dashboard Principal -- [x] Affichage des incidents -- [x] Metrics en temps réel -- [x] Checkboxes de sélection -- [x] Boutons d'action -- [x] Top Menaces tableau - -### Threat Intel -- [x] Statistiques par label -- [x] Filtres (recherche, label, tags) -- [x] Tags populaires -- [x] Tableau des classifications - -### Navigation -- [x] Liens fonctionnels -- [x] État actif des liens -- [x] URLs correctes - -### UI/UX -- [x] QuickSearch (Cmd+K) -- [x] Code couleur (CRITICAL/HIGH/MEDIUM/LOW) -- [x] Drapeaux pays -- [x] Scores de risque -- [x] Tendances (↑ 23%) - ---- - -## 📊 DONNÉES AFFICHÉES - -### Metrics (24h) -``` -CRITICAL: 0 -HIGH: 0 -MEDIUM: 4,677 -TOTAL: 27,145 détections -IPs uniques: 17,966 -``` - -### Incidents (20 affichés) -``` -Pays représentés: 🇩🇪 DE, 🇺🇸 US, 🇫🇷 FR, 🇨🇳 CN, 🇵🇷 PR, 🇧🇪 BE, 🇨🇮 CI -Scores: 13-20/100 -Subnets: /24 -ASN: AS51396, AS16509, AS24940, etc. -``` - -### Classifications (2) -``` -✅ LEGITIMATE: 1 (ja4-rotation, ua-rotation) -❌ MALICIOUS: 1 (ua-rotation, distributed) -Confiance: 45% -Analyste: soc_user -``` - ---- - -## ⚠️ POINTS D'ATTENTION - -### IPv4 Fix -✅ **APPLIQUÉ** - Plus de `::ffff:` dans l'affichage - -**Avant:** `::ffff:176.65.132.0` -**Après:** `176.65.132.0` - -### Réseau HOST -✅ **CONFIGURÉ** - MCP peut se connecter - -**URL MCP:** `http://192.168.1.2:8000` -**Port:** 8000 (direct, pas de NAT) - ---- - -## 📈 PERFORMANCES - -| Métrique | Valeur | -|----------|--------| -| **Chargement page** | < 1s | -| **Navigation** | Instantanée | -| **Rendu incidents** | 20 items | -| **Rendu tableau** | 10 lignes | -| **QuickSearch** | Opérationnel | - ---- - -## 🎨 CAPTURES D'ÉCRAN (Snapshot) - -### Dashboard -``` -SOC Dashboard -├─ Metrics: CRITICAL/HIGH/MEDIUM/TOTAL -├─ Incidents Prioritaires: 20 items -│ ├─ Checkbox -│ ├─ Sévérité + ID + Subnet -│ ├─ Score de risque -│ ├─ Stats (IPs, Détections, Pays, ASN) -│ ├─ JA4 Principal -│ └─ Actions (Investiguer, Voir détails, Classifier, Export) -└─ Top Menaces Actives: 10 lignes -``` - -### Threat Intel -``` -📚 Threat Intelligence -├─ Metrics: Malicious/Suspicious/Légitime/Total -├─ Filtres: Recherche + Label + Tags -├─ Tags Populaires: ja4-rotation, ua-rotation, distributed -└─ Classifications Récentes: 2 lignes -``` - ---- - -## ✅ CONCLUSION - -**Statut global:** 🟢 **TOUS LES TESTS PASSÉS** - -### Points forts: -- ✅ MCP browser connecté avec succès -- ✅ Dashboard 100% fonctionnel -- ✅ Navigation fluide -- ✅ Toutes les pages accessibles -- ✅ Données affichées correctement -- ✅ IPv4 fix appliqué (::ffff: supprimé) -- ✅ Boutons d'action présents -- ✅ QuickSearch opérationnel - -### Fonctionnalités validées: -- ✅ Incidents clusterisés -- ✅ Metrics en temps réel -- ✅ Threat Intelligence -- ✅ Top Menaces -- ✅ Classification -- ✅ Export STIX (bouton présent) - ---- - -**Interface entièrement testée et validée via MCP Browser !** 🛡️ - -**URL de test:** `http://192.168.1.2:8000` -**Configuration:** HOST NETWORK -**Status:** ✅ OPÉRATIONNEL diff --git a/NAVIGATION_GRAPH.md b/NAVIGATION_GRAPH.md deleted file mode 100644 index 56df782..0000000 --- a/NAVIGATION_GRAPH.md +++ /dev/null @@ -1,658 +0,0 @@ -# 🗺️ Graph de Navigation du Dashboard Bot Detector - -## Vue d'ensemble - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ BOT DETECTOR DASHBOARD │ -│ (Page d'accueil / Dashboard) │ -└─────────────────────────────────────────────────────────────────────────────────┘ - │ - ┌───────────────────────────────┼───────────────────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ 📊 Dashboard │ │ 📋 Détections │ │ ⚙️ API /docs │ -│ (Accueil) │───────────▶│ (Liste) │ │ (Swagger) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ - │ │ - │ ▼ - │ ┌─────────────────────────────────────────┐ - │ │ FILTRES & RECHERCHE │ - │ │ • Recherche: IP, JA4, Host │ - │ │ • Modèle: Complet / Applicatif │ - │ │ • Niveau menace: CRITICAL/HIGH/MEDIUM/ │ - │ │ • Pays, ASN │ - │ │ • Tri: Score, Date, IP, ASN, etc. │ - │ │ • Toggle: Grouper par IP / Individuel │ - │ └─────────────────────────────────────────┘ - │ │ - │ │ (Clic sur ligne) - │ ▼ - │ ┌─────────────────────────────────────────┐ - │ │ 🔍 DETAILS VIEW │ - │ │ /detections/:type/:value │ - │ │ │ - │ │ Types supportés: │ - │ │ • ip │ - │ │ • ja4 │ - │ │ • country │ - │ │ • asn │ - │ │ • host │ - │ │ • user_agent │ - │ │ │ - │ │ Affiche: │ - │ │ • Stats (total, IPs uniques, dates) │ - │ │ • Insights (auto-générés) │ - │ │ • Variabilité des attributs │ - │ └─────────────────────────────────────────┘ - │ │ - │ ┌───────────────────┼───────────────────┐ - │ │ │ │ - │ ▼ ▼ ▼ - │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ - │ │ Investigation│ │ Investigation│ │ Entity │ - │ │ IP │ │ JA4 │ │ Investigation│ - │ │ │ │ │ │ │ - │ │ /investigati │ │ /investigati │ │ /entities/:t │ - │ │ on/:ip │ │ on/ja4/:ja4 │ │ ype/:value │ - │ └──────────────┘ └──────────────┘ └──────────────┘ - │ - │ - │ (Accès rapide depuis Dashboard) - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ ACCÈS RAPIDE (Dashboard) │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ Voir détections │ │ Menaces │ │ Modèle Complet │ │ -│ │ → /detections │ │ Critiques │ │ → /detections? │ │ -│ │ │ │ → /detections? │ │ model_name= │ │ -│ │ │ │ threat_level= │ │ Complet │ │ -│ │ │ │ CRITICAL │ │ │ │ -│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📊 ARBORESCENCE COMPLÈTE - -### Niveau 1 - Pages Principales - -``` -/ (Dashboard) -├── /detections (Liste des détections) -│ ├── Filtres: ?threat_level=CRITICAL -│ ├── Filtres: ?model_name=Complet -│ ├── Filtres: ?country_code=FR -│ ├── Filtres: ?asn_number=16276 -│ ├── Recherche: ?search=192.168.1.1 -│ └── Tri: ?sort_by=anomaly_score&sort_order=desc -│ -├── /docs (Swagger UI - API documentation) -└── /health (Health check endpoint) -``` - -### Niveau 2 - Vues de Détails - -``` -/detections/:type/:value -├── /detections/ip/192.168.1.100 -├── /detections/ja4/t13d190900_... -├── /detections/country/FR -├── /detections/asn/16276 -├── /detections/host/example.com -└── /detections/user_agent/Mozilla/5.0... -``` - -### Niveau 3 - Investigations - -``` -/detections/ip/:ip -└── → /investigation/:ip (Investigation complète) - -/detections/ja4/:ja4 -└── → /investigation/ja4/:ja4 (Investigation JA4) - -/detections/:type/:value -└── → /entities/:type/:value (Investigation entité) - ├── /entities/ip/192.168.1.100 - ├── /entities/ja4/t13d190900_... - ├── /entities/user_agent/Mozilla/5.0... - ├── /entities/client_header/Accept-Language - ├── /entities/host/example.com - ├── /entities/path/api/login - └── /entities/query_param/token,userId -``` - ---- - -## 🔍 INVESTIGATION IP - SOUS-PANELS - -``` -/investigation/:ip -│ -├── Panel 1: SUBNET / ASN ANALYSIS -│ ├── Calcul subnet /24 -│ ├── Liste IPs du subnet -│ ├── ASN number & org -│ └── Total IPs dans l'ASN -│ -├── Panel 2: COUNTRY ANALYSIS -│ ├── Pays de l'IP -│ └── Répartition autres pays du même ASN -│ -├── Panel 3: JA4 ANALYSIS -│ ├── JA4 fingerprint de l'IP -│ ├── IPs partageant le même JA4 -│ ├── Top subnets pour ce JA4 -│ └── Autres JA4 pour cette IP -│ -├── Panel 4: USER-AGENT ANALYSIS -│ ├── User-Agents de l'IP -│ ├── Classification (normal/bot/script) -│ └── Pourcentage bots -│ -└── Panel 5: CORRELATION SUMMARY + CLASSIFICATION - ├── Indicateurs de corrélation - │ ├── subnet_ips_count - │ ├── asn_ips_count - │ ├── ja4_shared_ips - │ ├── bot_ua_percentage - │ └── user_agents_count - │ - ├── Recommandation auto - │ ├── label: legitimate/suspicious/malicious - │ ├── confidence: 0-1 - │ ├── suggested_tags: [] - │ └── reason: string - │ - └── Formulaire classification SOC - ├── Sélection label (3 boutons) - ├── Tags prédéfinis (20 tags) - ├── Commentaire libre - ├── Sauvegarder → classifications table - └── Export ML → JSON -``` - ---- - -## 🔍 INVESTIGATION JA4 - SOUS-PANELS - -``` -/investigation/ja4/:ja4 -│ -├── Stats principales -│ ├── Total détections (24h) -│ ├── IPs uniques -│ ├── Première détection -│ ├── Dernière détection -│ └── Nombre User-Agents -│ -├── Panel 1: TOP IPs -│ └── Liste IPs utilisant ce JA4 (top 10) -│ -├── Panel 2: TOP Pays -│ └── Répartition géographique -│ -├── Panel 3: TOP ASN -│ └── ASNs utilisant ce JA4 -│ -├── Panel 4: TOP Hosts -│ └── Hosts ciblés avec ce JA4 -│ -└── Panel 5: USER-AGENTS + CLASSIFICATION - ├── Liste User-Agents - ├── Classification (normal/bot/script) - └── JA4CorrelationSummary -``` - ---- - -## 📋 ENTITÉ INVESTIGATION - SOUS-PANELS - -``` -/entities/:type/:value -│ -├── Stats générales -│ ├── Total requêtes -│ ├── IPs uniques -│ ├── Première détection -│ └── Dernière détection -│ -├── Panel 1: IPs Associées -│ └── Top 20 IPs + navigation -│ -├── Panel 2: JA4 Fingerprints -│ └── Top 10 JA4 + investigation -│ -├── Panel 3: User-Agents -│ ├── Top 10 UAs -│ ├── Count & percentage -│ └── Tronqué (150 chars) -│ -├── Panel 4: Client Headers -│ ├── Top 10 headers -│ ├── Count & percentage -│ └── Format mono -│ -├── Panel 5: Hosts Ciblés -│ └── Top 15 hosts -│ -├── Panel 6: Paths -│ ├── Top 15 paths -│ └── Count & percentage -│ -├── Panel 7: Query Params -│ ├── Top 15 query params -│ └── Count & percentage -│ -└── Panel 8: ASNs & Pays - ├── Top 10 ASNs - └── Top 10 Pays (avec drapeaux) -``` - ---- - -## 🎯 WORKFLOWS SOC TYPIQUES - -### Workflow 1: Investigation d'une IP suspecte - -``` -1. Dashboard - └── Voir métriques (CRITICAL: 45, HIGH: 120) - -2. Clic sur "Menaces Critiques" - └── /detections?threat_level=CRITICAL - -3. Repérer IP: 192.168.1.100 (Score: -0.95) - └── Clic sur ligne IP - -4. Details View: /detections/ip/192.168.1.100 - ├── Stats: 250 détections, 1 UA, 1 JA4 - ├── Insight: "1 User-Agent → Possible script" - └── Bouton: "🔍 Investigation complète" - -5. Investigation: /investigation/192.168.1.100 - ├── Panel 1: 15 IPs du subnet /24 ⚠️ - ├── Panel 2: Pays: CN (95%) - ├── Panel 3: JA4 unique, 50 IPs partagent - ├── Panel 4: 100% bot UA (python-requests) - └── Panel 5: Classification - ├── Label: MALICIOUS (auto) - ├── Tags: scraping, bot-network, hosting-asn - ├── Comment: "Bot de scraping distribué" - └── Action: 💾 Sauvegarder + 📤 Export ML -``` - -### Workflow 2: Analyse d'un JA4 fingerprint - -``` -1. Dashboard - └── Voir série temporelle (pic à 14:00) - -2. /detections - └── Tri par JA4 (groupé) - -3. Repérer JA4: t13d190900_9dc949149365_... - └── Clic: /detections/ja4/:ja4 - -4. Details View JA4 - ├── Stats: 1500 détections, 89 IPs - ├── Insight: "89 IPs différentes → Infrastructure distribuée" - └── Bouton: "🔍 Investigation JA4" - -5. Investigation JA4: /investigation/ja4/:ja4 - ├── Panel 1: Top 10 IPs (CN: 45%, US: 30%) - ├── Panel 2: Top Pays (CN, US, DE, FR) - ├── Panel 3: Top ASN (OVH, Amazon, Google) - ├── Panel 4: Top Hosts (api.example.com) - └── Panel 5: User-Agents - ├── 60% curl/7.68.0 (script) - ├── 30% python-requests (script) - └── 10% Mozilla (normal) -``` - -### Workflow 3: Investigation par ASN - -``` -1. /detections?asn_number=16276 (OVH) - └── 523 détections en 24h - -2. Clic sur ASN dans tableau - └── /detections/asn/16276 - -3. Details View ASN - ├── Stats: 523 détections, 89 IPs - ├── Variabilité: - │ ├── 15 User-Agents différents - │ ├── 8 JA4 fingerprints - │ ├── 12 pays - │ └── 45 hosts ciblés - └── Insights: - ├── "ASN de type hosting/cloud" - └── "12 pays → Distribution géographique large" - -4. Navigation enchaînable - └── Clic sur User-Agent "python-requests" - └── /entities/user_agent/python-requests/2.28.0 - ├── 250 IPs utilisent cet UA - ├── Top paths: /api/login, /api/users - └── Query params: token, userId, action -``` - ---- - -## 📡 API ENDPOINTS UTILISÉS - -``` -GET /api/metrics -└── Résumé + timeseries + distribution - -GET /api/detections -├── page, page_size -├── threat_level, model_name -├── country_code, asn_number -├── search, sort_by, sort_order -└── items[], total, page, total_pages - -GET /api/detections/:id -└── Détails complets d'une détection - -GET /api/variability/:type/:value -├── type: ip, ja4, country, asn, host -├── Stats globales -├── attributes: -│ ├── user_agents[] -│ ├── ja4[] -│ ├── countries[] -│ ├── asns[] -│ └── hosts[] -└── insights[] - -GET /api/variability/:type/:value/ips -└── Liste des IPs associées - -GET /api/variability/:type/:value/attributes -├── target_attr: user_agents, ja4, countries, asns, hosts -└── items[] avec count, percentage - -GET /api/variability/:type/:value/user_agents -└── User-Agents avec classification - -GET /api/attributes/:type -└── Liste des valeurs uniques (top 100) - -GET /api/entities/:type/:value -├── type: ip, ja4, user_agent, client_header, host, path, query_param -├── stats: EntityStats -├── related: EntityRelatedAttributes -├── user_agents[] -├── client_headers[] -├── paths[] -└── query_params[] - -GET /api/analysis/:ip/subnet -└── Subnet /24 + ASN analysis - -GET /api/analysis/:ip/country -└── Pays + répartition ASN - -GET /api/analysis/:ip/ja4 -└── JA4 fingerprint analysis - -GET /api/analysis/:ip/user-agents -└── User-Agents + classification - -GET /api/analysis/:ip/recommendation -├── Indicateurs de corrélation -├── label, confidence -├── suggested_tags[] -└── reason - -POST /api/analysis/classifications -└── Sauvegarde classification SOC -``` - ---- - -## 🎨 COMPOSANTS UI - -``` -App.tsx -├── Navigation (Navbar) -│ ├── Logo: "Bot Detector" -│ ├── Link: Dashboard -│ └── Link: Détections -│ -├── Dashboard (Page d'accueil) -│ ├── MetricCard[] (4 cartes) -│ │ ├── Total Détections -│ │ ├── Menaces (CRITICAL+HIGH) -│ │ ├── Bots Connus -│ │ └── IPs Uniques -│ │ -│ ├── ThreatBar[] (4 barres) -│ │ ├── CRITICAL -│ │ ├── HIGH -│ │ ├── MEDIUM -│ │ └── LOW -│ │ -│ ├── TimeSeriesChart -│ └── Accès Rapide (3 liens) -│ -├── DetectionsList -│ ├── Header -│ │ ├── Toggle: Grouper par IP -│ │ ├── Sélecteur colonnes -│ │ └── Recherche -│ │ -│ ├── Filtres -│ │ ├── Modèle (dropdown) -│ │ └── Effacer filtres -│ │ -│ └── Tableau -│ ├── Colonnes: -│ │ ├── IP / JA4 -│ │ ├── Host -│ │ ├── Client Headers -│ │ ├── Modèle -│ │ ├── Score -│ │ ├── Hits -│ │ ├── Velocity -│ │ ├── ASN -│ │ ├── Pays -│ │ └── Date -│ │ -│ └── Pagination -│ -├── DetailsView -│ ├── Breadcrumb -│ ├── Header (type + value) -│ ├── Stats rapides (4 boxes) -│ ├── Insights[] -│ ├── VariabilityPanel -│ └── Bouton retour -│ -├── InvestigationView (IP) -│ ├── SubnetAnalysis -│ ├── CountryAnalysis -│ ├── JA4Analysis -│ ├── UserAgentAnalysis -│ └── CorrelationSummary -│ -├── JA4InvestigationView -│ ├── Stats principales -│ ├── Top IPs -│ ├── Top Pays -│ ├── Top ASN -│ ├── Top Hosts -│ ├── User-Agents -│ └── JA4CorrelationSummary -│ -└── EntityInvestigationView - ├── Stats générales - ├── Panel 1: IPs Associées - ├── Panel 2: JA4 Fingerprints - ├── Panel 3: User-Agents - ├── Panel 4: Client Headers - ├── Panel 5: Hosts - ├── Panel 6: Paths - ├── Panel 7: Query Params - └── Panel 8: ASNs & Pays -``` - ---- - -## 🔣 ÉTATS & DONNÉES - -### Hooks React - -``` -useMetrics() -├── data: MetricsResponse -├── loading: boolean -├── error: Error | null -└── refresh: 30s auto - -useDetections(params) -├── params: { -│ ├── page, page_size -│ ├── threat_level -│ ├── model_name -│ ├── country_code -│ ├── asn_number -│ ├── search -│ ├── sort_by, sort_order -│ } -├── data: DetectionsListResponse -├── loading: boolean -└── error: Error | null - -useVariability(type, value) -├── type: string -├── value: string -├── data: VariabilityResponse -├── loading: boolean -└── error: Error | null -``` - ---- - -## 📊 MODÈLES DE DONNÉES - -```typescript -MetricsResponse { - summary: MetricsSummary { - total_detections: number - critical_count: number - high_count: number - medium_count: number - low_count: number - known_bots_count: number - anomalies_count: number - unique_ips: number - } - timeseries: TimeSeriesPoint[] - threat_distribution: Record -} - -Detection { - detected_at: datetime - src_ip: string - ja4: string - host: string - bot_name: string - anomaly_score: float - threat_level: string - model_name: string - recurrence: int - asn_number: string - asn_org: string - country_code: string - hits: int - hit_velocity: float - fuzzing_index: float - post_ratio: float - reason: string -} - -VariabilityResponse { - type: string - value: string - total_detections: number - unique_ips: number - date_range: { first_seen, last_seen } - attributes: VariabilityAttributes { - user_agents: AttributeValue[] - ja4: AttributeValue[] - countries: AttributeValue[] - asns: AttributeValue[] - hosts: AttributeValue[] - } - insights: Insight[] -} - -ClassificationRecommendation { - label: 'legitimate' | 'suspicious' | 'malicious' - confidence: float (0-1) - indicators: CorrelationIndicators { - subnet_ips_count: int - asn_ips_count: int - ja4_shared_ips: int - bot_ua_percentage: float - user_agents_count: int - } - suggested_tags: string[] - reason: string -} -``` - ---- - -## 🚀 POINTS D'ENTRÉE POUR SOC - -### Scénarios de démarrage rapide - -``` -1. URGENCE: Pic d'activité suspecte - → / (Dashboard) - → Voir pic dans TimeSeries - → Clic sur "Menaces Critiques" - → Identifier pattern - → Investigation - -2. ALERT: IP blacklistée - → /detections?search= - → Voir historique - → /investigation/ - → Analyser corrélations - → Classifier + Export ML - -3. INVESTIGATION: Nouveau botnet - → /detections?threat_level=CRITICAL - → Trier par ASN - → Identifier cluster - → /investigation/ja4/ - → Cartographier infrastructure - -4. REVIEW: Classification SOC - → /entities/ip/ - → Vue complète activité - → Décider classification - → Sauvegarder -``` - ---- - -## 📝 NOTES - -- **Navigation principale:** Dashboard → Détections → Détails → Investigation -- **Navigation secondaire:** Investigation → Entités → Investigation croisée -- **Breadcrumb:** Présent sur toutes les pages de détails -- **Retour:** Bouton "← Retour" sur chaque page d'investigation -- **URL state:** Tous les filtres sont dans l'URL (partageable) -- **Auto-refresh:** Dashboard rafraîchi toutes les 30s -- **Grouping:** Option "Grouper par IP" pour vue consolidée diff --git a/NETWORK_CONFIG.md b/NETWORK_CONFIG.md deleted file mode 100644 index d64c9ac..0000000 --- a/NETWORK_CONFIG.md +++ /dev/null @@ -1,136 +0,0 @@ -# 🌐 Configuration Réseau - MCP Browser - -## ✅ CONFIGURATION ACTUELLE - -### Mode: HOST NETWORK -Le dashboard utilise le réseau de l'hôte directement (pas de NAT Docker). - -**URL d'accès:** -- **MCP Browser:** `http://192.168.1.2:8000` -- **Local:** `http://localhost:8000` -- **Container:** `http://localhost:8000` (depuis l'intérieur) - ---- - -## 🔧 CONFIGURATION - -### docker-compose.yaml -```yaml -services: - dashboard_web: - network_mode: "host" - # Pas de mapping de ports nécessaire -``` - -### IP de l'hôte -```bash -hostname -I | awk '{print $1}' -# Résultat: 192.168.1.2 -``` - ---- - -## 📝 URLS À UTILISER - -### Pour le MCP Browser -``` -http://192.168.1.2:8000 # Dashboard -http://192.168.1.2:8000/health # Health check -http://192.168.1.2:8000/api/metrics # API -``` - -### Pour les tests locaux -``` -http://localhost:8000 # Dashboard -http://localhost:8000/docs # Swagger UI -``` - ---- - -## 🧪 COMMANDES DE TEST - -### Health check -```bash -curl http://localhost:8000/health -# {"status":"healthy","clickhouse":"connected"} -``` - -### Dashboard -```bash -curl http://localhost:8000 | grep title -# Bot Detector Dashboard -``` - -### API Metrics -```bash -curl http://localhost:8000/api/metrics | jq '.summary' -``` - ---- - -## ⚠️ POINTS IMPORTANTS - -### Ports utilisés -- **8000** - API + Frontend (mode host) -- ~~3000~~ - N'est plus utilisé (ancien mode bridge) - -### Avantages du mode host -- ✅ MCP peut se connecter -- ✅ Pas de NAT → meilleures perfs -- ✅ Plus simple à déboguer - -### Inconvénients -- ⚠️ Port 8000 doit être libre sur l'hôte -- ⚠️ Moins d'isolation réseau - ---- - -## 🔄 ROLLBACK (si nécessaire) - -Pour revenir au mode bridge avec mapping de ports : - -```yaml -services: - dashboard_web: - ports: - - "3000:8000" - # network_mode: null -``` - -Puis : -```bash -docker compose down -docker compose up -d -``` - ---- - -## 📊 DIAGNOSTIC - -### Vérifier que le dashboard écoute -```bash -curl http://localhost:8000/health -``` - -### Vérifier l'IP de l'hôte -```bash -hostname -I | awk '{print $1}' -``` - -### Logs du container -```bash -docker compose logs -f dashboard_web -``` - -### Status du container -```bash -docker compose ps -``` - ---- - -**Date:** 2026-03-14 -**Configuration:** HOST NETWORK -**IP Hôte:** 192.168.1.2 -**Port:** 8000 -**Status:** ✅ OPÉRATIONNEL diff --git a/ROUTES_NAVIGATION_PROGRESS.md b/ROUTES_NAVIGATION_PROGRESS.md new file mode 100644 index 0000000..33dfc81 --- /dev/null +++ b/ROUTES_NAVIGATION_PROGRESS.md @@ -0,0 +1,57 @@ +# Plan d'exécution — Routes & Navigation + +## Contexte + +- Authentification applicative **hors périmètre** (gérée par `htaccess`). +- Objectif: rendre les routes/navigation cohérentes et sans liens cassés. + +## Étapes et avancement + +| Étape | Description | Statut | Notes | +|---|---|---|---| +| 1 | Préparer ce document de suivi | ✅ Fait | Document créé et utilisé comme source de progression. | +| 2 | Lancer un baseline (checks existants) | ✅ Fait | `docker compose build dashboard_web` exécuté (OK). | +| 3 | Corriger les routes déclarées (aliases + routes manquantes) | ✅ Fait | Ajout de `/incidents`, `/investigate`, `/investigate/:type/:value`, `/bulk-classify` + wrappers tools route params. | +| 4 | Corriger la navigation (liens/boutons/quick search) | ✅ Fait | Navigation top enrichie, quick actions corrigées, suppression de `window.location.href`. | +| 5 | Valider après changements (build/checks) | ✅ Fait | `docker compose build dashboard_web` OK après modifications. | +| 6 | Finaliser ce document avec résultats | ✅ Fait | Synthèse et statut final complétés. | +| 7 | Réécriture graph de corrélations | ✅ Fait | Custom node types, layout radial, fitView, séparation fetch/filtre, erreur gérée, hauteur 700px. | + +## Journal d’avancement + +### Étape 1 — Préparer le document +- Statut: ✅ Fait +- Action: création du document de suivi avec étapes et statuts. + +### Étape 2 — Baseline Docker +- Statut: ✅ Fait +- Action: exécution de `docker compose build dashboard_web`. +- Résultat: build OK (code de sortie 0), warning non bloquant sur `version` obsolète dans compose. + +### Étape 3 — Correction des routes +- Statut: ✅ Fait +- Actions: + - ajout route alias `/incidents` vers la vue incidents; + - ajout routes `/investigate` et `/investigate/:type/:value` avec redirection intelligente; + - ajout route `/bulk-classify` avec wrapper d’intégration; + - remplacement des usages `window.location.pathname` par des wrappers route basés sur `useParams`. + +### Étape 4 — Correction de la navigation +- Statut: ✅ Fait +- Actions: + - ajout d’un onglet navigation `Détections`; + - activation menu corrigée (gestion des alias/sous-routes); + - remplacement de `window.location.href` dans `DetectionsList` par `navigate(...)`; + - action rapide “Investigation avancée” alignée vers `/detections`. + +### Étape 5 — Validation Docker post-modifications +- Statut: ✅ Fait +- Action: exécution de `docker compose build dashboard_web`. +- Résultat: build OK (code de sortie 0), warning compose `version` obsolète non bloquant. + +### Étape 6 — Clôture +- Statut: ✅ Fait +- Résultat global: + - routes invalides couvertes via aliases/wrappers; + - navigation interne homogène en SPA; + - build Docker validé avant/après. diff --git a/SOC_OPTIMIZATION_PROPOSAL.md b/SOC_OPTIMIZATION_PROPOSAL.md deleted file mode 100644 index 367ad00..0000000 --- a/SOC_OPTIMIZATION_PROPOSAL.md +++ /dev/null @@ -1,491 +0,0 @@ -# 🛡️ SOC Incident Response Dashboard - Réorganisation Optimisée - -## 🎯 Objectif - -Optimiser le dashboard pour la **réponse aux incidents** en minimisant le nombre de clics et en maximisant l'information contextuelle pour les analystes SOC. - ---- - -## 📋 PROBLÈMES ACTUELS IDENTIFIÉS - -### ❌ Problèmes de navigation -1. **Trop de clics** pour atteindre l'information critique (5-7 clics moyens) -2. **Information fragmentée** entre différentes vues -3. **Pas de vue "Incident"** consolidée -4. **Recherche non priorisée** pour les cas d'usage SOC - -### ❌ Problèmes d'ergonomie SOC -1. **Pas de timeline d'incident** visuelle -2. **Pas de scoring de risque** visible immédiatement -3. **Classification trop enfouie** (au bout de 5 panels) -4. **Pas de vue "comparaison"** avant/après classification - ---- - -## ✅ NOUVELLE ARCHITECTURE PROPOSÉE - -### Vue d'ensemble réorganisée - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 🚨 SOC DASHBOARD - INCIDENT RESPONSE │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ [🔍 QUICK SEARCH: IP / JA4 / ASN / Host] [🎯 PRIORITÉS] [⏰ TIMELINE] │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ├──▶ /incidents (NOUVEAU - Vue principale SOC) - │ - ├──▶ /investigate (Recherche avancée) - │ - └──▶ /threat-intel (Base de connaissances) -``` - ---- - -## 🔄 NOUVELLES PAGES PRINCIPALES - -### 1. `/incidents` - Vue Incident (REMPPLACE Dashboard) - -**Objectif:** Vue immédiate des incidents actifs prioritaires - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 🚨 INCIDENTS ACTIFS (24h) │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📊 MÉTRIQUES CRITIQUES │ -│ ┌──────────────┬──────────────┬──────────────┬──────────────┐ │ -│ │ 🔴 CRITICAL │ 🟠 HIGH │ 🟡 MEDIUM │ 📈 TREND │ │ -│ │ 45 │ 120 │ 340 │ +23% │ │ -│ │ +12 depuis │ +34 depuis │ -15 depuis │ vs 24h prev │ │ -│ │ 1h │ 1h │ 1h │ │ │ -│ └──────────────┴──────────────┴──────────────┴──────────────┘ │ -│ │ -│ 🎯 INCIDENTS PRIORITAIRES (Auto-clusterisés) │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ 🔴 INCIDENT #INC-2024-0314-001 Score: 95/100 │ │ -│ │ ├─ 15 IPs du subnet 192.168.1.0/24 (CN, OVH) │ │ -│ │ ├─ JA4: t13d190900_... (50 IPs) │ │ -│ │ ├─ 100% Bot UA (python-requests) │ │ -│ │ ├─ Cible: /api/login (85% des requêtes) │ │ -│ │ └─ [🔍 Investiguer] [📊 Timeline] [🏷️ Classifier] │ │ -│ ├────────────────────────────────────────────────────────────────────┤ │ -│ │ 🟠 INCIDENT #INC-2024-0314-002 Score: 78/100 │ │ -│ │ ├─ 89 IPs, 12 pays, ASN: Amazon AWS │ │ -│ │ ├─ JA4 rotation: 8 fingerprints │ │ -│ │ ├─ 60% Script UA │ │ -│ │ └─ [🔍 Investiguer] [📊 Timeline] [🏷️ Classifier] │ │ -│ └────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 🗺️ CARTE DES MENACES (Géolocalisation) │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ [Carte interactive avec clusters par pays] │ │ -│ │ 🇨🇳 CN: 45% 🇺🇸 US: 23% 🇩🇪 DE: 12% 🇫🇷 FR: 8% Autres: 12% │ │ -│ └────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 📈 TIMELINE DES ATTAQUES (24h) │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ [Graphique temporel avec pics annotés] │ │ -│ │ 00h 04h 08h 12h 16h 20h 24h │ │ -│ │ │ │ │ │ │ │ │ │ │ -│ │ │ 🟡 │ 🟢 │ 🟠 │ 🔴 │ 🟠 │ 🟡 │ │ │ -│ └────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 🔥 TOP ACTIFS (Dernière heure) │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ # IP JA4 ASN Pays Score Hits/s │ │ -│ │ 1 192.168.1.100 t13d... OVH 🇨🇳 95 450 │ │ -│ │ 2 10.0.0.50 9dc9... AWS 🇺🇸 88 320 │ │ -│ │ 3 172.16.0.23 a1b2... Google 🇩🇪 82 280 │ │ -│ └────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -**Actions rapides depuis cette vue:** -- 🔍 **Investiguer** → Ouvre panel latéral sans quitter la vue -- 📊 **Timeline** → Voir l'historique complet de l'incident -- 🏷️ **Classifier** → Classification rapide (1 clic) -- 📤 **Exporter** → Export IOC (IPs, JA4, UA) - ---- - -### 2. `/investigate` - Investigation Avancée (NOUVEAU) - -**Object:** Recherche multi-critères pour investigation proactive - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 🔍 INVESTIGATION AVANCÉE │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ [🔍 Recherche: IP, JA4, ASN, Host, UA, CIDR] │ -│ │ -│ FILTRES RAPIDES │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Menace: [🔴 CRITICAL] [🟠 HIGH] [🟡 MEDIUM] [🟢 LOW] [Tous] │ │ -│ │ Modèle: [✓ Complet] [✓ Applicatif] │ │ -│ │ Temps: [1h] [6h] [24h] [7j] [30j] [Personnalisé] │ │ -│ │ Pays: [🇨🇳 CN] [🇺🇸 US] [🇷🇺 RU] [🇫🇷 FR] [Tous] │ │ -│ │ ASN: [OVH] [AWS] [Google] [Azure] [Tous] │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ RÉSULTATS (Tableau enrichi) │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ☐ │ IP │ JA4 │ Host │ ASN │ Pays │ ⚡Score │ 📊Hits │ 🏷️Tags │ ⚡ │ │ -│ │───┼────┼─────┼──────┼─────┼──────┼─────────┼────────┼────────┼────│ │ -│ │ ☐ │ 🔴 │ 🔴 │ API │ OVH │ 🇨🇳 │ 95 │ 450 │ 🤖 Bot │ ⚡ │ │ -│ │ ☐ │ 🟠 │ 🟡 │ Web │ AWS │ 🇺🇸 │ 78 │ 320 │ 🕷️ Scr │ ⚡ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ACTIONS EN MASSE │ -│ [🏷️ Taguer sélection] [📤 Export IOC] [🚫 Blacklister] [📊 Rapport] │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -### 3. `/incident/:id` - Vue Incident Détaillée (NOUVEAU) - -**Objectif:** Vue complète d'un incident clusterisé - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 🔴 INCIDENT #INC-2024-0314-001 Score: 95/100 │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📊 RÉSUMÉ │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Période: 14/03 08:00 - 14/03 14:00 (6h) │ │ -│ │ IPs impliquées: 15 (subnet 192.168.1.0/24) │ │ -│ │ Total requêtes: 45,234 │ │ -│ │ Cible principale: /api/login (85%) │ │ -│ │ Classification: 🤖 Bot Network - Scraping │ │ -│ │ Analyste: En attente │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 🗺️ GRAPH DE CORRÉLATION (NOUVEAU) │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ [Subnet 192.168.1.0/24] │ │ -│ │ │ │ │ -│ │ ┌──────┴──────┐ │ │ -│ │ ▼ ▼ │ │ -│ │ [JA4: t13d...] [JA4: 9dc9...] │ │ -│ │ │ │ │ │ -│ │ └──────┬──────┘ │ │ -│ │ ▼ │ │ -│ │ [python-requests/2.28] │ │ -│ │ │ │ │ -│ │ ┌──────┴──────────────────┐ │ │ -│ │ ▼ ▼ │ │ -│ │ [/api/login] [/api/users] │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 📈 TIMELINE DÉTAILLÉE │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ [Graphique avec événements annotés] │ │ -│ │ 08:00 🟢 Détection initiale │ │ -│ │ 09:15 🟠 Escalade (100 req/s) │ │ -│ │ 10:30 🔴 Pic (450 req/s) │ │ -│ │ 11:00 🟡 Stabilisation │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 🎯 ENTITÉS IMPLiquÉES │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ IPs (15) │ JA4 (2) │ UA (1) │ Hosts (2) │ │ -│ │ ───────────────── │ ──────────── │ ──────────── │ ───────────────── │ │ -│ │ • 192.168.1.100 │ • t13d... │ • python- │ • api.example.com │ │ -│ │ • 192.168.1.101 │ • 9dc9... │ requests │ • web.example.com │ │ -│ │ • 192.168.1.102 │ │ │ │ │ -│ │ [+12 autres] │ │ │ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 🏷️ CLASSIFICATION │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Label: [🤖 MALICIOUS] │ │ -│ │ Tags: [scraping] [bot-network] [hosting-asn] [country-cn] │ │ -│ │ Confiance: 95% │ │ -│ │ Analyste: [__________] │ │ -│ │ Comment: [________________________________] │ │ -│ │ [💾 Sauvegarder] [📤 Export IOC] [📊 Rapport PDF] │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 📝 NOTES D'INCIDENT │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ [Timeline des actions analystes] │ │ -│ │ 14/03 10:45 - User1: Classification MALICIOUS │ │ -│ │ 14/03 11:00 - User1: Export IOC vers firewall │ │ -│ │ [➕ Ajouter une note] │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -### 4. `/threat-intel` - Base de Connaissances (NOUVEAU) - -**Objectif:** Historique et recherche dans les classifications - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 📚 THREAT INTELLIGENCE │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🔍 [Recherche: IP, JA4, Tag, Commentaire, Analyste] │ -│ │ -│ STATISTIQUES │ -│ ┌──────────────┬──────────────┬──────────────┬──────────────┐ │ -│ │ 🤖 Malicious │ ⚠️ Suspicious│ ✅ Legitimate│ 📊 Total │ │ -│ │ 1,234 │ 2,567 │ 8,901 │ 12,702 │ │ -│ └──────────────┴──────────────┴──────────────┴──────────────┘ │ -│ │ -│ CLASSIFICATIONS RÉCENTES │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Date │ Entité │ Valeur │ Label │ Tags │ │ -│ │────────────│───────────│───────────────│───────────│───────────────│ │ -│ │ 14/03 11:0 │ IP │ 192.168.1.100 │ 🤖 Malic. │ 🤖🕷️☁️ │ │ -│ │ 14/03 10:5 │ JA4 │ t13d... │ ⚠️ Suspic. | 🤖🔄 │ │ -│ │ 14/03 10:3 │ IP │ 10.0.0.50 │ ✅ Legit. | ✅🏢 │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ TOP TAGS (30j) │ -│ [scraping: 234] [bot-network: 189] [hosting-asn: 156] [scanner: 123] │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 🎯 WORKFLOWS OPTIMISÉS - -### Workflow 1: Réponse à incident (5 clics → 2 clics) - -**AVANT:** -``` -Dashboard → Détections → Filtre CRITICAL → Clic IP → Details → Investigation → Classification -(7 clics) -``` - -**MAINTENANT:** -``` -/incidents → Incident #1 → [Panel latéral] → Classifier -(2 clics) -``` - -### Workflow 2: Investigation d'IP (6 clics → 1 clic) - -**AVANT:** -``` -Dashboard → Détections → Recherche IP → Clic → Details → Investigation -(6 clics) -``` - -**MAINTENANT:** -``` -[Barre de recherche globale] → IP → [Panel latéral complet] -(1 clic + search) -``` - -### Workflow 3: Classification en masse (nouvelle fonctionnalité) - -``` -/investigate → Filtre → Sélection multiple → [Action en masse] → Taguer/Exporter -``` - ---- - -## 🔧 COMPOSANTS À CRÉER - -### 1. Panel Latéral d'Investigation (Slide-over) - -```typescript -// Composant à ajouter: InvestigationPanel.tsx -// S'ouvre par dessus n'importe quelle page -// Affiche: -// - Stats rapides de l'entité -// - Corrélations principales -// - Actions rapides (Classifier, Export, Blacklister) -// - Historique des classifications -``` - -### 2. Graph de Corrélations - -```typescript -// Composant à ajouter: CorrelationGraph.tsx -// Visualisation graphique des relations: -// IP → JA4 → UA → Hosts → Paths -// Utiliser D3.js ou React Flow -``` - -### 3. Timeline Interactive - -```typescript -// Composant à ajouter: IncidentTimeline.tsx -// Timeline horizontale avec: -// - Events annotés -// - Zoomable -// - Filtrable par type d'événement -``` - -### 4. Quick Search Bar - -```typescript -// Composant à ajouter: QuickSearch.tsx -// Barre de recherche globale avec: -// - Auto-complete -// - Détection de type (IP, JA4, CIDR, etc.) -// - Historique des recherches -// - Raccourcis clavier (Cmd+K) -``` - -### 5. Incident Clusterizer - -```typescript -// Backend: /api/incidents/clusters -// Algorithme de clustering automatique: -// - Par subnet /24 -// - Par JA4 -// - Par UA -// - Par pattern temporel -``` - ---- - -## 📊 NOUVELLES API À CRÉER - -```python -# Backend routes à ajouter - -# 1. Incidents clustering -GET /api/incidents/clusters - → Retourne les incidents auto-clusterisés - -GET /api/incidents/:id - → Détails complets d'un incident - -POST /api/incidents/:id/classify - → Classification rapide - -# 2. Threat Intel -GET /api/threat-intel/search - → Recherche multi-critères - -GET /api/threat-intel/statistics - → Stats de classification - -# 3. Quick actions -POST /api/actions/blacklist - → Ajout à blacklist - -POST /api/actions/export-ioc - → Export IOC (STIX/TAXII) - -# 4. Correlation graph -GET /api/correlation/graph?ip=... - → Retourne graphe de corrélations -``` - ---- - -## 🎨 AMÉLIORATIONS UX - -### 1. Code couleur cohérent - -``` -🔴 CRITICAL / MALICIOUS → Rouge (#EF4444) -🟠 HIGH / SUSPICIOUS → Orange (#F59E0B) -🟡 MEDIUM → Jaune (#EAB308) -🟢 LOW / LEGITIMATE → Vert (#10B981) -🔵 INFO → Bleu (#3B82F6) -``` - -### 2. Raccourcis clavier - -``` -Cmd+K → Quick search -Cmd+I → Voir incidents -Cmd+E → Export sélection -Cmd+F → Filtrer -Esc → Fermer panel -``` - -### 3. Indicateurs visuels - -``` -⚡ Score de risque (0-100) -🔥 Trend (vs période précédente) -📊 Volume (hits/s) -🏷️ Tags (couleur par catégorie) -``` - ---- - -## 📈 MÉTRIQUES DE PERFORMANCE - -### Objectifs de réduction - -| Métrique | Actuel | Cible | Gain | -|-----------------------------|--------|-------|------| -| Clics pour classification | 7 | 2 | 71% | -| Temps investigation IP | 45s | 10s | 78% | -| Pages pour vue complète | 5 | 1 | 80% | -| Actions en masse | 0 | ✓ | NEW | - ---- - -## 🚀 PLAN DE MIGRATION - -### Phase 1: Quick wins (1 semaine) -- [ ] Ajouter Quick Search bar -- [ ] Créer panel latéral d'investigation -- [ ] Ajouter raccourcis clavier -- [ ] Améliorer page /incidents - -### Phase 2: Core features (2 semaines) -- [ ] Créer système de clustering auto -- [ ] Développer graph de corrélations -- [ ] Implémenter timeline interactive -- [ ] Ajouter actions en masse - -### Phase 3: Advanced (2 semaines) -- [ ] Base Threat Intelligence -- [ ] Export IOC (STIX/TAXII) -- [ ] Rapports PDF auto -- [ ] Intégration SIEM - ---- - -## 📝 RECOMMANDATIONS FINALES - -### Pour les analystes SOC - -1. **Prioriser par score de risque** - Ne pas tout investiguer -2. **Utiliser le clustering** - Voir les patterns, pas juste les IPs -3. **Classifier rapidement** - Même avec confiance moyenne -4. **Exporter les IOC** - Automatiser la réponse - -### Pour les développeurs - -1. **Garder l'état dans l'URL** - Pour partage et refresh -2. **Panel latéral > Navigation** - Moins de context switching -3. **Auto-refresh intelligent** - Seulement si page visible -4. **Optimiser requêtes ClickHouse** - Agrégations pré-calculées - -### Pour la sécurité - -1. **Audit logs** - Tracker toutes les actions analystes -2. **RBAC** - Rôles (Analyste, Senior, Admin) -3. **Rate limiting** - Par utilisateur -4. **Session timeout** - 15min d'inactivité - ---- - -## 🎯 CONCLUSION - -Cette réorganisation transforme le dashboard d'un **outil de visualisation** en un **outil de réponse aux incidents**, réduisant considérablement le temps de traitement et améliorant l'efficacité des analystes SOC. - -**Gain estimé:** 70% de temps gagné sur les investigations courantes. diff --git a/SOC_PHASE1_SUMMARY.md b/SOC_PHASE1_SUMMARY.md deleted file mode 100644 index 2f8d57e..0000000 --- a/SOC_PHASE1_SUMMARY.md +++ /dev/null @@ -1,380 +0,0 @@ -# 🚀 SOC Dashboard - Optimisations Phase 1 - -## ✅ Modifications Implémentées - -### 1. 📄 Page `/incidents` - Vue Clusterisée - -**Fichier:** `frontend/src/components/IncidentsView.tsx` - -**Fonctionnalités:** -- ✅ Métriques critiques en temps réel (CRITICAL, HIGH, MEDIUM, TREND) -- ✅ Clustering automatique par subnet /24 -- ✅ Scores de risque (0-100) avec indicateurs de sévérité -- ✅ Timeline des attaques sur 24h -- ✅ Top actifs avec hits/s -- ✅ Carte des menaces (placeholder) -- ✅ Boutons d'action rapide (Investiguer, Timeline, Classifier) - -**API utilisée:** -- `GET /api/incidents/clusters` - Nouveauté! -- `GET /api/metrics` - Existant - ---- - -### 2. 🔍 QuickSearch (Cmd+K) - -**Fichier:** `frontend/src/components/QuickSearch.tsx` - -**Fonctionnalités:** -- ✅ Raccourci clavier `Cmd+K` / `Ctrl+K` -- ✅ Détection automatique du type (IP, JA4, ASN, Host) -- ✅ Auto-complétion avec résultats suggérés -- ✅ Navigation clavier (↑/↓/Enter/Esc) -- ✅ Actions rapides intégrées -- ✅ Click outside pour fermer - -**Types détectés:** -- 🌐 IPv4 / IPv6 -- 🔐 JA4 fingerprint -- 🏢 ASN (AS12345) -- 🖥️ Host (example.com) -- 🤖 User-Agent - ---- - -### 3. 📑 Panel Latéral d'Investigation - -**Fichier:** `frontend/src/components/InvestigationPanel.tsx` - -**Fonctionnalités:** -- ✅ S'ouvre par dessus n'importe quelle page -- ✅ Stats rapides (détections, IPs uniques) -- ✅ Score de risque estimé avec barre de progression -- ✅ User-Agents associés -- ✅ JA4 fingerprints (navigables) -- ✅ Pays avec drapeaux -- ✅ Classification rapide (3 boutons) -- ✅ Export IOC (JSON) -- ✅ Lien vers investigation complète - -**Utilisation:** -```typescript -// À intégrer dans les vues existantes - setShowPanel(false)} -/> -``` - ---- - -### 4. 🔌 API Incidents Clustering - -**Fichier:** `backend/routes/incidents.py` - -**Endpoints:** - -#### `GET /api/incidents/clusters` -```bash -curl http://localhost:8000/api/incidents/clusters?hours=24&limit=20 -``` - -**Réponse:** -```json -{ - "items": [ - { - "id": "INC-20240314-001", - "score": 95, - "severity": "CRITICAL", - "total_detections": 45, - "unique_ips": 15, - "subnet": "192.168.1.0/24", - "ja4": "t13d190900_...", - "countries": [{"code": "CN", "percentage": 100}], - "asn": "4134", - "trend": "up", - "trend_percentage": 23 - } - ], - "total": 10, - "period_hours": 24 -} -``` - -**Algorithme de clustering:** -- Regroupement par subnet /24 -- Calcul du score de risque: - - `critical_count * 30` - - `high_count * 20` - - `unique_ips * 5` - - `avg_score * 100` -- Détermination de la sévérité (CRITICAL/HIGH/MEDIUM/LOW) - -#### `GET /api/incidents/:id` -- Détails d'un incident (placeholder) - -#### `POST /api/incidents/:id/classify` -- Classification rapide d'un incident - ---- - -## 📊 Gains de Performance - -| Métrique | Avant | Après | Gain | -|----------|-------|-------|------| -| **Clics pour classification** | 7 | 2 | **-71%** | -| **Temps investigation IP** | 45s | 10s | **-78%** | -| **Pages pour vue complète** | 5 | 1 (panel) | **-80%** | -| **Recherche d'entité** | 3 clics | 1 (Cmd+K) | **-66%** | - ---- - -## 🎯 Workflows Optimisés - -### Workflow 1: Classification d'urgence - -**AVANT:** -``` -Dashboard → Détections → Filtre CRITICAL → Clic IP → Details → Investigation → Classification -(7 clics, ~45s) -``` - -**MAINTENANT:** -``` -/incidents → Incident #1 → Panel latéral → Classifier (1 clic) -(2 clics, ~10s) -``` - -### Workflow 2: Investigation d'IP - -**AVANT:** -``` -Dashboard → Détections → Recherche IP → Clic → Details → Investigation -(6 clics, ~30s) -``` - -**MAINTENANT:** -``` -Cmd+K → IP → Entrée → [Panel latéral complet] -(1 raccourci + search, ~5s) -``` - -### Workflow 3: Analyse de pattern - -**AVANT:** -``` -Dashboard → Détections → Tri par ASN → Identifier cluster → Clic → Details -(5 clics, ~25s) -``` - -**MAINTENANT:** -``` -/incidents → Voir cluster par subnet → Investiguer -(2 clics, ~8s) -``` - ---- - -## 🔧 Installation / Déploiement - -### Build Docker -```bash -cd /home/antitbone/work/ja4/dashboard -docker compose build dashboard_web -docker compose up -d dashboard_web -``` - -### Vérifier le statut -```bash -docker compose logs -f dashboard_web -``` - -### Accéder au dashboard -``` -http://localhost:3000/incidents ← NOUVELLE PAGE PRINCIPALE -http://localhost:3000 ← Dashboard classique -http://localhost:8000/docs ← Documentation API -``` - ---- - -## 🧪 Tests Rapides - -### 1. QuickSearch -```bash -# Ouvrir le dashboard -# Appuyer sur Cmd+K -# Taper une IP (ex: 192.168) -# Vérifier l'auto-complétion -# Appuyer sur Entrée -``` - -### 2. Page Incidents -```bash -curl http://localhost:3000/incidents -# Vérifier: -# - Métriques critiques -# - Clusters d'incidents -# - Scores de risque -# - Timeline -``` - -### 3. API Clusters -```bash -curl http://localhost:8000/api/incidents/clusters | jq -# Vérifier: -# - Items clusterisés par subnet -# - Scores de risque calculés -# - Sévérités correctes -``` - -### 4. Panel Latéral -```bash -# Depuis /incidents ou /detections -# Cliquer sur "🔍 Investiguer" -# Vérifier: -# - Panel s'ouvre à droite -# - Stats rapides affichées -# - Score de risque visible -# - Boutons de classification fonctionnels -``` - ---- - -## 📁 Fichiers Modifiés/Créés - -### Créés: -- `backend/routes/incidents.py` (220 lignes) -- `frontend/src/components/QuickSearch.tsx` (230 lignes) -- `frontend/src/components/IncidentsView.tsx` (465 lignes) -- `frontend/src/components/InvestigationPanel.tsx` (343 lignes) - -### Modifiés: -- `backend/main.py` (+1 ligne: import incidents) -- `frontend/src/App.tsx` (+QuickSearch, +Route /incidents) - -**Total:** ~1265 lignes ajoutées - ---- - -## 🎨 Captures d'Écran (Description) - -### Page /incidents -``` -┌─────────────────────────────────────────────────────────────┐ -│ 🚨 Incidents Actifs [🔍 QuickSearch] │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────┬─────────┬─────────┬─────────┐ │ -│ │ 🔴 45 │ 🟠 120 │ 🟡 340 │ 📈 +23% │ │ -│ │Critical │ High │ Medium │ Trend │ │ -│ └─────────┴─────────┴─────────┴─────────┘ │ -│ │ -│ 🎯 Incidents Prioritaires │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 🔴 INC-20240314-001 Score: 95/100 📈 23% │ │ -│ │ ├─ 15 IPs du subnet 192.168.1.0/24 (CN, OVH) │ │ -│ │ ├─ JA4: t13d190900_... (50 IPs) │ │ -│ │ └─ [🔍 Investiguer] [📊 Timeline] [🏷️ Classifier] │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ 📈 Timeline (24h) │ -│ [Graphique en barres avec pics annotés] │ -└─────────────────────────────────────────────────────────────┘ -``` - -### QuickSearch (Cmd+K) -``` -┌─────────────────────────────────────────┐ -│ 🔍 192.168 ⌘ K │ -├─────────────────────────────────────────┤ -│ Résultats suggérés │ -│ ┌───────────────────────────────────┐ │ -│ │ 🌐 192.168.1.100 │ │ -│ │ ip • 45 détections [IP] │ │ -│ ├───────────────────────────────────┤ │ -│ │ 🌐 192.168.1.101 │ │ -│ │ ip • 32 détections [IP] │ │ -│ └───────────────────────────────────┘ │ -│ │ -│ Actions rapides │ -│ [🔴 Menaces Critiques] [🔍 Investig..]│ -└─────────────────────────────────────────┘ -``` - -### Panel Latéral -``` -┌─────────────────────────────────┐ -│ ← Fermer Vue complète → │ -├─────────────────────────────────┤ -│ 🌐 IP │ -│ 192.168.1.100 │ -├─────────────────────────────────┤ -│ ┌──────────┬──────────┐ │ -│ │ 45 │ 15 │ │ -│ │Détections│IPs Uniq. │ │ -│ └──────────┴──────────┘ │ -│ │ -│ Score de Risque Estimé │ -│ [CRITICAL] ████████░░ 85/100 │ -│ │ -│ 🤖 User-Agents (3) │ -│ ┌─────────────────────────┐ │ -│ │ python-requests/2.28 │ │ -│ │ 45 détections • 100% │ │ -│ └─────────────────────────┘ │ -│ │ -│ ⚡ Classification Rapide │ -│ [✅ Légitime] [⚠️ Suspect] │ -│ [❌ Malveillant] │ -│ │ -│ [🔍 Investigation Complète] │ -│ [📤 Export IOC] │ -└─────────────────────────────────┘ -``` - ---- - -## 🚧 Prochaines Étapes (Phase 2) - -### À implémenter: -- [ ] Graph de corrélations (D3.js / React Flow) -- [ ] Timeline interactive zoomable -- [ ] Classification en masse -- [ ] Export STIX/TAXII -- [ ] Base Threat Intelligence (`/threat-intel`) -- [ ] Rapports PDF auto -- [ ] RBAC (Rôles Analyste/Senior/Admin) -- [ ] Audit logs - -### Améliorations UX: -- [ ] Animations fluides -- [ ] Notifications toast -- [ ] Sauvegarde automatique -- [ ] Historique de navigation -- [ ] Favoris/Bookmarks - ---- - -## 📞 Support - -Pour toute question ou problème: -```bash -# Logs du dashboard -docker compose logs -f dashboard_web - -# Redémarrer le service -docker compose restart dashboard_web - -# Rebuild complet -docker compose build --no-cache dashboard_web -docker compose up -d dashboard_web -``` - ---- - -**Date:** 2024-03-14 -**Version:** 1.1.0 -**Commit:** 3b700e8 -**Build:** ✅ SUCCESS diff --git a/TEST_PLAN.md b/TEST_PLAN.md deleted file mode 100644 index 3d164f6..0000000 --- a/TEST_PLAN.md +++ /dev/null @@ -1,985 +0,0 @@ -# 🧪 Plan de Test - Bot Detector Dashboard - -**Version:** 1.0 -**Date:** 2025 -**Projet:** Dashboard Bot Detector IA -**Stack:** FastAPI + React + ClickHouse - ---- - -## 📑 Table des Matières - -1. [Vue d'ensemble](#1-vue-densemble) -2. [Tests Backend (API)](#2-tests-backend-api) -3. [Tests Frontend (React)](#3-tests-frontend-react) -4. [Tests ClickHouse (Base de données)](#4-tests-clickhouse-base-de-données) -5. [Tests d'Intégration](#5-tests-dintégration) -6. [Tests de Sécurité](#6-tests-de-sécurité) -7. [Tests de Performance](#7-tests-de-performance) -8. [Matrice de Couverture](#8-matrice-de-couverture) -9. [Scripts de Test Existants](#9-scripts-de-test-existants) -10. [Recommandations](#10-recommandations) -11. [Prioritisation](#11-prioritisation) - ---- - -## 1. Vue d'ensemble - -### Architecture testée - -``` -┌─────────────────────────────────────────────────────────┐ -│ Docker Compose │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ -│ │ ClickHouse │ │ bot_detector│ │ dashboard_web │ │ -│ │ :8123 │ │ (existant) │ │ :3000 (web) │ │ -│ │ :9000 │ │ │ │ :8000 (API) │ │ -│ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │ -│ └────────────────┴───────────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -### Composants - -| Composant | Technologie | Port | Tests | -|-----------|-------------|------|-------| -| **Frontend** | React + TypeScript + Tailwind | 3000 | 25+ tests | -| **Backend API** | FastAPI (Python) | 8000 | 80+ tests | -| **Database** | ClickHouse (existant) | 8123 | 15+ tests | - -### Endpoints API (20+ endpoints) - -| Routeur | Endpoints | Description | -|---------|-----------|-------------| -| `/health` | 1 | Health check | -| `/api/metrics` | 2 | Métriques globales + distribution | -| `/api/detections` | 2 | Liste des détections + détails | -| `/api/variability` | 4 | Variabilité attributs + IPs + user_agents | -| `/api/attributes` | 1 | Liste attributs uniques | -| `/api/analysis` | 6 | Analyse subnet, country, JA4, UA, recommendation | -| `/api/entities` | 7 | Investigation entités unifiées | - ---- - -## 2. Tests Backend (API) - -### 2.1 Endpoint `/health` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| H1 | Health check basique | GET /health | `{"status": "healthy", "clickhouse": "connected"}` | -| H2 | Health check ClickHouse down | ClickHouse indisponible | `{"status": "unhealthy", "clickhouse": "disconnected"}` | -| H3 | Temps de réponse | Mesure latence | < 500ms | - -**Commande de test:** -```bash -curl http://localhost:3000/health | jq -``` - ---- - -### 2.2 Endpoint `/api/metrics` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| M1 | Métriques globales | GET /api/metrics | Summary avec total_detections, counts par niveau | -| M2 | Série temporelle | Données 24h groupées par heure | timeseries avec 24 points | -| M3 | Distribution par menace | threat_distribution | 4 niveaux (CRITICAL, HIGH, MEDIUM, LOW) | -| M4 | Aucune donnée (24h) | Base vide | Retourne 0 ou erreur gérée proprement | -| M5 | Performance requête | Temps d'exécution | < 2s | - -**Commande de test:** -```bash -curl http://localhost:3000/api/metrics | jq -``` - -**Vérifications:** -- [ ] `summary.total_detections` > 0 -- [ ] `summary.threat_distribution` contient 4 niveaux -- [ ] `timeseries` contient 24 points (une par heure) -- [ ] Somme des counts = total_detections - ---- - -### 2.3 Endpoint `/api/metrics/threats` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| MT1 | Distribution complète | GET /api/metrics/threats | Items avec threat_level, count, percentage | -| MT2 | Cohérence pourcentages | Somme des percentages | ≈ 100% | - -**Commande de test:** -```bash -curl http://localhost:3000/api/metrics/threats | jq -``` - ---- - -### 2.4 Endpoint `/api/detections` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| D1 | Liste par défaut | GET /api/detections?page=1&page_size=25 | Items triés par detected_at DESC | -| D2 | Pagination | page, page_size, total, total_pages | total_pages = ceil(total/page_size) | -| D3 | Filtre threat_level | `?threat_level=CRITICAL` | Uniquement CRITICAL | -| D4 | Filtre model_name | `?model_name=Complet` | Uniquement ce modèle | -| D5 | Filtre country_code | `?country_code=CN` | Uniquement China | -| D6 | Filtre asn_number | `?asn_number=16276` | Uniquement cet ASN | -| D7 | Recherche texte | `?search=192.168` | IP, JA4, Host correspondants | -| D8 | Tri anomaly_score ASC | `?sort_by=anomaly_score&sort_order=asc` | Scores croissants | -| D9 | Tri detected_at DESC | `?sort_by=detected_at&sort_order=DESC` | Chronologique inverse | -| D10 | Limite page_size | `?page_size=100` | Maximum 100 items | -| D11 | Page inexistante | `?page=9999` | Liste vide, total_pages correct | - -**Commandes de test:** -```bash -# Liste par défaut -curl "http://localhost:3000/api/detections?page=1&page_size=25" | jq - -# Filtre CRITICAL -curl "http://localhost:3000/api/detections?threat_level=CRITICAL" | jq '.items[].threat_level' - -# Recherche IP -curl "http://localhost:3000/api/detections?search=192.168" | jq - -# Tri par score -curl "http://localhost:3000/api/detections?sort_by=anomaly_score&sort_order=asc" | jq '.items[0].anomaly_score' -``` - -**Vérifications:** -- [ ] Structure `DetectionsListResponse` respectée -- [ ] Pagination cohérente -- [ ] Filtres appliqués correctement -- [ ] Tri fonctionnel -- [ ] Recherche texte (LIKE ILIKE) - ---- - -### 2.5 Endpoint `/api/detections/{id}` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DD1 | Détails par IP | GET /api/detections/192.168.1.1 | Tous les champs remplis | -| DD2 | IP inexistante | GET /api/detections/0.0.0.0 | 404 "Détection non trouvée" | -| DD3 | Structure nested | asn, country, metrics, tcp, tls, headers, behavior, advanced | Tous les objets présents | - -**Commande de test:** -```bash -curl http://localhost:3000/api/detections/116.179.33.143 | jq -``` - -**Vérifications:** -- [ ] Objet `asn` avec number, org, detail, domain, label -- [ ] Objet `country` avec code -- [ ] Objet `metrics` avec hits, hit_velocity, fuzzing_index, post_ratio, etc. -- [ ] Objet `tcp` avec jitter_variance, shared_count, etc. -- [ ] Objet `tls` avec alpn flags -- [ ] Objet `headers` avec count, has_accept_language, etc. -- [ ] Objet `behavior` avec ip_id_zero_ratio, etc. -- [ ] Objet `advanced` avec asset_ratio, etc. - ---- - -### 2.6 Endpoint `/api/variability/{type}/{value}` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| V1 | Variabilité IP | GET /api/variability/ip/192.168.1.1 | user_agents, ja4, countries, asns, hosts, threat_levels | -| V2 | Variabilité JA4 | GET /api/variability/ja4/{fingerprint} | Même structure | -| V3 | Variabilité Pays | GET /api/variability/country/FR | Même structure | -| V4 | Variabilité ASN | GET /api/variability/asn/16276 | Même structure | -| V5 | Variabilité Host | GET /api/variability/host/example.com | Même structure | -| V6 | Type invalide | GET /api/variability/invalid/xyz | 400 "Type invalide" | -| V7 | Aucune donnée | GET /api/variability/ip/0.0.0.0 | 404 | -| V8 | Insights générés | Selon données | Messages pertinents (rotation UA, hosting ASN, etc.) | - -**Commande de test:** -```bash -curl http://localhost:3000/api/variability/ip/116.179.33.143 | jq -``` - -**Vérifications:** -- [ ] `total_detections` > 0 -- [ ] `unique_ips` >= 1 -- [ ] `attributes.user_agents` liste avec percentages -- [ ] `attributes.ja4` fingerprints -- [ ] `attributes.countries` distribution -- [ ] `attributes.asns` informations -- [ ] `insights` messages contextuels générés - -**Insights attendus:** -- [ ] "X User-Agents différents → Possible rotation/obfuscation" (si > 1 UA) -- [ ] "X JA4 fingerprints différents → Possible rotation" (si > 1 JA4) -- [ ] "ASN de type hosting → Souvent utilisé pour des bots" (si OVH, AWS, etc.) -- [ ] "X% de détections CRITICAL → Menace sévère" (si > 30%) - ---- - -### 2.7 Endpoint `/api/variability/{type}/{value}/ips` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| VI1 | IPs associées | GET /api/variability/country/CN/ips | Liste d'IPs uniques | -| VI2 | Limite respectée | `?limit=50` | Maximum 50 items retournés | -| VI3 | Total correct | `total` vs `showing` | Count distinct réel | - -**Commande de test:** -```bash -curl "http://localhost:3000/api/variability/country/CN/ips?limit=10" | jq -``` - ---- - -### 2.8 Endpoint `/api/variability/{type}/{value}/attributes` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| VA1 | Attributs cibles | `?target_attr=user_agents` | Items avec value, count, percentage | -| VA2 | Target invalide | `?target_attr=invalid` | 400 | -| VA3 | Pourcentages | Somme des percentages | ≈ 100% | - -**Commande de test:** -```bash -curl "http://localhost:3000/api/variability/ip/116.179.33.143/attributes?target_attr=ja4&limit=10" | jq -``` - ---- - -### 2.9 Endpoint `/api/variability/{type}/{value}/user_agents` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| VU1 | User-Agents depuis vue | GET /api/variability/ip/{ip}/user_agents | Liste avec first_seen, last_seen | -| VU2 | Classification implicite | UA bots détectables | python-requests, curl, etc. | - -**Commande de test:** -```bash -curl http://localhost:3000/api/variability/ip/116.179.33.143/user_agents | jq -``` - ---- - -### 2.10 Endpoint `/api/attributes/{type}` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| A1 | Liste IPs uniques | GET /api/attributes/ip | Top 100 par count | -| A2 | Liste JA4 uniques | GET /api/attributes/ja4 | idem | -| A3 | Liste pays | GET /api/attributes/country | idem | -| A4 | Liste ASNs | GET /api/attributes/asn | idem | -| A5 | Liste hosts | GET /api/attributes/host | idem | -| A6 | Type invalide | GET /api/attributes/invalid | 400 | -| A7 | Valeurs vides filtrées | Pas de NULL ou "" | Exclus du résultat | - -**Commande de test:** -```bash -curl "http://localhost:3000/api/attributes/ip?limit=10" | jq -``` - ---- - -### 2.11 Endpoint `/api/analysis/{ip}/subnet` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| AS1 | Analyse subnet /24 | GET /api/analysis/192.168.1.1/subnet | ips_in_subnet, total_in_subnet | -| AS2 | Alert si > 10 IPs | Subnet avec 15 IPs | alert=true | -| AS3 | Informations ASN | asn_number, asn_org, total_in_asn | Données complètes | -| AS4 | IP privée/local | 10.0.0.1 | Géré correctement | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/subnet | jq -``` - ---- - -### 2.12 Endpoint `/api/analysis/{ip}/country` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| AC1 | Pays de l'IP | code, name | FR, France | -| AC2 | Distribution ASN par pays | asn_countries | Liste avec percentages | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/country | jq -``` - ---- - -### 2.13 Endpoint `/api/analysis/country` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| ANC1 | Top 10 pays | GET /api/analysis/country | Avec count et percentage | -| ANC2 | Baseline (7 jours) | Comparaison disponible | baseline object | -| ANC3 | Alert country détectée | Pays surreprésenté | alert_country positionné | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/country | jq -``` - ---- - -### 2.14 Endpoint `/api/analysis/{ip}/ja4` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| AJ1 | JA4 fingerprint | ja4, shared_ips_count | Nombre d'IPs partageant ce JA4 | -| AJ2 | Top subnets | groupés par /24 | top_subnets list | -| AJ3 | Autres JA4 pour IP | other_ja4_for_ip | Liste des autres fingerprints | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/ja4 | jq -``` - ---- - -### 2.15 Endpoint `/api/analysis/{ip}/user-agents` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| AU1 | User-Agents IP | ip_user_agents | Avec classification (normal/bot/script) | -| AU2 | Bot percentage | Calcul correct | bot_percentage | -| AU3 | Alert si > 20% bots | alert=true | Si bot_percentage > 20 | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/user-agents | jq -``` - ---- - -### 2.16 Endpoint `/api/analysis/{ip}/recommendation` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| AR1 | Recommandation complète | label, confidence, indicators | Classification suggérée | -| AR2 | Tags suggérés | Basés sur corrélations | suggested_tags list | -| AR3 | Reason détaillé | Explication | reason string | - -**Commande de test:** -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/recommendation | jq -``` - -**Vérifications:** -- [ ] `label` ∈ {legitimate, suspicious, malicious} -- [ ] `confidence` entre 0 et 1 -- [ ] `indicators` avec subnet_ips_count, ja4_shared_ips, bot_ua_percentage, etc. -- [ ] `suggested_tags` pertinents (distributed, bot-ua, hosting-asn, etc.) -- [ ] `reason` explicatif - ---- - -### 2.17 Endpoint `/api/entities/{type}/{value}` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| E1 | Investigation IP | GET /api/entities/ip/192.168.1.1 | stats, related, user_agents, client_headers, paths, query_params | -| E2 | Investigation JA4 | GET /api/entities/ja4/{fingerprint} | idem | -| E3 | Investigation User-Agent | GET /api/entities/user_agent/{ua} | idem | -| E4 | Investigation Client-Header | GET /api/entities/client_header/{header} | idem | -| E5 | Investigation Host | GET /api/entities/host/example.com | idem | -| E6 | Investigation Path | GET /api/entities/path/api/login | idem | -| E7 | Investigation Query-Param | GET /api/entities/query_param/q|id | idem | -| E8 | Type invalide | GET /api/entities/invalid/xyz | 400 | -| E9 | Entité inexistante | GET /api/entities/ip/0.0.0.0 | 404 | -| E10 | Fenêtre temporelle | `?hours=48` | Filtre appliqué (défaut 24h) | - -**Commande de test:** -```bash -curl http://localhost:3000/api/entities/ip/116.179.33.143 | jq -``` - -**Vérifications:** -- [ ] `stats` avec entity_type, entity_value, total_requests, unique_ips, first_seen, last_seen -- [ ] `related` avec ips, ja4s, hosts, asns, countries -- [ ] `user_agents` liste avec value, count, percentage -- [ ] `client_headers` liste -- [ ] `paths` liste -- [ ] `query_params` liste - ---- - -### 2.18 Endpoint `/api/entities/{type}/{value}/related` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| ER1 | Attributs associés | GET /api/entities/ip/192.168.1.1/related | ips, ja4s, hosts, asns, countries | - -**Commande de test:** -```bash -curl http://localhost:3000/api/entities/ip/116.179.33.143/related | jq -``` - ---- - -### 2.19 Endpoints spécifiques entities - -| ID | Test | Endpoint | Résultat attendu | -|----|------|----------|------------------| -| EU1 | User-Agents | `/{type}/{value}/user_agents` | Liste des UAs | -| EU2 | Client-Headers | `/{type}/{value}/client_headers` | Liste des headers | -| EU3 | Paths | `/{type}/{value}/paths` | Liste des paths | -| EU4 | Query-Params | `/{type}/{value}/query_params` | Liste des params | - ---- - -### 2.20 Endpoint `/api/entities/types` - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| ET1 | Liste des types | GET /api/entities/types | 7 types avec descriptions | - -**Commande de test:** -```bash -curl http://localhost:3000/api/entities/types | jq -``` - -**Vérifications:** -- [ ] 7 types: ip, ja4, user_agent, client_header, host, path, query_param -- [ ] Descriptions pour chaque type - ---- - -## 3. Tests Frontend (React) - -### 3.1 Navigation et Routing - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| N1 | Page d'accueil | GET http://localhost:3000/ | Dashboard s'affiche | -| N2 | Navigation Détections | Clic menu "Détections" | Tableau affiché | -| N3 | Navigation Investigation | Menu "Investigation" | Formulaire recherche | -| N4 | Breadcrumb fonctionnel | Clic breadcrumb | Navigation retour | -| N5 | URL directe (deep link) | http://localhost:3000/detections | Page correcte | - -**Commandes de test:** -```bash -# Vérifier que le HTML est servi -curl -s http://localhost:3000/ | grep -o "Bot Detector Dashboard" - -# Vérifier les assets -curl -s http://localhost:3000/ | grep -o "assets/[^\"]*" -``` - ---- - -### 3.2 Dashboard Principal - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DH1 | Métriques affichées | 4 cartes | total, menaces, bots, IPs | -| DH2 | Graphique temporel | Série 24h | Recharts line/area chart | -| DH3 | Distribution par menace | Pie/bar chart | 4 segments | -| DH4 | Rafraîchissement auto | 30s | Données à jour | -| DH5 | Loading states | Spinners | Pendant chargement | -| DH6 | Gestion erreurs | Message utilisateur | Si API échoue | -| DH7 | Responsive design | Mobile/desktop | Adaptatif | - -**Vérifications manuelles:** -- [ ] Ouvrir http://localhost:3000 -- [ ] Vérifier 4 cartes de métriques -- [ ] Vérifier graphique temporel -- [ ] Vérifier distribution menaces -- [ ] Attendre 30s, vérifier rafraîchissement -- [ ] Tester sur mobile (DevTools) - ---- - -### 3.3 Liste des Détections - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DL1 | Tableau affiché | Colonnes correctes | detected_at, src_ip, threat_level, etc. | -| DL2 | Pagination | Navigation pages | Page 1, 2, 3... | -| DL3 | Tri colonnes | Clic header | ASC/DESC fonctionnel | -| DL4 | Filtre threat_level | Dropdown | CRITICAL, HIGH, MEDIUM, LOW | -| DL5 | Recherche texte | Input search | Filtre en temps réel | -| DL6 | Codes couleur menaces | CRITICAL=rouge, HIGH=orange, etc. | Visuel cohérent | -| DL7 | Clic sur IP | Ligne cliquable | Ouvre détails | -| DL8 | Empty state | Aucune donnée | Message "Aucune détection" | - -**Vérifications manuelles:** -- [ ] Naviguer vers /detections -- [ ] Tester pagination -- [ ] Trier par anomaly_score -- [ ] Filtrer par CRITICAL -- [ ] Rechercher une IP -- [ ] Cliquer sur une ligne - ---- - -### 3.4 Vue Détails (Investigation) - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DV1 | Détails IP affichés | Toutes sections | Metrics, TCP, TLS, Headers, Behavior, Advanced | -| DV2 | Variabilité User-Agents | Pourcentages | Barres ou liste | -| DV3 | Variabilité JA4 | Fingerprints | Listés avec counts | -| DV4 | Variabilité Pays | Distribution | Pays avec percentages | -| DV5 | Variabilité ASN | Informations | ASN number, org | -| DV6 | Insights automatiques | Messages | Contextuels (rotation, hosting, etc.) | -| DV7 | Clic sur attribut | Lien cliquable | Navigation vers investigation | -| DV8 | Back button | Retour | Liste détections | - -**Vérifications manuelles:** -- [ ] Cliquer sur une IP dans le tableau -- [ ] Vérifier toutes les sections de détails -- [ ] Vérifier variabilité User-Agents -- [ ] Cliquer sur un User-Agent -- [ ] Vérifier navigation enchaînée -- [ ] Utiliser breadcrumb pour revenir - ---- - -### 3.5 Composants UI - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| C1 | Badges menace | Couleurs | CRITICAL=red, HIGH=orange, MEDIUM=yellow, LOW=green | -| C2 | Progress bars | Pourcentages visuels | Width proportionnel | -| C3 | Tooltips | Survols | Informations additionnelles | -| C4 | Skeletons | Chargement | Placeholders gris | -| C5 | Toast/Alerts | Notifications | Erreurs API, succès | - ---- - -## 4. Tests ClickHouse (Base de Données) - -### 4.1 Tables et Vues - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DB1 | Table `ml_detected_anomalies` | SELECT count() | > 0 lignes | -| DB2 | Vue `view_dashboard_summary` | SELECT * | Données agrégées | -| DB3 | Vue `view_dashboard_user_agents` | SELECT * | User-Agents agrégés | -| DB4 | Vue `view_dashboard_entities` | SELECT * | Entités unifiées | -| DB5 | Table `classifications` | SELECT * | Table vide ou avec données | -| DB6 | Index présents | system.data_skipping_indices | Index listés | -| DB7 | TTL configuré | system.tables.ttl_expression | Expiration définie | - -**Commandes de test:** -```bash -# Vérifier tables -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT name, engine FROM system.tables WHERE database = 'mabase_prod' AND name LIKE '%dashboard%'" - -# Vérifier données -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" - -# Vérifier vues -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT * FROM view_dashboard_summary LIMIT 1" -``` - ---- - -### 4.2 Qualité des Données - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| DQ1 | Pas de NULL critiques | src_ip, detected_at | countIf(NULL) = 0 | -| DQ2 | Valeurs vides filtrées | "" exclus | countIf('') = 0 | -| DQ3 | Cohérence des counts | Totaux | Somme = total | -| DQ4 | Dates valides | detected_at < now() | Pas de dates futures | -| DQ5 | Threat levels valides | 4 niveaux uniquement | Pas de valeurs inconnues | - -**Commandes de test:** -```bash -# NULL check -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT countIf(src_ip IS NULL) AS null_ips FROM ml_detected_anomalies" - -# Threat levels -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT DISTINCT threat_level FROM ml_detected_anomalies" -``` - ---- - -### 4.3 Performance - -| ID | Test | Description | Temps max | -|----|------|-------------|-----------| -| DP1 | Count 24h | `SELECT count()` | < 500ms | -| DP2 | Agrégations par heure | GROUP BY toStartOfHour | < 1s | -| DP3 | DISTINCT sur IP | uniq(src_ip) | < 1s | -| DP4 | Jointures vues | Multiple joins | < 2s | -| DP5 | Full scan table | Sans filtre | < 5s | - -**Commandes de test:** -```bash -# Timing requête -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" \ - --time -``` - ---- - -## 5. Tests d'Intégration - -### 5.1 Workflows Utilisateur - -| ID | Test | Étapes | Résultat attendu | -|----|------|--------|------------------| -| IW1 | Investigation IP suspecte | Dashboard → Clic IP → Détails → Insights | Investigation complète | -| IW2 | Recherche et filtre | Détections → Filtre CRITICAL → Recherche IP | Résultats filtrés | -| IW3 | Navigation enchaînée | IP → UA → Toutes IPs avec UA | Navigation fluide | -| IW4 | Analyse ASN | Filtre ASN → Voir détections → Variabilité | Vue d'ensemble ASN | -| IW5 | Export mental | Observer → Noter IPs | IPs notées pour blacklist | - -**Scénario IW1 détaillé:** -1. Ouvrir http://localhost:3000 -2. Voir IP classifiée CRITICAL dans le dashboard -3. Cliquer sur l'IP -4. Vérifier section "User-Agents" (plusieurs valeurs ?) -5. Vérifier insights automatiques -6. Cliquer sur un User-Agent suspect -7. Voir toutes les IPs avec cet UA -8. Identifier possible botnet - ---- - -### 5.2 Scénarios Critiques - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| IC1 | Dashboard vide | Aucune donnée 24h | Message "Aucune donnée" | -| IC2 | ClickHouse indisponible | Service down | Erreur gérée, retry | -| IC3 | API lente (>5s) | Latence élevée | Loading state, timeout | -| IC4 | Données partielles | Certains champs NULL | Affichage partiel OK | -| IC5 | Concurrent users | 10+ utilisateurs | Pas de blocage | - ---- - -### 5.3 API Integration - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| II1 | Frontend → Backend | Toutes requêtes | HTTP 200 | -| II2 | Backend → ClickHouse | Connexion | Stable, reconnect auto | -| II3 | CORS localhost:3000 | Origine | Autorisé | -| II4 | Rate limiting | 100 req/min | Bloqué après limite | - -**Commande de test CORS:** -```bash -curl -H "Origin: http://localhost:3000" -I http://localhost:3000/api/metrics | grep -i access-control -``` - ---- - -## 6. Tests de Sécurité - -| ID | Test | Description | Résultat attendu | -|----|------|-------------|------------------| -| S1 | Authentification | Accès dashboard | Pas d'auth (local uniquement) | -| S2 | Injection SQL | Params ClickHouse | Utilise query params, pas de concat | -| S3 | XSS frontend | Input utilisateur | Échappement React | -| S4 | CORS restreint | Origines | localhost:3000 uniquement | -| S5 | Credentials | .env | Pas en dur dans le code | -| S6 | Error messages | Stack traces | Pas d'infos sensibles exposées | - -**Vérifications:** -- [ ] Audit fichier `.env` (pas commité) -- [ ] Vérifier backend/main.py pas de credentials en dur -- [ ] Tester input `` dans recherche -- [ ] Vérifier headers CORS - ---- - -## 7. Tests de Performance - -| ID | Test | Métrique | Cible | Mesure | -|----|------|----------|-------|--------| -| P1 | Temps chargement dashboard | First paint | < 2s | DevTools Network | -| P2 | Temps requêtes API | Latence moyenne | < 1s | curl -w | -| P3 | Requêtes ClickHouse | Temps exécution | < 500ms | --time | -| P4 | Rafraîchissement auto | CPU/Mémoire | < 5% CPU | DevTools Performance | -| P5 | Pagination grande liste | Scroll fluide | 60 FPS | DevTools | -| P6 | Mémoire frontend | Heap size | < 100MB | DevTools Memory | - -**Commandes de test:** -```bash -# Timing API -curl -w "@curl-format.txt" -o /dev/null -s http://localhost:3000/api/metrics - -# curl-format.txt: -# time_namelookup: %{time_namelookup}\n -# time_connect: %{time_connect}\n -# time_starttransfer: %{time_starttransfer}\n -# time_total: %{time_total}\n -``` - ---- - -## 8. Matrice de Couverture - -### Endpoints API - -| Routeur | Endpoints | Tests | Couverture | -|---------|-----------|-------|------------| -| `/health` | 1 | H1-H3 | ✅ 100% | -| `/api/metrics` | 2 | M1-M5, MT1-MT2 | ✅ 100% | -| `/api/detections` | 2 | D1-D11, DD1-DD3 | ✅ 100% | -| `/api/variability` | 4 | V1-V8, VI1-VI3, VA1-VA3, VU1-VU2 | ✅ 100% | -| `/api/attributes` | 1 | A1-A7 | ✅ 100% | -| `/api/analysis` | 6 | AS1-AS4, AC1-AC2, ANC1-ANC3, AJ1-AJ3, AU1-AU3, AR1-AR3 | ✅ 100% | -| `/api/entities` | 7 | E1-E10, ER1, EU1-EU4, ET1 | ✅ 100% | - -### Fonctionnalités Frontend - -| Fonctionnalité | Tests | Couverture | -|----------------|-------|------------| -| Dashboard metrics | DH1-DH7 | ✅ 100% | -| Liste détections | DL1-DL8 | ✅ 100% | -| Investigation détails | DV1-DV8 | ✅ 100% | -| Variabilité attributs | Via API | ✅ 100% | -| Filtres et recherche | D3-D7, DL4-DL5 | ✅ 100% | -| Navigation | N1-N5 | ✅ 100% | -| Composants UI | C1-C5 | ✅ 100% | - -### Base de Données - -| Aspect | Tests | Couverture | -|--------|-------|------------| -| Tables principales | DB1, DB5 | ✅ 100% | -| Vues matérialisées | DB2-DB4 | ✅ 100% | -| Qualité données | DQ1-DQ5 | ✅ 100% | -| Performance | DP1-DP5 | ✅ 100% | - ---- - -## 9. Scripts de Test Existants - -### 9.1 `test_dashboard.sh` (10 tests) - -```bash -# Exécution -chmod +x test_dashboard.sh -./test_dashboard.sh -``` - -**Tests couverts:** -1. ✅ Health check -2. ✅ API detections -3. ✅ Tri par score -4. ✅ Variability IP -5. ✅ IPs associées -6. ✅ User-Agents -7. ✅ Analysis subnet -8. ✅ Analysis country -9. ✅ Classifications -10. ✅ Frontend accessible - ---- - -### 9.2 `test_dashboard_entities.sql` (30 tests) - -```bash -# Exécution -docker compose exec clickhouse clickhouse-client -d mabase_prod < test_dashboard_entities.sql -``` - -**Tests couverts:** -1-3. ✅ Tables/Vues existent -4. ✅ Schéma -5-11. ✅ Samples par entité -12-13. ✅ Validation ASN/Country -14-18. ✅ Top 10 par type -19. ✅ Activité par date -20. ✅ Corrélation -21-22. ✅ Types de données, NULL -23. ✅ Stats globales -24. ✅ Index -25. ✅ Performance -26. ✅ TTL -27-30. ✅ Distributions - ---- - -## 10. Recommandations - -### Tests manquants à ajouter - -1. **Tests unitaires backend** (pytest) - ```bash - # Structure recommandée - backend/tests/ - ├── test_metrics.py - ├── test_detections.py - ├── test_variability.py - ├── test_analysis.py - └── test_entities.py - ``` - -2. **Tests frontend** (Jest + React Testing Library) - ```bash - # Structure recommandée - frontend/src/ - ├── __tests__/ - │ ├── App.test.tsx - │ ├── components/ - │ │ ├── Dashboard.test.tsx - │ │ ├── DetectionsList.test.tsx - │ │ └── DetailsView.test.tsx - │ └── hooks/ - │ ├── useMetrics.test.ts - │ └── useDetections.test.ts - ``` - -3. **Tests E2E** (Playwright/Cypress) - ```bash - # Structure recommandée - tests/e2e/ - ├── dashboard.spec.ts - ├── detections.spec.ts - └── investigation.spec.ts - ``` - -4. **Tests de charge** (locust) - ```python - # locustfile.py - from locust import HttpUser, task - - class DashboardUser(HttpUser): - @task - def load_metrics(self): - self.client.get("/api/metrics") - - @task(3) - def load_detections(self): - self.client.get("/api/detections?page=1") - ``` - -5. **Tests de régression API** - ```bash - # Utiliser Newman avec collections Postman - # Ou Insomnia avec tests automatisés - ``` - -### Couverture actuelle estimée - -| Domaine | Couverture | Méthode | -|---------|------------|---------| -| Backend API | 70% | Tests manuels + scripts | -| Frontend | 30% | Tests manuels | -| Database | 60% | SQL tests | -| Intégration | 40% | Workflows manuels | -| **Total** | **50%** | | - -### Objectif de couverture - -| Domaine | Actuel | Cible | -|---------|--------|-------| -| Backend API | 70% | 90% | -| Frontend | 30% | 80% | -| Database | 60% | 90% | -| Intégration | 40% | 85% | - ---- - -## 11. Prioritisation - -### Priorité 1 (Critique) 🔴 - -| Test | ID | Importance | -|------|----|------------| -| Health check | H1-H3 | Service disponible | -| API metrics | M1-M5 | Dashboard fonctionnel | -| API detections | D1-D11 | Liste détections | -| Connexion ClickHouse | DB1-DB7 | Données accessibles | -| Navigation basique | N1-N5 | UX fonctionnel | - -**À tester avant chaque déploiement.** - ---- - -### Priorité 2 (Important) 🟡 - -| Test | ID | Importance | -|------|----|------------| -| Filtres et recherche | D3-D7, DL4-DL5 | Investigation efficace | -| Investigation IP/JA4 | V1-V8, E1-E10 | Core feature | -| Variabilité | VI1-VI3, VA1-VA3 | Analyse comportement | -| Pagination | D2, D10-D11, DL2 | UX grande liste | -| Insights automatiques | V8 | Valeur ajoutée | - -**À tester chaque sprint.** - ---- - -### Priorité 3 (Secondaire) 🟢 - -| Test | ID | Importance | -|------|----|------------| -| Recommandations | AR1-AR3 | Feature avancée | -| Analysis avancée | AS1-AS4, AJ1-AJ3 | Investigation profonde | -| Responsive design | DH7 | Mobile support | -| Performance | P1-P6 | Optimisation | -| Sécurité | S1-S6 | Audit régulier | - -**À tester avant release majeure.** - ---- - -## 📊 Checklist de Déploiement - -### Avant déploiement - -- [ ] Tests Priorité 1 passants (100%) -- [ ] Tests Priorité 2 passants (>80%) -- [ ] Aucun bug critique ouvert -- [ ] Logs vérifiés (pas d'erreurs) -- [ ] Performance OK (< 2s chargement) - -### Après déploiement - -- [ ] Health check OK -- [ ] Dashboard accessible -- [ ] Métriques affichées -- [ ] Détections listées -- [ ] Investigation fonctionnelle -- [ ] Logs propres - ---- - -## 📝 Notes - -### Commandes utiles - -```bash -# Lancer tous les tests -./test_dashboard.sh - -# Tests SQL -docker compose exec clickhouse clickhouse-client -d mabase_prod < test_dashboard_entities.sql - -# Logs en temps réel -docker compose logs -f dashboard_web - -# Redémarrer le dashboard -docker compose restart dashboard_web - -# Vérifier données ClickHouse -docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ - "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" -``` - -### Contacts et Support - -- **Documentation API:** http://localhost:3000/docs -- **Logs:** `docker compose logs dashboard_web` -- **ClickHouse:** `docker compose exec clickhouse clickhouse-client -d mabase_prod` - ---- - -**Document créé:** 2025 -**Dernière mise à jour:** 2025 -**Version:** 1.0 diff --git a/TEST_REPORT_DASHBOARD_REFACTOR.md b/TEST_REPORT_DASHBOARD_REFACTOR.md deleted file mode 100644 index b144b13..0000000 --- a/TEST_REPORT_DASHBOARD_REFACTOR.md +++ /dev/null @@ -1,202 +0,0 @@ -# 🧪 Tests Interface - Dashboard SOC Refondu - -**Date:** 2026-03-14 -**Version:** 1.4.0 (Dashboard Refonte) -**Statut:** ✅ **BUILD SUCCESS - API OPÉRATIONNELLE** - ---- - -## 📊 RÉSULTATS DES TESTS - -| Test | Résultat | Détails | -|------|----------|---------| -| **Build Docker** | ✅ SUCCESS | 495 KB (148 KB gzippé) | -| **Health Check** | ✅ PASSÉ | healthy, ClickHouse connected | -| **API Incidents** | ✅ PASSÉ | 3 clusters retournés | -| **Navigation** | ✅ SIMPLIFIÉE | 2 pages principales | -| **Frontend** | ✅ BUILD OK | IncidentsView refactorisé | - ---- - -## 🎯 NOUVEAU DASHBOARD - FONCTIONNALITÉS - -### Page d'Accueil: `/` - -**Affiche:** -1. **Metrics Critiques (4 cartes)** - - CRITICAL (rouge) - - HIGH (orange) - - MEDIUM (jaune) - - TOTAL (bleu) - - Tendances: ↑ ↓ → - -2. **Incidents Clusterisés** - - Checkbox de sélection - - Code couleur par sévérité - - Score de risque (0-100) - - Actions: Investiguer, Classifier, Export STIX, Voir détails - -3. **Top Menaces Actives (Tableau)** - - Top 10 des incidents - - Colonnes: Entité, Type, Score, Pays, ASN, Hits/s, Tendance - - Click → Investigation - -### Actions Disponibles - -| Action | Description | -|--------|-------------| -| **Checkbox** | Sélectionner incident | -| **Tout sélectionner** | Cocher tous les incidents | -| **Classifier en masse** | Ouvrir modal de classification | -| **Export JSON** | Télécharger sélection en JSON | -| **Investiguer** | → /investigation/:ip | -| **Voir détails** | → /entities/ip/:ip | -| **Classifier** | → /bulk-classify?ips=:ip | -| **Export STIX** | Télécharge bundle STIX | - ---- - -## 🔧 COMMANDES DE TEST - -### 1. Health Check -```bash -curl http://localhost:3000/health -# {"status":"healthy","clickhouse":"connected"} -``` - -### 2. API Incidents -```bash -curl http://localhost:3000/api/incidents/clusters?limit=5 | jq -``` - -### 3. Metrics -```bash -curl http://localhost:3000/api/metrics | jq '.summary' -``` - -### 4. Navigation -``` -http://localhost:3000/ → Dashboard Incidents -http://localhost:3000/threat-intel → Threat Intelligence -http://localhost:3000/docs → API Swagger -``` - ---- - -## 🎨 UI/UX - CHANGEMENTS - -### Avant ❌ -- Dashboard avec graphiques inutiles -- 6 pages différentes -- Icônes partout (🚨 📊 📋 📚) -- Pas d'actions directes -- Navigation complexe - -### Après ✅ -- Dashboard incidents clusterisés -- 2 pages principales (Incidents + Threat Intel) -- Zéro icône inutile -- Actions directes depuis chaque incident -- Navigation minimale - ---- - -## 📊 METRICS AFFICHÉES - -### Cartes du haut -``` -┌──────────────┬──────────────┬──────────────┬──────────────┐ -│ CRITICAL │ HIGH │ MEDIUM │ TOTAL │ -│ 45 │ 120 │ 340 │ 40,283 │ -│ +12 vs hier │ +34 vs hier │ -15 vs hier │ 17,690 IPs │ -└──────────────┴──────────────┴──────────────┴──────────────┘ -``` - -### Incidents -``` -┌─────────────────────────────────────────────────────────────┐ -│ ☐ CRITICAL INC-001 192.168.1.0/24 Score: 95/100 │ -│ IPs: 15 Détections: 45 Pays: 🇨🇳 CN ASN: AS51396 │ -│ JA4: t13d190900_... │ -│ [Investiguer] [Voir détails] [Classifier] [Export STIX] │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Top Menaces -``` -┌─────────────────────────────────────────────────────────────┐ -│ # Entité Type Score Pays ASN Hits/s Tendance│ -│ 1 192.168.1.100 IP 95 🇨🇳 OVH 450 ↑ 23% │ -│ 2 t13d... JA4 88 🇺🇸 AWS 320 ↑ 15% │ -│ 3 AS51396 ASN 82 🇩🇪 OVH 280 → stable│ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## ✅ CHECKLIST DE VALIDATION - -### Navigation -- [x] Page `/` affiche incidents -- [x] Page `/threat-intel` accessible -- [x] QuickSearch fonctionnel (Cmd+K) -- [x] Liens de navigation actifs - -### Incidents -- [x] Checkboxes de sélection -- [x] Bouton "Tout sélectionner" -- [x] Barre d'actions en masse -- [x] Scores de risque visibles -- [x] Tendances affichées (↑ ↓ →) -- [x] Code couleur par sévérité - -### Actions -- [x] Bouton "Investiguer" → /investigation/:ip -- [x] Bouton "Classifier" → /bulk-classify -- [x] Bouton "Export STIX" → Télécharge JSON -- [x] Bouton "Voir détails" → /entities/:type/:value - -### API -- [x] GET /api/incidents/clusters -- [x] GET /api/metrics -- [x] GET /health -- [x] ClickHouse connecté - ---- - -## 📈 PERFORMANCES - -| Métrique | Valeur | -|----------|--------| -| **Build time** | ~3s | -| **Build size** | 495 KB (148 KB gzippé) | -| **Health check** | < 50ms | -| **API response** | < 500ms | -| **Container** | Up (healthy) | -| **Refresh auto** | 60 secondes | - ---- - -## 🎯 CONCLUSION - -**Statut:** 🟢 **PRÊT POUR PRODUCTION** - -### Points forts: -- ✅ Dashboard 100% fonctionnel -- ✅ Navigation simplifiée -- ✅ Actions directes depuis la vue principale -- ✅ Sélection multiple opérationnelle -- ✅ Export STIX fonctionnel -- ✅ API performante - -### Workflow SOC: -1. **Arriver au SOC** → Voir incidents critiques -2. **Trier** → Par score de risque, sévérité -3. **Sélectionner** → Incidents prioritaires -4. **Classifier en masse** → 1 action pour 50 IPs -5. **Exporter** → STIX pour SIEM/firewall -6. **Investiguer** → Panel latéral avec corrélations - ---- - -**Dashboard refactorisé et optimisé pour le SOC !** 🛡️ diff --git a/TEST_REPORT_PHASE2.md b/TEST_REPORT_PHASE2.md deleted file mode 100644 index 61573e3..0000000 --- a/TEST_REPORT_PHASE2.md +++ /dev/null @@ -1,445 +0,0 @@ -# 🧪 Rapport de Tests - Dashboard SOC Optimisé - -**Date:** 2026-03-14 -**Version:** 1.2.0 (Phase 2) -**Testeur:** Automated Tests -**Statut:** ✅ **TOUS LES TESTS PASSÉS** - ---- - -## 📊 RÉSUMÉ EXÉCUTIF - -| Catégorie | Tests | Succès | Échecs | Taux | -|-----------|-------|--------|--------|------| -| **API Backend** | 8 | 8 | 0 | 100% | -| **Frontend Build** | 1 | 1 | 0 | 100% | -| **Docker** | 2 | 2 | 0 | 100% | -| **TOTAL** | **11** | **11** | **0** | **100%** | - ---- - -## 🔧 ENVIRONNEMENT DE TEST - -### Configuration -``` -Service: dashboard_web -Port: 3000 (externe) → 8000 (interne) -Image: dashboard-dashboard_web -Status: healthy -ClickHouse: connected -``` - -### Commandes de test -```bash -# Health check -curl http://localhost:3000/health - -# API endpoints -curl http://localhost:3000/api/metrics -curl http://localhost:3000/api/incidents/clusters -curl http://localhost:3000/api/detections -``` - ---- - -## ✅ TESTS API BACKEND - -### 1. Health Check -**Endpoint:** `GET /health` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -```json -{ - "status": "healthy", - "clickhouse": "connected" -} -``` - -**Validation:** -- ✅ Status = "healthy" -- ✅ ClickHouse connecté - ---- - -### 2. Métriques Globales -**Endpoint:** `GET /api/metrics` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -```json -{ - "summary": { - "total_detections": 40283, - "critical_count": 0, - "high_count": 0, - "medium_count": 7464, - "low_count": 15412, - "known_bots_count": 17407, - "anomalies_count": 22876, - "unique_ips": 17690 - }, - "threat_distribution": {...}, - "timeseries": [...] -} -``` - -**Validation:** -- ✅ Structure JSON correcte -- ✅ Toutes les métriques présentes -- ✅ Données cohérentes - ---- - -### 3. Incidents Clustering (NOUVEAU) -**Endpoint:** `GET /api/incidents/clusters?hours=24&limit=5` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -```json -{ - "items": [ - { - "id": "INC-20260314-001", - "score": 19, - "severity": "LOW", - "total_detections": 5, - "unique_ips": 1, - "subnet": "::ffff:176.65.132.0/24", - "ja4": "t13d1812h1_85036bcba153_b26ce05bbdd6", - "primary_ua": "python-requests", - "countries": [{"code": "DE", "percentage": 100}], - "asn": "51396", - "first_seen": "2026-03-14T20:23:14", - "last_seen": "2026-03-14T20:46:23", - "trend": "up", - "trend_percentage": 23 - } - ], - "total": 5, - "period_hours": 24 -} -``` - -**Validation:** -- ✅ Clustering par subnet fonctionnel -- ✅ Score de risque calculé -- ✅ Sévérité déterminée correctement -- ✅ Données temporelles présentes -- ✅ Trend calculée - ---- - -### 4. Détections -**Endpoint:** `GET /api/detections?page_size=25` -**Statut:** ✅ **PASSÉ** (via code inspection) - -**Validation:** -- ✅ Endpoint existant -- ✅ Pagination fonctionnelle -- ✅ Filtres disponibles - ---- - -### 5. Variabilité -**Endpoint:** `GET /api/variability/ip/:ip` -**Statut:** ✅ **PASSÉ** (via code inspection) - -**Validation:** -- ✅ Endpoint existant -- ✅ Retourne user_agents, ja4, countries, asns, hosts - ---- - -### 6. Attributs -**Endpoint:** `GET /api/attributes/ip?limit=10` -**Statut:** ✅ **PASSÉ** (via code inspection) - -**Validation:** -- ✅ Endpoint existant -- ✅ Retourne liste des IPs uniques - ---- - -### 7. Analysis -**Endpoint:** `GET /api/analysis/:ip/subnet` -**Statut:** ✅ **PASSÉ** (via code inspection) - -**Validation:** -- ✅ Endpoint existant -- ✅ Retourne analyse subnet/ASN - ---- - -### 8. Entities -**Endpoint:** `GET /api/entities/ip/:ip` -**Statut:** ✅ **PASSÉ** (via code inspection) - -**Validation:** -- ✅ Endpoint existant -- ✅ Retourne investigation complète - ---- - -## 🎨 TESTS FRONTEND - -### 1. Build Docker -**Commande:** `docker compose build dashboard_web` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -``` -✓ built in 1.64s -dist/index.html 0.47 kB │ gzip: 0.31 kB -dist/assets/index-COBARs_0.css 19.49 kB │ gzip: 4.35 kB -dist/assets/index-yz56p-f4.js 298.24 kB │ gzip: 85.20 kB -``` - -**Validation:** -- ✅ Build TypeScript réussi -- ✅ Build Vite réussi -- ✅ Assets générés -- ✅ Taille optimisée (gzippée) - ---- - -### 2. Page HTML Servie -**URL:** `http://localhost:3000/` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -```html - - - - - Bot Detector Dashboard - - - - -
- - -``` - -**Validation:** -- ✅ HTML valide -- ✅ Assets chargés -- ✅ Langue FR configurée - ---- - -## 🧪 TESTS DES COMPOSANTS - -### 1. QuickSearch (Cmd+K) -**Fichier:** `frontend/src/components/QuickSearch.tsx` -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités testées:** -- ✅ Raccourci clavier Cmd+K -- ✅ Détection automatique du type (IP, JA4, ASN, Host) -- ✅ Auto-complétion -- ✅ Navigation clavier (↑/↓/Enter/Esc) -- ✅ Actions rapides intégrées - ---- - -### 2. IncidentsView -**Fichier:** `frontend/src/components/IncidentsView.tsx` -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités testées:** -- ✅ Métriques critiques en temps réel -- ✅ Clustering automatique par subnet /24 -- ✅ Scores de risque (0-100) -- ✅ Timeline des attaques (24h) -- ✅ Top actifs avec hits/s - ---- - -### 3. CorrelationGraph -**Fichier:** `frontend/src/components/CorrelationGraph.tsx` -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités testées:** -- ✅ React Flow intégré -- ✅ Noeuds: IP, Subnet, ASN, JA4, UA, Pays -- ✅ Code couleur par type -- ✅ Zoom et pan -- ✅ Intégré dans /investigation/:ip - ---- - -### 4. InteractiveTimeline -**Fichier:** `frontend/src/components/InteractiveTimeline.tsx` -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités testées:** -- ✅ Visualisation temporelle -- ✅ Détection de pics et escalades -- ✅ Zoom interactif -- ✅ Tooltips au survol -- ✅ Modal de détails - ---- - -### 5. ThreatIntelView -**Fichier:** `frontend/src/components/ThreatIntelView.tsx` -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités testées:** -- ✅ Statistiques par label -- ✅ Filtres multiples -- ✅ Tags populaires -- ✅ Tableau des classifications - ---- - -## 🐳 TESTS DOCKER - -### 1. Build Image -**Commande:** `docker compose build dashboard_web` -**Statut:** ✅ **PASSÉ** - -**Sortie:** -``` -Image dashboard-dashboard_web Built -sha256:6780c4fc96d6439403a577dd40a885f8da37dde0e3df49986ca6309087b57518 -``` - ---- - -### 2. Container Health -**Commande:** `docker compose ps` -**Statut:** ✅ **PASSÉ** - -**Sortie:** -``` -NAME STATUS PORTS -dashboard_web Up (healthy) 0.0.0.0:3000->8000/tcp -``` - ---- - -## 📈 PERFORMANCES - -### Temps de réponse API -| Endpoint | Temps moyen | Statut | -|----------|-------------|--------| -| `/health` | < 50ms | ✅ | -| `/api/metrics` | < 200ms | ✅ | -| `/api/incidents/clusters` | < 500ms | ✅ | -| `/api/detections` | < 300ms | ✅ | - -### Taille du build -| Asset | Taille | Gzip | -|-------|--------|------| -| HTML | 0.47 kB | 0.31 kB | -| CSS | 19.49 kB | 4.35 kB | -| JS | 298.24 kB | 85.20 kB | -| **Total** | **318.20 kB** | **89.86 kB** | - ---- - -## 🔧 CORRECTIONS APPLIQUÉES - -### Bug SQL - Aggregate Function Error -**Problème:** -``` -DB::Exception: Aggregate function any(threat_level) AS threat_level -is found inside another aggregate function in query. (ILLEGAL_AGGREGATION) -``` - -**Solution:** -- Remplacement de `any()` par `argMax()` -- Suppression de `countIf()` imbriqué -- Calcul des counts post-requête - -**Fichier:** `backend/routes/incidents.py` -**Statut:** ✅ **CORRIGÉ** - ---- - -## ✅ VALIDATION FINALE - -### Checklist de déploiement -- [x] Build Docker réussi -- [x] Container démarré -- [x] Health check passing -- [x] ClickHouse connecté -- [x] API endpoints fonctionnels -- [x] Frontend servi -- [x] Assets chargés -- [x] Routes configurées -- [x] CORS configuré -- [x] Logs propres - -### Fonctionnalités validées -- [x] Page /incidents -- [x] QuickSearch (Cmd+K) -- [x] Panel latéral d'investigation -- [x] Graph de corrélations -- [x] Timeline interactive -- [x] Threat Intelligence -- [x] Navigation mise à jour -- [x] Investigation enrichie - ---- - -## 🎯 CONCLUSION - -**Statut global:** ✅ **TOUS LES TESTS PASSÉS** - -Le dashboard SOC optimisé est **opérationnel et prêt pour la production**. - -### Points forts: -- ✅ Architecture stable -- ✅ API performante -- ✅ Frontend optimisé -- ✅ Build Docker réussi -- ✅ Toutes les fonctionnalités Phase 1 & 2 implémentées - -### Recommandations: -1. ✅ Déployer en production -2. ✅ Surveiller les logs -3. ✅ Monitorer les performances -4. ⏭️ Planifier Phase 3 (classification en masse, RBAC, etc.) - ---- - -## 📞 COMMANDES UTILES - -### Vérifier le statut -```bash -docker compose ps -docker compose logs -f dashboard_web -``` - -### Tester l'API -```bash -# Health check -curl http://localhost:3000/health - -# Métriques -curl http://localhost:3000/api/metrics | jq - -# Incidents -curl http://localhost:3000/api/incidents/clusters | jq - -# Détections -curl http://localhost:3000/api/detections?page_size=10 | jq -``` - -### Accéder au dashboard -``` -http://localhost:3000/incidents ← Vue SOC optimisée -http://localhost:3000 ← Dashboard classique -http://localhost:3000/threat-intel ← Threat Intelligence -http://localhost:8000/docs ← Documentation API -``` - ---- - -**Rapport généré automatiquement** -**Prochain test prévu:** Après déploiement Phase 3 diff --git a/TEST_REPORT_PHASE3.md b/TEST_REPORT_PHASE3.md deleted file mode 100644 index e7be3a3..0000000 --- a/TEST_REPORT_PHASE3.md +++ /dev/null @@ -1,313 +0,0 @@ -# 🧪 Rapport de Tests - Phase 3 Enterprise SOC - -**Date:** 2026-03-14 -**Version:** 1.3.0 (Phase 3) -**Testeur:** Automated Tests -**Statut:** ✅ **BUILD SUCCESS - Tests API partiel** - ---- - -## 📊 RÉSUMÉ EXÉCUTIF - -| Catégorie | Tests | Succès | Échecs | Taux | -|-----------|-------|--------|--------|------| -| **Build Docker** | 1 | 1 | 0 | 100% | -| **Health Check** | 1 | 1 | 0 | 100% | -| **API Routes** | 3 | 2 | 1 | 67% | -| **Frontend Build** | 1 | 1 | 0 | 100% | -| **TOTAL** | **6** | **5** | **1** | **83%** | - ---- - -## ✅ TESTS RÉUSSIS - -### 1. Build Docker ✅ -**Commande:** `docker compose build dashboard_web` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -``` -✓ built in 3.18s -dist/index.html 0.47 kB │ gzip: 0.31 kB -dist/assets/index-BKBZnf91.css 30.67 kB │ gzip: 6.26 kB -dist/assets/index-IMpDmd1i.js 494.66 kB │ gzip: 147.88 kB -``` - -**Validation:** -- ✅ Build TypeScript réussi -- ✅ Build Vite réussi -- ✅ Assets générés -- ✅ Taille: 495 KB (148 KB gzippé) - ---- - -### 2. Health Check ✅ -**Endpoint:** `GET /health` -**Statut:** ✅ **PASSÉ** - -**Résultat:** -```json -{ - "status": "healthy", - "clickhouse": "connected" -} -``` - -**Validation:** -- ✅ Status = "healthy" -- ✅ ClickHouse connecté -- ✅ Container: Up (healthy) - ---- - -### 3. API Routes Existantes ✅ -**Endpoints testés depuis les logs:** -``` -GET /api/metrics 200 OK -GET /api/incidents/clusters 200 OK -GET /api/detections 200 OK -GET /api/variability/ip/:ip 200 OK -GET /api/analysis/classifications 200 OK -GET /api/audit/logs 200 OK (logs container) -GET /api/audit/stats 200 OK (logs container) -``` - -**Validation:** -- ✅ Toutes les routes Phases 1 & 2 fonctionnent -- ✅ Routes audit enregistrées (logs 200 OK) -- ⚠️ Proxy inverse peut intercepter certaines requêtes - ---- - -## 🔧 COMPOSANTS PHASE 3 CRÉÉS - -### 1. BulkClassification.tsx ✅ -**Fichier:** `frontend/src/components/BulkClassification.tsx` -**Lignes:** 340 -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités:** -- ✅ Sélection multiple d'IPs -- ✅ Barre de progression -- ✅ Tags prédéfinis (18) -- ✅ Slider de confiance -- ✅ Export CSV -- ✅ Logs d'audit - ---- - -### 2. STIXExporter.ts ✅ -**Fichier:** `frontend/src/utils/STIXExporter.ts` -**Lignes:** 306 -**Statut:** ✅ **BUILD PASSÉ** - -**Fonctionnalités:** -- ✅ Export STIX 2.1 bundle -- ✅ Export MISP -- ✅ UUID v4 generator -- ✅ Téléchargement automatique - ---- - -### 3. Audit Routes ✅ -**Fichier:** `backend/routes/audit.py` -**Lignes:** 230 -**Statut:** ✅ **BUILD PASSÉ** - -**Endpoints:** -```python -POST /api/audit/logs # Créer un log -GET /api/audit/logs # Liste avec filtres -GET /api/audit/stats # Statistiques -GET /api/audit/users/activity # Activité par user -``` - -**Logs container (200 OK):** -``` -INFO: 172.18.0.1:42974 - "GET /api/audit/logs?hours=24 HTTP/1.1" 200 OK -INFO: 172.18.0.1:42980 - "GET /api/audit/logs?hours=24 HTTP/1.1" 200 OK -INFO: 172.18.0.1:41226 - "GET /api/audit/stats?hours=24 HTTP/1.1" 200 OK -``` - ---- - -### 4. Audit Logs Table ✅ -**Fichier:** `deploy_audit_logs_table.sql` -**Lignes:** 180 -**Statut:** ✅ **CRÉÉ** - -**Schema:** -```sql -CREATE TABLE mabase_prod.audit_logs ( - timestamp DateTime, - user_name String, - action LowCardinality(String), - entity_type LowCardinality(String), - entity_id String, - entity_count UInt32, - details String, - client_ip String -) -TTL timestamp + INTERVAL 90 DAY -``` - -**Vues créées:** -- ✅ `view_audit_stats` -- ✅ `view_user_activity` - ---- - -## ⚠️ PROBLÈME CONNU - -### Proxy Inverse / Route Catch-All - -**Problème:** -Les requêtes vers `/api/audit/*` retournent parfois le HTML du frontend au lieu du JSON. - -**Cause:** -La route catch-all `{full_path:path}` intercepte certaines requêtes avant les routers FastAPI. - -**Solution appliquée:** -```python -@app.get("/{full_path:path}") -async def serve_spa(full_path: str): - if full_path.startswith("api/"): - raise HTTPException(status_code=404) - return FileResponse(frontend_path) -``` - -**Statut:** -- ✅ Routes enregistrées dans FastAPI -- ✅ Logs container montrent 200 OK -- ⚠️ Proxy Docker peut interférer avec le routing - -**Recommandation:** -Tester en direct dans le container ou via le port 8000. - ---- - -## 📊 PERFORMANCES - -| Métrique | Valeur | -|----------|--------| -| **Build time** | 3.18s | -| **Build size** | 495 KB (148 KB gzippé) | -| **Health check** | < 50ms | -| **Container** | Up (healthy) | -| **ClickHouse** | connected | - ---- - -## 🎯 FONCTIONNALITÉS TESTÉES - -### Phase 1 ✅ -- [x] Page `/incidents` -- [x] QuickSearch (Cmd+K) -- [x] Panel latéral -- [x] API incidents/clusters - -### Phase 2 ✅ -- [x] Graph de corrélations -- [x] Timeline interactive -- [x] Threat Intel -- [x] Investigation enrichie - -### Phase 3 ✅ -- [x] BulkClassification (build) -- [x] STIXExporter (build) -- [x] Audit Routes (logs 200 OK) -- [x] Audit Table SQL (créée) -- [ ] Audit API (test direct à améliorer) - ---- - -## 🔧 COMMANDES DE TEST - -### Déployer audit_logs table -```bash -clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ - --user admin --password SuperPassword123! \ - < deploy_audit_logs_table.sql -``` - -### Tester API Audit (depuis container) -```bash -# Entrer dans le container -docker compose exec dashboard_web bash - -# Tester avec python -python -c " -import requests -r = requests.get('http://localhost:8000/api/audit/stats?hours=24') -print(r.json()) -" -``` - -### Tester classification en masse -```bash -curl -X POST http://localhost:3000/api/audit/logs \ - -H "Content-Type: application/json" \ - -d '{ - "action": "BULK_CLASSIFICATION", - "entity_type": "ip", - "entity_count": 50, - "details": {"label": "malicious", "tags": ["scraping"]} - }' -``` - -### Voir logs container -```bash -docker compose logs -f dashboard_web | grep audit -``` - ---- - -## ✅ CONCLUSION - -**Statut global:** 🟡 **BUILD SUCCESS - Tests partiellement passés** - -### Points forts: -- ✅ Build Docker réussi -- ✅ Tous les composants frontend buildés -- ✅ Health check passing -- ✅ ClickHouse connecté -- ✅ Routes API enregistrées (logs 200 OK) -- ✅ Schema audit_logs créé - -### Points d'attention: -- ⚠️ Proxy Docker peut interférer avec tests API directs -- ⚠️ Tests à effectuer depuis l'intérieur du container - -### Recommandations: -1. ✅ Déployer la table `audit_logs` dans ClickHouse -2. ✅ Tester les endpoints depuis le container -3. ✅ Utiliser Swagger UI (`/docs`) pour tests API -4. ⏭️ Phase 3 fonctionnelle, prête pour production - ---- - -## 📝 COMMITS - -``` -18dccda feat(phase3): Classification en masse, Export STIX, Audit Logs -b81d31f test: Rapport de tests Phase 2 + correction SQL -dc029c5 feat(phase2): Graph de corrélations, Timeline interactive, Threat Intel -3b700e8 feat: Optimisations SOC - Phase 1 -a61828d Initial commit: Bot Detector Dashboard -``` - ---- - -## 🚀 ACCÈS AU DASHBOARD - -``` -http://localhost:3000/incidents ← Vue SOC optimisée -http://localhost:3000/threat-intel ← Threat Intelligence -http://localhost:3000/docs ← Documentation API (Swagger) -http://localhost:8000/docs ← API directe (recommandé pour tests) -``` - ---- - -**Rapport généré automatiquement** -**Prochain test:** Déploiement table audit_logs + tests manuels diff --git a/VERIFICATION_COMPLETE.md b/VERIFICATION_COMPLETE.md deleted file mode 100644 index 6e3caef..0000000 --- a/VERIFICATION_COMPLETE.md +++ /dev/null @@ -1,247 +0,0 @@ -# 🧪 Rapport de Vérifications Complètes - -**Date:** 2026-03-14 -**Version:** 1.5.0 (Graph + IPv4 Fix) -**Statut:** ✅ **TOUS LES TESTS PASSÉS** - ---- - -## 📊 RÉSULTATS DES TESTS - -| Test | Résultat | Détails | -|------|----------|---------| -| **Health Check** | ✅ PASSÉ | healthy, ClickHouse connected | -| **API Metrics** | ✅ PASSÉ | 36,664 détections | -| **API Incidents** | ✅ PASSÉ | 3 clusters retournés | -| **Container Status** | ✅ UP | health: starting | -| **Frontend HTML** | ✅ PASSÉ | Title présent | -| **Composants** | ✅ 13 fichiers | Tous créés | -| **Git Commits** | ✅ 3 commits | Historique propre | - ---- - -## ✅ TESTS DÉTAILLÉS - -### 1. Health Check -```bash -curl http://localhost:3000/health -``` -**Résultat:** -```json -{ - "status": "healthy", - "clickhouse": "connected" -} -``` -✅ **VALIDÉ** - ---- - -### 2. API Metrics -```bash -curl http://localhost:3000/api/metrics -``` -**Résultat:** `36,664 détections` -✅ **VALIDÉ** - ---- - -### 3. API Incidents Clusters -```bash -curl "http://localhost:3000/api/incidents/clusters?limit=3" -``` -**Résultat:** `3 clusters` -✅ **VALIDÉ** - ---- - -### 4. Container Status -``` -NAME STATUS PORTS -dashboard_web Up 15 seconds (health: starting) 0.0.0.0:3000->8000/tcp -``` -✅ **VALIDÉ** - ---- - -### 5. Frontend HTML -```bash -curl http://localhost:3000 -``` -**Résultat:** `Bot Detector Dashboard` -✅ **VALIDÉ** - ---- - -### 6. Composants Frontend -**13 composants trouvés:** -- ✅ IncidentsView.tsx (9KB) -- ✅ CorrelationGraph.tsx (9KB) -- ✅ BulkClassification.tsx (9KB) -- ✅ QuickSearch.tsx -- ✅ InvestigationPanel.tsx -- ✅ InteractiveTimeline.tsx -- ✅ ThreatIntelView.tsx -- ✅ + 6 autres - -✅ **VALIDÉ** - ---- - -### 7. Git History -``` -f6d4027 feat: Graph de corrélations complet + Fix IPv4 -6c72f02 test: Rapport de tests - Dashboard Refondu -571bff4 refactor: Dashboard SOC - Refonte totale sans conneries -``` -✅ **VALIDÉ** - ---- - -## 🔧 FONCTIONNALITÉS TESTÉES - -### Dashboard Principal (/) -- [x] Affichage des incidents clusterisés -- [x] Metrics CRITICAL/HIGH/MEDIUM/TOTAL -- [x] Checkboxes de sélection -- [x] Boutons d'action (Investiguer, Classifier, Export) -- [x] Top Menaces Actives (tableau) -- [x] QuickSearch (Cmd+K) - -### Graph de Corrélations -- [x] IP Source (centre) -- [x] Subnet /24 -- [x] ASN -- [x] JA4 (jusqu'à 8) -- [x] User-Agent (jusqu'à 6) -- [x] Host (jusqu'à 6) -- [x] Pays -- [x] Path URL (jusqu'à 4) -- [x] Query Params (jusqu'à 4) -- [x] Filtres par type -- [x] Légende -- [x] Zoom/Pan/Scroll -- [x] Fix IPv4 (::ffff: supprimé) - -### API Endpoints -- [x] GET /api/metrics -- [x] GET /api/incidents/clusters -- [x] GET /api/audit/stats (table non créée, retourne warning) -- [x] GET /health -- [x] GET / (frontend) - ---- - -## ⚠️ POINTS D'ATTENTION - -### Audit Logs API -```bash -curl "http://localhost:3000/api/audit/stats?hours=24" -# Retourne: {"detail": "Erreur: Table doesn't exist"} -``` - -**Cause:** Table `mabase_prod.audit_logs` non créée dans ClickHouse - -**Solution:** -```bash -clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ - --user admin --password SuperPassword123! \ - < deploy_audit_logs_table.sql -``` - ---- - -## 📈 PERFORMANCES - -| Métrique | Valeur | -|----------|--------| -| **Build size** | ~500 KB gzippé | -| **Health check** | < 50ms | -| **API Metrics** | < 200ms | -| **API Incidents** | < 500ms | -| **Container** | Up (healthy) | -| **Composants** | 13 fichiers | -| **Lignes de code** | ~3000+ | - ---- - -## 🎯 CHECKLIST FINALE - -### Backend -- [x] API fonctionnelle -- [x] ClickHouse connecté -- [x] Routes enregistrées -- [x] Health check OK -- [ ] Audit logs table (à déployer) - -### Frontend -- [x] Build réussi -- [x] Dashboard affiché -- [x] Incidents clusterisés -- [x] Graph de corrélations -- [x] QuickSearch -- [x] Navigation simplifiée - -### UI/UX -- [x] Zéro icône inutile -- [x] Code couleur cohérent -- [x] Actions directes -- [x] Sélection multiple -- [x] Filtres graph - -### DevOps -- [x] Docker build OK -- [x] Container healthy -- [x] Logs propres -- [x] Git commits propres - ---- - -## 🚀 COMMANDES DE VÉRIFICATION - -### Test rapide -```bash -# Health check -curl http://localhost:3000/health - -# API Metrics -curl http://localhost:3000/api/metrics | jq '.summary' - -# API Incidents -curl http://localhost:3000/api/incidents/clusters | jq '.items | length' - -# Frontend -curl http://localhost:3000 | grep title -``` - -### Logs en temps réel -```bash -docker compose logs -f dashboard_web -``` - -### Redémarrer -```bash -docker compose restart dashboard_web -``` - ---- - -## ✅ CONCLUSION - -**Statut global:** 🟢 **TOUS LES TESTS PASSÉS** - -### Points forts: -- ✅ Dashboard 100% fonctionnel -- ✅ API opérationnelle -- ✅ Graph de corrélations complet -- ✅ Fix IPv4 appliqué -- ✅ 13 composants frontend -- ✅ Build Docker réussi -- ✅ Git propre - -### À faire: -- ⚠️ Déployer table audit_logs dans ClickHouse - ---- - -**Dashboard entièrement testé et validé !** 🛡️ diff --git a/backend/main.py b/backend/main.py index 4c6bb69..c5ed9ee 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,7 +12,7 @@ import os from .config import settings from .database import db -from .routes import metrics, detections, variability, attributes, analysis, entities, incidents, audit, reputation +from .routes import metrics, detections, variability, attributes, analysis, entities, incidents, audit, reputation, fingerprints # Configuration logging logging.basicConfig( @@ -73,6 +73,7 @@ app.include_router(entities.router) app.include_router(incidents.router) app.include_router(audit.router) app.include_router(reputation.router) +app.include_router(fingerprints.router) # Route pour servir le frontend diff --git a/backend/routes/analysis.py b/backend/routes/analysis.py index 25211a2..c92c8b0 100644 --- a/backend/routes/analysis.py +++ b/backend/routes/analysis.py @@ -318,7 +318,7 @@ async def analyze_ja4(ip: str): from collections import defaultdict subnet_counts = defaultdict(int) for row in subnets_result.result_rows: - ip_addr = row[0] + ip_addr = str(row[0]) parts = ip_addr.split('.') if len(parts) == 4: subnet = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24" diff --git a/backend/routes/entities.py b/backend/routes/entities.py index 2b717dd..865bed9 100644 --- a/backend/routes/entities.py +++ b/backend/routes/entities.py @@ -45,7 +45,7 @@ def get_entity_stats(entity_type: str, entity_value: str, hours: int = 24) -> Op FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s - AND log_date >= now() - INTERVAL %(hours)s HOUR + AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) GROUP BY entity_type, entity_value """ @@ -76,11 +76,11 @@ def get_related_attributes(entity_type: str, entity_value: str, hours: int = 24) # Requête pour agréger tous les attributs associés query = """ SELECT - (SELECT groupUniqArray(toString(src_ip)) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR) as ips, - (SELECT groupUniqArray(ja4) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND ja4 != '') as ja4s, - (SELECT groupUniqArray(host) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND host != '') as hosts, - (SELECT groupUniqArrayArray(asns) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND notEmpty(asns)) as asns, - (SELECT groupUniqArrayArray(countries) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND notEmpty(countries)) as countries + (SELECT groupUniqArray(toString(src_ip)) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR)) as ips, + (SELECT groupUniqArray(ja4) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND ja4 != '') as ja4s, + (SELECT groupUniqArray(host) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND host != '') as hosts, + (SELECT groupUniqArrayArray(asns) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND notEmpty(asns)) as asns, + (SELECT groupUniqArrayArray(countries) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND notEmpty(countries)) as countries """ result = db.connect().query(query, { @@ -123,7 +123,7 @@ def get_array_values(entity_type: str, entity_value: str, array_field: str, hour FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s - AND log_date >= now() - INTERVAL %(hours)s HOUR + AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND notEmpty({array_field}) ) GROUP BY value @@ -193,7 +193,7 @@ async def get_subnet_investigation( arrayJoin(user_agents) AS user_agent FROM view_dashboard_entities WHERE entity_type = 'ip' - AND log_date >= now() - INTERVAL %(hours)s HOUR + AND log_date >= toDate(now() - INTERVAL %(hours)s HOUR) AND splitByChar('.', entity_value)[1] = %(subnet_prefix)s AND splitByChar('.', entity_value)[2] = %(subnet_mask)s AND splitByChar('.', entity_value)[3] = %(subnet_third)s diff --git a/backend/routes/fingerprints.py b/backend/routes/fingerprints.py new file mode 100644 index 0000000..96ea8c1 --- /dev/null +++ b/backend/routes/fingerprints.py @@ -0,0 +1,737 @@ +""" +Endpoints pour l'analyse des fingerprints JA4 et User-Agents + +Objectifs: + - Détecter le spoofing JA4 (fingerprint TLS qui prétend être un navigateur mais + dont les User-Agents, les headers HTTP ou les métriques comportementales trahissent + une origine bot/script) + - Construire une matrice JA4 × User-Agent pour visualiser les associations suspectes + - Analyser la distribution des User-Agents pour identifier les rotateurs et les bots + qui usurpent des UA de navigateurs légitimes +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional +import re + +from ..database import db + +router = APIRouter(prefix="/api/fingerprints", tags=["fingerprints"]) + + +# ─── Helpers ────────────────────────────────────────────────────────────────── + +# Patterns indiquant clairement un bot/script sans simulation de navigateur +_BOT_PATTERNS = re.compile( + r"bot|crawler|spider|scraper|python|curl|wget|go-http|java/|axios|" + r"libwww|httpclient|okhttp|requests|aiohttp|httpx|playwright|puppeteer|" + r"selenium|headless|phantomjs", + re.IGNORECASE, +) + +# Navigateurs légitimes communs — un JA4 de type "browser" devrait venir avec ces UAs +_BROWSER_PATTERNS = re.compile( + r"mozilla|chrome|safari|firefox|edge|opera|trident", + re.IGNORECASE, +) + + +def _classify_ua(ua: str) -> str: + """Retourne 'bot', 'browser', ou 'script'""" + if not ua: + return "empty" + if _BOT_PATTERNS.search(ua): + return "bot" + if _BROWSER_PATTERNS.search(ua): + return "browser" + return "script" + + +# ============================================================================= +# ENDPOINT 1 — Détection de spoofing JA4 +# ============================================================================= + +@router.get("/spoofing") +async def get_ja4_spoofing( + hours: int = Query(24, ge=1, le=168, description="Fenêtre temporelle"), + min_detections: int = Query(10, ge=1, description="Nombre minimum de détections"), + limit: int = Query(50, ge=1, le=200), +): + """ + Identifie les JA4 fingerprints suspects de spoofing navigateur. + + Un JA4 est considéré suspect quand: + - Il présente un taux élevé de ua_ch_mismatch (header UA ≠ Client Hints) + - Son modern_browser_score est élevé mais les UAs associés sont des bots/scripts + - Il apparaît avec un taux élevé de sni_host_mismatch ou alpn_http_mismatch + - is_rare_ja4 = true avec un volume important + + Retourne un score de confiance de spoofing [0-100] pour chaque JA4. + """ + try: + # Agrégation par JA4 avec tous les indicateurs de spoofing + query = """ + SELECT + ja4, + count() AS total_detections, + uniq(src_ip) AS unique_ips, + + -- Indicateurs de mismatch + countIf(ua_ch_mismatch = true) AS ua_ch_mismatch_count, + round(countIf(ua_ch_mismatch = true) * 100.0 / count(), 2) AS ua_ch_mismatch_pct, + countIf(sni_host_mismatch = true) AS sni_mismatch_count, + round(countIf(sni_host_mismatch = true) * 100.0 / count(), 2) AS sni_mismatch_pct, + countIf(alpn_http_mismatch = true) AS alpn_mismatch_count, + round(countIf(alpn_http_mismatch = true) * 100.0 / count(), 2) AS alpn_mismatch_pct, + + -- Indicateurs comportementaux + avg(modern_browser_score) AS avg_browser_score, + countIf(is_rare_ja4 = true) AS rare_ja4_count, + round(countIf(is_rare_ja4 = true) * 100.0 / count(), 2) AS rare_ja4_pct, + countIf(is_ua_rotating = true) AS ua_rotating_count, + round(countIf(is_ua_rotating = true) * 100.0 / count(), 2) AS ua_rotating_pct, + + -- Métriques TLS/TCP + countIf(is_alpn_missing = true) AS alpn_missing_count, + avg(distinct_ja4_count) AS avg_distinct_ja4_per_ip, + + -- Répartition threat levels + countIf(threat_level = 'CRITICAL') AS critical_count, + countIf(threat_level = 'HIGH') AS high_count, + + -- Botnet indicators + avg(ja4_asn_concentration) AS avg_asn_concentration, + avg(ja4_country_concentration) AS avg_country_concentration, + + argMax(threat_level, detected_at) AS last_threat_level + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + AND ja4 != '' AND ja4 IS NOT NULL + GROUP BY ja4 + HAVING total_detections >= %(min_detections)s + ORDER BY ua_ch_mismatch_pct DESC, total_detections DESC + LIMIT %(limit)s + """ + + result = db.query(query, { + "hours": hours, + "min_detections": min_detections, + "limit": limit, + }) + + # Fetch top UA per JA4 from view_dashboard_user_agents + ja4_list = [str(r[0]) for r in result.result_rows if r[0]] + ua_by_ja4: dict = {} + if ja4_list: + ja4_sql = ", ".join(f"'{j}'" for j in ja4_list[:100]) + ua_q = f""" + SELECT ja4, groupArray(5)(ua) AS top_uas + FROM ( + SELECT ja4, arrayJoin(user_agents) AS ua, sum(requests) AS cnt + FROM view_dashboard_user_agents + WHERE ja4 IN ({ja4_sql}) + AND hour >= now() - INTERVAL {hours} HOUR + AND ua != '' + GROUP BY ja4, ua + ORDER BY ja4, cnt DESC + ) + GROUP BY ja4 + """ + try: + ua_res = db.query(ua_q) + for ua_row in ua_res.result_rows: + j4 = str(ua_row[0]) + if ua_row[1]: + ua_by_ja4[j4] = list(ua_row[1]) + except Exception: + pass + + items = [] + for row in result.result_rows: + ja4 = str(row[0]) + ua_ch_mismatch_pct = float(row[4] or 0) + sni_mismatch_pct = float(row[6] or 0) + alpn_mismatch_pct = float(row[8] or 0) + avg_browser_score = float(row[9] or 0) + rare_ja4_pct = float(row[11] or 0) + ua_rotating_pct = float(row[13] or 0) + alpn_missing_count = int(row[14] or 0) + total = int(row[1] or 1) + + top_uas = ua_by_ja4.get(ja4, []) + ua_classes = [_classify_ua(u) for u in top_uas] + has_bot_ua = any(c == "bot" for c in ua_classes) + has_browser_ua = any(c == "browser" for c in ua_classes) + + # Spoofing confidence score [0-100]: + # UA/CH mismatch est le signal le plus fort (poids 40) + # Browser UA avec score navigateur élevé mais indicateurs bot (poids 25) + # SNI/ALPN mismatches (poids 15) + # is_rare_ja4 avec gros volume (poids 10) + # UA rotating (poids 10) + spoof_score = min(100, round( + ua_ch_mismatch_pct * 0.40 + + (avg_browser_score * 25 / 100 if has_bot_ua else 0) + + sni_mismatch_pct * 0.10 + + alpn_mismatch_pct * 0.05 + + rare_ja4_pct * 0.10 + + ua_rotating_pct * 0.10 + + (10 if alpn_missing_count > total * 0.3 else 0) + )) + + # Classification du JA4 + if spoof_score >= 60: + classification = "spoofed_browser" + elif has_bot_ua and avg_browser_score < 30: + classification = "known_bot" + elif has_browser_ua and ua_ch_mismatch_pct < 10: + classification = "legitimate_browser" + else: + classification = "suspicious" + + items.append({ + "ja4": ja4, + "classification": classification, + "spoofing_score": spoof_score, + "total_detections": int(row[1] or 0), + "unique_ips": int(row[2] or 0), + "indicators": { + "ua_ch_mismatch_pct": ua_ch_mismatch_pct, + "sni_mismatch_pct": sni_mismatch_pct, + "alpn_mismatch_pct": alpn_mismatch_pct, + "avg_browser_score": round(avg_browser_score, 1), + "rare_ja4_pct": rare_ja4_pct, + "ua_rotating_pct": ua_rotating_pct, + "alpn_missing_count": alpn_missing_count, + "avg_asn_concentration": round(float(row[18] or 0), 3), + "avg_country_concentration": round(float(row[19] or 0), 3), + }, + "top_user_agents": [ + {"ua": u, "type": _classify_ua(u)} for u in top_uas + ], + "threat_breakdown": { + "critical": int(row[16] or 0), + "high": int(row[17] or 0), + "last_level": str(row[20] or "LOW"), + }, + }) + + # Trier: spoofed_browser d'abord, puis par score + items.sort(key=lambda x: (-x["spoofing_score"], -x["total_detections"])) + + return { + "items": items, + "total": len(items), + "period_hours": hours, + "summary": { + "spoofed_browser": sum(1 for i in items if i["classification"] == "spoofed_browser"), + "known_bot": sum(1 for i in items if i["classification"] == "known_bot"), + "suspicious": sum(1 for i in items if i["classification"] == "suspicious"), + "legitimate_browser": sum(1 for i in items if i["classification"] == "legitimate_browser"), + }, + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ENDPOINT 2 — Matrice JA4 × User-Agent +# ============================================================================= + +@router.get("/ja4-ua-matrix") +async def get_ja4_ua_matrix( + hours: int = Query(24, ge=1, le=168), + min_ips: int = Query(3, ge=1, description="Nombre minimum d'IPs pour inclure un JA4"), + limit: int = Query(30, ge=1, le=100), +): + """ + Matrice JA4 × User-Agent. + + Pour chaque JA4: + - Top User-Agents associés (depuis view_dashboard_entities) + - Taux de ua_ch_mismatch + - Classification UA (bot / browser / script) + - Indicateur de spoofing si browser_score élevé + UA non-navigateur + """ + try: + # Stats JA4 depuis ml_detected_anomalies + stats_query = """ + SELECT + ja4, + uniq(src_ip) AS unique_ips, + count() AS total_detections, + round(countIf(ua_ch_mismatch = true) * 100.0 / count(), 2) AS ua_ch_mismatch_pct, + avg(modern_browser_score) AS avg_browser_score, + countIf(is_rare_ja4 = true) AS rare_count, + countIf(is_ua_rotating = true) AS rotating_count, + argMax(threat_level, detected_at) AS last_threat + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + AND ja4 != '' AND ja4 IS NOT NULL + GROUP BY ja4 + HAVING unique_ips >= %(min_ips)s + ORDER BY ua_ch_mismatch_pct DESC, unique_ips DESC + LIMIT %(limit)s + """ + + stats_res = db.query(stats_query, {"hours": hours, "min_ips": min_ips, "limit": limit}) + ja4_list = [str(r[0]) for r in stats_res.result_rows] + + if not ja4_list: + return {"items": [], "total": 0, "period_hours": hours} + + # UAs par JA4 depuis view_dashboard_user_agents + ja4_sql = ", ".join(f"'{j}'" for j in ja4_list) + ua_query = f""" + SELECT + ja4, + ua, + sum(requests) AS cnt + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE ja4 IN ({ja4_sql}) + AND hour >= now() - INTERVAL {hours} HOUR + AND ua != '' + GROUP BY ja4, ua + ORDER BY ja4, cnt DESC + """ + + ua_by_ja4: dict = {} + try: + ua_res = db.query(ua_query) + for row in ua_res.result_rows: + j4 = str(row[0]) + if j4 not in ua_by_ja4: + ua_by_ja4[j4] = [] + if len(ua_by_ja4[j4]) < 8: + ua_by_ja4[j4].append({"ua": str(row[1]), "count": int(row[2] or 0)}) + except Exception: + pass + + items = [] + for row in stats_res.result_rows: + ja4 = str(row[0]) + unique_ips = int(row[1] or 0) + ua_ch_mismatch_pct = float(row[3] or 0) + avg_browser_score = float(row[4] or 0) + + top_uas = ua_by_ja4.get(ja4, []) + ua_total = sum(u["count"] for u in top_uas) or 1 + + classified_uas = [] + for u in top_uas: + ua_type = _classify_ua(u["ua"]) + classified_uas.append({ + "ua": u["ua"], + "count": u["count"], + "pct": round(u["count"] * 100 / ua_total, 1), + "type": ua_type, + }) + + bot_pct = sum(u["pct"] for u in classified_uas if u["type"] == "bot") + browser_pct = sum(u["pct"] for u in classified_uas if u["type"] == "browser") + + # Spoofing flag: JA4 ressemble à un navigateur (browser_score élevé) + # mais les UAs sont des bots/scripts + is_spoofing = avg_browser_score > 50 and bot_pct > 30 and ua_ch_mismatch_pct > 20 + + items.append({ + "ja4": ja4, + "unique_ips": unique_ips, + "total_detections": int(row[2] or 0), + "ua_ch_mismatch_pct": ua_ch_mismatch_pct, + "avg_browser_score": round(avg_browser_score, 1), + "rare_count": int(row[5] or 0), + "rotating_count": int(row[6] or 0), + "last_threat": str(row[7] or "LOW"), + "user_agents": classified_uas, + "ua_summary": { + "bot_pct": round(bot_pct, 1), + "browser_pct": round(browser_pct, 1), + "script_pct": round(100 - bot_pct - browser_pct, 1), + "total_distinct": len(top_uas), + }, + "is_spoofing_suspect": is_spoofing, + }) + + return { + "items": items, + "total": len(items), + "period_hours": hours, + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ENDPOINT 3 — Analyse globale des User-Agents +# ============================================================================= + +@router.get("/ua-analysis") +async def get_ua_analysis( + hours: int = Query(24, ge=1, le=168), + limit: int = Query(50, ge=1, le=200), +): + """ + Analyse globale des User-Agents dans les détections. + + Identifie: + - UAs de type bot/script + - UAs browser légitimes vs UAs browser utilisés par des bots (via ua_ch_mismatch) + - UAs rares/suspects qui tournent (is_ua_rotating) + - Distribution JA4 par UA pour détecter les UAs multi-fingerprints (rotation) + """ + try: + # Top UAs globaux depuis view_dashboard_user_agents + ua_global_query = """ + SELECT + ua, + sum(requests) AS ip_count + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE hour >= now() - INTERVAL %(hours)s HOUR + AND ua != '' + GROUP BY ua + ORDER BY ip_count DESC + LIMIT %(limit)s + """ + + ua_global_res = db.query(ua_global_query, {"hours": hours, "limit": limit}) + top_uas = [str(r[0]) for r in ua_global_res.result_rows] + + # Pour chaque UA, chercher ses JA4 via view_dashboard_user_agents + ua_sql = ", ".join(f"'{u.replace(chr(39), chr(39)*2)}'" for u in top_uas[:50]) if top_uas else "''" + ja4_per_ua_query = f""" + SELECT + ua, + uniq(ja4) AS unique_ja4s, + groupUniqArray(3)(ja4) AS sample_ja4s + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE ua IN ({ua_sql}) + AND hour >= now() - INTERVAL {hours} HOUR + AND ua != '' + AND ja4 != '' + GROUP BY ua + """ + ja4_by_ua: dict = {} + try: + ja4_res = db.query(ja4_per_ua_query) + for r in ja4_res.result_rows: + ja4_by_ua[str(r[0])] = { + "unique_ja4s": int(r[1] or 0), + "sample_ja4s": list(r[2] or []), + } + except Exception: + pass + + # IPs avec is_ua_rotating depuis ml_detected_anomalies + rotating_query = """ + SELECT + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip, + avg(ua_ch_mismatch) AS avg_ua_ch_mismatch + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + AND is_ua_rotating = true + GROUP BY clean_ip + ORDER BY avg_ua_ch_mismatch DESC + LIMIT 100 + """ + rotating_ips: list = [] + try: + rot_res = db.query(rotating_query, {"hours": hours}) + rotating_ips = [str(r[0]) for r in rot_res.result_rows] + except Exception: + pass + + # Construire la réponse + items = [] + for row in ua_global_res.result_rows: + ua = str(row[0]) + ip_count = int(row[1] or 0) + ua_type = _classify_ua(ua) + ja4_info = ja4_by_ua.get(ua, {"unique_ja4s": 0, "sample_ja4s": []}) + + # UA multi-JA4 est suspect: un vrai navigateur a généralement 1-2 JA4 + multi_ja4_flag = ja4_info["unique_ja4s"] > 3 + + items.append({ + "user_agent": ua, + "type": ua_type, + "ip_count": ip_count, + "unique_ja4_count": ja4_info["unique_ja4s"], + "sample_ja4s": ja4_info["sample_ja4s"], + "is_multi_ja4_suspect": multi_ja4_flag, + "risk_flags": _build_ua_risk_flags(ua, ua_type, ja4_info["unique_ja4s"], ip_count), + }) + + # IPs avec rotation d'UA + ua_rotating_stats = { + "rotating_ip_count": len(rotating_ips), + "sample_rotating_ips": rotating_ips[:10], + } + + return { + "items": items, + "total": len(items), + "period_hours": hours, + "ua_rotating_stats": ua_rotating_stats, + "summary": { + "bot_count": sum(1 for i in items if i["type"] == "bot"), + "browser_count": sum(1 for i in items if i["type"] == "browser"), + "script_count": sum(1 for i in items if i["type"] == "script"), + "multi_ja4_suspect_count": sum(1 for i in items if i["is_multi_ja4_suspect"]), + }, + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +def _build_ua_risk_flags(ua: str, ua_type: str, unique_ja4s: int, ip_count: int) -> list: + flags = [] + if ua_type == "bot": + flags.append("ua_bot_signature") + elif ua_type == "script": + flags.append("ua_script_library") + if unique_ja4s > 5: + flags.append("ja4_rotation_suspect") + if unique_ja4s > 3 and ua_type == "browser": + flags.append("browser_ua_multi_fingerprint") + if ip_count > 100: + flags.append("high_volume") + return flags + + +# ============================================================================= +# ENDPOINT 4 — JA4 d'un IP spécifique: analyse de cohérence UA/JA4 +# ============================================================================= + +@router.get("/ip/{ip}/coherence") +async def get_ip_fingerprint_coherence(ip: str): + """ + Analyse la cohérence JA4/UA pour une IP spécifique. + + Répond à la question: "Cette IP spoofait-elle son fingerprint?" + + Calcule un score de cohérence basé sur: + - Correspondance entre JA4 (TLS client fingerprint) et User-Agent + - ua_ch_mismatch (User-Agent vs Client Hints) + - modern_browser_score vs type d'UA réel + - Nombre de JA4 distincts utilisés (rotation) + - sni_host_mismatch, alpn_http_mismatch + """ + try: + # Données depuis ml_detected_anomalies + ml_query = """ + SELECT + ja4, + ua_ch_mismatch, + modern_browser_score, + sni_host_mismatch, + alpn_http_mismatch, + is_alpn_missing, + is_rare_ja4, + is_ua_rotating, + distinct_ja4_count, + header_count, + has_accept_language, + has_cookie, + has_referer, + header_order_shared_count, + detected_at, + threat_level, + window_mss_ratio, + tcp_jitter_variance, + multiplexing_efficiency + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + ORDER BY detected_at DESC + LIMIT 20 + """ + ml_res = db.query(ml_query, {"ip": ip}) + + if not ml_res.result_rows: + raise HTTPException(status_code=404, detail="IP non trouvée dans les détections") + + # User-agents réels depuis view_dashboard_user_agents + ua_query = """ + SELECT ua, sum(requests) AS cnt + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE toString(src_ip) = %(ip)s + AND hour >= now() - INTERVAL 72 HOUR + AND ua != '' + GROUP BY ua ORDER BY cnt DESC LIMIT 10 + """ + ua_res = db.query(ua_query, {"ip": ip}) + top_uas = [{"ua": str(r[0]), "count": int(r[1] or 0), "type": _classify_ua(str(r[0]))} + for r in ua_res.result_rows] + + # Agréger les indicateurs de la dernière session + rows = ml_res.result_rows + latest = rows[0] + total_rows = len(rows) + + ua_ch_mismatch_count = sum(1 for r in rows if r[1]) + sni_mismatch_count = sum(1 for r in rows if r[3]) + alpn_mismatch_count = sum(1 for r in rows if r[4]) + is_rare_count = sum(1 for r in rows if r[6]) + is_rotating = any(r[7] for r in rows) + distinct_ja4s = {str(r[0]) for r in rows if r[0]} + avg_browser_score = sum(int(r[2] or 0) for r in rows) / total_rows + + # UA analysis + has_browser_ua = any(u["type"] == "browser" for u in top_uas) + has_bot_ua = any(u["type"] == "bot" for u in top_uas) + primary_ua_type = top_uas[0]["type"] if top_uas else "empty" + + # Calcul du score de spoofing + spoof_score = min(100, round( + (ua_ch_mismatch_count / total_rows * 100) * 0.40 + + (avg_browser_score * 0.20 if has_bot_ua else 0) + + (sni_mismatch_count / total_rows * 100) * 0.10 + + (alpn_mismatch_count / total_rows * 100) * 0.05 + + (len(distinct_ja4s) * 5 if len(distinct_ja4s) > 2 else 0) + + (15 if is_rotating else 0) + + (10 if is_rare_count > total_rows * 0.5 else 0) + )) + + # Verdict + if spoof_score >= 70: + verdict = "high_confidence_spoofing" + elif spoof_score >= 40: + verdict = "suspicious_spoofing" + elif has_bot_ua and avg_browser_score < 20: + verdict = "known_bot_no_spoofing" + elif has_browser_ua and spoof_score < 20: + verdict = "legitimate_browser" + else: + verdict = "inconclusive" + + # Explication humaine + explanation = [] + if ua_ch_mismatch_count > total_rows * 0.3: + explanation.append(f"UA-Client-Hints mismatch sur {round(ua_ch_mismatch_count*100/total_rows)}% des requêtes") + if has_bot_ua and avg_browser_score > 40: + explanation.append(f"JA4 ressemble à un navigateur (score {round(avg_browser_score)}/100) mais UA est de type bot") + if len(distinct_ja4s) > 2: + explanation.append(f"{len(distinct_ja4s)} JA4 distincts utilisés → rotation de fingerprint") + if is_rotating: + explanation.append("is_ua_rotating détecté → rotation d'User-Agent confirmée") + if sni_mismatch_count > 0: + explanation.append(f"SNI ≠ Host header sur {sni_mismatch_count}/{total_rows} requêtes") + if not explanation: + explanation.append("Aucun indicateur de spoofing majeur détecté") + + return { + "ip": ip, + "verdict": verdict, + "spoofing_score": spoof_score, + "explanation": explanation, + "indicators": { + "ua_ch_mismatch_rate": round(ua_ch_mismatch_count / total_rows * 100, 1), + "sni_mismatch_rate": round(sni_mismatch_count / total_rows * 100, 1), + "alpn_mismatch_rate": round(alpn_mismatch_count / total_rows * 100, 1), + "avg_browser_score": round(avg_browser_score, 1), + "distinct_ja4_count": len(distinct_ja4s), + "is_ua_rotating": is_rotating, + "rare_ja4_rate": round(is_rare_count / total_rows * 100, 1), + }, + "fingerprints": { + "ja4_list": list(distinct_ja4s), + "latest_ja4": str(latest[0] or ""), + }, + "user_agents": top_uas, + "latest_detection": { + "detected_at": latest[14].isoformat() if latest[14] else "", + "threat_level": str(latest[15] or "LOW"), + "modern_browser_score": int(latest[2] or 0), + "header_count": int(latest[9] or 0), + "has_accept_language": bool(latest[10]), + "has_cookie": bool(latest[11]), + "has_referer": bool(latest[12]), + "header_order_shared_count": int(latest[13] or 0), + }, + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ENDPOINT 5 — JA4 légitimes (baseline / whitelist) +# ============================================================================= + +@router.get("/legitimate-ja4") +async def get_legitimate_ja4( + hours: int = Query(168, ge=24, le=720, description="Fenêtre pour établir la baseline"), + min_ips: int = Query(50, ge=5, description="Nombre minimum d'IPs pour qualifier un JA4 de légitime"), +): + """ + Établit une baseline des JA4 fingerprints légitimes. + + Un JA4 est considéré légitime si: + - Il est utilisé par un grand nombre d'IPs distinctes (> min_ips) + - Son taux de ua_ch_mismatch est faible (< 5%) + - Son modern_browser_score est élevé (> 60) + - Il n'est PAS is_rare_ja4 + - Ses UAs sont dominés par des navigateurs connus + + Utile comme whitelist pour réduire les faux positifs. + """ + try: + query = """ + SELECT + ja4, + uniq(src_ip) AS unique_ips, + count() AS total_detections, + round(countIf(ua_ch_mismatch = true) * 100.0 / count(), 2) AS ua_ch_mismatch_pct, + avg(modern_browser_score) AS avg_browser_score, + countIf(is_rare_ja4 = true) AS rare_count, + round(countIf(threat_level = 'CRITICAL') * 100.0 / count(), 2) AS critical_pct, + round(countIf(threat_level = 'HIGH') * 100.0 / count(), 2) AS high_pct + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + AND ja4 != '' AND ja4 IS NOT NULL + GROUP BY ja4 + HAVING unique_ips >= %(min_ips)s + AND ua_ch_mismatch_pct < 5.0 + AND avg_browser_score > 60 + AND rare_count = 0 + ORDER BY unique_ips DESC + LIMIT 100 + """ + + result = db.query(query, {"hours": hours, "min_ips": min_ips}) + + items = [ + { + "ja4": str(row[0]), + "unique_ips": int(row[1] or 0), + "total_detections": int(row[2] or 0), + "ua_ch_mismatch_pct": float(row[3] or 0), + "avg_browser_score": round(float(row[4] or 0), 1), + "critical_pct": float(row[6] or 0), + "high_pct": float(row[7] or 0), + "legitimacy_confidence": min(100, round( + (1 - float(row[3] or 0) / 100) * 40 + + float(row[4] or 0) * 0.40 + + min(int(row[1] or 0) / min_ips, 1) * 20 + )), + } + for row in result.result_rows + ] + + return { + "items": items, + "total": len(items), + "period_hours": hours, + "note": "Ces JA4 sont candidats à une whitelist. Vérifier manuellement avant de whitelister.", + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/backend/routes/incidents.py b/backend/routes/incidents.py index 2058a21..081adc9 100644 --- a/backend/routes/incidents.py +++ b/backend/routes/incidents.py @@ -81,25 +81,94 @@ async def get_incident_clusters( result = db.query(cluster_query, {"hours": hours, "limit": limit}) + # Collect sample IPs to fetch real UA and trend data in bulk + sample_ips = [row[10] for row in result.result_rows if row[10]] + subnets_list = [row[0] for row in result.result_rows] + + # Fetch real primary UA per sample IP from view_dashboard_entities + ua_by_ip: dict = {} + if sample_ips: + ip_list_sql = ", ".join(f"'{ip}'" for ip in sample_ips[:50]) + ua_query = f""" + SELECT entity_value, arrayElement(user_agents, 1) AS top_ua + FROM view_dashboard_entities + WHERE entity_type = 'ip' + AND entity_value IN ({ip_list_sql}) + AND notEmpty(user_agents) + GROUP BY entity_value, top_ua + ORDER BY entity_value + """ + try: + ua_result = db.query(ua_query) + for ua_row in ua_result.result_rows: + if ua_row[0] not in ua_by_ip and ua_row[1]: + ua_by_ip[str(ua_row[0])] = str(ua_row[1]) + except Exception: + pass # UA enrichment is best-effort + + # Compute real trend: compare current window vs previous window of same duration + trend_query = """ + WITH cleaned AS ( + SELECT + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip, + detected_at, + concat( + splitByChar('.', clean_ip)[1], '.', + splitByChar('.', clean_ip)[2], '.', + splitByChar('.', clean_ip)[3], '.0/24' + ) AS subnet + FROM ml_detected_anomalies + ), + current_window AS ( + SELECT subnet, count() AS cnt + FROM cleaned + WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + GROUP BY subnet + ), + prev_window AS ( + SELECT subnet, count() AS cnt + FROM cleaned + WHERE detected_at >= now() - INTERVAL %(hours2)s HOUR + AND detected_at < now() - INTERVAL %(hours)s HOUR + GROUP BY subnet + ) + SELECT c.subnet, c.cnt AS current_cnt, p.cnt AS prev_cnt + FROM current_window c + LEFT JOIN prev_window p ON c.subnet = p.subnet + """ + trend_by_subnet: dict = {} + try: + trend_result = db.query(trend_query, {"hours": hours, "hours2": hours * 2}) + for tr in trend_result.result_rows: + subnet_key = tr[0] + curr = tr[1] or 0 + prev = tr[2] or 0 + if prev == 0: + trend_by_subnet[subnet_key] = ("new", 100) + else: + pct = round(((curr - prev) / prev) * 100) + trend_by_subnet[subnet_key] = ("up" if pct >= 0 else "down", abs(pct)) + except Exception: + pass + clusters = [] for row in result.result_rows: - # Calcul du score de risque + subnet = row[0] threat_level = row[8] or 'LOW' unique_ips = row[2] or 1 avg_score = abs(row[9] or 0) - - # Score based on threat level and other factors + sample_ip = row[10] if row[10] else subnet.split('/')[0] + critical_count = 1 if threat_level == 'CRITICAL' else 0 high_count = 1 if threat_level == 'HIGH' else 0 - + risk_score = min(100, round( - (critical_count * 30) + - (high_count * 20) + - (unique_ips * 5) + + (critical_count * 30) + + (high_count * 20) + + (unique_ips * 5) + (avg_score * 100) )) - - # Détermination de la sévérité + if critical_count > 0 or risk_score >= 80: severity = "CRITICAL" elif high_count > (row[1] or 1) * 0.3 or risk_score >= 60: @@ -108,31 +177,27 @@ async def get_incident_clusters( severity = "MEDIUM" else: severity = "LOW" - - # Calcul de la tendance - trend = "up" - trend_percentage = 23 - + + trend_dir, trend_pct = trend_by_subnet.get(subnet, ("stable", 0)) + primary_ua = ua_by_ip.get(sample_ip, "") + clusters.append({ "id": f"INC-{datetime.now().strftime('%Y%m%d')}-{len(clusters)+1:03d}", "score": risk_score, "severity": severity, "total_detections": row[1], "unique_ips": row[2], - "subnet": row[0], - "sample_ip": row[10] if row[10] else row[0].split('/')[0], + "subnet": subnet, + "sample_ip": sample_ip, "ja4": row[5] or "", - "primary_ua": "python-requests", - "primary_target": "Unknown", - "countries": [{ - "code": row[6] or "XX", - "percentage": 100 - }], + "primary_ua": primary_ua, + "primary_target": row[3].strftime('%H:%M') if row[3] else "Unknown", + "countries": [{"code": row[6] or "XX", "percentage": 100}], "asn": str(row[7]) if row[7] else "", "first_seen": row[3].isoformat() if row[3] else "", "last_seen": row[4].isoformat() if row[4] else "", - "trend": trend, - "trend_percentage": trend_percentage + "trend": trend_dir, + "trend_percentage": trend_pct, }) return { diff --git a/backend/routes/variability.py b/backend/routes/variability.py index 3a26443..c51aa9a 100644 --- a/backend/routes/variability.py +++ b/backend/routes/variability.py @@ -103,7 +103,7 @@ async def get_associated_attributes( # Mapping des attributs cibles target_column_map = { - "user_agents": "''", # Pas de user_agent + "user_agents": None, # handled separately via view_dashboard_entities "ja4": "ja4", "countries": "country_code", "asns": "asn_number", @@ -122,9 +122,33 @@ async def get_associated_attributes( column = type_column_map[attr_type] target_column = target_column_map[target_attr] - # Pour user_agent, retourne liste vide - if target_column == "''": - return {"type": attr_type, "value": value, "target": target_attr, "items": [], "total": 0} + # Pour user_agents: requête via view_dashboard_user_agents + # Colonnes: src_ip, ja4, hour, log_date, user_agents, requests + if target_column is None: + if attr_type == "ip": + ua_where = "toString(src_ip) = %(value)s" + elif attr_type == "ja4": + ua_where = "ja4 = %(value)s" + else: + # country/asn/host: pivot via ml_detected_anomalies + ua_where = f"""toString(src_ip) IN ( + SELECT DISTINCT replaceRegexpAll(toString(src_ip), '^::ffff:', '') + FROM ml_detected_anomalies + WHERE {column} = %(value)s AND detected_at >= now() - INTERVAL 24 HOUR + )""" + ua_q = f""" + SELECT ua AS value, sum(requests) AS count, + round(sum(requests) * 100.0 / sum(sum(requests)) OVER (), 2) AS percentage + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE {ua_where} + AND hour >= now() - INTERVAL 24 HOUR AND ua != '' + GROUP BY value ORDER BY count DESC LIMIT %(limit)s + """ + ua_result = db.query(ua_q, {"value": value, "limit": limit}) + items = [{"value": str(r[0]), "count": r[1], "percentage": round(float(r[2]), 2) if r[2] else 0.0} + for r in ua_result.result_rows] + return {"type": attr_type, "value": value, "target": target_attr, "items": items, "total": len(items), "showing": len(items)} query = f""" SELECT @@ -193,8 +217,8 @@ async def get_user_agents( type_column_map = { "ip": "src_ip", "ja4": "ja4", - "country": "src_country_code", - "asn": "src_asn", + "country": "country_code", + "asn": "asn_number", "host": "host", } @@ -206,25 +230,51 @@ async def get_user_agents( column = type_column_map[attr_type] - # Requête sur la vue materialisée - # user_agents est un Array, on utilise arrayJoin pour l'aplatir + # view_dashboard_user_agents colonnes: src_ip, ja4, hour, log_date, user_agents, requests + if attr_type == "ip": + where = "toString(src_ip) = %(value)s" + params: dict = {"value": value, "limit": limit} + elif attr_type == "ja4": + where = "ja4 = %(value)s" + params = {"value": value, "limit": limit} + else: + # country / asn / host: pivot via ml_detected_anomalies → IPs connus → vue par src_ip + ml_col = {"country": "country_code", "asn": "asn_number", "host": "host"}[attr_type] + where = f"""toString(src_ip) IN ( + SELECT DISTINCT replaceRegexpAll(toString(src_ip), '^::ffff:', '') + FROM ml_detected_anomalies + WHERE {ml_col} = %(value)s + AND detected_at >= now() - INTERVAL 24 HOUR + )""" + params = {"value": value, "limit": limit} + query = f""" SELECT ua AS user_agent, sum(requests) AS count, - round(count * 100.0 / sum(count) OVER (), 2) AS percentage, - min(hour) AS first_seen, - max(hour) AS last_seen - FROM mabase_prod.view_dashboard_user_agents + round(sum(requests) * 100.0 / sum(sum(requests)) OVER (), 2) AS percentage, + min(log_date) AS first_seen, + max(log_date) AS last_seen + FROM view_dashboard_user_agents ARRAY JOIN user_agents AS ua - WHERE {column} = %(value)s + WHERE {where} AND hour >= now() - INTERVAL 24 HOUR + AND ua != '' GROUP BY user_agent ORDER BY count DESC LIMIT %(limit)s """ + result = db.query(query, params) - result = db.query(query, {"value": value, "limit": limit}) + count_query = f""" + SELECT uniqExact(ua) AS total + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE {where} + AND hour >= now() - INTERVAL 24 HOUR + AND ua != '' + """ + count_result = db.query(count_query, params) user_agents = [ UserAgentValue( @@ -237,16 +287,6 @@ async def get_user_agents( for row in result.result_rows ] - # Compter le total - count_query = f""" - SELECT uniq(ua) AS total - FROM mabase_prod.view_dashboard_user_agents - ARRAY JOIN user_agents AS ua - WHERE {column} = %(value)s - AND hour >= now() - INTERVAL 24 HOUR - """ - - count_result = db.query(count_query, {"value": value}) total = count_result.result_rows[0][0] if count_result.result_rows else 0 return { @@ -451,38 +491,41 @@ async def get_variability(attr_type: str, value: str): first_seen = stats_row[2] last_seen = stats_row[3] - # User-Agents - ua_query = f""" - SELECT - user_agent, - count() AS count, - round(count() * 100.0 / sum(count()) OVER (), 2) AS percentage, - min(detected_at) AS first_seen, - max(detected_at) AS last_seen, - groupArray((threat_level, 1)) AS threats - FROM ({base_query}) - WHERE user_agent != '' AND user_agent IS NOT NULL - GROUP BY user_agent - ORDER BY count DESC - LIMIT 10 - """ - - # Simplified query without complex threat parsing + # User-Agents via view_dashboard_user_agents (source principale pour les UAs) + # Colonnes disponibles: src_ip, ja4, hour, log_date, user_agents, requests + if attr_type == "ip": + _ua_where = "toString(src_ip) = %(value)s" + _ua_params: dict = {"value": value} + elif attr_type == "ja4": + _ua_where = "ja4 = %(value)s" + _ua_params = {"value": value} + else: + # country / asn / host: pivot via ml_detected_anomalies → IPs + _ua_where = f"""toString(src_ip) IN ( + SELECT DISTINCT replaceRegexpAll(toString(src_ip), '^::ffff:', '') + FROM ml_detected_anomalies + WHERE {column} = %(value)s AND detected_at >= now() - INTERVAL 24 HOUR + )""" + _ua_params = {"value": value} + ua_query_simple = f""" SELECT - user_agent, - count() AS count, - round(count() * 100.0 / (SELECT count() FROM ({base_query}) WHERE user_agent != '' AND user_agent IS NOT NULL), 2) AS percentage, - min(detected_at) AS first_seen, - max(detected_at) AS last_seen - FROM ({base_query}) - WHERE user_agent != '' AND user_agent IS NOT NULL + ua AS user_agent, + sum(requests) AS count, + round(sum(requests) * 100.0 / sum(sum(requests)) OVER (), 2) AS percentage, + min(log_date) AS first_seen, + max(log_date) AS last_seen + FROM view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE {_ua_where} + AND hour >= now() - INTERVAL 24 HOUR + AND ua != '' GROUP BY user_agent ORDER BY count DESC LIMIT 10 """ - - ua_result = db.query(ua_query_simple, {"value": value}) + + ua_result = db.query(ua_query_simple, _ua_params) user_agents = [get_attribute_value(row, 1, 2, 3, 4) for row in ua_result.result_rows] # JA4 fingerprints diff --git a/create_classifications_table.sql b/create_classifications_table.sql deleted file mode 100644 index 6f2fc5c..0000000 --- a/create_classifications_table.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS mabase_prod.classifications -( - ip String, - ja4 String, - label LowCardinality(String), - tags Array(String), - comment String, - confidence Float32, - features String, - analyst String, - created_at DateTime DEFAULT now() -) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(created_at) -ORDER BY (created_at, ip, ja4) -SETTINGS index_granularity = 8192; diff --git a/deploy_audit_logs_table.sql b/deploy_audit_logs_table.sql deleted file mode 100644 index 1c571de..0000000 --- a/deploy_audit_logs_table.sql +++ /dev/null @@ -1,165 +0,0 @@ --- ============================================================================= --- Table audit_logs - Dashboard Bot Detector --- ============================================================================= --- Stocke tous les logs d'activité des utilisateurs pour audit et conformité --- --- Usage: --- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ --- --user admin --password SuperPassword123! < deploy_audit_logs_table.sql --- --- ============================================================================= - -USE mabase_prod; - --- ============================================================================= --- Table pour stocker les logs d'audit --- ============================================================================= - -CREATE TABLE IF NOT EXISTS mabase_prod.audit_logs -( - -- Identification - timestamp DateTime DEFAULT now(), - user_name String, -- Nom de l'utilisateur - action LowCardinality(String), -- Action effectuée - - -- Entité concernée - entity_type LowCardinality(String), -- Type: ip, ja4, incident, classification - entity_id String, -- ID de l'entité - entity_count UInt32 DEFAULT 0, -- Nombre d'entités (pour bulk operations) - - -- Détails - details String, -- JSON avec détails de l'action - client_ip String, -- IP du client - - -- Métadonnées - session_id String DEFAULT '', -- ID de session - user_agent String DEFAULT '' -- User-Agent du navigateur -) -ENGINE = MergeTree() -PARTITION BY toYYYYMMDD(timestamp) -ORDER BY (timestamp, user_name, action) -TTL timestamp + INTERVAL 90 DAY -- Garder 90 jours de logs -SETTINGS index_granularity = 8192; - --- ============================================================================= --- Index pour accélérer les recherches --- ============================================================================= - -CREATE INDEX IF NOT EXISTS idx_audit_logs_user -ON TABLE mabase_prod.audit_logs (user_name) TYPE minmax GRANULARITY 1; - -CREATE INDEX IF NOT EXISTS idx_audit_logs_action -ON TABLE mabase_prod.audit_logs (action) TYPE minmax GRANULARITY 1; - -CREATE INDEX IF NOT EXISTS idx_audit_logs_entity -ON TABLE mabase_prod.audit_logs (entity_type, entity_id) TYPE minmax GRANULARITY 1; - -CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp -ON TABLE mabase_prod.audit_logs (timestamp) TYPE minmax GRANULARITY 1; - --- ============================================================================= --- Vue pour les statistiques d'audit --- ============================================================================= - -CREATE VIEW IF NOT EXISTS mabase_prod.view_audit_stats AS -SELECT - toDate(timestamp) AS log_date, - user_name, - action, - count() AS total_actions, - uniq(entity_id) AS unique_entities, - sum(entity_count) AS total_entity_count -FROM mabase_prod.audit_logs -GROUP BY log_date, user_name, action; - --- ============================================================================= --- Vue pour l'activité par utilisateur --- ============================================================================= - -CREATE VIEW IF NOT EXISTS mabase_prod.view_user_activity AS -SELECT - user_name, - toDate(timestamp) AS activity_date, - count() AS actions, - uniq(action) AS action_types, - min(timestamp) AS first_action, - max(timestamp) AS last_action, - dateDiff('hour', min(timestamp), max(timestamp)) AS session_duration_hours -FROM mabase_prod.audit_logs -GROUP BY user_name, activity_date; - --- ============================================================================= --- Actions d'audit standardisées --- ============================================================================= --- --- CLASSIFICATION: --- - CLASSIFICATION_CREATE --- - CLASSIFICATION_UPDATE --- - CLASSIFICATION_DELETE --- - BULK_CLASSIFICATION --- --- INVESTIGATION: --- - INVESTIGATION_START --- - INVESTIGATION_COMPLETE --- - CORRELATION_GRAPH_VIEW --- - TIMELINE_VIEW --- --- EXPORT: --- - EXPORT_CSV --- - EXPORT_JSON --- - EXPORT_STIX --- - EXPORT_MISP --- --- INCIDENT: --- - INCIDENT_CREATE --- - INCIDENT_UPDATE --- - INCIDENT_CLOSE --- --- ADMIN: --- - USER_LOGIN --- - USER_LOGOUT --- - PERMISSION_CHANGE --- - CONFIG_UPDATE --- --- ============================================================================= - --- ============================================================================= --- Exemples d'insertion --- ============================================================================= - --- Classification simple --- INSERT INTO mabase_prod.audit_logs --- (user_name, action, entity_type, entity_id, details) --- VALUES --- ('analyst1', 'CLASSIFICATION_CREATE', 'ip', '192.168.1.100', --- '{"label": "malicious", "tags": ["scraping", "bot-network"], "confidence": 0.95}'); - --- Classification en masse --- INSERT INTO mabase_prod.audit_logs --- (user_name, action, entity_type, entity_count, details) --- VALUES --- ('analyst1', 'BULK_CLASSIFICATION', 'ip', 50, --- '{"label": "suspicious", "tags": ["scanner"], "confidence": 0.7}'); - --- Export STIX --- INSERT INTO mabase_prod.audit_logs --- (user_name, action, entity_type, entity_count, details) --- VALUES --- ('analyst2', 'EXPORT_STIX', 'incident', 1, --- '{"incident_id": "INC-20240314-001", "format": "stix-2.1"}'); - --- ============================================================================= --- FIN --- ============================================================================= --- --- Vérifier que la table est créée : --- SELECT count() FROM mabase_prod.audit_logs; --- --- Voir les dernières actions : --- SELECT * FROM mabase_prod.audit_logs ORDER BY timestamp DESC LIMIT 10; --- --- Statistiques par utilisateur : --- SELECT user_name, count() AS actions FROM mabase_prod.audit_logs --- WHERE timestamp >= now() - INTERVAL 24 HOUR GROUP BY user_name; --- --- ============================================================================= diff --git a/deploy_classifications_table.sql b/deploy_classifications_table.sql deleted file mode 100644 index b2285dc..0000000 --- a/deploy_classifications_table.sql +++ /dev/null @@ -1,73 +0,0 @@ --- ============================================================================= --- Table classifications - Dashboard Bot Detector --- ============================================================================= --- Stocke les classifications des IPs pour l'apprentissage supervisé --- --- Usage: --- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ --- --user default --password < deploy_classifications_table.sql --- --- ============================================================================= - -USE mabase_prod; - --- ============================================================================= --- Table pour stocker les classifications des IPs --- ============================================================================= - -CREATE TABLE IF NOT EXISTS mabase_prod.classifications -( - -- Identification - ip String, - - -- Classification - label LowCardinality(String), -- "legitimate", "suspicious", "malicious" - tags Array(String), -- Tags associés - comment String, -- Commentaire de l'analyste - - -- Métriques pour ML - confidence Float32, -- Confiance de la classification (0-1) - features String, -- JSON avec toutes les features - - -- Métadonnées - analyst String, -- Nom de l'analyste - created_at DateTime DEFAULT now() -- Date de classification -) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(created_at) -ORDER BY (created_at, ip) -SETTINGS index_granularity = 8192; - --- ============================================================================= --- Index pour accélérer les recherches par IP --- ============================================================================= - -CREATE INDEX IF NOT EXISTS idx_classifications_ip -ON TABLE mabase_prod.classifications (ip) TYPE minmax GRANULARITY 1; - --- ============================================================================= --- Vue pour les statistiques de classification --- ============================================================================= - -CREATE VIEW IF NOT EXISTS mabase_prod.view_classifications_stats AS -SELECT - label, - count() AS total, - uniq(ip) AS unique_ips, - avg(confidence) AS avg_confidence, - min(created_at) AS first_classification, - max(created_at) AS last_classification -FROM mabase_prod.classifications -GROUP BY label; - --- ============================================================================= --- FIN --- ============================================================================= --- --- Vérifier que la table est créée : --- SELECT count() FROM mabase_prod.classifications; --- --- Voir les statistiques : --- SELECT * FROM mabase_prod.view_classifications_stats; --- --- ============================================================================= diff --git a/deploy_dashboard_entities_view.sql b/deploy_dashboard_entities_view.sql deleted file mode 100644 index dbbb2b0..0000000 --- a/deploy_dashboard_entities_view.sql +++ /dev/null @@ -1,377 +0,0 @@ --- ============================================================================= --- Vue materialisée unique pour Dashboard Entities - Bot Detector --- ============================================================================= --- --- Entités gérées : --- - ip : Adresses IP sources --- - ja4 : Fingerprints JA4 --- - user_agent : User-Agents HTTP --- - client_header : Client Headers --- - host : Hosts HTTP --- - path : Paths URL --- - query_param : Noms de paramètres de query (concaténés: foo,baz) --- --- Instructions d'installation : --- ----------------------------- --- 1. Se connecter à ClickHouse en CLI : --- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ --- --user admin --password SuperPassword123! --- --- 2. Copier-coller CHAQUE BLOC séparément (un par un) --- --- 3. Vérifier que la vue est créée : --- SELECT count() FROM mabase_prod.view_dashboard_entities; --- --- ============================================================================= - -USE mabase_prod; - --- ============================================================================= --- BLOC 0/3 : Nettoyer l'existant (IMPORTANT) --- ============================================================================= - -DROP TABLE IF EXISTS mabase_prod.view_dashboard_entities_mv; -DROP TABLE IF EXISTS mabase_prod.view_dashboard_entities; - --- ============================================================================= --- BLOC 1/3 : Créer la table --- ============================================================================= - -CREATE TABLE IF NOT EXISTS mabase_prod.view_dashboard_entities -( - -- Identification de l'entité - entity_type LowCardinality(String), - entity_value String, - - -- Contexte - src_ip IPv4, - ja4 String, - host String, - - -- Temps (granularité journalière) - log_date Date, - - -- Métriques - requests UInt64, - unique_ips UInt64, - - -- Attributs associés (pour investigation croisée) - user_agents Array(String), - client_headers Array(String), - paths Array(String), - query_params Array(String), - asns Array(String), - countries Array(String) -) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(log_date) -ORDER BY (entity_type, entity_value, log_date) -TTL log_date + INTERVAL 90 DAY -- Garder 90 jours (au lieu de 30) -SETTINGS index_granularity = 8192; - --- ============================================================================= --- BLOC 2/3 : Créer la vue materialisée --- ============================================================================= - -CREATE MATERIALIZED VIEW IF NOT EXISTS mabase_prod.view_dashboard_entities_mv -TO mabase_prod.view_dashboard_entities -AS --- 1. Entité : IP -SELECT - 'ip' AS entity_type, - toString(src_ip) AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM mabase_prod.http_logs -WHERE src_ip IS NOT NULL -GROUP BY src_ip, ja4, host, log_date - -UNION ALL - --- 2. Entité : JA4 -SELECT - 'ja4' AS entity_type, - ja4 AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM mabase_prod.http_logs -WHERE ja4 != '' AND ja4 IS NOT NULL -GROUP BY src_ip, ja4, host, log_date - -UNION ALL - --- 3. Entité : User-Agent -SELECT - 'user_agent' AS entity_type, - ua AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(ua) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM -( - SELECT - src_ip, - ja4, - host, - time, - src_asn, - src_country_code, - header_user_agent AS ua, - client_headers, - path, - query - FROM mabase_prod.http_logs -) -WHERE ua != '' AND ua IS NOT NULL -GROUP BY src_ip, ja4, host, log_date, ua - -UNION ALL - --- 4. Entité : Client Header -SELECT - 'client_header' AS entity_type, - ch AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(ch) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM -( - SELECT - src_ip, - ja4, - host, - time, - src_asn, - src_country_code, - header_user_agent, - client_headers AS ch, - path, - query - FROM mabase_prod.http_logs -) -WHERE ch != '' AND ch IS NOT NULL -GROUP BY src_ip, ja4, host, log_date, ch - -UNION ALL - --- 5. Entité : Host -SELECT - 'host' AS entity_type, - host AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM mabase_prod.http_logs -WHERE host != '' AND host IS NOT NULL -GROUP BY src_ip, ja4, host, log_date - -UNION ALL - --- 6. Entité : Path -SELECT - 'path' AS entity_type, - p AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(p) AS paths, - groupArrayDistinct( - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) - ) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM -( - SELECT - src_ip, - ja4, - host, - time, - src_asn, - src_country_code, - header_user_agent, - client_headers, - path AS p, - query - FROM mabase_prod.http_logs -) -WHERE p != '' AND p IS NOT NULL -GROUP BY src_ip, ja4, host, log_date, p - -UNION ALL - --- 7. Entité : Query Param (noms concaténés) -SELECT - 'query_param' AS entity_type, - query_params_string AS entity_value, - src_ip, - ja4, - host, - toDate(time) AS log_date, - count() AS requests, - uniq(src_ip) AS unique_ips, - groupArrayDistinct(header_user_agent) AS user_agents, - groupArrayDistinct(client_headers) AS client_headers, - groupArrayDistinct(path) AS paths, - groupArrayDistinct(query_params_string) AS query_params, - groupArrayDistinct(toString(src_asn)) AS asns, - groupArrayDistinct(src_country_code) AS countries -FROM ( - SELECT - src_ip, ja4, host, time, src_asn, src_country_code, - header_user_agent, client_headers, path, - arrayStringConcat( - arrayMap( - x -> splitByChar('=', x)[1], - splitByChar('&', replaceOne(query, '?', '')) - ), - ',' - ) AS query_params_string - FROM mabase_prod.http_logs - WHERE query != '' AND query IS NOT NULL -) -WHERE query_params_string != '' -GROUP BY src_ip, ja4, host, log_date, query_params_string; - --- ============================================================================= --- BLOC 3/3 : Créer les index (optionnel - améliore les performances) --- ============================================================================= - -ALTER TABLE mabase_prod.view_dashboard_entities -ADD INDEX IF NOT EXISTS idx_entities_type (entity_type) TYPE minmax GRANULARITY 1; - -ALTER TABLE mabase_prod.view_dashboard_entities -ADD INDEX IF NOT EXISTS idx_entities_value (entity_value) TYPE minmax GRANULARITY 1; - -ALTER TABLE mabase_prod.view_dashboard_entities -ADD INDEX IF NOT EXISTS idx_entities_ip (src_ip) TYPE minmax GRANULARITY 1; - --- ============================================================================= --- FIN --- ============================================================================= --- --- Pour vérifier que la vue fonctionne : --- ------------------------------------- --- SELECT entity_type, count() FROM mabase_prod.view_dashboard_entities GROUP BY entity_type; --- --- Pour rafraîchir manuellement (si nécessaire) : --- ---------------------------------------------- --- OPTIMIZE TABLE mabase_prod.view_dashboard_entities FINAL; --- --- Exemples de requêtes : --- ---------------------- --- -- Stats pour une IP --- SELECT * FROM mabase_prod.view_dashboard_entities --- WHERE entity_type = 'ip' AND entity_value = '116.179.33.143'; --- --- -- Stats pour un JA4 --- SELECT * FROM mabase_prod.view_dashboard_entities --- WHERE entity_type = 'ja4' AND entity_value = 't13d190900_9dc949149365_97f8aa674fd9'; --- --- -- Top 10 des user-agents --- SELECT entity_value, sum(requests) as total --- FROM mabase_prod.view_dashboard_entities --- WHERE entity_type = 'user_agent' --- GROUP BY entity_value --- ORDER BY total DESC --- LIMIT 10; --- --- ============================================================================= diff --git a/deploy_user_agents_view.sql b/deploy_user_agents_view.sql deleted file mode 100644 index 6bc221c..0000000 --- a/deploy_user_agents_view.sql +++ /dev/null @@ -1,79 +0,0 @@ --- ============================================================================= --- Vue materialisée pour les User-Agents - Dashboard Bot Detector --- ============================================================================= --- --- Instructions d'installation : --- ----------------------------- --- 1. Se connecter à ClickHouse en CLI : --- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ --- --user default --password --- --- 2. Copier-coller CHAQUE BLOC séparément (un par un) --- --- 3. Vérifier que la vue est créée : --- SELECT count() FROM mabase_prod.view_dashboard_user_agents; --- --- ============================================================================= - -USE mabase_prod; - --- ============================================================================= --- BLOC 1/3 : Créer la table --- ============================================================================= - -CREATE TABLE IF NOT EXISTS mabase_prod.view_dashboard_user_agents -( - src_ip IPv4, - ja4 String, - hour DateTime, - log_date Date, - user_agents Array(String), - requests UInt64 -) -ENGINE = AggregatingMergeTree() -PARTITION BY log_date -ORDER BY (src_ip, ja4, hour) -TTL log_date + INTERVAL 90 DAY -- Garder 90 jours (au lieu de 7) -SETTINGS index_granularity = 8192; - --- ============================================================================= --- BLOC 2/3 : Créer la vue materialisée --- ============================================================================= - -CREATE MATERIALIZED VIEW IF NOT EXISTS mabase_prod.view_dashboard_user_agents_mv -TO mabase_prod.view_dashboard_user_agents -AS SELECT - src_ip, - ja4, - toStartOfHour(time) AS hour, - toDate(time) AS log_date, - groupArrayDistinct(header_user_agent) AS user_agents, - count() AS requests -FROM mabase_prod.http_logs -WHERE header_user_agent != '' AND header_user_agent IS NOT NULL - AND time >= now() - INTERVAL 7 DAY -GROUP BY src_ip, ja4, hour, log_date; - --- ============================================================================= --- BLOC 3/3 : Créer les index (optionnel - améliore les performances) --- ============================================================================= - -ALTER TABLE mabase_prod.view_dashboard_user_agents -ADD INDEX IF NOT EXISTS idx_user_agents_ip (src_ip) TYPE minmax GRANULARITY 1; - -ALTER TABLE mabase_prod.view_dashboard_user_agents -ADD INDEX IF NOT EXISTS idx_user_agents_ja4 (ja4) TYPE minmax GRANULARITY 1; - --- ============================================================================= --- FIN --- ============================================================================= --- --- Pour vérifier que la vue fonctionne : --- ------------------------------------- --- SELECT * FROM mabase_prod.view_dashboard_user_agents LIMIT 10; --- --- Pour rafraîchir manuellement (si nécessaire) : --- ---------------------------------------------- --- OPTIMIZE TABLE mabase_prod.view_dashboard_user_agents FINAL; --- --- ============================================================================= diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5c34198..282638e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ -import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Link, Navigate, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useEffect, useState, useCallback } from 'react'; import { DetectionsList } from './components/DetectionsList'; import { DetailsView } from './components/DetailsView'; import { InvestigationView } from './components/InvestigationView'; @@ -10,65 +11,343 @@ import { ThreatIntelView } from './components/ThreatIntelView'; import { CorrelationGraph } from './components/CorrelationGraph'; import { InteractiveTimeline } from './components/InteractiveTimeline'; import { SubnetInvestigation } from './components/SubnetInvestigation'; +import { BulkClassification } from './components/BulkClassification'; +import { PivotView } from './components/PivotView'; +import { FingerprintsView } from './components/FingerprintsView'; +import { CampaignsView } from './components/CampaignsView'; +import { useTheme } from './ThemeContext'; -// Navigation -function Navigation() { +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface AlertCounts { + critical: number; + high: number; + medium: number; + total: number; +} + +interface RecentItem { + type: 'ip' | 'ja4' | 'subnet'; + value: string; + ts: number; +} + +// ─── Recent investigations (localStorage) ──────────────────────────────────── + +const RECENTS_KEY = 'soc_recent_investigations'; +const MAX_RECENTS = 8; + +function loadRecents(): RecentItem[] { + try { + return JSON.parse(localStorage.getItem(RECENTS_KEY) || '[]'); + } catch { + return []; + } +} + +function saveRecent(item: Omit) { + const all = loadRecents().filter(r => !(r.type === item.type && r.value === item.value)); + all.unshift({ ...item, ts: Date.now() }); + localStorage.setItem(RECENTS_KEY, JSON.stringify(all.slice(0, MAX_RECENTS))); +} + +// ─── Sidebar ───────────────────────────────────────────────────────────────── + +function Sidebar({ counts }: { counts: AlertCounts | null }) { const location = useLocation(); + const { theme, setTheme } = useTheme(); + const [recents, setRecents] = useState(loadRecents()); - const links = [ - { path: '/', label: 'Incidents' }, - { path: '/threat-intel', label: 'Threat Intel' }, + // Refresh recents when location changes + useEffect(() => { + setRecents(loadRecents()); + }, [location.pathname]); + + const navLinks = [ + { path: '/', label: 'Dashboard', icon: '📊', aliases: ['/incidents'] }, + { path: '/detections', label: 'Détections', icon: '🎯', aliases: ['/investigate'] }, + { path: '/campaigns', label: 'Campagnes / Botnets', icon: '🕸️', aliases: [] }, + { path: '/fingerprints', label: 'Fingerprints JA4', icon: '🔏', aliases: [] }, + { path: '/pivot', label: 'Pivot / Corrélation', icon: '🔗', aliases: [] }, + { path: '/threat-intel', label: 'Threat Intel', icon: '📚', aliases: [] }, + ]; + + const isActive = (link: typeof navLinks[0]) => + location.pathname === link.path || + link.aliases.some(a => location.pathname.startsWith(a)) || + (link.path !== '/' && location.pathname.startsWith(`${link.path}/`)); + + const themeOptions: { value: typeof theme; icon: string; label: string }[] = [ + { value: 'dark', icon: '🌙', label: 'Sombre' }, + { value: 'light', icon: '☀️', label: 'Clair' }, + { value: 'auto', icon: '🔄', label: 'Auto' }, ]; return ( - + + {/* Footer */} +
+
Analyste SOC
+
+ ); } -// App principale +// ─── Top header ─────────────────────────────────────────────────────────────── + +function TopHeader({ counts }: { counts: AlertCounts | null }) { + const location = useLocation(); + + const getBreadcrumb = () => { + const p = location.pathname; + if (p === '/' || p === '/incidents') return 'Dashboard'; + if (p.startsWith('/investigation/ja4/')) return `JA4 · ${decodeURIComponent(p.split('/investigation/ja4/')[1] || '')}`; + if (p.startsWith('/investigation/')) return `IP · ${decodeURIComponent(p.split('/investigation/')[1] || '')}`; + if (p.startsWith('/detections/')) return `Détection · ${decodeURIComponent(p.split('/').pop() || '')}`; + if (p.startsWith('/detections')) return 'Détections'; + if (p.startsWith('/entities/subnet/')) return `Subnet · ${decodeURIComponent(p.split('/entities/subnet/')[1] || '')}`; + if (p.startsWith('/entities/')) return `Entité · ${decodeURIComponent(p.split('/').pop() || '')}`; + if (p.startsWith('/fingerprints')) return 'Fingerprints JA4'; + if (p.startsWith('/campaigns')) return 'Campagnes / Botnets'; + if (p.startsWith('/pivot')) return 'Pivot / Corrélation'; + if (p.startsWith('/bulk-classify')) return 'Classification en masse'; + return ''; + }; + + return ( +
+ {/* Breadcrumb */} +
{getBreadcrumb()}
+ + {/* Search */} +
+ +
+ + {/* Critical alert badge */} + {counts && counts.critical > 0 && ( + + 🔴 {counts.critical} CRITICAL + + )} +
+ ); +} + +// ─── Route helpers ──────────────────────────────────────────────────────────── + +function CorrelationGraphRoute() { + const { ip } = useParams<{ ip: string }>(); + return ; +} + +function TimelineRoute() { + const { ip } = useParams<{ ip?: string }>(); + return ; +} + +function InvestigateRoute() { + const { type, value } = useParams<{ type?: string; value?: string }>(); + if (!type || !value) return ; + const decodedValue = decodeURIComponent(value); + if (type === 'ip') return ; + if (type === 'ja4') return ; + return ; +} + +function BulkClassificationRoute() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const ipsParam = searchParams.get('ips') || ''; + const selectedIPs = ipsParam.split(',').map(ip => ip.trim()).filter(Boolean); + if (selectedIPs.length === 0) return ; + return ( + navigate('/')} + onSuccess={() => navigate('/threat-intel')} + /> + ); +} + +// Track investigations for the recents list +function RouteTracker() { + const location = useLocation(); + useEffect(() => { + const p = location.pathname; + if (p.startsWith('/investigation/ja4/')) { + saveRecent({ type: 'ja4', value: decodeURIComponent(p.split('/investigation/ja4/')[1] || '') }); + } else if (p.startsWith('/investigation/')) { + saveRecent({ type: 'ip', value: decodeURIComponent(p.split('/investigation/')[1] || '') }); + } else if (p.startsWith('/entities/subnet/')) { + saveRecent({ type: 'subnet', value: decodeURIComponent(p.split('/entities/subnet/')[1] || '') }); + } + }, [location.pathname]); + return null; +} + +// ─── App ────────────────────────────────────────────────────────────────────── + export default function App() { + const [counts, setCounts] = useState(null); + + const fetchCounts = useCallback(async () => { + try { + const res = await fetch('/api/metrics'); + if (res.ok) { + const data = await res.json(); + const s = data.summary; + setCounts({ + critical: s.critical_count ?? 0, + high: s.high_count ?? 0, + medium: s.medium_count ?? 0, + total: s.total_detections ?? 0, + }); + } + } catch { + // silently ignore — metrics are informational + } + }, []); + + useEffect(() => { + fetchCounts(); + const id = setInterval(fetchCounts, 30_000); + return () => clearInterval(id); + }, [fetchCounts]); + return ( -
- -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
+ +
+ {/* Fixed sidebar */} + + + {/* Main area (offset by sidebar width) */} +
+ {/* Fixed top header */} + + + {/* Scrollable page content */} +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
); diff --git a/frontend/src/ThemeContext.tsx b/frontend/src/ThemeContext.tsx new file mode 100644 index 0000000..e19e3c6 --- /dev/null +++ b/frontend/src/ThemeContext.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +export type Theme = 'dark' | 'light' | 'auto'; + +interface ThemeContextValue { + theme: Theme; + resolved: 'dark' | 'light'; + setTheme: (t: Theme) => void; +} + +const ThemeContext = createContext({ + theme: 'dark', + resolved: 'dark', + setTheme: () => {}, +}); + +const STORAGE_KEY = 'soc_theme'; + +function resolveTheme(theme: Theme): 'dark' | 'light' { + if (theme === 'auto') { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return theme; +} + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setThemeState] = useState(() => { + const stored = localStorage.getItem(STORAGE_KEY) as Theme | null; + return stored ?? 'dark'; // SOC default: dark + }); + + const [resolved, setResolved] = useState<'dark' | 'light'>(() => resolveTheme( + (localStorage.getItem(STORAGE_KEY) as Theme | null) ?? 'dark' + )); + + const applyTheme = (t: Theme) => { + const r = resolveTheme(t); + setResolved(r); + document.documentElement.setAttribute('data-theme', r); + }; + + const setTheme = (t: Theme) => { + setThemeState(t); + localStorage.setItem(STORAGE_KEY, t); + applyTheme(t); + }; + + // Apply on mount + useEffect(() => { + applyTheme(theme); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Watch system preference changes when in 'auto' mode + useEffect(() => { + if (theme !== 'auto') return; + const mq = window.matchMedia('(prefers-color-scheme: light)'); + const handler = () => applyTheme('auto'); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theme]); + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/frontend/src/components/CampaignsView.tsx b/frontend/src/components/CampaignsView.tsx new file mode 100644 index 0000000..b2f14cc --- /dev/null +++ b/frontend/src/components/CampaignsView.tsx @@ -0,0 +1,793 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ClusterData { + id: string; + score: number; + severity: string; + total_detections: number; + unique_ips: number; + subnet: string; + sample_ip: string; + ja4: string; + primary_ua: string; + primary_target: string; + countries: { code: string; percentage: number }[]; + asn: string; + first_seen: string; + last_seen: string; + trend: string; + trend_percentage: number; +} + +interface SubnetIPEntry { + ip: string; + detections: number; + confidence: number; + ja4: string; + user_agent: string; +} + +interface SubnetIPData { + subnet: string; + ips: SubnetIPEntry[]; +} + +interface JA4AttributeItem { + value: string; + count: number; +} + +interface JA4AttributesResponse { + items: JA4AttributeItem[]; +} + +type ActiveTab = 'clusters' | 'ja4' | 'behavioral'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + + +function getThreatColors(level: string): { bg: string; text: string; border: string } { + switch (level) { + case 'critical': return { bg: 'bg-threat-critical/20', text: 'text-threat-critical', border: 'border-threat-critical' }; + case 'high': return { bg: 'bg-threat-high/20', text: 'text-threat-high', border: 'border-threat-high' }; + case 'medium': return { bg: 'bg-threat-medium/20', text: 'text-threat-medium', border: 'border-threat-medium' }; + case 'low': return { bg: 'bg-threat-low/20', text: 'text-threat-low', border: 'border-threat-low' }; + default: return { bg: 'bg-background-card', text: 'text-text-secondary', border: 'border-border' }; + } +} + +function getThreatLabel(level: string): string { + switch (level) { + case 'critical': return '🔴 CRITIQUE'; + case 'high': return '🟠 ÉLEVÉ'; + case 'medium': return '🟡 MOYEN'; + case 'low': return '🟢 FAIBLE'; + default: return level.toUpperCase(); + } +} + + +function getConfidenceTextColor(confidence: number): string { + if (confidence >= 0.8) return 'text-threat-critical'; + if (confidence >= 0.6) return 'text-threat-high'; + if (confidence >= 0.4) return 'text-threat-medium'; + return 'text-threat-low'; +} + +function getJA4CountColor(count: number): string { + if (count >= 50) return 'bg-threat-critical/20 text-threat-critical'; + if (count >= 20) return 'bg-threat-high/20 text-threat-high'; + return 'bg-threat-medium/20 text-threat-medium'; +} + +function sortClusters(clusters: ClusterData[]): ClusterData[] { + const SEV_ORDER: Record = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }; + return [...clusters].sort((a, b) => { + const levelDiff = (SEV_ORDER[a.severity] ?? 4) - (SEV_ORDER[b.severity] ?? 4); + return levelDiff !== 0 ? levelDiff : b.score - a.score; + }); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export function CampaignsView() { + const navigate = useNavigate(); + + const [clusters, setClusters] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [ja4Items, setJA4Items] = useState([]); + const [ja4Loading, setJA4Loading] = useState(false); + const [ja4Error, setJA4Error] = useState(null); + const [ja4Loaded, setJA4Loaded] = useState(false); + + const [expandedSubnets, setExpandedSubnets] = useState>(new Set()); + const [subnetIPs, setSubnetIPs] = useState>(new Map()); + const [subnetLoading, setSubnetLoading] = useState>(new Set()); + + const [activeTab, setActiveTab] = useState('clusters'); + const [minIPs, setMinIPs] = useState(3); + const [severityFilter, setSeverityFilter] = useState('all'); + + // Fetch clusters on mount + useEffect(() => { + const fetchClusters = async () => { + setLoading(true); + try { + const response = await fetch('/api/incidents/clusters'); + if (!response.ok) throw new Error('Erreur chargement des clusters'); + const data: { items: ClusterData[] } = await response.json(); + setClusters(sortClusters(data.items ?? [])); + } catch (err) { + setError(err instanceof Error ? err.message : 'Erreur inconnue'); + } finally { + setLoading(false); + } + }; + fetchClusters(); + }, []); + + // Lazy-load JA4 data on tab switch + const fetchJA4 = useCallback(async () => { + if (ja4Loaded) return; + setJA4Loading(true); + setJA4Error(null); + try { + const response = await fetch('/api/attributes/ja4?limit=100'); + if (!response.ok) throw new Error('Erreur chargement des fingerprints JA4'); + const data: JA4AttributesResponse = await response.json(); + setJA4Items(data.items ?? []); + setJA4Loaded(true); + } catch (err) { + setJA4Error(err instanceof Error ? err.message : 'Erreur inconnue'); + } finally { + setJA4Loading(false); + } + }, [ja4Loaded]); + + useEffect(() => { + if (activeTab === 'ja4') fetchJA4(); + }, [activeTab, fetchJA4]); + + // Toggle subnet expansion and fetch IPs on first expand + const toggleSubnet = useCallback(async (subnet: string) => { + const isCurrentlyExpanded = expandedSubnets.has(subnet); + + setExpandedSubnets(prev => { + const next = new Set(prev); + isCurrentlyExpanded ? next.delete(subnet) : next.add(subnet); + return next; + }); + + if (isCurrentlyExpanded || subnetIPs.has(subnet)) return; + + setSubnetLoading(prev => new Set(prev).add(subnet)); + try { + const response = await fetch(`/api/entities/subnet/${encodeURIComponent(subnet)}`); + if (!response.ok) throw new Error('Erreur chargement IPs'); + const data: SubnetIPData = await response.json(); + setSubnetIPs(prev => new Map(prev).set(subnet, data.ips ?? [])); + } catch { + setSubnetIPs(prev => new Map(prev).set(subnet, [])); + } finally { + setSubnetLoading(prev => { + const next = new Set(prev); + next.delete(subnet); + return next; + }); + } + }, [expandedSubnets, subnetIPs]); + + const filteredClusters = clusters.filter(c => { + if (c.unique_ips < minIPs) return false; + if (severityFilter !== 'all' && c.severity.toLowerCase() !== severityFilter) return false; + return true; + }); + + const activeClusters = clusters.length; + const coordinatedIPs = clusters.reduce((sum, c) => sum + c.unique_ips, 0); + const criticalCampaigns = clusters.filter(c => c.severity === 'CRITICAL').length; + const ja4Campaigns = ja4Items.filter(j => j.count >= 5); + + if (loading) { + return ( +
+
Chargement des campagnes...
+
+ ); + } + + if (error) { + return ( +
+
Erreur: {error}
+ +
+ ); + } + + return ( +
+ + {/* ── Row 1: Header + stat cards ── */} +
+
+

🎯 Détection de Campagnes

+

+ Identification de groupes d'IPs coordonnées partageant les mêmes fingerprints JA4, profils UA, + cibles et comportements temporels — indicateurs de botnets et campagnes organisées. +

+
+ +
+
+
Clusters actifs
+
{activeClusters}
+
sous-réseaux suspects
+
+
+
IPs coordinées
+
{coordinatedIPs.toLocaleString()}
+
total dans tous les clusters
+
+
+
Campagnes critiques
+
{criticalCampaigns}
+
niveau critique · >10 IPs
+
+
+
+ + {/* ── Row 2: Tabs + Filters ── */} +
+
+
+ {( + [ + { id: 'clusters', label: 'Clusters réseau' }, + { id: 'ja4', label: 'Fingerprints JA4' }, + { id: 'behavioral', label: 'Analyse comportementale' }, + ] as const + ).map(tab => ( + + ))} +
+ +
+
+ + setMinIPs(parseInt(e.target.value))} + className="w-24 accent-blue-500" + /> + {minIPs} +
+ +
+
+
+ + {/* ── Tab Content ── */} + {activeTab === 'clusters' && ( + + )} + + {activeTab === 'ja4' && ( + + )} + + {activeTab === 'behavioral' && ( + + )} +
+ ); +} + +// ─── Tab: Clusters réseau ───────────────────────────────────────────────────── + +interface ClustersTabProps { + clusters: ClusterData[]; + expandedSubnets: Set; + subnetIPs: Map; + subnetLoading: Set; + onToggleSubnet: (subnet: string) => void; + onNavigate: (path: string) => void; +} + +function ClustersTab({ + clusters, + expandedSubnets, + subnetIPs, + subnetLoading, + onToggleSubnet, + onNavigate, +}: ClustersTabProps) { + if (clusters.length === 0) { + return ( +
+
🔍
+
Aucun cluster correspondant aux filtres
+
+ ); + } + + return ( +
+ {clusters.map(cluster => ( + onToggleSubnet(cluster.subnet)} + onNavigate={onNavigate} + /> + ))} +
+ ); +} + +// ─── Cluster Card ───────────────────────────────────────────────────────────── + +interface ClusterCardProps { + cluster: ClusterData; + expanded: boolean; + ips: SubnetIPEntry[] | undefined; + loadingIPs: boolean; + onToggle: () => void; + onNavigate: (path: string) => void; +} + +function ClusterCard({ cluster, expanded, ips, loadingIPs, onToggle, onNavigate }: ClusterCardProps) { + const threatLevel = cluster.severity.toLowerCase(); + const { bg, text, border } = getThreatColors(threatLevel); + const isHighRisk = cluster.score >= 70; + const scoreColor = getConfidenceTextColor(cluster.score / 100); + + return ( +
+ {/* Coloured header strip */} +
+ {getThreatLabel(threatLevel)} + {cluster.subnet} + {isHighRisk && ( + + BOTNET + + )} + {cluster.trend === 'new' && ( + + NOUVEAU + + )} + {cluster.trend === 'up' && ( + + ↑ +{cluster.trend_percentage}% + + )} + + {cluster.id} + +
+ + {/* Card body */} +
+ {/* Stats row */} +
+ + {cluster.unique_ips} IP{cluster.unique_ips !== 1 ? 's' : ''} + + + {cluster.total_detections.toLocaleString()} détections + + {cluster.asn && ( + + ASN {cluster.asn} + + )} + {cluster.primary_target && ( + + Cible : {cluster.primary_target} + + )} +
+ + {/* JA4 + UA */} +
+ {cluster.ja4 && ( +
+ JA4 + + {cluster.ja4} + +
+ )} + {cluster.primary_ua && ( +
+ UA + + {cluster.primary_ua} + +
+ )} +
+ + {/* Countries */} + {cluster.countries?.length > 0 && ( +
+ {cluster.countries.map(c => ( + + {c.code} {c.percentage < 100 ? `${c.percentage}%` : ''} + + ))} +
+ )} + + {/* Score bar */} +
+
+ Score menace + {cluster.score}/100 +
+
+
+
+
+ + {/* Action buttons */} +
+ + + {cluster.ja4 && cluster.ja4 !== 'HTTP_CLEAR_TEXT' && ( + + )} + +
+ + {/* Expanded IP list */} + {expanded && ( +
+ {loadingIPs ? ( +
+
Chargement des IPs...
+
+ ) : ips && ips.length > 0 ? ( + + + + + + + + + + + {ips.map((entry, idx) => ( + + + + + + + + ))} + +
IPJA4DétectionsConfiance +
{entry.ip} + + {entry.ja4 || '—'} + + + {entry.detections.toLocaleString()} + + + {Math.round(entry.confidence * 100)}% + + + +
+ ) : ( +
+ Aucune IP disponible pour ce subnet +
+ )} +
+ )} +
+
+ ); +} + +// ─── Tab: Fingerprints JA4 ─────────────────────────────────────────────────── + +interface JA4TabProps { + items: JA4AttributeItem[]; + loading: boolean; + error: string | null; + onNavigate: (path: string) => void; +} + +function JA4Tab({ items, loading, error, onNavigate }: JA4TabProps) { + if (loading) { + return ( +
+
Chargement des fingerprints JA4...
+
+ ); + } + + if (error) { + return ( +
+
Erreur : {error}
+
+ ); + } + + if (items.length === 0) { + return ( +
+
🔑
+
Aucun fingerprint JA4 avec 5+ IPs détecté
+
+ ); + } + + const sorted = [...items].sort((a, b) => b.count - a.count); + + return ( +
+ {sorted.map(item => ( +
+
+ + {item.value} + + + {item.count} IPs + +
+
+ + +
+
+ ))} +
+ ); +} + +// ─── Tab: Analyse comportementale ───────────────────────────────────────────── + +interface BehavioralTabProps { + clusters: ClusterData[]; +} + +function BehavioralTab({ clusters }: BehavioralTabProps) { + if (clusters.length === 0) { + return ( +
+
📊
+
Aucun cluster disponible pour l'analyse comportementale
+
+ ); + } + + // Group by shared JA4 (multi-subnet same fingerprint = coordinated campaign) + const ja4Groups: Record = {}; + for (const cluster of clusters) { + if (!cluster.ja4 || cluster.ja4 === 'HTTP_CLEAR_TEXT') continue; + const key = cluster.ja4.slice(0, 20); // group by JA4 prefix + if (!ja4Groups[key]) ja4Groups[key] = []; + ja4Groups[key].push(cluster); + } + const groupsSorted = Object.entries(ja4Groups) + .filter(([, g]) => g.length >= 2) + .sort((a, b) => b[1].length - a[1].length); + + return ( +
+ {/* JA4-correlated clusters */} + {groupsSorted.length > 0 && ( +
+

Clusters partageant le même JA4

+

+ Subnets distincts utilisant le même fingerprint TLS — indicateur fort de botnet centralisé. +

+
+ {groupsSorted.map(([ja4prefix, group]) => ( +
+
+ {ja4prefix}… + + {group.length} subnet{group.length !== 1 ? 's' : ''} + + + ⚠ Campagne probable + +
+
+ {group.map(c => { + const { bg, text } = getThreatColors(c.severity.toLowerCase()); + return ( + + {c.subnet} + + ); + })} +
+
+ ))} +
+
+ )} + + {/* Behavioral matrix */} +
+

Matrice de signaux comportementaux

+
+ + + + + + + + + + + + + + {clusters.map((cluster, idx) => { + const { bg, text } = getThreatColors(cluster.severity.toLowerCase()); + return ( + + + + + + + + + + ); + })} + +
SubnetScoreTendanceNiveau menacePaysIPsDétections
{cluster.subnet} + + {cluster.score} + + + {cluster.trend === 'up' ? ( + + ↑ +{cluster.trend_percentage}% + + ) : cluster.trend === 'new' ? ( + + Nouveau + + ) : ( + {cluster.trend} + )} + + + {getThreatLabel(cluster.severity.toLowerCase())} + + + {cluster.countries?.slice(0, 2).map(c => ( + {c.code} + ))} + + {cluster.unique_ips} + + {cluster.total_detections.toLocaleString()} +
+
+
+
+ ); +} diff --git a/frontend/src/components/CorrelationGraph.tsx b/frontend/src/components/CorrelationGraph.tsx index 5f73f93..0d93e1d 100644 --- a/frontend/src/components/CorrelationGraph.tsx +++ b/frontend/src/components/CorrelationGraph.tsx @@ -7,614 +7,582 @@ import ReactFlow, { useEdgesState, MarkerType, Panel, + useReactFlow, + ReactFlowProvider, + NodeTypes, + Handle, + Position, } from 'reactflow'; import 'reactflow/dist/style.css'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback, memo } from 'react'; + +// ─── Types ─────────────────────────────────────────────────────────────────── interface CorrelationGraphProps { ip: string; height?: string; } -interface GraphData { - nodes: Node[]; - edges: Edge[]; -} - interface FilterState { - showIP: boolean; showSubnet: boolean; showASN: boolean; showJA4: boolean; showUA: boolean; showHost: boolean; showCountry: boolean; - showPath: boolean; - showQueryParam: boolean; } -export function CorrelationGraph({ ip, height = '600px' }: CorrelationGraphProps) { - const [graphData, setGraphData] = useState({ nodes: [], edges: [] }); - const [loading, setLoading] = useState(true); +interface RawData { + variability: any; + subnet: any; + entities: any; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function cleanIP(address: string): string { + if (!address) return ''; + return address.replace(/^::ffff:/i, ''); +} + +function getCountryFlag(code: string): string { + return (code || '').toUpperCase().replace(/./g, (char) => + String.fromCodePoint(char.charCodeAt(0) + 127397) + ); +} + +function classifyUA(ua: string): 'bot' | 'script' | 'normal' { + const u = ua.toLowerCase(); + if (u.includes('bot') || u.includes('crawler') || u.includes('spider')) return 'bot'; + if ( + u.includes('python') || + u.includes('curl') || + u.includes('wget') || + u.includes('go-http') || + u.includes('java/') || + u.includes('axios') + ) + return 'script'; + return 'normal'; +} + +// ─── Custom node components (must be OUTSIDE the main component) ───────────── + +const IPNode = memo(({ data }: { data: any }) => ( +
+ +
🌐 IP SOURCE
+
{data.label}
+
+ {(data.detections ?? 0).toLocaleString()} détections +
+
+)); +IPNode.displayName = 'IPNode'; + +const SubnetNode = memo(({ data }: { data: any }) => ( +
+ +
🔷 SUBNET /24
+
{data.label}
+
{data.ipsInSubnet ?? 0} IPs actives
+
+)); +SubnetNode.displayName = 'SubnetNode'; + +const ASNNode = memo(({ data }: { data: any }) => ( +
+ +
🏢 ASN
+
{data.label}
+
{data.org}
+
+ {(data.totalInAsn ?? 0).toLocaleString()} IPs +
+
+)); +ASNNode.displayName = 'ASNNode'; + +const CountryNode = memo(({ data }: { data: any }) => ( +
+ +
🌍 PAYS
+
{data.flag}
+
{data.label}
+
+ {(data.percentage ?? 0).toFixed(0)}% · {(data.count ?? 0)} det. +
+
+)); +CountryNode.displayName = 'CountryNode'; + +const JA4Node = memo(({ data }: { data: any }) => ( +
+ +
🔐 JA4 Fingerprint
+
+ {data.label} +
+
+ {data.count} det. + {(data.percentage ?? 0).toFixed(1)}% +
+
+)); +JA4Node.displayName = 'JA4Node'; + +const UANode = memo(({ data }: { data: any }) => { + const borderClass = + data.classification === 'bot' + ? 'border-red-400' + : data.classification === 'script' + ? 'border-yellow-400' + : 'border-indigo-400'; + const badge = + data.classification === 'bot' + ? '🔴 BOT' + : data.classification === 'script' + ? '🟡 SCRIPT' + : '🟢 Normal'; + return ( +
+ +
+ 🤖 User-Agent {badge} +
+
+ {data.label} +
+
+ {data.count} det. + {(data.percentage ?? 0).toFixed(1)}% +
+
+ ); +}); +UANode.displayName = 'UANode'; + +const HostNode = memo(({ data }: { data: any }) => ( +
+ +
🖥️ Host cible
+
+ {data.label} +
+
+)); +HostNode.displayName = 'HostNode'; + +// nodeTypes must be defined outside the component (stable reference) +const nodeTypes: NodeTypes = { + ipNode: IPNode, + subnetNode: SubnetNode, + asnNode: ASNNode, + countryNode: CountryNode, + ja4Node: JA4Node, + uaNode: UANode, + hostNode: HostNode, +}; + +// ─── Layout builder ─────────────────────────────────────────────────────────── + +function buildGraph(rawData: RawData, filters: FilterState): { nodes: Node[]; edges: Edge[] } { + const { variability, subnet, entities } = rawData; + const newNodes: Node[] = []; + const newEdges: Edge[] = []; + + const makeEdge = ( + id: string, + source: string, + target: string, + color: string, + label: string + ): Edge => ({ + id, + source, + target, + type: 'smoothstep', + style: { stroke: color, strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color }, + label, + labelStyle: { fill: color, fontWeight: 600, fontSize: 11 }, + labelBgStyle: { fill: '#1e293b', fillOpacity: 0.85 }, + labelBgPadding: [4, 2], + }); + + // Center: IP + newNodes.push({ + id: 'ip', + type: 'ipNode', + data: { label: cleanIP(variability?.value || ''), detections: variability?.total_detections }, + position: { x: 0, y: 0 }, + }); + + // Inner ring (r=320): Subnet, ASN, Country — evenly spaced + const innerItems: Array<{ + id: string; + type: string; + data: any; + color: string; + label: string; + }> = []; + + if (filters.showSubnet && subnet?.subnet) { + innerItems.push({ + id: 'subnet', + type: 'subnetNode', + data: { + label: cleanIP(subnet.subnet), + ipsInSubnet: subnet.total_in_subnet, + }, + color: '#a855f7', + label: 'appartient à', + }); + } + + if (filters.showASN && subnet?.asn_number) { + innerItems.push({ + id: 'asn', + type: 'asnNode', + data: { + label: `AS${subnet.asn_number}`, + org: subnet.asn_org || 'Unknown', + totalInAsn: subnet.total_in_asn, + }, + color: '#f97316', + label: 'hébergé par', + }); + } + + if ( + filters.showCountry && + variability?.attributes?.countries?.length > 0 + ) { + const c = variability.attributes.countries[0]; + innerItems.push({ + id: 'country', + type: 'countryNode', + data: { + label: c.value, + flag: getCountryFlag(c.value), + percentage: c.percentage, + count: c.count, + }, + color: '#eab308', + label: 'localisé', + }); + } + + const r1 = 320; + innerItems.forEach((item, idx) => { + const angle = (2 * Math.PI * idx) / Math.max(innerItems.length, 1) - Math.PI / 2; + const x = r1 * Math.cos(angle); + const y = r1 * Math.sin(angle); + newNodes.push({ id: item.id, type: item.type, data: item.data, position: { x, y } }); + newEdges.push(makeEdge(`ip-${item.id}`, 'ip', item.id, item.color, item.label)); + }); + + // Outer ring (r=640): JA4, UA, Host — evenly spaced, interleaved + const outerItems: Array<{ + id: string; + type: string; + data: any; + color: string; + label: string; + }> = []; + + if (filters.showJA4 && variability?.attributes?.ja4) { + variability.attributes.ja4.slice(0, 6).forEach((ja4: any, idx: number) => { + outerItems.push({ + id: `ja4-${idx}`, + type: 'ja4Node', + data: { label: ja4.value, count: ja4.count, percentage: ja4.percentage }, + color: '#22c55e', + label: 'JA4', + }); + }); + } + + if (filters.showUA && variability?.attributes?.user_agents) { + variability.attributes.user_agents.slice(0, 5).forEach((ua: any, idx: number) => { + const classification = classifyUA(ua.value); + outerItems.push({ + id: `ua-${idx}`, + type: 'uaNode', + data: { label: ua.value, count: ua.count, percentage: ua.percentage, classification }, + color: + classification === 'bot' + ? '#ef4444' + : classification === 'script' + ? '#f59e0b' + : '#818cf8', + label: 'User-Agent', + }); + }); + } + + if (filters.showHost && entities?.related?.hosts) { + (entities.related.hosts as string[]).slice(0, 5).forEach((host, idx) => { + outerItems.push({ + id: `host-${idx}`, + type: 'hostNode', + data: { label: host }, + color: '#f59e0b', + label: 'cible', + }); + }); + } + + const r2 = 640; + outerItems.forEach((item, idx) => { + const angle = (2 * Math.PI * idx) / Math.max(outerItems.length, 1) - Math.PI / 2; + const x = r2 * Math.cos(angle); + const y = r2 * Math.sin(angle); + newNodes.push({ id: item.id, type: item.type, data: item.data, position: { x, y } }); + newEdges.push(makeEdge(`ip-${item.id}`, 'ip', item.id, item.color, item.label)); + }); + + return { nodes: newNodes, edges: newEdges }; +} + +// ─── Inner graph component (needs useReactFlow, must be child of ReactFlowProvider) ── + +interface GraphInnerProps { + rawData: RawData | null; + loading: boolean; + error: string | null; + filters: FilterState; + toggleFilter: (k: keyof FilterState) => void; + height: string; + ip: string; +} + +function GraphInner({ rawData, loading, error, filters, toggleFilter, height, ip }: GraphInnerProps) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - - // Filtres + const { fitView } = useReactFlow(); + + useEffect(() => { + if (!rawData) return; + const { nodes: n, edges: e } = buildGraph(rawData, filters); + setNodes(n); + setEdges(e); + // fitView after React renders the new nodes + setTimeout(() => fitView({ padding: 0.15, duration: 400 }), 60); + }, [rawData, filters, setNodes, setEdges, fitView]); + + const filterConfig: [keyof FilterState, string, string][] = [ + ['showSubnet', 'Subnet', '#a855f7'], + ['showASN', 'ASN', '#f97316'], + ['showCountry', 'Pays', '#eab308'], + ['showJA4', 'JA4', '#22c55e'], + ['showUA', 'User-Agent', '#ef4444'], + ['showHost', 'Hosts', '#f59e0b'], + ]; + + if (loading) { + return ( +
+
+
Chargement du graphe de corrélations…
+
+ ); + } + + if (error) { + return ( +
+
⚠️
+
{error}
+
+ ); + } + + if (nodes.length === 0) { + return ( +
+
🕸️
+
Aucune corrélation trouvée pour {cleanIP(ip)}
+
+ ); + } + + return ( +
+ + + + + {/* Filtres */} + +
Filtres
+
+ {filterConfig.map(([key, label, color]) => ( + + ))} +
+
+ + {/* Légende */} + +
Légende
+
+ {[ + ['bg-blue-700', 'IP Source'], + ['bg-purple-700', 'Subnet /24'], + ['bg-orange-700', 'ASN'], + ['bg-slate-600', 'Pays'], + ['bg-emerald-700', 'JA4'], + ['bg-rose-900', 'User-Agent'], + ['bg-amber-700', 'Host cible'], + ].map(([bg, lbl]) => ( +
+
+ {lbl} +
+ ))} +
+
🔴 UA = Bot
+
🟡 UA = Script
+
🟢 UA = Normal
+
+
+ + + {/* Stats */} + + {nodes.length} nœuds · {edges.length} arêtes + + +
+ ); +} + +// ─── Exported component ─────────────────────────────────────────────────────── + +export function CorrelationGraph({ ip, height = '700px' }: CorrelationGraphProps) { + const [rawData, setRawData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [filters, setFilters] = useState({ - showIP: true, showSubnet: true, showASN: true, showJA4: true, showUA: true, showHost: true, showCountry: true, - showPath: false, - showQueryParam: false, }); - // Nettoyer une adresse IP (enlever ::ffff: prefix) - const cleanIP = (address: string): string => { - if (!address) return ''; - // Enlever le préfixe IPv6-mapped IPv4 - return address.replace(/^::ffff:/i, ''); - }; - + // Fetch data only when IP changes — filters are applied client-side useEffect(() => { - const fetchCorrelationData = async () => { + if (!ip) return; + const cleaned = cleanIP(ip); + let cancelled = false; + + const fetchData = async () => { setLoading(true); + setError(null); try { - // Fetch toutes les données de corrélation - const [variabilityResponse, subnetResponse, entitiesResponse] = await Promise.all([ - fetch(`/api/variability/ip/${encodeURIComponent(cleanIP(ip))}`), - fetch(`/api/analysis/${encodeURIComponent(cleanIP(ip))}/subnet`), - fetch(`/api/entities/ip/${encodeURIComponent(cleanIP(ip))}`), + const [varRes, subnetRes, entitiesRes] = await Promise.all([ + fetch(`/api/variability/ip/${encodeURIComponent(cleaned)}`), + fetch(`/api/analysis/${encodeURIComponent(cleaned)}/subnet`), + fetch(`/api/entities/ip/${encodeURIComponent(cleaned)}`), ]); - - const variability = await variabilityResponse.json().catch(() => null); - const subnet = await subnetResponse.json().catch(() => null); - const entities = await entitiesResponse.json().catch(() => null); - - const newNodes: Node[] = []; - const newEdges: Edge[] = []; - const nodePositions = new Map(); - - // Positionnement en cercle - const centerX = 400; - const centerY = 300; - const radius = 200; - - // Node IP (centre) - const cleanIpAddress = cleanIP(ip); - nodePositions.set('ip', { x: centerX, y: centerY }); - newNodes.push({ - id: 'ip', - type: 'default', - data: { - label: ( -
-
🌐 IP SOURCE
-
{cleanIpAddress}
-
- {variability?.total_detections?.toLocaleString() || 0} détections -
-
- ) - }, - position: { x: centerX, y: centerY }, - style: { background: 'transparent', border: 'none', width: 200, zIndex: 10 }, - }); - - // Subnet node (haut gauche) - if (filters.showSubnet && subnet?.subnet) { - const subnetClean = subnet.subnet.replace(/^::ffff:/i, ''); - const angle = (180 + 135) * (Math.PI / 180); - const x = centerX + radius * Math.cos(angle); - const y = centerY + radius * Math.sin(angle); - nodePositions.set('subnet', { x, y }); - - newNodes.push({ - id: 'subnet', - type: 'default', - data: { - label: ( -
-
🔷 SUBNET /24
-
{subnetClean}
-
- {subnet.total_in_subnet || 0} IPs actives -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 200 }, - }); - newEdges.push({ - id: 'ip-subnet', - source: 'ip', - target: 'subnet', - type: 'smoothstep', - animated: true, - style: { stroke: '#a855f7', strokeWidth: 3 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#a855f7' }, - label: 'appartient à', - labelStyle: { fill: '#a855f7', fontWeight: 600, fontSize: 12 }, - }); - } - - // ASN node (haut droite) - if (filters.showASN && subnet?.asn_number) { - const angle = (180 + 45) * (Math.PI / 180); - const x = centerX + radius * Math.cos(angle); - const y = centerY + radius * Math.sin(angle); - nodePositions.set('asn', { x, y }); - - newNodes.push({ - id: 'asn', - type: 'default', - data: { - label: ( -
-
🏢 ASN
-
AS{subnet.asn_number}
-
- {subnet.asn_org || 'Unknown'} -
-
- {subnet.total_in_asn?.toLocaleString() || 0} IPs totales -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 200 }, - }); - newEdges.push({ - id: 'ip-asn', - source: 'ip', - target: 'asn', - type: 'smoothstep', - style: { stroke: '#f97316', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#f97316' }, - label: 'via', - labelStyle: { fill: '#f97316', fontWeight: 600, fontSize: 12 }, - }); - } - - // JA4 nodes (bas gauche) - if (filters.showJA4 && variability?.attributes?.ja4) { - variability.attributes.ja4.slice(0, 8).forEach((ja4: any, idx: number) => { - const ja4Id = `ja4-${idx}`; - const angle = (225 + (idx * 15)) * (Math.PI / 180); - const x = centerX + (radius + 80) * Math.cos(angle); - const y = centerY + (radius + 80) * Math.sin(angle); - - newNodes.push({ - id: ja4Id, - type: 'default', - data: { - label: ( -
-
🔐 JA4 Fingerprint
-
{ja4.value}
-
- {ja4.count} détections • {ja4.percentage?.toFixed(1) || 0}% -
- {ja4.unique_ips && ( -
- {ja4.unique_ips} IPs uniques -
- )} -
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 220 }, - }); - newEdges.push({ - id: `ip-ja4-${idx}`, - source: 'ip', - target: ja4Id, - type: 'smoothstep', - style: { stroke: '#22c55e', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#22c55e' }, - label: 'utilise', - labelStyle: { fill: '#22c55e', fontWeight: 600, fontSize: 12 }, - }); - }); - } - - // User-Agent nodes (bas droite) - if (filters.showUA && variability?.attributes?.user_agents) { - variability.attributes.user_agents.slice(0, 6).forEach((ua: any, idx: number) => { - const uaId = `ua-${idx}`; - const angle = (315 + (idx * 10)) * (Math.PI / 180); - const x = centerX + (radius + 80) * Math.cos(angle); - const y = centerY + (radius + 80) * Math.sin(angle); - - // Classification UA - const uaLower = ua.value.toLowerCase(); - let classification = 'normal'; - let borderColor = 'border-green-400'; - if (uaLower.includes('bot') || uaLower.includes('crawler') || uaLower.includes('spider')) { - classification = 'bot'; - borderColor = 'border-red-400'; - } else if (uaLower.includes('python') || uaLower.includes('curl') || uaLower.includes('wget')) { - classification = 'script'; - borderColor = 'border-yellow-400'; - } - - newNodes.push({ - id: uaId, - type: 'default', - data: { - label: ( -
-
- 🤖 User-Agent {classification !== 'normal' && `(${classification.toUpperCase()})`} -
-
- {ua.value} -
-
- {ua.count} détections - {ua.percentage?.toFixed(1) || 0}% -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 240 }, - }); - newEdges.push({ - id: `ip-ua-${idx}`, - source: 'ip', - target: uaId, - type: 'smoothstep', - style: { stroke: '#ef4444', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#ef4444' }, - label: 'utilise', - labelStyle: { fill: '#ef4444', fontWeight: 600, fontSize: 12 }, - }); - }); - } - - // Country node (bas centre) - if (filters.showCountry && variability?.attributes?.countries && variability.attributes.countries.length > 0) { - const country = variability.attributes.countries[0]; - const angle = 270 * (Math.PI / 180); - const x = centerX + (radius - 50) * Math.cos(angle); - const y = centerY + (radius - 50) * Math.sin(angle); - - newNodes.push({ - id: 'country', - type: 'default', - data: { - label: ( -
-
🌍 PAYS
-
{getCountryFlag(country.value)}
-
{country.value}
-
- {country.percentage?.toFixed(0) || 0}% • {country.count} détections -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 150 }, - }); - newEdges.push({ - id: 'ip-country', - source: 'ip', - target: 'country', - type: 'smoothstep', - style: { stroke: '#eab308', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#eab308' }, - label: 'localisé', - labelStyle: { fill: '#eab308', fontWeight: 600, fontSize: 12 }, - }); - } - - // Hosts (depuis entities) - if (filters.showHost && entities?.related?.hosts) { - entities.related.hosts.slice(0, 6).forEach((host: string, idx: number) => { - const hostId = `host-${idx}`; - const angle = (300 + (idx * 12)) * (Math.PI / 180); - const x = centerX + (radius + 150) * Math.cos(angle); - const y = centerY + (radius + 150) * Math.sin(angle); - - newNodes.push({ - id: hostId, - type: 'default', - data: { - label: ( -
-
🖥️ Host
-
{host}
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 200 }, - }); - newEdges.push({ - id: `ip-host-${idx}`, - source: 'ip', - target: hostId, - type: 'smoothstep', - style: { stroke: '#eab308', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#eab308' }, - label: 'cible', - labelStyle: { fill: '#eab308', fontWeight: 600, fontSize: 12 }, - }); - }); - } - - // Paths (depuis entities) - if (filters.showPath && entities?.paths) { - entities.paths.slice(0, 4).forEach((path: any, idx: number) => { - const pathId = `path-${idx}`; - const angle = (320 + (idx * 8)) * (Math.PI / 180); - const x = centerX + (radius + 220) * Math.cos(angle); - const y = centerY + (radius + 220) * Math.sin(angle); - - newNodes.push({ - id: pathId, - type: 'default', - data: { - label: ( -
-
📁 Path URL
-
{path.value}
-
- {path.count} req • {path.percentage?.toFixed(1) || 0}% -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 220 }, - }); - newEdges.push({ - id: `ip-path-${idx}`, - source: 'ip', - target: pathId, - type: 'smoothstep', - style: { stroke: '#06b6d4', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#06b6d4' }, - label: 'accède à', - labelStyle: { fill: '#06b6d4', fontWeight: 600, fontSize: 12 }, - }); - }); - } - - // Query Params (depuis entities) - if (filters.showQueryParam && entities?.query_params) { - entities.query_params.slice(0, 4).forEach((qp: any, idx: number) => { - const qpId = `qp-${idx}`; - const angle = (340 + (idx * 8)) * (Math.PI / 180); - const x = centerX + (radius + 220) * Math.cos(angle); - const y = centerY + (radius + 220) * Math.sin(angle); - - newNodes.push({ - id: qpId, - type: 'default', - data: { - label: ( -
-
🔑 Query Params
-
{qp.value}
-
- {qp.count} fois -
-
- ) - }, - position: { x, y }, - style: { background: 'transparent', border: 'none', width: 200 }, - }); - newEdges.push({ - id: `ip-qp-${idx}`, - source: 'ip', - target: qpId, - type: 'smoothstep', - style: { stroke: '#ec4899', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, - label: 'avec', - labelStyle: { fill: '#ec4899', fontWeight: 600, fontSize: 12 }, - }); - }); - } - - setGraphData({ nodes: newNodes, edges: newEdges }); - setNodes(newNodes); - setEdges(newEdges); - } catch (error) { - console.error('Error fetching correlation data:', error); + if (cancelled) return; + const [variability, subnet, entities] = await Promise.all([ + varRes.ok ? varRes.json().catch(() => null) : null, + subnetRes.ok ? subnetRes.json().catch(() => null) : null, + entitiesRes.ok ? entitiesRes.json().catch(() => null) : null, + ]); + if (cancelled) return; + setRawData({ variability, subnet, entities }); + } catch { + if (!cancelled) setError('Erreur de chargement des données de corrélation'); } finally { - setLoading(false); + if (!cancelled) setLoading(false); } }; - if (ip) { - fetchCorrelationData(); - } - }, [ip, filters, setNodes, setEdges]); + fetchData(); + return () => { + cancelled = true; + }; + }, [ip]); - const getCountryFlag = (code: string) => { - return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); - }; - - const toggleFilter = (key: keyof FilterState) => { - setFilters(prev => ({ ...prev, [key]: !prev[key] })); - }; - - if (loading) { - return ( -
-
Chargement du graph de corrélations...
-
- ); - } - - if (graphData.nodes.length === 0) { - return ( -
-
-
🕸️
-
Aucune corrélation trouvée pour {cleanIP(ip)}
-
-
- ); - } + const toggleFilter = useCallback((key: keyof FilterState) => { + setFilters((prev) => ({ ...prev, [key]: !prev[key] })); + }, []); return ( -
- - - - - {/* Panneau de filtres */} - -
Filtres
-
- - - - - - - - -
-
- - {/* Légende */} - -
Légende
-
-
-
- IP Source -
-
-
- Subnet /24 -
-
-
- ASN -
-
-
- JA4 -
-
-
- User-Agent -
-
-
- Host -
-
-
- Pays -
-
-
- - {/* Stats rapides */} - -
Statistiques
-
-
Nœuds: {nodes.length}
-
Arêtes: {edges.length}
-
IP: {cleanIP(ip)}
-
-
-
-
+ + + ); } diff --git a/frontend/src/components/DetailsView.tsx b/frontend/src/components/DetailsView.tsx index a0de8cb..0d65281 100644 --- a/frontend/src/components/DetailsView.tsx +++ b/frontend/src/components/DetailsView.tsx @@ -106,18 +106,21 @@ export function DetailsView() {
- {/* Insights */} - {data.insights.length > 0 && ( -
-

Insights

- {data.insights.map((insight, i) => ( - - ))} -
- )} + {/* Insights + Variabilité côte à côte */} +
+ {data.insights.length > 0 && ( +
+

Insights

+ {data.insights.map((insight, i) => ( + + ))} +
+ )} - {/* Variabilité */} - +
0 ? 'col-span-2' : 'col-span-3'}> + +
+
{/* Bouton retour */}
diff --git a/frontend/src/components/DetectionsList.tsx b/frontend/src/components/DetectionsList.tsx index 95bacec..e533ddc 100644 --- a/frontend/src/components/DetectionsList.tsx +++ b/frontend/src/components/DetectionsList.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { useDetections } from '../hooks/useDetections'; type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity'; @@ -13,6 +13,7 @@ interface ColumnConfig { } export function DetectionsList() { + const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const page = parseInt(searchParams.get('page') || '1'); @@ -311,13 +312,13 @@ export function DetectionsList() { {processedData.items.map((detection) => ( - { - window.location.href = `/detections/ip/${encodeURIComponent(detection.src_ip)}`; - }} - > + { + navigate(`/detections/ip/${encodeURIComponent(detection.src_ip)}`); + }} + > {columns.filter(col => col.visible).map(col => { if (col.key === 'ip_ja4') { const detectionAny = detection as any; diff --git a/frontend/src/components/EntityInvestigationView.tsx b/frontend/src/components/EntityInvestigationView.tsx index 1d54bea..4dd770c 100644 --- a/frontend/src/components/EntityInvestigationView.tsx +++ b/frontend/src/components/EntityInvestigationView.tsx @@ -39,6 +39,7 @@ export function EntityInvestigationView() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [showAllUA, setShowAllUA] = useState(false); useEffect(() => { if (!type || !value) { @@ -90,10 +91,6 @@ export function EntityInvestigationView() { return flags[code] || code; }; - const truncateUA = (ua: string, maxLength: number = 150) => { - if (ua.length <= maxLength) return ua; - return ua.substring(0, maxLength) + '...'; - }; if (loading) { return ( @@ -227,10 +224,10 @@ export function EntityInvestigationView() {

3. User-Agents

- {data.user_agents.slice(0, 10).map((ua, idx) => ( + {(showAllUA ? data.user_agents : data.user_agents.slice(0, 10)).map((ua, idx) => (
-
- {truncateUA(ua.value)} +
+ {ua.value}
{ua.count} requêtes
@@ -243,9 +240,12 @@ export function EntityInvestigationView() {
Aucun User-Agent
)} {data.user_agents.length > 10 && ( -
- +{data.user_agents.length - 10} autres User-Agents -
+ )}
diff --git a/frontend/src/components/FingerprintsView.tsx b/frontend/src/components/FingerprintsView.tsx new file mode 100644 index 0000000..5bcb175 --- /dev/null +++ b/frontend/src/components/FingerprintsView.tsx @@ -0,0 +1,1385 @@ +import { useState, useEffect, useCallback, Fragment } from 'react'; +import { useNavigate } from 'react-router-dom'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface JA4AttributeItem { + value: string; + count: number; +} + +interface AttributeValue { + value: string; + count: number; + percentage: number; +} + +interface VariabilityData { + total_detections: number; + unique_ips: number; + attributes: { + user_agents: AttributeValue[]; + countries: AttributeValue[]; + asns: AttributeValue[]; + hosts: AttributeValue[]; + }; + insights: { type: string; message: string }[]; +} + +interface IPsData { + ips: string[]; + total: number; +} + +type SortField = 'ip_count' | 'detections' | 'botnet_score'; +type ActiveTab = 'ja4' | 'spoofing' | 'ua_analysis'; + +// ─── Spoofing types ─────────────────────────────────────────────────────────── + +interface SpoofingItem { + ja4: string; + classification: 'spoofed_browser' | 'known_bot' | 'suspicious' | 'legitimate_browser'; + spoofing_score: number; + total_detections: number; + unique_ips: number; + indicators: { + ua_ch_mismatch_pct: number; + sni_mismatch_pct: number; + alpn_mismatch_pct: number; + avg_browser_score: number; + rare_ja4_pct: number; + ua_rotating_pct: number; + alpn_missing_count: number; + avg_asn_concentration: number; + avg_country_concentration: number; + }; + top_user_agents: { ua: string; type: string }[]; + threat_breakdown: { critical: number; high: number; last_level: string }; +} + +interface SpoofingResponse { + items: SpoofingItem[]; + total: number; + period_hours: number; + summary: { spoofed_browser: number; known_bot: number; suspicious: number; legitimate_browser: number }; +} + +interface UAItem { + user_agent: string; + type: 'bot' | 'browser' | 'script' | 'empty'; + ip_count: number; + unique_ja4_count: number; + sample_ja4s: string[]; + is_multi_ja4_suspect: boolean; + risk_flags: string[]; +} + +interface UAAnalysisResponse { + items: UAItem[]; + total: number; + ua_rotating_stats: { rotating_ip_count: number; sample_rotating_ips: string[] }; + summary: { bot_count: number; browser_count: number; script_count: number; multi_ja4_suspect_count: number }; +} + +interface JA4UAMatrixItem { + ja4: string; + unique_ips: number; + total_detections: number; + ua_ch_mismatch_pct: number; + avg_browser_score: number; + last_threat: string; + user_agents: { ua: string; count: number; pct: number; type: string }[]; + ua_summary: { bot_pct: number; browser_pct: number; script_pct: number; total_distinct: number }; + is_spoofing_suspect: boolean; +} + + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function classifyUA(ua: string): 'bot' | 'script' | 'normal' { + const u = ua.toLowerCase(); + if (u.includes('bot') || u.includes('crawler') || u.includes('spider')) return 'bot'; + if ( + u.includes('python') || + u.includes('curl') || + u.includes('wget') || + u.includes('go-http') || + u.includes('java/') || + u.includes('axios') + ) + return 'script'; + return 'normal'; +} + +function botnetScore(uniqueIps: number, botUaPct: number): number { + const ipScore = Math.min(uniqueIps / 200, 1) * 70; + const botBonus = (botUaPct / 100) * 30; + return Math.round(ipScore + botBonus); +} + +function getCountryFlag(code: string): string { + return code + .toUpperCase() + .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); +} + +function formatNumber(n: number): string { + return n.toLocaleString('fr-FR'); +} + +function botUaPercentage(userAgents: AttributeValue[]): number { + if (!userAgents.length) return 0; + const botOrScript = userAgents.filter((ua) => classifyUA(ua.value) !== 'normal'); + const total = userAgents.reduce((s, ua) => s + ua.count, 0); + const botTotal = botOrScript.reduce((s, ua) => s + ua.count, 0); + return total > 0 ? Math.round((botTotal / total) * 100) : 0; +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function StatCard({ + label, + value, + accent, +}: { + label: string; + value: string | number; + accent?: string; +}) { + return ( +
+
+ {typeof value === 'number' ? formatNumber(value) : value} +
+
{label}
+
+ ); +} + +function IPCountBadge({ count }: { count: number }) { + let cls = 'text-text-secondary'; + if (count > 50) cls = 'text-threat-critical font-bold'; + else if (count > 10) cls = 'text-threat-high font-semibold'; + else if (count > 3) cls = 'text-threat-medium'; + return {formatNumber(count)}; +} + +function BotnetScoreBar({ score }: { score: number }) { + let color = 'bg-threat-low'; + if (score >= 70) color = 'bg-threat-critical'; + else if (score >= 40) color = 'bg-threat-high'; + else if (score >= 20) color = 'bg-threat-medium'; + + return ( +
+
+
+
+ {score} +
+ ); +} + +function UABadge({ type }: { type: 'bot' | 'script' | 'normal' }) { + if (type === 'bot') + return ( + + bot + + ); + if (type === 'script') + return ( + + script + + ); + return ( + + normal + + ); +} + +function Skeleton({ width = 'w-24' }: { width?: string }) { + return
; +} + +function BarRow({ label, pct, count }: { label: string; pct: number; count: number }) { + return ( +
+ + {label} + +
+
+
+ {pct.toFixed(0)}% + {formatNumber(count)} +
+ ); +} + +// ─── Expanded row panel ──────────────────────────────────────────────────────── + +function ExpandedPanel({ + ja4, + variability, + ipsData, + loadingIps, + onNavigate, +}: { + ja4: string; + variability: VariabilityData; + ipsData: IPsData | null; + loadingIps: boolean; + onNavigate: (path: string) => void; +}) { + const { attributes } = variability; + + return ( +
+
+ {/* Panel 1 – IPs */} +
+
+ IPs utilisant ce JA4 + {ipsData && ( + ({formatNumber(ipsData.total)} total) + )} +
+ {loadingIps ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : ipsData && ipsData.ips.length > 0 ? ( +
    + {ipsData.ips.slice(0, 10).map((ip) => ( +
  • + +
  • + ))} +
+ ) : ( +

Aucune IP disponible

+ )} +
+ + {/* Panel 2 – User-Agents */} +
+
+ User-Agents +
+ {attributes.user_agents.length > 0 ? ( +
    + {attributes.user_agents.slice(0, 6).map((ua) => { + const type = classifyUA(ua.value); + return ( +
  • + + + {ua.value || '(vide)'} + + + {ua.percentage.toFixed(0)}% + +
  • + ); + })} +
+ ) : ( +

Aucun UA disponible

+ )} +
+ + {/* Panel 3 – Géographie */} +
+
+ Géographie +
+ {attributes.countries.length > 0 ? ( +
+ {attributes.countries.slice(0, 6).map((c) => ( +
+ + {getCountryFlag(c.value || 'XX')} + + {c.value} +
+
+
+ + {c.percentage.toFixed(0)}% + +
+ ))} +
+ ) : ( +

Aucune donnée géo

+ )} +
+ + {/* Panel 4 – ASNs */} +
+
+ ASNs +
+ {attributes.asns.length > 0 ? ( +
+ {attributes.asns.slice(0, 6).map((a) => ( + + ))} +
+ ) : ( +

Aucune donnée ASN

+ )} +
+
+ + {/* Insights */} + {variability.insights.length > 0 && ( +
+ {variability.insights.map((ins, i) => ( +
+ ⚠️ + {ins.message} +
+ ))} +
+ )} + + {/* Actions */} +
+ +
+
+ ); +} + +// ─── Spoofing Panel ─────────────────────────────────────────────────────────── + +function SpoofingPanel() { + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [matrixData, setMatrixData] = useState([]); + const [loading, setLoading] = useState(true); + const [activeSubTab, setActiveSubTab] = useState<'spoofing' | 'matrix'>('spoofing'); + const [hours, setHours] = useState(24); + + useEffect(() => { + const load = async () => { + setLoading(true); + try { + const [sRes, mRes] = await Promise.all([ + fetch(`/api/fingerprints/spoofing?hours=${hours}&min_detections=5`), + fetch(`/api/fingerprints/ja4-ua-matrix?hours=${hours}&min_ips=2`), + ]); + if (sRes.ok) setData(await sRes.json()); + if (mRes.ok) { + const md = await mRes.json(); + setMatrixData(md.items || []); + } + } catch (e) { + console.error('SpoofingPanel:', e); + } finally { + setLoading(false); + } + }; + load(); + }, [hours]); + + const classLabel: Record = { + spoofed_browser: { label: 'Spoofing navigateur', cls: 'bg-threat-critical/20 text-threat-critical border border-threat-critical/30', icon: '🎭' }, + known_bot: { label: 'Bot connu', cls: 'bg-threat-high/20 text-threat-high border border-threat-high/30', icon: '🤖' }, + suspicious: { label: 'Suspect', cls: 'bg-threat-medium/20 text-threat-medium border border-threat-medium/30', icon: '⚠️' }, + legitimate_browser: { label: 'Navigateur légitime', cls: 'bg-threat-low/20 text-threat-low border border-threat-low/30', icon: '✅' }, + }; + + if (loading) { + return ( +
+
Analyse des fingerprints en cours…
+
+ ); + } + + return ( +
+ {/* Controls */} +
+
+ {(['spoofing', 'matrix'] as const).map((t) => ( + + ))} +
+ +
+ + {/* Summary cards */} + {data && activeSubTab === 'spoofing' && ( +
+
+
{data.summary.spoofed_browser}
+
🎭 Spoofing navigateur
+
+
+
{data.summary.known_bot}
+
🤖 Bots connus
+
+
+
{data.summary.suspicious}
+
⚠️ Suspects
+
+
+
{data.summary.legitimate_browser}
+
✅ Légitimes
+
+
+ )} + + {/* Spoofing list */} + {activeSubTab === 'spoofing' && data && ( +
+
+ + JA4 suspects de spoofing — {data.total} fingerprints analysés + + Score de spoofing [0-100] +
+
+ {data.items.length === 0 && ( +
+ Aucun JA4 suspect détecté sur cette période +
+ )} + {data.items.map((item) => { + const cl = classLabel[item.classification] || classLabel.suspicious; + return ( +
+
+ {/* JA4 + classification */} +
+
+ {item.ja4} + + {cl.icon} {cl.label} + +
+ {/* Top UAs */} + {item.top_user_agents.length > 0 && ( +
+ {item.top_user_agents.slice(0, 3).map((u, i) => ( + + {u.ua.length > 35 ? u.ua.slice(0, 35) + '…' : u.ua} + + ))} +
+ )} +
+ + {/* Score + stats */} +
+ {/* Spoofing score */} +
+
+
= 70 + ? 'bg-threat-critical' + : item.spoofing_score >= 40 + ? 'bg-threat-high' + : item.spoofing_score >= 20 + ? 'bg-threat-medium' + : 'bg-threat-low' + }`} + style={{ width: `${item.spoofing_score}%` }} + /> +
+ + {item.spoofing_score} + +
+
+ {formatNumber(item.unique_ips)} IPs · {formatNumber(item.total_detections)} det. +
+
+
+ + {/* Indicators */} +
+ {[ + { label: 'UA/CH mismatch', value: `${item.indicators.ua_ch_mismatch_pct}%`, warn: item.indicators.ua_ch_mismatch_pct > 20 }, + { label: 'Browser score', value: `${item.indicators.avg_browser_score}/100`, warn: item.indicators.avg_browser_score > 50 }, + { label: 'SNI mismatch', value: `${item.indicators.sni_mismatch_pct}%`, warn: item.indicators.sni_mismatch_pct > 10 }, + { label: 'JA4 rare', value: `${item.indicators.rare_ja4_pct}%`, warn: item.indicators.rare_ja4_pct > 50 }, + { label: 'UA rotation', value: `${item.indicators.ua_rotating_pct}%`, warn: item.indicators.ua_rotating_pct > 10 }, + ].map((ind) => ( +
+
+ {ind.value} +
+
{ind.label}
+
+ ))} +
+ + {/* Actions */} +
+ +
+
+ ); + })} +
+
+ )} + + {/* JA4 × UA Matrix */} + {activeSubTab === 'matrix' && ( +
+
+ + Matrice JA4 × User-Agent — {matrixData.length} fingerprints + +

+ Pour chaque JA4, répartition des User-Agents observés. 🎭 = spoofing suspect détecté. +

+
+
+ {matrixData.map((item) => ( +
+
+ {item.ja4} + {item.is_spoofing_suspect && ( + + 🎭 Spoofing suspect + + )} +
+ {formatNumber(item.unique_ips)} IPs + UA/CH: 20 ? 'text-threat-high font-semibold' : 'text-text-secondary'}>{item.ua_ch_mismatch_pct}% + Browser score: 60 ? 'text-accent-primary' : 'text-text-secondary'}>{item.avg_browser_score} +
+
+
+ {item.user_agents.map((ua, i) => ( +
+ + {ua.type} + +
+
+
+ {ua.pct}% + + {ua.ua.length > 60 ? ua.ua.slice(0, 60) + '…' : ua.ua} + +
+ ))} +
+
+ 🤖 Bot: 50 ? 'text-threat-critical font-semibold' : 'text-text-secondary'}>{item.ua_summary.bot_pct}% + 🌐 Browser: {item.ua_summary.browser_pct}% + ⚙️ Script: {item.ua_summary.script_pct}% + + + +
+
+ ))} + {matrixData.length === 0 && ( +
+ Aucune donnée disponible +
+ )} +
+
+ )} +
+ ); +} + +// ─── UA Analysis Panel ──────────────────────────────────────────────────────── + +function UAAnalysisPanel() { + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState<'all' | 'bot' | 'browser' | 'script'>('all'); + const [hours, setHours] = useState(24); + + useEffect(() => { + const load = async () => { + setLoading(true); + try { + const res = await fetch(`/api/fingerprints/ua-analysis?hours=${hours}`); + if (res.ok) setData(await res.json()); + } catch (e) { + console.error('UAAnalysisPanel:', e); + } finally { + setLoading(false); + } + }; + load(); + }, [hours]); + + const flagLabels: Record = { + ua_bot_signature: { label: '🤖 Signature bot', cls: 'bg-threat-critical/20 text-threat-critical' }, + ua_script_library: { label: '⚙️ Librairie script', cls: 'bg-threat-medium/20 text-threat-medium' }, + ja4_rotation_suspect: { label: '🔄 Rotation JA4', cls: 'bg-threat-high/20 text-threat-high' }, + browser_ua_multi_fingerprint: { label: '🎭 Browser multi-JA4', cls: 'bg-threat-high/20 text-threat-high' }, + high_volume: { label: '📈 Volume élevé', cls: 'bg-accent-primary/20 text-accent-primary' }, + }; + + if (loading) { + return ( +
+
Analyse des User-Agents…
+
+ ); + } + if (!data) return null; + + const filtered = data.items.filter((i) => filter === 'all' || i.type === filter); + + return ( +
+ {/* Controls */} +
+ +
+ {(['all', 'bot', 'browser', 'script'] as const).map((f) => ( + + ))} +
+
+ + {/* Summary */} +
+
+
{data.summary.bot_count}
+
🤖 UAs bot
+
+
+
{data.summary.browser_count}
+
🌐 UAs navigateur
+
+
+
{data.summary.script_count}
+
⚙️ UAs script
+
+
+
{data.summary.multi_ja4_suspect_count}
+
🎭 Multi-JA4 suspects
+
+
+ + {/* UA Rotation alert */} + {data.ua_rotating_stats.rotating_ip_count > 0 && ( +
+ 🔄 +
+
+ {formatNumber(data.ua_rotating_stats.rotating_ip_count)} IPs avec rotation d'User-Agent détectée +
+
+ IPs exemples:{' '} + {data.ua_rotating_stats.sample_rotating_ips.slice(0, 5).map((ip, i) => ( + + ))} +
+
+
+ )} + + {/* UA list */} +
+
+ + User-Agents — {filtered.length} résultats + +
+
+ {filtered.map((item, i) => ( +
+
+ + {item.type === 'bot' ? '🤖' : item.type === 'browser' ? '🌐' : '⚙️'} {item.type} + +
+

+ {item.user_agent || — vide —} +

+
+ {item.risk_flags.map((flag) => { + const fl = flagLabels[flag]; + return fl ? ( + + {fl.label} + + ) : null; + })} + {item.unique_ja4_count > 0 && ( + + {item.unique_ja4_count} JA4 distinct{item.unique_ja4_count > 1 ? 's' : ''} + + )} +
+ {item.sample_ja4s.length > 0 && ( +
+ {item.sample_ja4s.map((j4, j) => ( + + ))} +
+ )} +
+
+
{formatNumber(item.ip_count)}
+
IPs
+
+
+
+ ))} + {filtered.length === 0 && ( +
+ Aucun User-Agent de ce type détecté +
+ )} +
+
+
+ ); +} + +// ─── Main component ─────────────────────────────────────────────────────────── + +export function FingerprintsView() { + const navigate = useNavigate(); + + // Raw attribute list + const [ja4List, setJa4List] = useState([]); + const [loadingList, setLoadingList] = useState(true); + + // Variability cache keyed by ja4 string + const [variabilityCache, setVariabilityCache] = useState>( + new Map() + ); + // IPs cache keyed by ja4 string + const [ipsCache, setIpsCache] = useState>(new Map()); + // Track which ja4s are currently being fetched + const [loadingVariability, setLoadingVariability] = useState>(new Set()); + const [loadingIps, setLoadingIps] = useState>(new Set()); + + // UI state + const [activeTab, setActiveTab] = useState('ja4'); + const [expandedJa4, setExpandedJa4] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [sortField, setSortField] = useState('ip_count'); + const [minIps, setMinIps] = useState(0); + const [copiedJa4, setCopiedJa4] = useState(null); + + // ── Fetch variability data for a single JA4 ── + const fetchVariability = useCallback( + async (ja4: string) => { + if (variabilityCache.has(ja4) || loadingVariability.has(ja4)) return; + + setLoadingVariability((prev) => new Set(prev).add(ja4)); + try { + const res = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4)}`); + if (!res.ok) return; + const data: VariabilityData = await res.json(); + setVariabilityCache((prev) => new Map(prev).set(ja4, data)); + } catch { + // silent — cells will show skeleton until available + } finally { + setLoadingVariability((prev) => { + const next = new Set(prev); + next.delete(ja4); + return next; + }); + } + }, + [variabilityCache, loadingVariability] + ); + + // ── Fetch IPs for a single JA4 ── + const fetchIps = useCallback( + async (ja4: string) => { + if (ipsCache.has(ja4) || loadingIps.has(ja4)) return; + + setLoadingIps((prev) => new Set(prev).add(ja4)); + try { + const res = await fetch( + `/api/variability/ja4/${encodeURIComponent(ja4)}/ips?limit=20` + ); + if (!res.ok) return; + const data: IPsData = await res.json(); + setIpsCache((prev) => new Map(prev).set(ja4, data)); + } catch { + // silent + } finally { + setLoadingIps((prev) => { + const next = new Set(prev); + next.delete(ja4); + return next; + }); + } + }, + [ipsCache, loadingIps] + ); + + // ── Initial load ── + useEffect(() => { + const load = async () => { + setLoadingList(true); + try { + const res = await fetch('/api/attributes/ja4?limit=200'); + if (!res.ok) throw new Error('Erreur chargement JA4'); + const data = await res.json(); + const items: JA4AttributeItem[] = data.items || []; + setJa4List(items); + + // Auto-enrich top 30 by count + const top30 = items + .slice() + .sort((a, b) => b.count - a.count) + .slice(0, 30); + + await Promise.all( + top30.map(async (item) => { + try { + const vRes = await fetch( + `/api/variability/ja4/${encodeURIComponent(item.value)}` + ); + if (!vRes.ok) return; + const vData: VariabilityData = await vRes.json(); + setVariabilityCache((prev) => new Map(prev).set(item.value, vData)); + } catch { + // ignore individual errors + } + }) + ); + } catch (err) { + console.error('FingerprintsView:', err); + } finally { + setLoadingList(false); + } + }; + load(); + }, []); + + // ── Handle row expansion ── + const handleExpand = useCallback( + (ja4: string) => { + if (expandedJa4 === ja4) { + setExpandedJa4(null); + return; + } + setExpandedJa4(ja4); + fetchVariability(ja4); + fetchIps(ja4); + }, + [expandedJa4, fetchVariability, fetchIps] + ); + + const handleCopy = useCallback((ja4: string, e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(ja4).catch(() => {}); + setCopiedJa4(ja4); + setTimeout(() => setCopiedJa4(null), 1500); + }, []); + + // ── Derived stats ── + const totalActive = ja4List.length; + const withTenPlus = ja4List.filter((j) => { + const v = variabilityCache.get(j.value); + const ips = v ? v.unique_ips : j.count; + return ips >= 10; + }).length; + const withFiftyPlus = ja4List.filter((j) => { + const v = variabilityCache.get(j.value); + const ips = v ? v.unique_ips : j.count; + return ips >= 50; + }).length; + + const avgScore = (() => { + if (!ja4List.length) return 0; + const scores = ja4List.map((j) => { + const v = variabilityCache.get(j.value); + if (!v) return 0; + return botnetScore(v.unique_ips, botUaPercentage(v.attributes.user_agents)); + }); + return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length); + })(); + + // ── Filter + sort ── + const filtered = ja4List + .filter((j) => { + if (searchQuery && !j.value.toLowerCase().includes(searchQuery.toLowerCase())) return false; + const v = variabilityCache.get(j.value); + const ips = v ? v.unique_ips : j.count; + if (minIps > 0 && ips < minIps) return false; + return true; + }) + .sort((a, b) => { + const va = variabilityCache.get(a.value); + const vb = variabilityCache.get(b.value); + if (sortField === 'ip_count') { + const ia = va ? va.unique_ips : a.count; + const ib = vb ? vb.unique_ips : b.count; + return ib - ia; + } + if (sortField === 'detections') { + const da = va ? va.total_detections : a.count; + const db = vb ? vb.total_detections : b.count; + return db - da; + } + // botnet_score + const sa = va + ? botnetScore(va.unique_ips, botUaPercentage(va.attributes.user_agents)) + : 0; + const sb = vb + ? botnetScore(vb.unique_ips, botUaPercentage(vb.attributes.user_agents)) + : 0; + return sb - sa; + }); + + // ── Loading state ── + if (loadingList) { + return ( +
+
Chargement des fingerprints JA4…
+
+ ); + } + + return ( +
+ {/* ── Header ── */} +
+

🔏 JA4 Fingerprint Intelligence

+

+ Analyse des fingerprints TLS · détection de botnets · identification du spoofing +

+
+ + {/* ── Tab bar ── */} +
+ {([ + { id: 'ja4', label: '🔏 JA4 Actifs', desc: 'Fingerprints TLS & IPs associées' }, + { id: 'spoofing', label: '🎭 Spoofing JA4', desc: 'Détection spoofing navigateur' }, + { id: 'ua_analysis', label: '🧬 Analyse UA', desc: 'User-Agents & rotation' }, + ] as { id: ActiveTab; label: string; desc: string }[]).map((tab) => ( + + ))} +
+ + {/* ── Spoofing tab ── */} + {activeTab === 'spoofing' && } + + {/* ── UA Analysis tab ── */} + {activeTab === 'ua_analysis' && } + + {/* ── JA4 tab (original content) ── */} + {activeTab === 'ja4' && (<> + +
+ + 0 ? 'text-threat-high' : undefined} + /> + 0 ? 'text-threat-critical' : undefined} + /> + +
+ + {/* ── Filter bar ── */} +
+ setSearchQuery(e.target.value)} + className="flex-1 px-3 py-2 rounded-lg bg-background-secondary border border-background-card text-text-primary text-sm placeholder:text-text-disabled focus:outline-none focus:border-accent-primary" + /> + +
+ + setMinIps(Math.max(0, parseInt(e.target.value) || 0))} + className="w-20 px-2 py-2 rounded-lg bg-background-secondary border border-background-card text-text-primary text-sm focus:outline-none focus:border-accent-primary" + /> +
+
+ + {/* ── Table ── */} +
+ + + + + + + + + + + + + + + + {filtered.map((item) => { + const v = variabilityCache.get(item.value); + const isLoadingV = loadingVariability.has(item.value); + const isExpanded = expandedJa4 === item.value; + + const uniqueIps = v ? v.unique_ips : item.count; + const detections = v ? v.total_detections : item.count; + const botUaPct = v ? botUaPercentage(v.attributes.user_agents) : 0; + const score = v ? botnetScore(uniqueIps, botUaPct) : 0; + const topCountry = v?.attributes.countries[0]?.value ?? null; + const topAsn = v?.attributes.asns[0]?.value ?? null; + const warningInsights = v + ? v.insights.filter((ins) => ins.type === 'warning').length + : 0; + + return ( + + handleExpand(item.value)} + > + {/* JA4 */} + + + {/* IPs actives */} + + + {/* Détections */} + + + {/* Score botnet */} + + + {/* Top pays */} + + + {/* Top ASN */} + + + {/* % Bot UA */} + + + {/* Insights */} + + + {/* Actions */} + + + + {/* Expanded panel */} + {isExpanded && v && ( + + + + )} + + {/* Loading state for expanded panel */} + {isExpanded && !v && ( + + + + )} + + ); + })} + +
+ JA4 + + IPs actives + + Détections + + Score botnet + + Top pays + + Top ASN + + % Bot UA + + Insights + + Actions +
+
+ + {item.value.length > 26 + ? `${item.value.slice(0, 12)}…${item.value.slice(-10)}` + : item.value} + + +
+
+ {isLoadingV && !v ? : } + + {isLoadingV && !v ? : formatNumber(detections)} + + {isLoadingV && !v ? ( + + ) : ( + + )} + + {isLoadingV && !v ? ( + + ) : topCountry ? ( + {getCountryFlag(topCountry)} + ) : ( + + )} + + {isLoadingV && !v ? ( + + ) : topAsn ? ( + + {topAsn.length > 22 ? `${topAsn.slice(0, 22)}…` : topAsn} + + ) : ( + + )} + + {isLoadingV && !v ? ( + + ) : ( + 70 + ? 'text-threat-critical font-bold' + : botUaPct > 40 + ? 'text-threat-high' + : botUaPct > 20 + ? 'text-threat-medium' + : 'text-text-secondary' + } + > + {botUaPct}% + + )} + + {warningInsights > 0 ? ( + + {warningInsights} + + ) : ( + + )} + e.stopPropagation()}> + +
+ +
+ Chargement du profil… +
+ + {filtered.length === 0 && ( +
+ Aucun fingerprint JA4 trouvé +
+ )} +
+ + {/* Results count */} +
+ {formatNumber(filtered.length)} fingerprint{filtered.length !== 1 ? 's' : ''} affiché + {filtered.length !== 1 ? 's' : ''} sur {formatNumber(totalActive)} +
+ )} +
+ ); +} diff --git a/frontend/src/components/IncidentsView.tsx b/frontend/src/components/IncidentsView.tsx index e6158ab..0fd5600 100644 --- a/frontend/src/components/IncidentsView.tsx +++ b/frontend/src/components/IncidentsView.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { QuickSearch } from './QuickSearch'; interface IncidentCluster { id: string; @@ -124,10 +123,7 @@ export function IncidentsView() {

Surveillance en temps réel - 24 dernières heures

-
-
- -
+
{/* Critical Metrics */} @@ -212,8 +208,10 @@ export function IncidentsView() {
)} - {/* Priority Incidents */} -
+ {/* Main content: incidents list (2/3) + top threats table (1/3) */} +
+ {/* Incidents list — 2/3 */} +

Incidents Prioritaires @@ -367,41 +365,35 @@ export function IncidentsView() {

)}
-
+
{/* end col-span-2 */} - {/* Top Active Threats */} -
-

- Top Menaces Actives -

-
- - - - - - - - - - - - - - - {clusters.slice(0, 10).map((cluster, index) => ( - navigate(`/investigation/${cluster.subnet?.split('/')[0] || ''}`)} + {/* Top threats sidebar — 1/3 */} +
+
+
+

🔥 Top Menaces

+
+
+ {clusters.slice(0, 12).map((cluster, index) => ( +
navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)} > -
- - - - - - - - + + {cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} + + + ))} - -
#EntitéTypeScorePaysASNHits/sTendance
{index + 1} - {cluster.subnet?.split('/')[0] || 'Unknown'} - IP - {index + 1} +
+
+ {cluster.sample_ip || cluster.subnet?.split('/')[0] || 'Unknown'} +
+
+ {cluster.countries[0] && ( + {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} + )} + AS{cluster.asn || '?'} +
+
+
+ 80 ? 'bg-red-500 text-white' : cluster.score > 60 ? 'bg-orange-500 text-white' : cluster.score > 40 ? 'bg-yellow-500 text-white' : @@ -409,33 +401,25 @@ export function IncidentsView() { }`}> {cluster.score} -
- {cluster.countries[0] && ( - <> - {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} - - )} - - AS{cluster.asn || '?'} - - {Math.round(cluster.total_detections / 24) || 0} - - {cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} {cluster.trend_percentage}% -
+ {clusters.length === 0 && ( +
+ Aucune menace active +
+ )} +
+
-
+
{/* end grid */}
); } diff --git a/frontend/src/components/InvestigationPanel.tsx b/frontend/src/components/InvestigationPanel.tsx index 7b77e18..2255cf9 100644 --- a/frontend/src/components/InvestigationPanel.tsx +++ b/frontend/src/components/InvestigationPanel.tsx @@ -41,6 +41,7 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [classifying, setClassifying] = useState(false); + const [showAllUA, setShowAllUA] = useState(false); useEffect(() => { const fetchData = async () => { @@ -193,9 +194,9 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi 🤖 User-Agents ({data.attributes.user_agents.length})
- {data.attributes.user_agents.slice(0, 5).map((ua: any, idx: number) => ( + {(showAllUA ? data.attributes.user_agents : data.attributes.user_agents.slice(0, 5)).map((ua: any, idx: number) => (
-
+
{ua.value}
@@ -203,6 +204,14 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi
))} + {data.attributes.user_agents.length > 5 && ( + + )}
)} diff --git a/frontend/src/components/InvestigationView.tsx b/frontend/src/components/InvestigationView.tsx index 632b812..718e455 100644 --- a/frontend/src/components/InvestigationView.tsx +++ b/frontend/src/components/InvestigationView.tsx @@ -1,13 +1,156 @@ import { useParams, useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; import { SubnetAnalysis } from './analysis/SubnetAnalysis'; import { CountryAnalysis } from './analysis/CountryAnalysis'; import { JA4Analysis } from './analysis/JA4Analysis'; import { UserAgentAnalysis } from './analysis/UserAgentAnalysis'; import { CorrelationSummary } from './analysis/CorrelationSummary'; import { CorrelationGraph } from './CorrelationGraph'; -import { InteractiveTimeline } from './InteractiveTimeline'; import { ReputationPanel } from './ReputationPanel'; +// ─── Spoofing Coherence Widget ───────────────────────────────────────────── + +interface CoherenceData { + verdict: string; + spoofing_score: number; + explanation: string[]; + indicators: { + ua_ch_mismatch_rate: number; + sni_mismatch_rate: number; + avg_browser_score: number; + distinct_ja4_count: number; + is_ua_rotating: boolean; + rare_ja4_rate: number; + }; + fingerprints: { ja4_list: string[]; latest_ja4: string }; + user_agents: { ua: string; count: number; type: string }[]; +} + +const VERDICT_STYLE: Record = { + high_confidence_spoofing: { cls: 'bg-threat-critical/10 border-threat-critical/40 text-threat-critical', icon: '🎭', label: 'Spoofing haute confiance' }, + suspicious_spoofing: { cls: 'bg-threat-high/10 border-threat-high/40 text-threat-high', icon: '⚠️', label: 'Spoofing suspect' }, + known_bot_no_spoofing: { cls: 'bg-threat-medium/10 border-threat-medium/40 text-threat-medium', icon: '🤖', label: 'Bot connu (pas de spoofing)' }, + legitimate_browser: { cls: 'bg-threat-low/10 border-threat-low/40 text-threat-low', icon: '✅', label: 'Navigateur légitime' }, + inconclusive: { cls: 'bg-background-card border-background-card text-text-secondary', icon: '❓', label: 'Non concluant' }, +}; + +function FingerprintCoherenceWidget({ ip }: { ip: string }) { + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + setLoading(true); + setError(false); + fetch(`/api/fingerprints/ip/${encodeURIComponent(ip)}/coherence`) + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { if (d) setData(d); else setError(true); }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, [ip]); + + const vs = data ? (VERDICT_STYLE[data.verdict] ?? VERDICT_STYLE.inconclusive) : null; + + return ( +
+

🎭 Cohérence JA4 / User-Agent

+ {loading &&
Analyse en cours…
} + {error &&
Données insuffisantes pour cette IP
} + {data && vs && ( +
+ {/* Verdict badge + score */} +
+ {vs.icon} +
+
{vs.label}
+
+ Score de spoofing: {data.spoofing_score}/100 +
+
+
+
+
= 70 ? 'bg-threat-critical' : + data.spoofing_score >= 40 ? 'bg-threat-high' : + data.spoofing_score >= 20 ? 'bg-threat-medium' : 'bg-threat-low' + }`} + style={{ width: `${data.spoofing_score}%` }} + /> +
+
+
+ + {/* Explanation */} +
    + {data.explanation.map((e, i) => ( +
  • + {e} +
  • + ))} +
+ + {/* Key indicators */} +
+ {[ + { label: 'UA/CH mismatch', value: `${data.indicators.ua_ch_mismatch_rate}%`, warn: data.indicators.ua_ch_mismatch_rate > 20 }, + { label: 'Browser score', value: `${data.indicators.avg_browser_score}/100`, warn: data.indicators.avg_browser_score > 60 }, + { label: 'JA4 distincts', value: data.indicators.distinct_ja4_count, warn: data.indicators.distinct_ja4_count > 2 }, + { label: 'JA4 rares', value: `${data.indicators.rare_ja4_rate}%`, warn: data.indicators.rare_ja4_rate > 50 }, + ].map((ind) => ( +
+
{ind.value}
+
{ind.label}
+
+ ))} +
+ + {/* Top UAs */} + {data.user_agents.length > 0 && ( +
+
User-Agents observés
+
+ {data.user_agents.slice(0, 4).map((u, i) => ( +
+ {u.type} + + {u.ua.length > 45 ? u.ua.slice(0, 45) + '…' : u.ua} + +
+ ))} +
+
+ )} + + {/* JA4 links */} + {data.fingerprints.ja4_list.length > 0 && ( +
+
JA4 utilisés
+
+ {data.fingerprints.ja4_list.map((j4) => ( + + ))} +
+
+ )} +
+ )} +
+ ); +} + export function InvestigationView() { const { ip } = useParams<{ ip: string }>(); const navigate = useNavigate(); @@ -45,41 +188,47 @@ export function InvestigationView() {
- {/* Panels d'analyse */} -
- {/* NOUVEAU: Réputation IP */} -
-

🌍 Réputation IP (Bases publiques)

- + {/* Ligne 1 : Réputation (1/3) + Graph de corrélations (2/3) */} +
+
+

🌍 Réputation IP

+
- - {/* NOUVEAU: Graph de corrélations */} -
+

🕸️ Graph de Corrélations

- +
+
- {/* NOUVEAU: Timeline interactive */} -
-

📈 Timeline d'Activité

- -
- - {/* Panel 1: Subnet/ASN */} + {/* Ligne 2 : Subnet / Country / JA4 (3 colonnes) */} +
- - {/* Panel 2: Country (relatif à l'IP) */} - - {/* Panel 3: JA4 */} +
- {/* Panel 4: User-Agents */} + {/* Ligne 3 : User-Agents (1/2) + Classification (1/2) */} +
- - {/* Panel 5: Correlation Summary + Classification */}
+ + {/* Ligne 4 : Cohérence JA4/UA (spoofing) */} +
+ +
+

🔏 JA4 Légitimes (baseline)

+

+ Comparez les fingerprints de cette IP avec la baseline des JA4 légitimes pour évaluer le risque de spoofing. +

+ +
+
); } diff --git a/frontend/src/components/JA4InvestigationView.tsx b/frontend/src/components/JA4InvestigationView.tsx index c33f7de..f07e768 100644 --- a/frontend/src/components/JA4InvestigationView.tsx +++ b/frontend/src/components/JA4InvestigationView.tsx @@ -139,10 +139,6 @@ export function JA4InvestigationView() { } }; - const truncateUA = (ua: string, maxLength = 80) => { - if (ua.length <= maxLength) return ua; - return ua.substring(0, maxLength) + '...'; - }; return (
@@ -180,155 +176,127 @@ export function JA4InvestigationView() {
- - - - + + + +
- {/* Panel 1: Top IPs */} -
-

1. TOP IPs (Utilisant ce JA4)

-
- {data.top_ips.length > 0 ? ( - data.top_ips.map((ipData, idx) => ( -
-
- {idx + 1}. - -
-
-
{ipData.count.toLocaleString()}
-
{ipData.percentage.toFixed(1)}%
-
-
- )) - ) : ( -
- Aucune IP trouvée -
- )} -
- {data.unique_ips > 10 && ( -

- ... et {data.unique_ips - 10} autres IPs -

- )} -
- - {/* Panel 2: Top Pays */} -
-

2. TOP Pays

-
- {data.top_countries.map((country, idx) => ( -
-
-
- {getFlag(country.code)} -
- {country.name} ({country.code}) + {/* Ligne 2: Top IPs (gauche) | Top Pays + Top ASNs (droite empilés) */} +
+ {/* Top IPs */} +
+

📍 TOP IPs

+
+ {data.top_ips.length > 0 ? ( + data.top_ips.map((ipData, idx) => ( +
+
+ {idx + 1}. + +
+
+
{ipData.count.toLocaleString()}
+
{ipData.percentage.toFixed(1)}%
-
-
{country.count.toLocaleString()}
-
{country.percentage.toFixed(1)}%
+ )) + ) : ( +
Aucune IP trouvée
+ )} + {data.unique_ips > 10 && ( +

+ ... et {data.unique_ips - 10} autres IPs +

+ )} +
+
+ + {/* Top Pays + Top ASNs empilés */} +
+
+

🌍 TOP Pays

+
+ {data.top_countries.map((country, idx) => ( +
+
+
+ {getFlag(country.code)} + {country.name} ({country.code}) +
+
+
{country.count.toLocaleString()}
+
{country.percentage.toFixed(1)}%
+
+
+
+
+
-
-
-
-
+ ))}
- ))} +
+ +
+

🏢 TOP ASNs

+
+ {data.top_asns.map((asn, idx) => ( +
+
+
{asn.asn} - {asn.org}
+
+
{asn.count.toLocaleString()}
+
{asn.percentage.toFixed(1)}%
+
+
+
+
+
+
+ ))} +
+
- {/* Panel 3: Top ASN */} -
-

3. TOP ASN

-
- {data.top_asns.map((asn, idx) => ( -
-
-
- {asn.asn} - {asn.org} -
-
-
{asn.count.toLocaleString()}
-
{asn.percentage.toFixed(1)}%
-
-
-
-
-
-
- ))} -
-
- - {/* Panel 4: Top Hosts */} -
-

4. TOP Hosts Ciblés

-
- {data.top_hosts.map((host, idx) => ( -
-
-
- {host.host} -
-
-
{host.count.toLocaleString()}
-
{host.percentage.toFixed(1)}%
-
-
-
-
-
-
- ))} -
-
- - {/* Panel 5: User-Agents + Classification */} -
+ {/* Ligne 3: Top Hosts (gauche) | User-Agents (droite) */} +
-

5. User-Agents

+

🖥️ TOP Hosts Ciblés

+
+ {data.top_hosts.map((host, idx) => ( +
+
+
{host.host}
+
+
{host.count.toLocaleString()}
+
{host.percentage.toFixed(1)}%
+
+
+
+
+
+
+ ))} +
+
+ +
+

🤖 User-Agents

{data.user_agents.map((ua, idx) => (
-
- {truncateUA(ua.ua)} -
+
{ua.ua}
{getClassificationBadge(ua.classification)}
@@ -338,16 +306,14 @@ export function JA4InvestigationView() {
))} {data.user_agents.length === 0 && ( -
- Aucun User-Agent trouvé -
+
Aucun User-Agent trouvé
)}
- - {/* Classification JA4 */} -
+ + {/* Ligne 4: Classification JA4 (full width) */} +
); } diff --git a/frontend/src/components/PivotView.tsx b/frontend/src/components/PivotView.tsx new file mode 100644 index 0000000..034cce7 --- /dev/null +++ b/frontend/src/components/PivotView.tsx @@ -0,0 +1,367 @@ +/** + * PivotView — Cross-entity correlation matrix + * + * SOC analysts add multiple IPs or JA4 fingerprints. + * The page fetches variability data for each entity and renders a + * comparison matrix highlighting shared values (correlations). + * + * Columns = entities added by the analyst + * Rows = attribute categories (JA4, User-Agent, Country, ASN, Subnet) + * ★ = value shared by 2+ entities → possible campaign link + */ + +import { useState, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type EntityType = 'ip' | 'ja4'; + +interface EntityCol { + id: string; + type: EntityType; + value: string; + loading: boolean; + error: string | null; + data: VariabilityData | null; +} + +interface AttrItem { + value: string; + count: number; + percentage: number; +} + +interface VariabilityData { + total_detections: number; + unique_ips?: number; + attributes: { + ja4?: AttrItem[]; + user_agents?: AttrItem[]; + countries?: AttrItem[]; + asns?: AttrItem[]; + hosts?: AttrItem[]; + subnets?: AttrItem[]; + }; +} + +type AttrKey = keyof VariabilityData['attributes']; + +const ATTR_ROWS: { key: AttrKey; label: string; icon: string }[] = [ + { key: 'ja4', label: 'JA4 Fingerprint', icon: '🔐' }, + { key: 'user_agents', label: 'User-Agents', icon: '🤖' }, + { key: 'countries', label: 'Pays', icon: '🌍' }, + { key: 'asns', label: 'ASN', icon: '🏢' }, + { key: 'hosts', label: 'Hosts cibles', icon: '🖥️' }, + { key: 'subnets', label: 'Subnets', icon: '🔷' }, +]; + +const MAX_VALUES_PER_CELL = 4; + +function detectType(input: string): EntityType { + // JA4 fingerprints are ~36 chars containing letters, digits, underscores + // IP addresses match IPv4 pattern + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(input.trim())) return 'ip'; + return 'ja4'; +} + +function getCountryFlag(code: string): string { + return (code || '').toUpperCase().replace(/./g, c => + String.fromCodePoint(c.charCodeAt(0) + 127397) + ); +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export function PivotView() { + const navigate = useNavigate(); + const [cols, setCols] = useState([]); + const [input, setInput] = useState(''); + + const fetchEntity = useCallback(async (col: EntityCol): Promise => { + try { + const encoded = encodeURIComponent(col.value); + const url = col.type === 'ip' + ? `/api/variability/ip/${encoded}` + : `/api/variability/ja4/${encoded}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data: VariabilityData = await res.json(); + return { ...col, loading: false, data }; + } catch (e) { + return { ...col, loading: false, error: (e as Error).message }; + } + }, []); + + const addEntity = useCallback(async () => { + const val = input.trim(); + if (!val) return; + + // Support batch: comma-separated + const values = val.split(/[,\s]+/).map(v => v.trim()).filter(Boolean); + setInput(''); + + for (const v of values) { + const id = `${Date.now()}-${v}`; + const type = detectType(v); + const pending: EntityCol = { id, type, value: v, loading: true, error: null, data: null }; + setCols(prev => { + if (prev.some(c => c.value === v)) return prev; + return [...prev, pending]; + }); + const resolved = await fetchEntity(pending); + setCols(prev => prev.map(c => c.id === id ? resolved : c)); + } + }, [input, fetchEntity]); + + const removeCol = (id: string) => setCols(prev => prev.filter(c => c.id !== id)); + + // Find shared values across all loaded cols for a given attribute key + const getSharedValues = (key: AttrKey): Set => { + const loaded = cols.filter(c => c.data); + if (loaded.length < 2) return new Set(); + const valueCounts = new Map(); + for (const col of loaded) { + const items = col.data?.attributes[key] ?? []; + for (const item of items.slice(0, 10)) { + valueCounts.set(item.value, (valueCounts.get(item.value) ?? 0) + 1); + } + } + const shared = new Set(); + valueCounts.forEach((count, val) => { if (count >= 2) shared.add(val); }); + return shared; + }; + + const sharedByKey = Object.fromEntries( + ATTR_ROWS.map(r => [r.key, getSharedValues(r.key)]) + ) as Record>; + + const totalCorrelations = ATTR_ROWS.reduce( + (sum, r) => sum + sharedByKey[r.key].size, 0 + ); + + return ( +
+ {/* Header */} +
+

🔗 Pivot — Corrélation Multi-Entités

+

+ Ajoutez des IPs ou JA4. Les valeurs partagées révèlent des campagnes coordonnées. +

+
+ + {/* Input bar */} +
+ setInput(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') addEntity(); }} + placeholder="IP (ex: 1.2.3.4) ou JA4, séparés par des virgules…" + className="flex-1 bg-background-secondary border border-background-card rounded-lg px-4 py-2.5 text-text-primary placeholder-text-disabled font-mono text-sm focus:outline-none focus:border-accent-primary" + /> + + {cols.length > 0 && ( + + )} +
+ + {/* Empty state */} + {cols.length === 0 && ( +
+
🔗
+
Aucune entité ajoutée
+
+ Entrez plusieurs IPs ou fingerprints JA4 pour identifier des corrélations — JA4 partagés, même pays d'origine, mêmes cibles, même ASN. +
+
+ Exemple : 1.2.3.4, 5.6.7.8, 9.10.11.12 +
+
+ )} + + {/* Correlation summary badge */} + {cols.length >= 2 && cols.some(c => c.data) && ( +
0 + ? 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400' + : 'bg-background-secondary border-background-card text-text-secondary' + }`}> + {totalCorrelations > 0 ? '⚠️' : 'ℹ️'} +
+ {totalCorrelations > 0 ? ( + <> + {totalCorrelations} corrélation{totalCorrelations > 1 ? 's' : ''} détectée{totalCorrelations > 1 ? 's' : ''} + {' '}— attributs partagés par 2+ entités. Possible campagne coordonnée. + + ) : ( + 'Aucune corrélation détectée entre les entités analysées.' + )} +
+
+ )} + + {/* Matrix */} + {cols.length > 0 && ( +
+ + {/* Column headers */} + + + + {cols.map(col => ( + + ))} + + + + {/* Attribute rows */} + + {ATTR_ROWS.map(row => { + const shared = sharedByKey[row.key]; + const hasAnyData = cols.some(c => (c.data?.attributes[row.key]?.length ?? 0) > 0); + if (!hasAnyData && cols.every(c => c.data !== null && !c.loading)) return null; + + return ( + + + + {cols.map(col => { + const items = col.data?.attributes[row.key] ?? []; + return ( + + ); + })} + + ); + })} + +
+ Attribut + +
+
+
+ + {col.type === 'ip' ? '🌐' : '🔐'} + + +
+ {col.data && ( +
+ {col.data.total_detections.toLocaleString()} det. + {col.type === 'ja4' && col.data.unique_ips !== undefined && ( + <> · {col.data.unique_ips} IPs + )} +
+ )} + {col.loading && ( +
Chargement…
+ )} + {col.error && ( +
⚠ {col.error}
+ )} +
+ +
+
+
+ {row.icon} + {row.label} +
+ {shared.size > 0 && ( +
+ + ★ {shared.size} commun{shared.size > 1 ? 's' : ''} + +
+ )} +
+ {col.loading ? ( +
+ ) : items.length === 0 ? ( + + ) : ( +
+ {items.slice(0, MAX_VALUES_PER_CELL).map((item, i) => { + const isShared = shared.has(item.value); + return ( +
+
+
+ {isShared && } + {row.key === 'countries' + ? `${getCountryFlag(item.value)} ${item.value}` + : item.value.length > 60 + ? item.value.slice(0, 60) + '…' + : item.value} +
+
+
+ {item.count.toLocaleString()} + {item.percentage.toFixed(1)}% +
+
+ ); + })} + {items.length > MAX_VALUES_PER_CELL && ( +
+ +{items.length - MAX_VALUES_PER_CELL} autres +
+ )} +
+ )} +
+
+ )} + + {/* Legend */} + {cols.length >= 2 && ( +
+ + + Valeur partagée par 2+ entités ★ + + | + Cliquer sur une entité → Investigation complète +
+ )} +
+ ); +} diff --git a/frontend/src/components/QuickSearch.tsx b/frontend/src/components/QuickSearch.tsx index e6af991..9cab761 100644 --- a/frontend/src/components/QuickSearch.tsx +++ b/frontend/src/components/QuickSearch.tsx @@ -235,7 +235,7 @@ export function QuickSearch({ onNavigate }: QuickSearchProps) {
-
- -
- {/* Stats Summary */} -
+ {/* Stats Summary — 4 colonnes compact */} +
Total IPs
{stats.total_ips}
@@ -150,6 +146,10 @@ export function SubnetInvestigation() {
User-Agents Uniques
{stats.unique_ua}
+
+ + {/* Infos secondaires — 4 colonnes */} +
Hosts Uniques
{stats.unique_hosts}
@@ -166,8 +166,8 @@ export function SubnetInvestigation() {
Période
-
- {formatDate(stats.first_seen)} - {formatDate(stats.last_seen)} +
+ {formatDate(stats.first_seen)} – {formatDate(stats.last_seen)}
diff --git a/frontend/src/components/ThreatIntelView.tsx b/frontend/src/components/ThreatIntelView.tsx index a521fa7..724ba5c 100644 --- a/frontend/src/components/ThreatIntelView.tsx +++ b/frontend/src/components/ThreatIntelView.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { QuickSearch } from './QuickSearch'; interface Classification { ip?: string; @@ -119,7 +118,6 @@ export function ThreatIntelView() { Base de connaissances des classifications SOC

-
{/* Statistics */} @@ -150,169 +148,140 @@ export function ThreatIntelView() { />
- {/* Filters */} -
-
- {/* Search */} - setSearch(e.target.value)} - placeholder="Rechercher IP, JA4, tag, commentaire..." - className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary placeholder-text-secondary focus:outline-none focus:border-accent-primary" - /> - - {/* Label Filter */} - - - {/* Tag Filter */} - -
- - {(search || filterLabel !== 'all' || filterTag) && ( -
-
- {filteredClassifications.length} résultat(s) -
- + {/* Main content: sidebar filtres (1/4) + table (3/4) */} +
+ {/* Sidebar filtres + tags */} +
+
+

🔍 Recherche

+ setSearch(e.target.value)} + placeholder="IP, JA4, tag, commentaire..." + className="w-full bg-background-card border border-background-card rounded-lg px-3 py-2 text-text-primary placeholder-text-secondary focus:outline-none focus:border-accent-primary text-sm" + />
- )} -
- {/* Top Tags */} -
-

🏷️ Tags Populaires (30j)

-
- {allTags.slice(0, 20).map(tag => { - const count = classifications.filter(c => c.tags.includes(tag)).length; - return ( - - ); - })} -
-
- - {/* Classifications Table */} -
-
-

- 📋 Classifications Récentes -

-
-
- - - - - - - - - - - - - {filteredClassifications.slice(0, 50).map((classification, idx) => ( - - - - - - - - +
+

🏷️ Label

+
+ {(['all', 'malicious', 'suspicious', 'legitimate'] as const).map(lbl => ( + ))} -
-
DateEntitéLabelTagsConfianceAnalyste
- {new Date(classification.created_at).toLocaleDateString('fr-FR', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - })} - -
- {classification.ip || classification.ja4} -
-
- - {getLabelIcon(classification.label)} {classification.label.toUpperCase()} - - -
- {classification.tags.slice(0, 5).map((tag, tagIdx) => ( - - {tag} - - ))} - {classification.tags.length > 5 && ( - - +{classification.tags.length - 5} - - )} -
-
-
-
-
-
- - {(classification.confidence * 100).toFixed(0)}% - -
-
- {classification.analyst} -
-
- {filteredClassifications.length === 0 && ( -
-
🔍
-
Aucune classification trouvée
+
- )} + +
+

🏷️ Tags populaires

+
+ {allTags.slice(0, 20).map(tag => { + const count = classifications.filter(c => c.tags.includes(tag)).length; + return ( + + ); + })} +
+
+ + {(search || filterLabel !== 'all' || filterTag) && ( +
+
{filteredClassifications.length} résultat(s)
+ +
+ )} +
+ + {/* Table classifications (3/4) */} +
+
+

+ 📋 Classifications Récentes + ({filteredClassifications.length}) +

+
+
+ + + + + + + + + + + + + {filteredClassifications.slice(0, 50).map((classification, idx) => ( + + + + + + + + + ))} + +
DateEntitéLabelTagsConfianceAnalyste
+ {new Date(classification.created_at).toLocaleDateString('fr-FR', { + day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' + })} + +
+ {classification.ip || classification.ja4} +
+
+ + {getLabelIcon(classification.label)} {classification.label.toUpperCase()} + + +
+ {classification.tags.slice(0, 5).map((tag, tagIdx) => ( + {tag} + ))} + {classification.tags.length > 5 && ( + +{classification.tags.length - 5} + )} +
+
+
+
+
+
+ {(classification.confidence * 100).toFixed(0)}% +
+
{classification.analyst}
+
+ {filteredClassifications.length === 0 && ( +
+
🔍
+
Aucune classification trouvée
+
+ )} +
); diff --git a/frontend/src/components/VariabilityPanel.tsx b/frontend/src/components/VariabilityPanel.tsx index cc7db84..9b17dab 100644 --- a/frontend/src/components/VariabilityPanel.tsx +++ b/frontend/src/components/VariabilityPanel.tsx @@ -53,37 +53,7 @@ export function VariabilityPanel({ attributes }: VariabilityPanelProps) { {/* User-Agents */} {attributes.user_agents && attributes.user_agents.length > 0 && ( -
-

- User-Agents ({attributes.user_agents.length}) -

-
- {attributes.user_agents.slice(0, 10).map((item, index) => ( -
-
-
- {item.value} -
-
-
{item.count}
-
{item.percentage?.toFixed(1)}%
-
-
-
-
-
-
- ))} -
- {attributes.user_agents.length > 10 && ( -

- ... et {attributes.user_agents.length - 10} autres (top 10 affiché) -

- )} -
+ )} {/* Pays */} @@ -199,6 +169,50 @@ export function VariabilityPanel({ attributes }: VariabilityPanelProps) { ); } +// Composant UASection — jamais de troncature, expand/collapse +function UASection({ items }: { items: AttributeValue[] }) { + const [showAll, setShowAll] = useState(false); + const INITIAL = 5; + const displayed = showAll ? items : items.slice(0, INITIAL); + + return ( +
+

+ User-Agents ({items.length}) +

+
+ {displayed.map((item, index) => ( +
+
+
+ {item.value} +
+
+
{item.count}
+
{item.percentage?.toFixed(1)}%
+
+
+
+
+
+
+ ))} +
+ {items.length > INITIAL && ( + + )} +
+ ); +} + // Composant AttributeSection function AttributeSection({ title, @@ -284,7 +298,7 @@ function AttributeRow({
{getValue(value)} diff --git a/frontend/src/components/analysis/UserAgentAnalysis.tsx b/frontend/src/components/analysis/UserAgentAnalysis.tsx index aa07bd3..cb3d322 100644 --- a/frontend/src/components/analysis/UserAgentAnalysis.tsx +++ b/frontend/src/components/analysis/UserAgentAnalysis.tsx @@ -22,6 +22,8 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [showAllIpUA, setShowAllIpUA] = useState(false); + const [showAllJa4UA, setShowAllJa4UA] = useState(false); useEffect(() => { const fetchUserAgentAnalysis = async () => { @@ -60,20 +62,17 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) { const getClassificationBadge = (classification: string) => { switch (classification) { case 'normal': - return ✅ Normal; + return ✅ Normal; case 'bot': - return ⚠️ Bot; + return ⚠️ Bot; case 'script': - return ❌ Script; + return ❌ Script; default: return null; } }; - const truncateUA = (ua: string, maxLength = 80) => { - if (ua.length <= maxLength) return ua; - return ua.substring(0, maxLength) + '...'; - }; + const INITIAL_COUNT = 5; return (
@@ -86,18 +85,18 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) { )}
-
+
{/* User-Agents pour cette IP */}
User-Agents pour cette IP ({data.ip_user_agents.length})
- {data.ip_user_agents.slice(0, 5).map((ua, idx) => ( + {(showAllIpUA ? data.ip_user_agents : data.ip_user_agents.slice(0, INITIAL_COUNT)).map((ua, idx) => (
-
- {truncateUA(ua.value)} +
+ {ua.value}
{getClassificationBadge(ua.classification)}
@@ -111,6 +110,16 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
Aucun User-Agent trouvé
)}
+ {data.ip_user_agents.length > INITIAL_COUNT && ( + + )}
{/* User-Agents pour le JA4 */} @@ -119,11 +128,11 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) { User-Agents pour le JA4 (toutes IPs)
- {data.ja4_user_agents.slice(0, 5).map((ua, idx) => ( + {(showAllJa4UA ? data.ja4_user_agents : data.ja4_user_agents.slice(0, INITIAL_COUNT)).map((ua, idx) => (
-
- {truncateUA(ua.value)} +
+ {ua.value}
{getClassificationBadge(ua.classification)}
@@ -134,6 +143,16 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
))}
+ {data.ja4_user_agents.length > INITIAL_COUNT && ( + + )}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 679a9ec..04cd7a4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { ThemeProvider } from './ThemeContext' import './styles/globals.css' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index d2753d5..a8dd851 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -2,63 +2,58 @@ @tailwind components; @tailwind utilities; -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +/* ── Dark theme (default, SOC standard) ── */ +:root, +[data-theme="dark"] { color-scheme: dark; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + --color-bg: 15 23 42; /* Slate 900 */ + --color-bg-secondary: 30 41 59; /* Slate 800 */ + --color-bg-card: 51 65 85; /* Slate 700 */ + --color-text-primary: 248 250 252;/* Slate 50 */ + --color-text-secondary:148 163 184;/* Slate 400 */ + --color-text-disabled: 100 116 139;/* Slate 500 */ + --scrollbar-track: #1e293b; + --scrollbar-thumb: #475569; + --scrollbar-thumb-hover: #64748b; + --border-color: rgba(148,163,184,0.12); +} + +/* ── Light theme ── */ +[data-theme="light"] { + color-scheme: light; + --color-bg: 241 245 249;/* Slate 100 */ + --color-bg-secondary: 255 255 255;/* White */ + --color-bg-card: 226 232 240;/* Slate 200 */ + --color-text-primary: 15 23 42; /* Slate 900 */ + --color-text-secondary:71 85 105; /* Slate 600 */ + --color-text-disabled: 148 163 184;/* Slate 400 */ + --scrollbar-track: #f1f5f9; + --scrollbar-thumb: #cbd5e1; + --scrollbar-thumb-hover: #94a3b8; + --border-color: rgba(15,23,42,0.1); } body { margin: 0; min-width: 320px; min-height: 100vh; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Scrollbar personnalisée */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--scrollbar-track); } +::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } -::-webkit-scrollbar-track { - background: #1E293B; -} +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } +@keyframes pulse-red { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } } -::-webkit-scrollbar-thumb { - background: #475569; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #64748B; -} - -/* Animations */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.animate-fade-in { - animation: fadeIn 0.3s ease-in-out; -} - -.animate-slide-up { - animation: slideUp 0.4s ease-out; -} +.animate-fade-in { animation: fadeIn 0.25s ease-in-out; } +.animate-slide-up { animation: slideUp 0.35s ease-out; } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 73e20d7..4fb7c3a 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,34 +7,34 @@ export default { theme: { extend: { colors: { - // Thème sombre Security Dashboard + // Backgrounds — CSS-variable driven for dark/light theming background: { - DEFAULT: '#0F172A', // Slate 900 - secondary: '#1E293B', // Slate 800 - card: '#334155', // Slate 700 + DEFAULT: 'rgb(var(--color-bg) / )', + secondary: 'rgb(var(--color-bg-secondary) / )', + card: 'rgb(var(--color-bg-card) / )', }, text: { - primary: '#F8FAFC', // Slate 50 - secondary: '#94A3B8', // Slate 400 - disabled: '#64748B', // Slate 500 + primary: 'rgb(var(--color-text-primary) / )', + secondary: 'rgb(var(--color-text-secondary)/ )', + disabled: 'rgb(var(--color-text-disabled) / )', }, - // Menaces + // Threat levels — vivid, same in both themes threat: { - critical: '#EF4444', // Red 500 + critical: '#EF4444', critical_bg: '#7F1D1D', - high: '#F97316', // Orange 500 - high_bg: '#7C2D12', - medium: '#EAB308', // Yellow 500 - medium_bg: '#713F12', - low: '#22C55E', // Green 500 - low_bg: '#14532D', + high: '#F97316', + high_bg: '#7C2D12', + medium: '#EAB308', + medium_bg: '#713F12', + low: '#22C55E', + low_bg: '#14532D', }, // Accents accent: { - primary: '#3B82F6', // Blue 500 - success: '#10B981', // Emerald 500 - } - } + primary: '#3B82F6', + success: '#10B981', + }, + }, }, }, plugins: [], diff --git a/test_dashboard.sh b/test_dashboard.sh deleted file mode 100755 index e90b86a..0000000 --- a/test_dashboard.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -echo "╔════════════════════════════════════════════════════════════╗" -echo "║ Dashboard Bot Detector - Test Complet ║" -echo "╚════════════════════════════════════════════════════════════╝" -echo "" - -TESTS_PASSED=0 -TESTS_FAILED=0 - -# Test 1: Health check -echo "🧪 Test 1: Health check..." -HEALTH=$(curl -s http://localhost:3000/health | jq -r '.status') -if [ "$HEALTH" = "healthy" ]; then - echo "✅ Health check: $HEALTH" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Health check failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 2: API detections endpoint -echo "🧪 Test 2: API detections endpoint..." -DETECTIONS=$(curl -s "http://localhost:3000/api/detections?page=1&page_size=5" | jq '.total') -if [ "$DETECTIONS" != "null" ] && [ "$DETECTIONS" -gt 0 ]; then - echo "✅ Detections API: $DETECTIONS détections" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Detections API failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 3: Tri par score par défaut -echo "🧪 Test 3: Tri par score par défaut..." -FIRST_SCORE=$(curl -s "http://localhost:3000/api/detections?page=1&page_size=1&sort_by=anomaly_score&sort_order=asc" | jq '.items[0].anomaly_score') -if [ "$FIRST_SCORE" != "null" ]; then - echo "✅ Tri par score: $FIRST_SCORE (scores négatifs en premier)" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Tri par score failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 4: Endpoint variability IP -echo "🧪 Test 4: Endpoint variability IP..." -VAR_IP=$(curl -s "http://localhost:3000/api/variability/ip/116.179.33.143" | jq '.total_detections') -if [ "$VAR_IP" != "null" ]; then - echo "✅ Variability IP: $VAR_IP détections" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Variability IP failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 5: Endpoint IPs associées -echo "🧪 Test 5: Endpoint IPs associées..." -IPS=$(curl -s "http://localhost:3000/api/variability/country/CN/ips?limit=5" | jq '.total') -if [ "$IPS" != "null" ] && [ "$IPS" -gt 0 ]; then - echo "✅ IPs associées: $IPS IPs totales" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ IPs associées failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 6: Endpoint user_agents -echo "🧪 Test 6: Endpoint user_agents..." -UA=$(curl -s "http://localhost:3000/api/variability/ip/116.179.33.143/user_agents?limit=5" | jq '.total') -if [ "$UA" != "null" ]; then - echo "✅ User-Agents: $UA user-agents" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ User-Agents failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 7: Endpoint analysis subnet -echo "🧪 Test 7: Endpoint analysis subnet..." -SUBNET=$(curl -s "http://localhost:3000/api/analysis/116.179.33.143/subnet" | jq '.total_in_subnet') -if [ "$SUBNET" != "null" ]; then - echo "✅ Analysis Subnet: $SUBNET IPs" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Analysis Subnet failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 8: Endpoint analysis country -echo "🧪 Test 8: Endpoint analysis country..." -COUNTRY=$(curl -s "http://localhost:3000/api/analysis/116.179.33.143/country" | jq '.ip_country.code') -if [ "$COUNTRY" != "null" ]; then - echo "✅ Analysis Country: $COUNTRY" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Analysis Country failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 9: Endpoint classifications -echo "🧪 Test 9: Endpoint classifications..." -CLASSIF=$(curl -s "http://localhost:3000/api/analysis/classifications?limit=5" | jq '.total') -if [ "$CLASSIF" != "null" ]; then - echo "✅ Classifications: $CLASSIF classifications" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Classifications failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -# Test 10: Frontend accessible -echo "🧪 Test 10: Frontend accessible..." -FRONTEND=$(curl -s http://localhost:3000/ | grep -c "Bot Detector Dashboard") -if [ "$FRONTEND" -gt 0 ]; then - echo "✅ Frontend: Dashboard accessible" - TESTS_PASSED=$((TESTS_PASSED+1)) -else - echo "❌ Frontend failed" - TESTS_FAILED=$((TESTS_FAILED+1)) -fi - -echo "" -echo "════════════════════════════════════════════════════════════" -echo " Tests passés: $TESTS_PASSED" -echo " Tests échoués: $TESTS_FAILED" -echo "════════════════════════════════════════════════════════════" - -if [ "$TESTS_FAILED" -eq 0 ]; then - echo "" - echo "✅ Tous les tests sont passés avec succès ! 🎉" - echo "" - exit 0 -else - echo "" - echo "❌ Certains tests ont échoué." - echo "" - exit 1 -fi diff --git a/test_dashboard_entities.sql b/test_dashboard_entities.sql deleted file mode 100644 index bc10ea4..0000000 --- a/test_dashboard_entities.sql +++ /dev/null @@ -1,431 +0,0 @@ --- ============================================================================= --- TESTS DE VALIDATION - Dashboard Entities --- ============================================================================= --- Exécuter chaque bloc pour valider le fonctionnement complet --- ============================================================================= - -USE mabase_prod; - --- ============================================================================= --- TEST 1 : Vérifier que la table existe --- ============================================================================= -SELECT 'TEST 1: Table exists' AS test; -SELECT name, engine -FROM system.tables -WHERE database = 'mabase_prod' - AND name = 'view_dashboard_entities'; - --- ============================================================================= --- TEST 2 : Vérifier que la vue matérialisée existe --- ============================================================================= -SELECT 'TEST 2: Materialized View exists' AS test; -SELECT name, engine -FROM system.tables -WHERE database = 'mabase_prod' - AND name = 'view_dashboard_entities_mv'; - --- ============================================================================= --- TEST 3 : Vérifier le schéma de la table --- ============================================================================= -SELECT 'TEST 3: Table schema' AS test; -DESCRIBE TABLE mabase_prod.view_dashboard_entities; - --- ============================================================================= --- TEST 4 : Compter les entités par type --- ============================================================================= -SELECT 'TEST 4: Count by entity type' AS test; -SELECT - entity_type, - count() AS total_entities, - sum(requests) AS total_requests -FROM mabase_prod.view_dashboard_entities -GROUP BY entity_type -ORDER BY total_entities DESC; - --- ============================================================================= --- TEST 5 : Vérifier les données IP --- ============================================================================= -SELECT 'TEST 5: IP entities sample' AS test; -SELECT - entity_value AS ip, - requests, - unique_ips, - arraySlice(user_agents, 1, 3) AS sample_user_agents, - arraySlice(paths, 1, 5) AS sample_paths, - arraySlice(asns, 1, 2) AS sample_asns, - arraySlice(countries, 1, 2) AS sample_countries -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'ip' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 6 : Vérifier les données JA4 --- ============================================================================= -SELECT 'TEST 6: JA4 entities sample' AS test; -SELECT - entity_value AS ja4, - requests, - unique_ips, - arraySlice(user_agents, 1, 3) AS sample_user_agents, - arraySlice(countries, 1, 2) AS sample_countries -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'ja4' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 7 : Vérifier les données User-Agent --- ============================================================================= -SELECT 'TEST 7: User-Agent entities sample' AS test; -SELECT - entity_value AS user_agent, - requests, - unique_ips, - arraySlice(paths, 1, 3) AS sample_paths -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'user_agent' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 8 : Vérifier les données Client Header --- ============================================================================= -SELECT 'TEST 8: Client Header entities sample' AS test; -SELECT - entity_value AS client_header, - requests, - unique_ips -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'client_header' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 9 : Vérifier les données Host --- ============================================================================= -SELECT 'TEST 9: Host entities sample' AS test; -SELECT - entity_value AS host, - requests, - unique_ips, - arraySlice(user_agents, 1, 3) AS sample_user_agents -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'host' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 10 : Vérifier les données Path --- ============================================================================= -SELECT 'TEST 10: Path entities sample' AS test; -SELECT - entity_value AS path, - requests, - unique_ips, - arraySlice(user_agents, 1, 2) AS sample_user_agents -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'path' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 11 : Vérifier les données Query Param --- ============================================================================= -SELECT 'TEST 11: Query Param entities sample' AS test; -SELECT - entity_value AS query_params, - requests, - unique_ips -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'query_param' -ORDER BY requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 12 : Vérifier les ASN (ne doit pas être vide) --- ============================================================================= -SELECT 'TEST 12: ASN data validation' AS test; -SELECT - entity_type, - countIf(length(asns) > 0 AND asns[1] != '') AS with_asn, - countIf(length(asns) = 0 OR asns[1] = '') AS without_asn -FROM mabase_prod.view_dashboard_entities -GROUP BY entity_type; - --- ============================================================================= --- TEST 13 : Vérifier les Countries (ne doit pas être vide) --- ============================================================================= -SELECT 'TEST 13: Country data validation' AS test; -SELECT - entity_type, - countIf(length(countries) > 0 AND countries[1] != '') AS with_country, - countIf(length(countries) = 0 OR countries[1] = '') AS without_country -FROM mabase_prod.view_dashboard_entities -GROUP BY entity_type; - --- ============================================================================= --- TEST 14 : Top 10 des IPs les plus actives --- ============================================================================= -SELECT 'TEST 14: Top 10 IPs' AS test; -SELECT - entity_value AS ip, - sum(requests) AS total_requests, - sum(unique_ips) AS total_unique_ips -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'ip' -GROUP BY entity_value -ORDER BY total_requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 15 : Top 10 des JA4 les plus actifs --- ============================================================================= -SELECT 'TEST 15: Top 10 JA4' AS test; -SELECT - entity_value AS ja4, - sum(requests) AS total_requests, - sum(unique_ips) AS total_unique_ips -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'ja4' -GROUP BY entity_value -ORDER BY total_requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 16 : Top 10 des User-Agents --- ============================================================================= -SELECT 'TEST 16: Top 10 User-Agents' AS test; -SELECT - entity_value AS user_agent, - sum(requests) AS total_requests -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'user_agent' -GROUP BY entity_value -ORDER BY total_requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 17 : Top 10 des Hosts --- ============================================================================= -SELECT 'TEST 17: Top 10 Hosts' AS test; -SELECT - entity_value AS host, - sum(requests) AS total_requests -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'host' -GROUP BY entity_value -ORDER BY total_requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 18 : Top 10 des Paths --- ============================================================================= -SELECT 'TEST 18: Top 10 Paths' AS test; -SELECT - entity_value AS path, - sum(requests) AS total_requests -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'path' -GROUP BY entity_value -ORDER BY total_requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 19 : Activité par date (derniers 7 jours) --- ============================================================================= -SELECT 'TEST 19: Activity by date (last 7 days)' AS test; -SELECT - log_date, - entity_type, - sum(requests) AS total_requests, - sum(unique_ips) AS total_unique_ips -FROM mabase_prod.view_dashboard_entities -WHERE log_date >= today() - INTERVAL 7 DAY -GROUP BY log_date, entity_type -ORDER BY log_date DESC, entity_type; - --- ============================================================================= --- TEST 20 : Requête de corrélation IP + JA4 + User-Agent --- ============================================================================= -SELECT 'TEST 20: Correlation IP + JA4 + User-Agent' AS test; -SELECT - ip.entity_value AS ip, - ip.ja4, - arraySlice(ip.user_agents, 1, 3) AS user_agents, - ip.requests AS ip_requests -FROM mabase_prod.view_dashboard_entities AS ip -WHERE ip.entity_type = 'ip' -ORDER BY ip.requests DESC -LIMIT 5; - --- ============================================================================= --- TEST 21 : Vérifier les types de données --- ============================================================================= -SELECT 'TEST 21: Data types validation' AS test; -SELECT - 'requests type' AS check, - toTypeName(requests) AS type -FROM mabase_prod.view_dashboard_entities -LIMIT 1 -UNION ALL -SELECT - 'unique_ips type', - toTypeName(unique_ips) -FROM mabase_prod.view_dashboard_entities -LIMIT 1 -UNION ALL -SELECT - 'user_agents type', - toTypeName(user_agents) -FROM mabase_prod.view_dashboard_entities -LIMIT 1 -UNION ALL -SELECT - 'asns type', - toTypeName(asns) -FROM mabase_prod.view_dashboard_entities -LIMIT 1 -UNION ALL -SELECT - 'countries type', - toTypeName(countries) -FROM mabase_prod.view_dashboard_entities -LIMIT 1; - --- ============================================================================= --- TEST 22 : Vérifier qu'il n'y a pas de valeurs NULL critiques --- ============================================================================= -SELECT 'TEST 22: NULL check' AS test; -SELECT - 'entity_type' AS field, - countIf(entity_type IS NULL) AS null_count -FROM mabase_prod.view_dashboard_entities -UNION ALL -SELECT - 'entity_value', - countIf(entity_value IS NULL) -FROM mabase_prod.view_dashboard_entities -UNION ALL -SELECT - 'src_ip', - countIf(src_ip IS NULL) -FROM mabase_prod.view_dashboard_entities -UNION ALL -SELECT - 'log_date', - countIf(log_date IS NULL) -FROM mabase_prod.view_dashboard_entities -UNION ALL -SELECT - 'requests', - countIf(requests IS NULL) -FROM mabase_prod.view_dashboard_entities; - --- ============================================================================= --- TEST 23 : Stats globales --- ============================================================================= -SELECT 'TEST 23: Global stats' AS test; -SELECT - count() AS total_rows, - countDistinct(entity_value) AS unique_entities, - sum(requests) AS total_requests, - sum(unique_ips) AS total_unique_ips, - min(log_date) AS min_date, - max(log_date) AS max_date -FROM mabase_prod.view_dashboard_entities; - --- ============================================================================= --- TEST 24 : Vérifier les index --- ============================================================================= -SELECT 'TEST 24: Index check' AS test; -SELECT - name, - type, - expr, - granularity -FROM system.data_skipping_indices -WHERE table = 'view_dashboard_entities' - AND database = 'mabase_prod'; - --- ============================================================================= --- TEST 25 : Performance test - Requête avec filtre --- ============================================================================= -SELECT 'TEST 25: Performance test' AS test; -SELECT - entity_type, - count() AS count, - sum(requests) AS sum_requests -FROM mabase_prod.view_dashboard_entities -WHERE entity_type IN ('ip', 'ja4') - AND log_date >= today() - INTERVAL 1 DAY -GROUP BY entity_type; - --- ============================================================================= --- TEST 26 : Vérifier la TTL (date d'expiration) --- ============================================================================= -SELECT 'TEST 26: TTL check' AS test; -SELECT - name, - engine, - ttl_expression -FROM system.tables -WHERE database = 'mabase_prod' - AND name = 'view_dashboard_entities'; - --- ============================================================================= --- TEST 27 : Distinct values par entité --- ============================================================================= -SELECT 'TEST 27: Distinct values per entity' AS test; -SELECT - entity_type, - countDistinct(entity_value) AS distinct_values, - countDistinct(src_ip) AS distinct_src_ips, - countDistinct(ja4) AS distinct_ja4, - countDistinct(host) AS distinct_hosts -FROM mabase_prod.view_dashboard_entities -GROUP BY entity_type; - --- ============================================================================= --- TEST 28 : Query params avec plusieurs paramètres --- ============================================================================= -SELECT 'TEST 28: Multi query params' AS test; -SELECT - entity_value AS query_params, - requests, - length(splitByString(',', entity_value)) AS param_count -FROM mabase_prod.view_dashboard_entities -WHERE entity_type = 'query_param' -ORDER BY param_count DESC, requests DESC -LIMIT 10; - --- ============================================================================= --- TEST 29 : Countries distribution --- ============================================================================= -SELECT 'TEST 29: Countries distribution' AS test; -SELECT - arrayJoin(countries) AS country, - count() AS occurrences -FROM mabase_prod.view_dashboard_entities -WHERE length(countries) > 0 AND countries[1] != '' -GROUP BY country -ORDER BY occurrences DESC -LIMIT 15; - --- ============================================================================= --- TEST 30 : ASN distribution --- ============================================================================= -SELECT 'TEST 30: ASN distribution' AS test; -SELECT - arrayJoin(asns) AS asn, - count() AS occurrences -FROM mabase_prod.view_dashboard_entities -WHERE length(asns) > 0 AND asns[1] != '' -GROUP BY asn -ORDER BY occurrences DESC -LIMIT 15; - --- ============================================================================= --- FIN DES TESTS --- ============================================================================= -SELECT '=== ALL TESTS COMPLETED ===' AS status; diff --git a/test_report_2026-03-14.md b/test_report_2026-03-14.md deleted file mode 100644 index dc76496..0000000 --- a/test_report_2026-03-14.md +++ /dev/null @@ -1,244 +0,0 @@ -# 🧪 Rapport de Tests - Bot Detector Dashboard - -**Date:** 2026-03-14 19:57 UTC -**Version:** 1.0 -**Testeur:** MCP Playwright + Shell - ---- - -## 📊 Résumé Exécutif - -| Catégorie | Tests Passés | Tests Échoués | Total | Taux de Réussite | -|-----------|--------------|---------------|-------|------------------| -| **Backend API** | 9 | 1 | 10 | 90% | -| **Frontend (Playwright)** | 5 | 0 | 5 | 100% | -| **ClickHouse** | 0 | 0 | 0 | N/A ⚠️ | -| **TOTAL** | **14** | **1** | **15** | **93%** | - ---- - -## 1. Tests Backend API (9/10 ✅) - -### ✅ Tests Réussis - -| ID | Test | Résultat | Détails | -|----|------|----------|---------| -| H1 | Health check | ✅ PASS | `{"status": "healthy", "clickhouse": "connected"}` | -| M1 | Métriques globales | ✅ PASS | 23,879 détections, 4 niveaux de menace | -| M2 | Série temporelle 24h | ✅ PASS | 24 points horaires retournés | -| D1 | Liste détections | ✅ PASS | 23,879 détections, pagination fonctionnelle | -| D3 | Tri par score | ✅ PASS | Score: -0.147 (croissant) | -| VI1 | IPs associées (Country CN) | ✅ PASS | 1,530 IPs | -| VU1 | User-Agents par IP | ✅ PASS | 1 user-agent | -| AS1 | Analysis subnet | ✅ PASS | 57 IPs dans le subnet | -| AC1 | Analysis country | ✅ PASS | CN (China) | -| ET1 | Classifications | ✅ PASS | 2 classifications | -| F1 | Frontend accessible | ✅ PASS | Dashboard HTML servi | - -### ❌ Tests Échoués - -| ID | Test | Erreur | Cause Racine | -|----|------|--------|--------------| -| V1 | Variability IP | ❌ FAIL | **Bug Pydantic v2** - Erreur de sérialisation des `AttributeValue` dans les champs `ja4`, `countries`, `asns`, `hosts`, `threat_levels`, `model_names` | - -**Détail de l'erreur:** -``` -6 validation errors for VariabilityAttributes -ja4.0: Input should be a valid dictionary or instance of AttributeValue -countries.0: Input should be a valid dictionary or instance of AttributeValue -asns.0: Input should be a valid dictionary or instance of AttributeValue -hosts.0: Input should be a valid dictionary or instance of AttributeValue -threat_levels.0: Input should be a valid dictionary or instance of AttributeValue -model_names.0: Input should be a valid dictionary or instance of AttributeValue -``` - -**Recommandation:** Corriger le modèle Pydantic `VariabilityResponse` dans `backend/api/variability.py` pour utiliser la bonne sérialisation des objets `AttributeValue`. - ---- - -## 2. Tests Frontend (5/5 ✅) - -### Navigation et Routing - -| ID | Test | Résultat | Détails | -|----|------|----------|---------| -| N1 | Page d'accueil | ✅ PASS | Dashboard affiché avec 4 cartes de métriques | -| N2 | Navigation Détections | ✅ PASS | Tableau avec 15 lignes, pagination (956 pages) | -| N5 | URL directe | ✅ PASS | http://192.168.1.2:3000/detections fonctionnel | - -### Dashboard Principal - -| ID | Test | Résultat | Détails | -|----|------|----------|---------| -| DH1 | Métriques affichées | ✅ PASS | Total: 23,879, Bots: 7,001, IPs: 17,607 | -| DH2 | Graphique temporel | ✅ PASS | Évolution 24h avec 24 points | -| DH3 | Distribution par menace | ✅ PASS | CRITICAL: 0, HIGH: 0, MEDIUM: 5,221, LOW: 11,657 | - -### Liste des Détections - -| ID | Test | Résultat | Détails | -|----|------|----------|---------| -| DL1 | Tableau affiché | ✅ PASS | 9 colonnes (IP/JA4, Host, Modèle, Score, Hits, Velocity, ASN, Pays, Date) | -| DL2 | Pagination | ✅ PASS | Page 1/956, boutons Précédent/Suivant | -| DL3 | Tri colonnes | ✅ PASS | Headers cliquables avec indicateurs ⇅ | - ---- - -## 3. Tests ClickHouse (N/A ⚠️) - -| ID | Test | Statut | Commentaire | -|----|------|--------|-------------| -| DB1-DB7 | Tables et vues | ⚠️ N/A | ClickHouse local non démarré (service distant: test-sdv-anubis.sdv.fr) | -| DQ1-DQ5 | Qualité données | ⚠️ N/A | Nécessite accès direct au serveur distant | -| DP1-DP5 | Performance | ⚠️ N/A | Nécessite accès direct au serveur distant | - -**Recommandation:** Exécuter les tests SQL manuellement via: -```bash -docker compose exec clickhouse clickhouse-client -d mabase_prod < test_dashboard_entities.sql -``` - ---- - -## 4. Performance API - -| Endpoint | Temps de Réponse | Statut | -|----------|------------------|--------| -| GET /health | < 100ms | ✅ Excellent | -| GET /api/metrics | < 500ms | ✅ Bon | -| GET /api/detections?page=1 | < 1s | ✅ Bon | -| GET /api/analysis/subnet | < 500ms | ✅ Bon | - ---- - -## 5. Bugs Identifiés - -### 🔴 Bug Critique: Sérialisation Pydantic - -**Fichier:** `backend/api/variability.py` -**Endpoint:** `/api/variability/ip/{ip}` -**Impact:** Investigation IP indisponible (erreur 500) - -**Solution recommandée:** -```python -# Dans VariabilityAttributes, utiliser dict au lieu de AttributeValue -# Ou implémenter un custom serializer -from pydantic import field_serializer - -class VariabilityAttributes(BaseModel): - ja4: List[Dict] - countries: List[Dict] - # ... - - @field_serializer('ja4', 'countries', 'asns', 'hosts', 'threat_levels', 'model_names') - def serialize_attributes(self, value): - return [v.model_dump() if hasattr(v, 'model_dump') else v for v in value] -``` - ---- - -## 6. Couverture des Tests (vs TEST_PLAN.md) - -### Endpoints API Testés - -| Routeur | Endpoints dans le Plan | Tests Exécutés | Couverture | -|---------|------------------------|----------------|------------| -| `/health` | H1-H3 | H1 | 33% | -| `/api/metrics` | M1-M5, MT1-MT2 | M1, M2 | 25% | -| `/api/detections` | D1-D11, DD1-DD3 | D1, D3 | 14% | -| `/api/variability` | V1-V8, VI1-VI3, VA1-VA3, VU1-VU2 | V1❌, VI1, VU1 | 25% | -| `/api/analysis` | AS1-AS4, AC1-AC2 | AS1, AC1 | 33% | -| `/api/entities` | E1-E10, ER1, EU1-EU4, ET1 | ET1 | 7% | - -**Couverture API actuelle:** ~23% (3/13 endpoints principaux testés) - -### Frontend Testés - -| Fonctionnalité | Tests dans le Plan | Tests Exécutés | Couverture | -|----------------|-------------------|----------------|------------| -| Navigation | N1-N5 | N1, N2, N5 | 60% | -| Dashboard | DH1-DH7 | DH1, DH2, DH3 | 43% | -| Détections | DL1-DL8 | DL1, DL2, DL3 | 38% | -| Investigation | DV1-DV8 | 0 (bug backend) | 0% | - -**Couverture Frontend actuelle:** ~35% - ---- - -## 7. Recommandations - -### Priorité 1 (Critique) 🔴 - -1. **Corriger le bug Pydantic** dans `backend/api/variability.py` - - Impact: Investigation IP/JA4/Country/ASN indisponible - - Effort estimé: 1-2 heures - -2. **Ajouter des tests unitaires backend** avec pytest - - Structure: `backend/tests/test_variability.py` - - Couvrir les modèles Pydantic - -### Priorité 2 (Important) 🟡 - -3. **Exécuter les tests ClickHouse** sur le serveur distant - - Commande: `docker compose exec clickhouse clickhouse-client -h test-sdv-anubis.sdv.fr -d mabase_prod < test_dashboard_entities.sql` - -4. **Ajouter des tests E2E Playwright** - - Scénarios: Navigation, Filtres, Recherche - - Fichier: `tests/e2e/dashboard.spec.ts` - -### Priorité 3 (Secondaire) 🟢 - -5. **Améliorer la couverture des tests API** - - Endpoints manquants: `/api/entities/*`, `/api/analysis/{ip}/recommendation` - - Tests de filtres: threat_level, model_name, country_code, search - -6. **Tests de performance** - - Load testing avec locust - - Objectif: < 2s pour tous les endpoints - ---- - -## 8. Commandes Utiles - -```bash -# Exécuter les tests API -./test_dashboard.sh - -# Vérifier la santé du dashboard -curl http://localhost:3000/health | jq - -# Tester un endpoint spécifique -curl "http://localhost:3000/api/detections?page=1&page_size=25" | jq - -# Logs en temps réel -docker compose logs -f dashboard_web - -# Redémarrer le dashboard -docker compose restart dashboard_web -``` - ---- - -## 9. Conclusion - -**État général:** ✅ **Bon** (93% de tests passés) - -**Points forts:** -- Dashboard fonctionnel avec données en temps réel -- API performante (< 1s pour la plupart des endpoints) -- Frontend React responsive et navigable -- 23,879 détections analysées sur 24h - -**Points d'amélioration:** -- Bug critique sur l'investigation (variability endpoint) -- Couverture de tests insuffisante (~30%) -- Tests ClickHouse non automatisés - -**Prochaines étapes:** -1. Corriger le bug Pydantic (Priorité 1) -2. Ajouter des tests unitaires backend -3. Automatiser les tests E2E avec Playwright - ---- - -**Rapport généré par:** MCP Playwright + Shell -**Date:** 2026-03-14 19:57 UTC diff --git a/test_report_2026-03-14_mcp.md b/test_report_2026-03-14_mcp.md deleted file mode 100644 index f53341b..0000000 --- a/test_report_2026-03-14_mcp.md +++ /dev/null @@ -1,243 +0,0 @@ -# 🧪 Rapport de Tests - Bot Detector Dashboard - -**Date:** 14 mars 2026 -**Méthode:** Tests via services MCP et curl -**Version:** 1.0 - ---- - -## 📊 Résumé Exécutif - -| Métrique | Valeur | -|----------|--------| -| **Tests exécutés** | 29 | -| **Tests passés** | 27 ✅ | -| **Tests échoués** | 2 ❌ | -| **Taux de succès** | 93.10% | -| **État global** | ✅ **OPÉRATIONNEL** | - ---- - -## 🏗️ Architecture Testée - -``` -┌─────────────────────────────────────────────────────────┐ -│ Docker Compose │ -│ ┌─────────────┐ ┌─────────────────────────────────┐ │ -│ │ ClickHouse │ │ dashboard_web │ │ -│ │ (externe) │ │ FastAPI + React │ │ -│ │ :8123 │ │ Port: 3000 │ │ -│ └──────┬──────┘ └─────────────────────────────────┘ │ -│ └───────────────────────────────────────────────┘ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## ✅ Tests Réussis - -### 1. Health Check (2/2) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Health check status | `/health` | ✅ `{"status": "healthy"}` | -| Health check ClickHouse | `/health` | ✅ `{"clickhouse": "connected"}` | - -### 2. Metrics (3/3) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Metrics summary | `/api/metrics` | ✅ 35,774 détections totales | -| Metrics timeseries | `/api/metrics` | ✅ 24 points (24h) | -| Threat distribution | `/api/metrics/threats` | ✅ 3 niveaux (LOW, MEDIUM, KNOWN_BOT) | - -### 3. Détections (5/5) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Detections list | `/api/detections?page=1&page_size=10` | ✅ 10 items | -| Detections pagination | `/api/detections?page=1&page_size=5` | ✅ 5 items | -| Detection by IP | `/api/detections/116.179.33.143` | ✅ Données complètes | -| Detections filter | `/api/detections?threat_level=MEDIUM` | ✅ Filtrage fonctionnel | -| Detections sort | `/api/detections?sort_by=anomaly_score&sort_order=asc` | ✅ Tri fonctionnel | - -### 4. Variability (3/3) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Variability IP | `/api/variability/ip/116.179.33.143` | ✅ 5 détections, 1 IP unique | -| Variability country IPs | `/api/variability/country/CN/ips?limit=5` | ✅ 5 IPs (total: 1,530) | -| Variability user_agents | `/api/variability/ip/116.179.33.143/user_agents?limit=5` | ✅ 1 user-agent | - -### 5. Analysis (5/6) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Analysis subnet | `/api/analysis/116.179.33.143/subnet` | ✅ 57 IPs dans le subnet /24 | -| Analysis country | `/api/analysis/116.179.33.143/country` | ✅ CN (China) | -| Analysis user-agents | `/api/analysis/116.179.33.143/user-agents` | ✅ 1 UA, 0% bot | -| Analysis recommendation | `/api/analysis/116.179.33.143/recommendation` | ✅ `legitimate` (35% confidence) | -| Analysis top country | `/api/analysis/country` | ✅ Top 3: US (66.6%), CN (8.4%), RS (7.5%) | - -### 6. Entities (2/3) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Entities IP | `/api/entities/ip/116.179.33.143` | ✅ 17 requêtes, 1 UA, 4 paths | -| Entities types | `/api/entities/types` | ✅ 7 types supportés | - -### 7. Attributes (3/3) -| Test | Endpoint | Résultat | -|------|----------|----------| -| Attributes IP | `/api/attributes/ip?limit=5` | ✅ Top 5 IPs | -| Attributes JA4 | `/api/attributes/ja4?limit=5` | ✅ Top 5 JA4 | -| Attributes country | `/api/attributes/country?limit=5` | ✅ Top 5 pays | - -### 8. Frontend (4/4) -| Test | Résultat | -|------|----------| -| Frontend HTML served | ✅ Page React servie | -| Frontend assets referenced | ✅ 2 assets (CSS + JS) | -| CSS asset accessible | ✅ HTTP 200 | -| JS asset accessible | ✅ HTTP 200 | - ---- - -## ❌ Tests Échoués - -### 1. Analysis JA4 -- **Endpoint:** `/api/analysis/116.179.33.143/ja4` -- **Problème:** Retourne des valeurs nulles pour `ja4` et `shared_ips_count` -- **Cause probable:** Structure de réponse différente du modèle attendu -- **Impact:** Faible - Les autres analyses fonctionnent correctement - -### 2. Entities Related -- **Endpoint:** `/api/entities/ip/116.179.33.143/related` -- **Problème:** Retourne une erreur ou des données nulles -- **Cause probable:** Problème de sérialisation Pydantic -- **Impact:** Faible - L'endpoint principal `/api/entities/ip/{ip}` fonctionne - ---- - -## 🐛 Bugs Corrigés Pendant les Tests - -### Bug #1: Doublon de classe `AttributeValue` - -**Fichier:** `backend/models.py` - -**Problème:** Deux classes `AttributeValue` étaient définies : -- Ligne 92 : Pour la variabilité (avec `first_seen`, `last_seen`, `threat_levels`) -- Ligne 341 : Pour les entities (simple : `value`, `count`, `percentage`) - -Pydantic utilisait la mauvaise définition, causant des erreurs de validation. - -**Solution:** Renommage de la deuxième classe en `EntityAttributeValue` - -**Fichiers modifiés:** -1. `backend/models.py` - Ligne 341 : `class EntityAttributeValue` -2. `backend/routes/entities.py` - Import et usage de `EntityAttributeValue` -3. `backend/models.py` - Ligne 350-353 : `EntityInvestigation` utilise `List[EntityAttributeValue]` - -**Résultat:** ✅ Tous les endpoints de variabilité et entities fonctionnent maintenant - ---- - -## 📈 Statistiques ClickHouse - -| Métrique | Valeur | -|----------|--------| -| Total détections | 35,774 | -| IPs uniques | 17,634 | -| Distribution menaces | LOW: 41.4%, KNOWN_BOT: 38.9%, MEDIUM: 19.8% | -| Pays dominant | United States (66.6%) | -| Subnet le plus actif | 116.179.33.0/24 (57 IPs) | - ---- - -## 🔧 Commandes de Test Utilisées - -### Health Check -```bash -curl http://localhost:3000/health | jq -``` - -### Metrics -```bash -curl http://localhost:3000/api/metrics | jq -curl http://localhost:3000/api/metrics/threats | jq -``` - -### Détections -```bash -curl "http://localhost:3000/api/detections?page=1&page_size=25" | jq -curl "http://localhost:3000/api/detections?threat_level=CRITICAL" | jq -curl "http://localhost:3000/api/detections?sort_by=anomaly_score&sort_order=asc" | jq -``` - -### Variability -```bash -curl http://localhost:3000/api/variability/ip/116.179.33.143 | jq -curl "http://localhost:3000/api/variability/country/CN/ips?limit=10" | jq -``` - -### Analysis -```bash -curl http://localhost:3000/api/analysis/116.179.33.143/subnet | jq -curl http://localhost:3000/api/analysis/116.179.33.143/recommendation | jq -``` - -### Entities -```bash -curl http://localhost:3000/api/entities/ip/116.179.33.143 | jq -curl http://localhost:3000/api/entities/types | jq -``` - ---- - -## 📋 Couverture des Tests (vs TEST_PLAN.md) - -| Section | Tests prévus | Tests exécutés | Couverture | -|---------|--------------|----------------|------------| -| Health Check | 3 | 2 | 67% | -| Metrics | 5 | 3 | 60% | -| Detections | 11 | 5 | 45% | -| Variability | 8 | 3 | 38% | -| Analysis | 6 | 6 | 100% | -| Entities | 10 | 3 | 30% | -| Attributes | 7 | 3 | 43% | -| Frontend | 7 | 4 | 57% | -| **Total** | **57+** | **29** | **~51%** | - ---- - -## 🎯 Recommandations - -### Priorité Haute -1. ✅ **Déjà corrigé:** Bug `AttributeValue` dans `models.py` -2. 🔄 **À investiguer:** Endpoint `/api/analysis/{ip}/ja4` retourne des valeurs nulles -3. 🔄 **À investiguer:** Endpoint `/api/entities/{ip}/related` - -### Priorité Moyenne -4. Ajouter des tests unitaires pytest pour le backend -5. Ajouter des tests E2E pour le frontend (React Testing Library) -6. Implémenter des tests de charge (locust ou k6) - -### Priorité Basse -7. Ajouter des tests de sécurité (OWASP ZAP) -8. Mettre en place l'intégration continue (GitHub Actions) - ---- - -## 📝 Conclusion - -Le **Bot Detector Dashboard** est **opérationnel** avec un taux de succès de **93%** aux tests fonctionnels. - -**Points forts:** -- ✅ Tous les endpoints critiques fonctionnent (health, metrics, detections) -- ✅ Frontend React correctement servi -- ✅ Connexion ClickHouse stable -- ✅ 35,774 détections analysées avec succès - -**Points d'amélioration:** -- 2 endpoints mineurs à investiguer (analysis/ja4, entities/related) -- Couverture de tests à augmenter (51% → 80%+) - -**Statut global:** ✅ **PRÊT POUR LA PRODUCTION** (avec surveillance des endpoints échoués) - ---- - -*Généré automatiquement lors de la session de tests MCP - 14 mars 2026* diff --git a/test_report_api.sh b/test_report_api.sh deleted file mode 100755 index 06e1765..0000000 --- a/test_report_api.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash -# Rapport de tests API - Bot Detector Dashboard - -BASE_URL="http://localhost:3000" -PASS=0 -FAIL=0 - -test_endpoint() { - local name=$1 - local endpoint=$2 - local expected=$3 - local check=$4 - - response=$(curl -s "$BASE_URL$endpoint") - - if [ "$check" = "status" ]; then - result=$(echo "$response" | jq -r '.status' 2>/dev/null) - elif [ "$check" = "keys" ]; then - result=$(echo "$response" | jq 'keys | length' 2>/dev/null) - elif [ "$check" = "items_length" ]; then - result=$(echo "$response" | jq '.items | length' 2>/dev/null) - elif [ "$check" = "exists" ]; then - result=$(echo "$response" | jq -r "$check" 2>/dev/null) - else - result=$(echo "$response" | jq -r "$check" 2>/dev/null) - fi - - if [ "$result" != "null" ] && [ -n "$result" ]; then - echo "✅ PASS: $name" - ((PASS++)) - else - echo "❌ FAIL: $name (endpoint: $endpoint)" - ((FAIL++)) - fi -} - -echo "==========================================" -echo "📊 RAPPORT DE TESTS API" -echo "==========================================" -echo "" -echo "Date: $(date)" -echo "Base URL: $BASE_URL" -echo "" -echo "------------------------------------------" -echo "HEALTH CHECK" -echo "------------------------------------------" -test_endpoint "Health check status" "/health" "healthy" ".status" -test_endpoint "Health check ClickHouse" "/health" "connected" ".clickhouse" - -echo "" -echo "------------------------------------------" -echo "METRICS" -echo "------------------------------------------" -test_endpoint "Metrics summary" "/api/metrics" "total_detections" ".summary.total_detections" -test_endpoint "Metrics timeseries" "/api/metrics" "timeseries" ".timeseries | length" -test_endpoint "Threat distribution" "/api/metrics/threats" "items" ".items | length" - -echo "" -echo "------------------------------------------" -echo "DETECTIONS" -echo "------------------------------------------" -test_endpoint "Detections list" "/api/detections?page=1&page_size=10" "items" ".items | length" -test_endpoint "Detections pagination" "/api/detections?page=1&page_size=5" "5" ".items | length" -test_endpoint "Detection by IP" "/api/detections/116.179.33.143" "src_ip" ".src_ip" -test_endpoint "Detections filter threat_level" "/api/detections?threat_level=MEDIUM" "items" ".items | length" -test_endpoint "Detections sort" "/api/detections?sort_by=anomaly_score&sort_order=asc" "items" ".items | length" - -echo "" -echo "------------------------------------------" -echo "VARIABILITY" -echo "------------------------------------------" -test_endpoint "Variability IP" "/api/variability/ip/116.179.33.143" "total_detections" ".total_detections" -test_endpoint "Variability country IPs" "/api/variability/country/CN/ips?limit=5" "ips" ".ips | length" -test_endpoint "Variability user_agents" "/api/variability/ip/116.179.33.143/user_agents?limit=5" "user_agents" ".user_agents | length" - -echo "" -echo "------------------------------------------" -echo "ANALYSIS" -echo "------------------------------------------" -test_endpoint "Analysis subnet" "/api/analysis/116.179.33.143/subnet" "subnet" ".subnet" -test_endpoint "Analysis country" "/api/analysis/116.179.33.143/country" "ip_country" ".ip_country.code" -test_endpoint "Analysis JA4" "/api/analysis/116.179.33.143/ja4" "ja4" ".ja4" -test_endpoint "Analysis user-agents" "/api/analysis/116.179.33.143/user-agents" "ip_user_agents" ".ip_user_agents | length" -test_endpoint "Analysis recommendation" "/api/analysis/116.179.33.143/recommendation" "label" ".label" -test_endpoint "Analysis top country" "/api/analysis/country" "top_countries" ".top_countries | length" - -echo "" -echo "------------------------------------------" -echo "ENTITIES" -echo "------------------------------------------" -test_endpoint "Entities IP" "/api/entities/ip/116.179.33.143" "stats" ".stats.entity_type" -test_endpoint "Entities related" "/api/entities/ip/116.179.33.143/related" "related" ".related | keys | length" -test_endpoint "Entities types" "/api/entities/types" "entity_types" ".entity_types | length" - -echo "" -echo "------------------------------------------" -echo "ATTRIBUTES" -echo "------------------------------------------" -test_endpoint "Attributes IP" "/api/attributes/ip?limit=5" "items" ".items | length" -test_endpoint "Attributes JA4" "/api/attributes/ja4?limit=5" "items" ".items | length" -test_endpoint "Attributes country" "/api/attributes/country?limit=5" "items" ".items | length" - -echo "" -echo "------------------------------------------" -echo "FRONTEND" -echo "------------------------------------------" -response=$(curl -s "$BASE_URL/") -if echo "$response" | grep -q "Bot Detector Dashboard"; then - echo "✅ PASS: Frontend HTML served" - ((PASS++)) -else - echo "❌ FAIL: Frontend HTML not served" - ((FAIL++)) -fi - -if echo "$response" | grep -q "assets/"; then - echo "✅ PASS: Frontend assets referenced" - ((PASS++)) -else - echo "❌ FAIL: Frontend assets not referenced" - ((FAIL++)) -fi - -css_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/assets/index-04JQmLnn.css") -if [ "$css_status" = "200" ]; then - echo "✅ PASS: CSS asset accessible" - ((PASS++)) -else - echo "❌ FAIL: CSS asset not accessible" - ((FAIL++)) -fi - -js_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/assets/index-CRGscVYE.js") -if [ "$js_status" = "200" ]; then - echo "✅ PASS: JS asset accessible" - ((PASS++)) -else - echo "❌ FAIL: JS asset not accessible" - ((FAIL++)) -fi - -echo "" -echo "==========================================" -echo "📈 RÉSULTATS" -echo "==========================================" -echo "✅ PASS: $PASS" -echo "❌ FAIL: $FAIL" -echo "Total: $((PASS + FAIL))" -echo "Taux de succès: $(echo "scale=2; $PASS * 100 / ($PASS + $FAIL)" | bc)%" -echo "=========================================="