fix: correct CampaignsView, analysis.py IPv4 split, entities date filter
- CampaignsView: update ClusterData interface to match real API response
(severity/unique_ips/score instead of threat_level/total_ips/confidence_range)
Fix fetch to use data.items, rewrite ClusterCard and BehavioralTab
Remove unused getClassificationColor and THREAT_ORDER constants
- analysis.py: fix IPv4Address object has no attribute 'split' on line 322
Add str() conversion before calling .split('.')
- entities.py: fix Date vs DateTime comparison — log_date is a Date column,
comparing against now()-INTERVAL HOUR caused yesterday's entries to be excluded
Use toDate(now() - INTERVAL X HOUR) for correct Date-level comparison
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
114
.github/copilot-instructions.md
vendored
Normal file
114
.github/copilot-instructions.md
vendored
Normal file
@ -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/<domain>", 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) |
|
||||||
203
AUDIT_SOC_DASHBOARD.md
Normal file
203
AUDIT_SOC_DASHBOARD.md
Normal file
@ -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).
|
||||||
@ -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
|
|
||||||
@ -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<string, number>
|
|
||||||
}
|
|
||||||
|
|
||||||
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=<IP>
|
|
||||||
→ Voir historique
|
|
||||||
→ /investigation/<IP>
|
|
||||||
→ Analyser corrélations
|
|
||||||
→ Classifier + Export ML
|
|
||||||
|
|
||||||
3. INVESTIGATION: Nouveau botnet
|
|
||||||
→ /detections?threat_level=CRITICAL
|
|
||||||
→ Trier par ASN
|
|
||||||
→ Identifier cluster
|
|
||||||
→ /investigation/ja4/<JA4>
|
|
||||||
→ Cartographier infrastructure
|
|
||||||
|
|
||||||
4. REVIEW: Classification SOC
|
|
||||||
→ /entities/ip/<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
|
|
||||||
@ -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
|
|
||||||
# <title>Bot Detector Dashboard</title>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
57
ROUTES_NAVIGATION_PROGRESS.md
Normal file
57
ROUTES_NAVIGATION_PROGRESS.md
Normal file
@ -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.
|
||||||
@ -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.
|
|
||||||
@ -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
|
|
||||||
<InvestigationPanel
|
|
||||||
entityType="ip"
|
|
||||||
entityValue="192.168.1.100"
|
|
||||||
onClose={() => 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
|
|
||||||
985
TEST_PLAN.md
985
TEST_PLAN.md
@ -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 `<script>alert('xss')</script>` 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
|
|
||||||
@ -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 !** 🛡️
|
|
||||||
@ -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
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Bot Detector Dashboard</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-DGqwtGK4.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BRn5kqai.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
**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
|
|
||||||
@ -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
|
|
||||||
@ -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:** `<title>Bot Detector Dashboard</title>`
|
|
||||||
✅ **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é !** 🛡️
|
|
||||||
@ -12,7 +12,7 @@ import os
|
|||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import db
|
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
|
# Configuration logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -73,6 +73,7 @@ app.include_router(entities.router)
|
|||||||
app.include_router(incidents.router)
|
app.include_router(incidents.router)
|
||||||
app.include_router(audit.router)
|
app.include_router(audit.router)
|
||||||
app.include_router(reputation.router)
|
app.include_router(reputation.router)
|
||||||
|
app.include_router(fingerprints.router)
|
||||||
|
|
||||||
|
|
||||||
# Route pour servir le frontend
|
# Route pour servir le frontend
|
||||||
|
|||||||
@ -318,7 +318,7 @@ async def analyze_ja4(ip: str):
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
subnet_counts = defaultdict(int)
|
subnet_counts = defaultdict(int)
|
||||||
for row in subnets_result.result_rows:
|
for row in subnets_result.result_rows:
|
||||||
ip_addr = row[0]
|
ip_addr = str(row[0])
|
||||||
parts = ip_addr.split('.')
|
parts = ip_addr.split('.')
|
||||||
if len(parts) == 4:
|
if len(parts) == 4:
|
||||||
subnet = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
|
subnet = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
|
||||||
|
|||||||
@ -45,7 +45,7 @@ def get_entity_stats(entity_type: str, entity_value: str, hours: int = 24) -> Op
|
|||||||
FROM mabase_prod.view_dashboard_entities
|
FROM mabase_prod.view_dashboard_entities
|
||||||
WHERE entity_type = %(entity_type)s
|
WHERE entity_type = %(entity_type)s
|
||||||
AND entity_value = %(entity_value)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
|
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
|
# Requête pour agréger tous les attributs associés
|
||||||
query = """
|
query = """
|
||||||
SELECT
|
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(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 >= now() - INTERVAL %(hours)s HOUR AND ja4 != '') as ja4s,
|
(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 >= now() - INTERVAL %(hours)s HOUR AND host != '') as hosts,
|
(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 >= now() - INTERVAL %(hours)s HOUR AND notEmpty(asns)) as asns,
|
(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 >= now() - INTERVAL %(hours)s HOUR AND notEmpty(countries)) as countries
|
(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, {
|
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
|
FROM mabase_prod.view_dashboard_entities
|
||||||
WHERE entity_type = %(entity_type)s
|
WHERE entity_type = %(entity_type)s
|
||||||
AND entity_value = %(entity_value)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})
|
AND notEmpty({array_field})
|
||||||
)
|
)
|
||||||
GROUP BY value
|
GROUP BY value
|
||||||
@ -193,7 +193,7 @@ async def get_subnet_investigation(
|
|||||||
arrayJoin(user_agents) AS user_agent
|
arrayJoin(user_agents) AS user_agent
|
||||||
FROM view_dashboard_entities
|
FROM view_dashboard_entities
|
||||||
WHERE entity_type = 'ip'
|
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)[1] = %(subnet_prefix)s
|
||||||
AND splitByChar('.', entity_value)[2] = %(subnet_mask)s
|
AND splitByChar('.', entity_value)[2] = %(subnet_mask)s
|
||||||
AND splitByChar('.', entity_value)[3] = %(subnet_third)s
|
AND splitByChar('.', entity_value)[3] = %(subnet_third)s
|
||||||
|
|||||||
737
backend/routes/fingerprints.py
Normal file
737
backend/routes/fingerprints.py
Normal file
@ -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)}")
|
||||||
@ -81,25 +81,94 @@ async def get_incident_clusters(
|
|||||||
|
|
||||||
result = db.query(cluster_query, {"hours": hours, "limit": limit})
|
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 = []
|
clusters = []
|
||||||
for row in result.result_rows:
|
for row in result.result_rows:
|
||||||
# Calcul du score de risque
|
subnet = row[0]
|
||||||
threat_level = row[8] or 'LOW'
|
threat_level = row[8] or 'LOW'
|
||||||
unique_ips = row[2] or 1
|
unique_ips = row[2] or 1
|
||||||
avg_score = abs(row[9] or 0)
|
avg_score = abs(row[9] or 0)
|
||||||
|
sample_ip = row[10] if row[10] else subnet.split('/')[0]
|
||||||
# Score based on threat level and other factors
|
|
||||||
critical_count = 1 if threat_level == 'CRITICAL' else 0
|
critical_count = 1 if threat_level == 'CRITICAL' else 0
|
||||||
high_count = 1 if threat_level == 'HIGH' else 0
|
high_count = 1 if threat_level == 'HIGH' else 0
|
||||||
|
|
||||||
risk_score = min(100, round(
|
risk_score = min(100, round(
|
||||||
(critical_count * 30) +
|
(critical_count * 30) +
|
||||||
(high_count * 20) +
|
(high_count * 20) +
|
||||||
(unique_ips * 5) +
|
(unique_ips * 5) +
|
||||||
(avg_score * 100)
|
(avg_score * 100)
|
||||||
))
|
))
|
||||||
|
|
||||||
# Détermination de la sévérité
|
|
||||||
if critical_count > 0 or risk_score >= 80:
|
if critical_count > 0 or risk_score >= 80:
|
||||||
severity = "CRITICAL"
|
severity = "CRITICAL"
|
||||||
elif high_count > (row[1] or 1) * 0.3 or risk_score >= 60:
|
elif high_count > (row[1] or 1) * 0.3 or risk_score >= 60:
|
||||||
@ -108,31 +177,27 @@ async def get_incident_clusters(
|
|||||||
severity = "MEDIUM"
|
severity = "MEDIUM"
|
||||||
else:
|
else:
|
||||||
severity = "LOW"
|
severity = "LOW"
|
||||||
|
|
||||||
# Calcul de la tendance
|
trend_dir, trend_pct = trend_by_subnet.get(subnet, ("stable", 0))
|
||||||
trend = "up"
|
primary_ua = ua_by_ip.get(sample_ip, "")
|
||||||
trend_percentage = 23
|
|
||||||
|
|
||||||
clusters.append({
|
clusters.append({
|
||||||
"id": f"INC-{datetime.now().strftime('%Y%m%d')}-{len(clusters)+1:03d}",
|
"id": f"INC-{datetime.now().strftime('%Y%m%d')}-{len(clusters)+1:03d}",
|
||||||
"score": risk_score,
|
"score": risk_score,
|
||||||
"severity": severity,
|
"severity": severity,
|
||||||
"total_detections": row[1],
|
"total_detections": row[1],
|
||||||
"unique_ips": row[2],
|
"unique_ips": row[2],
|
||||||
"subnet": row[0],
|
"subnet": subnet,
|
||||||
"sample_ip": row[10] if row[10] else row[0].split('/')[0],
|
"sample_ip": sample_ip,
|
||||||
"ja4": row[5] or "",
|
"ja4": row[5] or "",
|
||||||
"primary_ua": "python-requests",
|
"primary_ua": primary_ua,
|
||||||
"primary_target": "Unknown",
|
"primary_target": row[3].strftime('%H:%M') if row[3] else "Unknown",
|
||||||
"countries": [{
|
"countries": [{"code": row[6] or "XX", "percentage": 100}],
|
||||||
"code": row[6] or "XX",
|
|
||||||
"percentage": 100
|
|
||||||
}],
|
|
||||||
"asn": str(row[7]) if row[7] else "",
|
"asn": str(row[7]) if row[7] else "",
|
||||||
"first_seen": row[3].isoformat() if row[3] else "",
|
"first_seen": row[3].isoformat() if row[3] else "",
|
||||||
"last_seen": row[4].isoformat() if row[4] else "",
|
"last_seen": row[4].isoformat() if row[4] else "",
|
||||||
"trend": trend,
|
"trend": trend_dir,
|
||||||
"trend_percentage": trend_percentage
|
"trend_percentage": trend_pct,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -103,7 +103,7 @@ async def get_associated_attributes(
|
|||||||
|
|
||||||
# Mapping des attributs cibles
|
# Mapping des attributs cibles
|
||||||
target_column_map = {
|
target_column_map = {
|
||||||
"user_agents": "''", # Pas de user_agent
|
"user_agents": None, # handled separately via view_dashboard_entities
|
||||||
"ja4": "ja4",
|
"ja4": "ja4",
|
||||||
"countries": "country_code",
|
"countries": "country_code",
|
||||||
"asns": "asn_number",
|
"asns": "asn_number",
|
||||||
@ -122,9 +122,33 @@ async def get_associated_attributes(
|
|||||||
column = type_column_map[attr_type]
|
column = type_column_map[attr_type]
|
||||||
target_column = target_column_map[target_attr]
|
target_column = target_column_map[target_attr]
|
||||||
|
|
||||||
# Pour user_agent, retourne liste vide
|
# Pour user_agents: requête via view_dashboard_user_agents
|
||||||
if target_column == "''":
|
# Colonnes: src_ip, ja4, hour, log_date, user_agents, requests
|
||||||
return {"type": attr_type, "value": value, "target": target_attr, "items": [], "total": 0}
|
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"""
|
query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
@ -193,8 +217,8 @@ async def get_user_agents(
|
|||||||
type_column_map = {
|
type_column_map = {
|
||||||
"ip": "src_ip",
|
"ip": "src_ip",
|
||||||
"ja4": "ja4",
|
"ja4": "ja4",
|
||||||
"country": "src_country_code",
|
"country": "country_code",
|
||||||
"asn": "src_asn",
|
"asn": "asn_number",
|
||||||
"host": "host",
|
"host": "host",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,25 +230,51 @@ async def get_user_agents(
|
|||||||
|
|
||||||
column = type_column_map[attr_type]
|
column = type_column_map[attr_type]
|
||||||
|
|
||||||
# Requête sur la vue materialisée
|
# view_dashboard_user_agents colonnes: src_ip, ja4, hour, log_date, user_agents, requests
|
||||||
# user_agents est un Array, on utilise arrayJoin pour l'aplatir
|
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"""
|
query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
ua AS user_agent,
|
ua AS user_agent,
|
||||||
sum(requests) AS count,
|
sum(requests) AS count,
|
||||||
round(count * 100.0 / sum(count) OVER (), 2) AS percentage,
|
round(sum(requests) * 100.0 / sum(sum(requests)) OVER (), 2) AS percentage,
|
||||||
min(hour) AS first_seen,
|
min(log_date) AS first_seen,
|
||||||
max(hour) AS last_seen
|
max(log_date) AS last_seen
|
||||||
FROM mabase_prod.view_dashboard_user_agents
|
FROM view_dashboard_user_agents
|
||||||
ARRAY JOIN user_agents AS ua
|
ARRAY JOIN user_agents AS ua
|
||||||
WHERE {column} = %(value)s
|
WHERE {where}
|
||||||
AND hour >= now() - INTERVAL 24 HOUR
|
AND hour >= now() - INTERVAL 24 HOUR
|
||||||
|
AND ua != ''
|
||||||
GROUP BY user_agent
|
GROUP BY user_agent
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT %(limit)s
|
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 = [
|
user_agents = [
|
||||||
UserAgentValue(
|
UserAgentValue(
|
||||||
@ -237,16 +287,6 @@ async def get_user_agents(
|
|||||||
for row in result.result_rows
|
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
|
total = count_result.result_rows[0][0] if count_result.result_rows else 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -451,38 +491,41 @@ async def get_variability(attr_type: str, value: str):
|
|||||||
first_seen = stats_row[2]
|
first_seen = stats_row[2]
|
||||||
last_seen = stats_row[3]
|
last_seen = stats_row[3]
|
||||||
|
|
||||||
# User-Agents
|
# User-Agents via view_dashboard_user_agents (source principale pour les UAs)
|
||||||
ua_query = f"""
|
# Colonnes disponibles: src_ip, ja4, hour, log_date, user_agents, requests
|
||||||
SELECT
|
if attr_type == "ip":
|
||||||
user_agent,
|
_ua_where = "toString(src_ip) = %(value)s"
|
||||||
count() AS count,
|
_ua_params: dict = {"value": value}
|
||||||
round(count() * 100.0 / sum(count()) OVER (), 2) AS percentage,
|
elif attr_type == "ja4":
|
||||||
min(detected_at) AS first_seen,
|
_ua_where = "ja4 = %(value)s"
|
||||||
max(detected_at) AS last_seen,
|
_ua_params = {"value": value}
|
||||||
groupArray((threat_level, 1)) AS threats
|
else:
|
||||||
FROM ({base_query})
|
# country / asn / host: pivot via ml_detected_anomalies → IPs
|
||||||
WHERE user_agent != '' AND user_agent IS NOT NULL
|
_ua_where = f"""toString(src_ip) IN (
|
||||||
GROUP BY user_agent
|
SELECT DISTINCT replaceRegexpAll(toString(src_ip), '^::ffff:', '')
|
||||||
ORDER BY count DESC
|
FROM ml_detected_anomalies
|
||||||
LIMIT 10
|
WHERE {column} = %(value)s AND detected_at >= now() - INTERVAL 24 HOUR
|
||||||
"""
|
)"""
|
||||||
|
_ua_params = {"value": value}
|
||||||
# Simplified query without complex threat parsing
|
|
||||||
ua_query_simple = f"""
|
ua_query_simple = f"""
|
||||||
SELECT
|
SELECT
|
||||||
user_agent,
|
ua AS user_agent,
|
||||||
count() AS count,
|
sum(requests) AS count,
|
||||||
round(count() * 100.0 / (SELECT count() FROM ({base_query}) WHERE user_agent != '' AND user_agent IS NOT NULL), 2) AS percentage,
|
round(sum(requests) * 100.0 / sum(sum(requests)) OVER (), 2) AS percentage,
|
||||||
min(detected_at) AS first_seen,
|
min(log_date) AS first_seen,
|
||||||
max(detected_at) AS last_seen
|
max(log_date) AS last_seen
|
||||||
FROM ({base_query})
|
FROM view_dashboard_user_agents
|
||||||
WHERE user_agent != '' AND user_agent IS NOT NULL
|
ARRAY JOIN user_agents AS ua
|
||||||
|
WHERE {_ua_where}
|
||||||
|
AND hour >= now() - INTERVAL 24 HOUR
|
||||||
|
AND ua != ''
|
||||||
GROUP BY user_agent
|
GROUP BY user_agent
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT 10
|
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]
|
user_agents = [get_attribute_value(row, 1, 2, 3, 4) for row in ua_result.result_rows]
|
||||||
|
|
||||||
# JA4 fingerprints
|
# JA4 fingerprints
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
--
|
|
||||||
-- =============================================================================
|
|
||||||
@ -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 <votre_mot_de_passe> < 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;
|
|
||||||
--
|
|
||||||
-- =============================================================================
|
|
||||||
@ -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;
|
|
||||||
--
|
|
||||||
-- =============================================================================
|
|
||||||
@ -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 <votre_mot_de_passe>
|
|
||||||
--
|
|
||||||
-- 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;
|
|
||||||
--
|
|
||||||
-- =============================================================================
|
|
||||||
@ -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 { DetectionsList } from './components/DetectionsList';
|
||||||
import { DetailsView } from './components/DetailsView';
|
import { DetailsView } from './components/DetailsView';
|
||||||
import { InvestigationView } from './components/InvestigationView';
|
import { InvestigationView } from './components/InvestigationView';
|
||||||
@ -10,65 +11,343 @@ import { ThreatIntelView } from './components/ThreatIntelView';
|
|||||||
import { CorrelationGraph } from './components/CorrelationGraph';
|
import { CorrelationGraph } from './components/CorrelationGraph';
|
||||||
import { InteractiveTimeline } from './components/InteractiveTimeline';
|
import { InteractiveTimeline } from './components/InteractiveTimeline';
|
||||||
import { SubnetInvestigation } from './components/SubnetInvestigation';
|
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
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
function Navigation() {
|
|
||||||
|
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<RecentItem, 'ts'>) {
|
||||||
|
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 location = useLocation();
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const [recents, setRecents] = useState<RecentItem[]>(loadRecents());
|
||||||
|
|
||||||
const links = [
|
// Refresh recents when location changes
|
||||||
{ path: '/', label: 'Incidents' },
|
useEffect(() => {
|
||||||
{ path: '/threat-intel', label: 'Threat Intel' },
|
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 (
|
return (
|
||||||
<nav className="bg-background-secondary border-b border-background-card">
|
<aside className="fixed inset-y-0 left-0 w-56 bg-background-secondary border-r border-background-card flex flex-col z-30">
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
{/* Logo */}
|
||||||
<div className="flex items-center h-16 gap-4">
|
<div className="h-14 flex items-center px-5 border-b border-background-card shrink-0">
|
||||||
<h1 className="text-xl font-bold text-text-primary">SOC Dashboard</h1>
|
<span className="text-lg font-bold text-text-primary">🛡️ SOC</span>
|
||||||
<div className="flex gap-2">
|
<span className="ml-2 text-xs text-text-disabled font-mono bg-background-card px-1.5 py-0.5 rounded">v2</span>
|
||||||
{links.map(link => (
|
</div>
|
||||||
|
|
||||||
|
{/* Main nav */}
|
||||||
|
<nav className="px-3 pt-4 space-y-0.5">
|
||||||
|
{navLinks.map(link => (
|
||||||
|
<Link
|
||||||
|
key={link.path}
|
||||||
|
to={link.path}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-sm font-medium ${
|
||||||
|
isActive(link)
|
||||||
|
? 'bg-accent-primary text-white'
|
||||||
|
: 'text-text-secondary hover:text-text-primary hover:bg-background-card'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{link.icon}</span>
|
||||||
|
<span className="flex-1">{link.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Alert stats */}
|
||||||
|
{counts && (
|
||||||
|
<div className="mx-3 mt-5 bg-background-card rounded-lg p-3 space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-text-disabled uppercase tracking-wider mb-2">Alertes 24h</div>
|
||||||
|
{counts.critical > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs text-red-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-red-500 inline-block animate-pulse" /> CRITICAL</span>
|
||||||
|
<span className="text-xs font-bold text-red-400 bg-red-500/20 px-1.5 py-0.5 rounded">{counts.critical}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs text-orange-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-orange-500 inline-block" /> HIGH</span>
|
||||||
|
<span className="text-xs font-bold text-orange-400 bg-orange-500/20 px-1.5 py-0.5 rounded">{counts.high}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs text-yellow-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-yellow-500 inline-block" /> MEDIUM</span>
|
||||||
|
<span className="text-xs font-bold text-yellow-400 bg-yellow-500/20 px-1.5 py-0.5 rounded">{counts.medium}</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-background-secondary pt-1.5 flex justify-between items-center mt-1">
|
||||||
|
<span className="text-xs text-text-secondary">Total détections</span>
|
||||||
|
<span className="text-xs font-bold text-text-primary">{counts.total.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recent investigations */}
|
||||||
|
{recents.length > 0 && (
|
||||||
|
<div className="mx-3 mt-4 flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div className="text-xs font-semibold text-text-disabled uppercase tracking-wider px-1 mb-2">Récents</div>
|
||||||
|
<div className="space-y-0.5 overflow-y-auto max-h-44">
|
||||||
|
{recents.map((r, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={i}
|
||||||
to={link.path}
|
to={r.type === 'ip' ? `/investigation/${r.value}` : r.type === 'ja4' ? `/investigation/ja4/${r.value}` : `/entities/subnet/${r.value}`}
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className="flex items-center gap-2 px-2 py-1.5 rounded text-xs text-text-secondary hover:text-text-primary hover:bg-background-card transition-colors"
|
||||||
location.pathname === link.path
|
|
||||||
? 'bg-accent-primary text-white'
|
|
||||||
: 'text-text-secondary hover:text-text-primary hover:bg-background-card'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{link.label}
|
<span className="shrink-0 text-text-disabled">
|
||||||
|
{r.type === 'ip' ? '🌐' : r.type === 'ja4' ? '🔐' : '🔷'}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono truncate">{r.value}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex-1 max-w-xl">
|
</div>
|
||||||
<QuickSearch />
|
)}
|
||||||
</div>
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Theme toggle */}
|
||||||
|
<div className="mx-3 mb-3 bg-background-card rounded-lg p-2">
|
||||||
|
<div className="text-xs font-semibold text-text-disabled uppercase tracking-wider px-1 mb-2">Thème</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{themeOptions.map(opt => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
onClick={() => setTheme(opt.value)}
|
||||||
|
title={opt.label}
|
||||||
|
className={`flex-1 flex flex-col items-center gap-0.5 py-1.5 rounded transition-colors text-xs ${
|
||||||
|
theme === opt.value
|
||||||
|
? 'bg-accent-primary text-white'
|
||||||
|
: 'text-text-secondary hover:text-text-primary hover:bg-background-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{opt.icon}</span>
|
||||||
|
<span className="text-[10px]">{opt.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-4 py-3 border-t border-background-card shrink-0">
|
||||||
|
<div className="text-xs text-text-disabled">Analyste SOC</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (
|
||||||
|
<header className="fixed top-0 right-0 left-56 h-14 bg-background-secondary border-b border-background-card flex items-center gap-4 px-5 z-20">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="text-sm font-medium text-text-secondary shrink-0">{getBreadcrumb()}</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="flex-1 max-w-xl">
|
||||||
|
<QuickSearch />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Critical alert badge */}
|
||||||
|
{counts && counts.critical > 0 && (
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="shrink-0 flex items-center gap-1.5 bg-red-500/20 border border-red-500/40 text-red-400 px-3 py-1 rounded-lg text-xs font-bold animate-pulse"
|
||||||
|
>
|
||||||
|
🔴 {counts.critical} CRITICAL
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Route helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function CorrelationGraphRoute() {
|
||||||
|
const { ip } = useParams<{ ip: string }>();
|
||||||
|
return <CorrelationGraph ip={ip || ''} height="600px" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimelineRoute() {
|
||||||
|
const { ip } = useParams<{ ip?: string }>();
|
||||||
|
return <InteractiveTimeline ip={ip} height="400px" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InvestigateRoute() {
|
||||||
|
const { type, value } = useParams<{ type?: string; value?: string }>();
|
||||||
|
if (!type || !value) return <Navigate to="/detections" replace />;
|
||||||
|
const decodedValue = decodeURIComponent(value);
|
||||||
|
if (type === 'ip') return <Navigate to={`/investigation/${encodeURIComponent(decodedValue)}`} replace />;
|
||||||
|
if (type === 'ja4') return <Navigate to={`/investigation/ja4/${encodeURIComponent(decodedValue)}`} replace />;
|
||||||
|
return <Navigate to={`/detections/${type}/${encodeURIComponent(decodedValue)}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Navigate to="/" replace />;
|
||||||
|
return (
|
||||||
|
<BulkClassification
|
||||||
|
selectedIPs={selectedIPs}
|
||||||
|
onClose={() => 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() {
|
export default function App() {
|
||||||
|
const [counts, setCounts] = useState<AlertCounts | null>(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 (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className="min-h-screen bg-background">
|
<RouteTracker />
|
||||||
<Navigation />
|
<div className="min-h-screen bg-background flex">
|
||||||
<main className="max-w-7xl mx-auto px-4 py-6">
|
{/* Fixed sidebar */}
|
||||||
<Routes>
|
<Sidebar counts={counts} />
|
||||||
<Route path="/" element={<IncidentsView />} />
|
|
||||||
<Route path="/threat-intel" element={<ThreatIntelView />} />
|
{/* Main area (offset by sidebar width) */}
|
||||||
<Route path="/detections" element={<DetectionsList />} />
|
<div className="flex-1 flex flex-col min-h-screen" style={{ marginLeft: '14rem' }}>
|
||||||
<Route path="/detections/:type/:value" element={<DetailsView />} />
|
{/* Fixed top header */}
|
||||||
<Route path="/investigation/:ip" element={<InvestigationView />} />
|
<TopHeader counts={counts} />
|
||||||
<Route path="/investigation/ja4/:ja4" element={<JA4InvestigationView />} />
|
|
||||||
<Route path="/entities/subnet/:subnet" element={<SubnetInvestigation />} />
|
{/* Scrollable page content */}
|
||||||
<Route path="/entities/:type/:value" element={<EntityInvestigationView />} />
|
<main className="flex-1 px-6 py-5 mt-14 overflow-auto">
|
||||||
<Route path="/tools/correlation-graph/:ip" element={<CorrelationGraph ip={window.location.pathname.split('/').pop() || ''} height="600px" />} />
|
<Routes>
|
||||||
<Route path="/tools/timeline/:ip?" element={<InteractiveTimeline ip={window.location.pathname.split('/').pop()} height="400px" />} />
|
<Route path="/" element={<IncidentsView />} />
|
||||||
</Routes>
|
<Route path="/incidents" element={<IncidentsView />} />
|
||||||
</main>
|
<Route path="/pivot" element={<PivotView />} />
|
||||||
|
<Route path="/fingerprints" element={<FingerprintsView />} />
|
||||||
|
<Route path="/campaigns" element={<CampaignsView />} />
|
||||||
|
<Route path="/threat-intel" element={<ThreatIntelView />} />
|
||||||
|
<Route path="/detections" element={<DetectionsList />} />
|
||||||
|
<Route path="/detections/:type/:value" element={<DetailsView />} />
|
||||||
|
<Route path="/investigate" element={<DetectionsList />} />
|
||||||
|
<Route path="/investigate/:type/:value" element={<InvestigateRoute />} />
|
||||||
|
<Route path="/investigation/ja4/:ja4" element={<JA4InvestigationView />} />
|
||||||
|
<Route path="/investigation/:ip" element={<InvestigationView />} />
|
||||||
|
<Route path="/entities/subnet/:subnet" element={<SubnetInvestigation />} />
|
||||||
|
<Route path="/entities/:type/:value" element={<EntityInvestigationView />} />
|
||||||
|
<Route path="/bulk-classify" element={<BulkClassificationRoute />} />
|
||||||
|
<Route path="/tools/correlation-graph/:ip" element={<CorrelationGraphRoute />} />
|
||||||
|
<Route path="/tools/timeline/:ip?" element={<TimelineRoute />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
73
frontend/src/ThemeContext.tsx
Normal file
73
frontend/src/ThemeContext.tsx
Normal file
@ -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<ThemeContextValue>({
|
||||||
|
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<Theme>(() => {
|
||||||
|
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 (
|
||||||
|
<ThemeContext.Provider value={{ theme, resolved, setTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
return useContext(ThemeContext);
|
||||||
|
}
|
||||||
793
frontend/src/components/CampaignsView.tsx
Normal file
793
frontend/src/components/CampaignsView.tsx
Normal file
@ -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<string, number> = { 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<ClusterData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [ja4Items, setJA4Items] = useState<JA4AttributeItem[]>([]);
|
||||||
|
const [ja4Loading, setJA4Loading] = useState(false);
|
||||||
|
const [ja4Error, setJA4Error] = useState<string | null>(null);
|
||||||
|
const [ja4Loaded, setJA4Loaded] = useState(false);
|
||||||
|
|
||||||
|
const [expandedSubnets, setExpandedSubnets] = useState<Set<string>>(new Set());
|
||||||
|
const [subnetIPs, setSubnetIPs] = useState<Map<string, SubnetIPEntry[]>>(new Map());
|
||||||
|
const [subnetLoading, setSubnetLoading] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<ActiveTab>('clusters');
|
||||||
|
const [minIPs, setMinIPs] = useState(3);
|
||||||
|
const [severityFilter, setSeverityFilter] = useState<string>('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 (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-text-secondary">Chargement des campagnes...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-threat-critical/10 border border-threat-critical rounded-lg p-6">
|
||||||
|
<div className="text-threat-critical mb-4">Erreur: {error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
|
||||||
|
{/* ── Row 1: Header + stat cards ── */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-text-primary">🎯 Détection de Campagnes</h1>
|
||||||
|
<p className="text-text-secondary mt-1">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<div className="text-text-secondary text-sm font-medium mb-2">Clusters actifs</div>
|
||||||
|
<div className="text-3xl font-bold text-text-primary">{activeClusters}</div>
|
||||||
|
<div className="text-text-disabled text-xs mt-1">sous-réseaux suspects</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<div className="text-text-secondary text-sm font-medium mb-2">IPs coordinées</div>
|
||||||
|
<div className="text-3xl font-bold text-accent-primary">{coordinatedIPs.toLocaleString()}</div>
|
||||||
|
<div className="text-text-disabled text-xs mt-1">total dans tous les clusters</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6 border border-threat-critical/30">
|
||||||
|
<div className="text-text-secondary text-sm font-medium mb-2">Campagnes critiques</div>
|
||||||
|
<div className="text-3xl font-bold text-threat-critical">{criticalCampaigns}</div>
|
||||||
|
<div className="text-text-disabled text-xs mt-1">niveau critique · >10 IPs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Row 2: Tabs + Filters ── */}
|
||||||
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||||
|
<div className="flex gap-1 bg-background-card rounded-lg p-1">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ id: 'clusters', label: 'Clusters réseau' },
|
||||||
|
{ id: 'ja4', label: 'Fingerprints JA4' },
|
||||||
|
{ id: 'behavioral', label: 'Analyse comportementale' },
|
||||||
|
] as const
|
||||||
|
).map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`py-2 px-4 rounded text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-accent-primary text-white'
|
||||||
|
: 'text-text-secondary hover:text-text-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-text-secondary text-sm whitespace-nowrap">IPs min :</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
value={minIPs}
|
||||||
|
onChange={e => setMinIPs(parseInt(e.target.value))}
|
||||||
|
className="w-24 accent-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-text-primary text-sm font-mono w-5 text-right">{minIPs}</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={severityFilter}
|
||||||
|
onChange={e => setSeverityFilter(e.target.value)}
|
||||||
|
className="bg-background-card text-text-primary text-sm rounded-lg px-3 py-2 border border-border focus:outline-none focus:border-accent-primary"
|
||||||
|
>
|
||||||
|
<option value="all">Tous niveaux</option>
|
||||||
|
<option value="critical">Critique</option>
|
||||||
|
<option value="high">Élevé</option>
|
||||||
|
<option value="medium">Moyen</option>
|
||||||
|
<option value="low">Faible</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Tab Content ── */}
|
||||||
|
{activeTab === 'clusters' && (
|
||||||
|
<ClustersTab
|
||||||
|
clusters={filteredClusters}
|
||||||
|
expandedSubnets={expandedSubnets}
|
||||||
|
subnetIPs={subnetIPs}
|
||||||
|
subnetLoading={subnetLoading}
|
||||||
|
onToggleSubnet={toggleSubnet}
|
||||||
|
onNavigate={navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'ja4' && (
|
||||||
|
<JA4Tab
|
||||||
|
items={ja4Campaigns}
|
||||||
|
loading={ja4Loading}
|
||||||
|
error={ja4Error}
|
||||||
|
onNavigate={navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'behavioral' && (
|
||||||
|
<BehavioralTab clusters={clusters} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tab: Clusters réseau ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ClustersTabProps {
|
||||||
|
clusters: ClusterData[];
|
||||||
|
expandedSubnets: Set<string>;
|
||||||
|
subnetIPs: Map<string, SubnetIPEntry[]>;
|
||||||
|
subnetLoading: Set<string>;
|
||||||
|
onToggleSubnet: (subnet: string) => void;
|
||||||
|
onNavigate: (path: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClustersTab({
|
||||||
|
clusters,
|
||||||
|
expandedSubnets,
|
||||||
|
subnetIPs,
|
||||||
|
subnetLoading,
|
||||||
|
onToggleSubnet,
|
||||||
|
onNavigate,
|
||||||
|
}: ClustersTabProps) {
|
||||||
|
if (clusters.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-12 text-center">
|
||||||
|
<div className="text-4xl mb-4">🔍</div>
|
||||||
|
<div className="text-text-secondary">Aucun cluster correspondant aux filtres</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{clusters.map(cluster => (
|
||||||
|
<ClusterCard
|
||||||
|
key={cluster.subnet}
|
||||||
|
cluster={cluster}
|
||||||
|
expanded={expandedSubnets.has(cluster.subnet)}
|
||||||
|
ips={subnetIPs.get(cluster.subnet)}
|
||||||
|
loadingIPs={subnetLoading.has(cluster.subnet)}
|
||||||
|
onToggle={() => onToggleSubnet(cluster.subnet)}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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 (
|
||||||
|
<div className={`bg-background-secondary rounded-lg border ${border}/30 overflow-hidden`}>
|
||||||
|
{/* Coloured header strip */}
|
||||||
|
<div className={`px-4 py-3 flex flex-wrap items-center gap-2 ${bg} border-b ${border}/20`}>
|
||||||
|
<span className={`${text} font-bold text-sm`}>{getThreatLabel(threatLevel)}</span>
|
||||||
|
<span className="text-text-primary font-mono font-semibold">{cluster.subnet}</span>
|
||||||
|
{isHighRisk && (
|
||||||
|
<span className="bg-threat-critical/20 text-threat-critical px-2 py-0.5 rounded text-xs font-bold">
|
||||||
|
BOTNET
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cluster.trend === 'new' && (
|
||||||
|
<span className="bg-accent-primary/20 text-accent-primary px-2 py-0.5 rounded text-xs font-medium">
|
||||||
|
NOUVEAU
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cluster.trend === 'up' && (
|
||||||
|
<span className="bg-threat-high/20 text-threat-high px-2 py-0.5 rounded text-xs font-medium">
|
||||||
|
↑ +{cluster.trend_percentage}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="ml-auto text-text-secondary text-xs font-mono">
|
||||||
|
{cluster.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card body */}
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Stats row */}
|
||||||
|
<div className="flex flex-wrap gap-6 text-sm">
|
||||||
|
<span className="text-text-secondary">
|
||||||
|
<span className="text-text-primary font-semibold">{cluster.unique_ips}</span> IP{cluster.unique_ips !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
<span className="text-text-secondary">
|
||||||
|
<span className="text-text-primary font-semibold">{cluster.total_detections.toLocaleString()}</span> détections
|
||||||
|
</span>
|
||||||
|
{cluster.asn && (
|
||||||
|
<span className="text-text-secondary">
|
||||||
|
ASN <span className="text-text-primary font-mono font-semibold">{cluster.asn}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cluster.primary_target && (
|
||||||
|
<span className="text-text-secondary">
|
||||||
|
Cible : <span className="text-text-primary font-semibold">{cluster.primary_target}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JA4 + UA */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{cluster.ja4 && (
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="text-text-disabled w-8">JA4</span>
|
||||||
|
<code className="font-mono text-text-secondary bg-background-card px-2 py-0.5 rounded truncate max-w-xs">
|
||||||
|
{cluster.ja4}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cluster.primary_ua && (
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="text-text-disabled w-8">UA</span>
|
||||||
|
<span className="text-text-secondary truncate max-w-xs" title={cluster.primary_ua}>
|
||||||
|
{cluster.primary_ua}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countries */}
|
||||||
|
{cluster.countries?.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{cluster.countries.map(c => (
|
||||||
|
<span key={c.code} className="bg-background-card text-text-secondary px-2 py-0.5 rounded text-xs">
|
||||||
|
{c.code} {c.percentage < 100 ? `${c.percentage}%` : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Score bar */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-text-secondary text-xs">Score menace</span>
|
||||||
|
<span className={`text-xs font-mono font-semibold ${scoreColor}`}>{cluster.score}/100</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-2 bg-background-card rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute h-full rounded-full bg-accent-primary/70 transition-all"
|
||||||
|
style={{ width: `${cluster.score}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex flex-wrap gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(`/investigation/${encodeURIComponent(cluster.sample_ip)}`)}
|
||||||
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
🔍 Investiguer IP
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(`/pivot?entities=${encodeURIComponent(cluster.sample_ip)}`)}
|
||||||
|
className="bg-background-card hover:bg-background-card/80 text-text-primary px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-border"
|
||||||
|
>
|
||||||
|
⇄ Pivot IP
|
||||||
|
</button>
|
||||||
|
{cluster.ja4 && cluster.ja4 !== 'HTTP_CLEAR_TEXT' && (
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(`/investigation/ja4/${encodeURIComponent(cluster.ja4)}`)}
|
||||||
|
className="bg-background-card hover:bg-background-card/80 text-text-secondary px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-border"
|
||||||
|
>
|
||||||
|
🔏 Investiguer JA4
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="bg-background-card hover:bg-background-card/80 text-text-secondary px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-border"
|
||||||
|
>
|
||||||
|
{expanded ? '▲ Masquer IPs' : '▼ Voir IPs'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded IP list */}
|
||||||
|
{expanded && (
|
||||||
|
<div className="mt-2 bg-background-card rounded-lg overflow-hidden border border-border">
|
||||||
|
{loadingIPs ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="text-text-secondary text-sm animate-pulse">Chargement des IPs...</div>
|
||||||
|
</div>
|
||||||
|
) : ips && ips.length > 0 ? (
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border text-left">
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">IP</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">JA4</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Détections</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Confiance</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ips.map((entry, idx) => (
|
||||||
|
<tr
|
||||||
|
key={entry.ip}
|
||||||
|
className={`hover:bg-background-secondary/50 transition-colors ${
|
||||||
|
idx < ips.length - 1 ? 'border-b border-border/50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-2 font-mono text-text-primary text-xs">{entry.ip}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span
|
||||||
|
className="font-mono text-text-secondary text-xs block max-w-[160px] truncate"
|
||||||
|
title={entry.ja4}
|
||||||
|
>
|
||||||
|
{entry.ja4 || '—'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-text-primary text-xs">
|
||||||
|
{entry.detections.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={`text-xs font-mono font-semibold ${getConfidenceTextColor(entry.confidence)}`}>
|
||||||
|
{Math.round(entry.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(`/investigation/${encodeURIComponent(entry.ip)}`)}
|
||||||
|
className="text-accent-primary hover:text-accent-primary/80 text-xs transition-colors"
|
||||||
|
>
|
||||||
|
Investiguer →
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-6 text-text-disabled text-sm text-center">
|
||||||
|
Aucune IP disponible pour ce subnet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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 (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-text-secondary">Chargement des fingerprints JA4...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-threat-critical/10 border border-threat-critical rounded-lg p-6">
|
||||||
|
<div className="text-threat-critical">Erreur : {error}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-12 text-center">
|
||||||
|
<div className="text-4xl mb-4">🔑</div>
|
||||||
|
<div className="text-text-secondary">Aucun fingerprint JA4 avec 5+ IPs détecté</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...items].sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||||
|
{sorted.map(item => (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className="bg-background-secondary rounded-lg p-4 border border-border hover:border-accent-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
|
<code className="font-mono text-text-primary text-xs break-all leading-relaxed">
|
||||||
|
{item.value}
|
||||||
|
</code>
|
||||||
|
<span className={`flex-shrink-0 px-2 py-0.5 rounded text-xs font-bold ${getJA4CountColor(item.count)}`}>
|
||||||
|
{item.count} IPs
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(`/investigation/ja4/${encodeURIComponent(item.value)}`)}
|
||||||
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
🔍 Investiguer JA4
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate('/fingerprints')}
|
||||||
|
className="bg-background-card hover:bg-background-card/80 text-text-secondary px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-border"
|
||||||
|
>
|
||||||
|
📋 Voir fingerprint
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tab: Analyse comportementale ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface BehavioralTabProps {
|
||||||
|
clusters: ClusterData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function BehavioralTab({ clusters }: BehavioralTabProps) {
|
||||||
|
if (clusters.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-12 text-center">
|
||||||
|
<div className="text-4xl mb-4">📊</div>
|
||||||
|
<div className="text-text-secondary">Aucun cluster disponible pour l'analyse comportementale</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by shared JA4 (multi-subnet same fingerprint = coordinated campaign)
|
||||||
|
const ja4Groups: Record<string, ClusterData[]> = {};
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* JA4-correlated clusters */}
|
||||||
|
{groupsSorted.length > 0 && (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
|
<h3 className="text-text-primary font-semibold mb-1">Clusters partageant le même JA4</h3>
|
||||||
|
<p className="text-text-secondary text-sm mb-4">
|
||||||
|
Subnets distincts utilisant le même fingerprint TLS — indicateur fort de botnet centralisé.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{groupsSorted.map(([ja4prefix, group]) => (
|
||||||
|
<div
|
||||||
|
key={ja4prefix}
|
||||||
|
className="rounded-lg p-3 border border-threat-high/40 bg-threat-high/5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 mb-2">
|
||||||
|
<code className="font-mono text-xs text-text-primary">{ja4prefix}…</code>
|
||||||
|
<span className="text-text-secondary text-sm">
|
||||||
|
{group.length} subnet{group.length !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
<span className="bg-threat-high/20 text-threat-high px-2 py-0.5 rounded text-xs font-medium">
|
||||||
|
⚠ Campagne probable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{group.map(c => {
|
||||||
|
const { bg, text } = getThreatColors(c.severity.toLowerCase());
|
||||||
|
return (
|
||||||
|
<span key={c.subnet} className={`px-2 py-0.5 rounded text-xs font-mono ${bg} ${text}`}>
|
||||||
|
{c.subnet}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Behavioral matrix */}
|
||||||
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
|
<h3 className="text-text-primary font-semibold mb-4">Matrice de signaux comportementaux</h3>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border text-left">
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Subnet</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Score</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Tendance</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Niveau menace</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs">Pays</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs text-right">IPs</th>
|
||||||
|
<th className="px-4 py-2 text-text-secondary font-medium text-xs text-right">Détections</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{clusters.map((cluster, idx) => {
|
||||||
|
const { bg, text } = getThreatColors(cluster.severity.toLowerCase());
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={cluster.subnet}
|
||||||
|
className={`hover:bg-background-card/50 transition-colors ${
|
||||||
|
idx < clusters.length - 1 ? 'border-b border-border/50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-2 font-mono text-text-primary text-xs">{cluster.subnet}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={`text-xs font-mono font-semibold ${getConfidenceTextColor(cluster.score / 100)}`}>
|
||||||
|
{cluster.score}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{cluster.trend === 'up' ? (
|
||||||
|
<span className="bg-threat-high/20 text-threat-high px-2 py-0.5 rounded text-xs">
|
||||||
|
↑ +{cluster.trend_percentage}%
|
||||||
|
</span>
|
||||||
|
) : cluster.trend === 'new' ? (
|
||||||
|
<span className="bg-accent-primary/20 text-accent-primary px-2 py-0.5 rounded text-xs">
|
||||||
|
Nouveau
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-text-disabled text-xs">{cluster.trend}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${bg} ${text}`}>
|
||||||
|
{getThreatLabel(cluster.severity.toLowerCase())}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{cluster.countries?.slice(0, 2).map(c => (
|
||||||
|
<span key={c.code} className="text-text-secondary text-xs mr-1">{c.code}</span>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-text-primary text-xs font-mono text-right">
|
||||||
|
{cluster.unique_ips}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-text-primary text-xs font-mono text-right">
|
||||||
|
{cluster.total_detections.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -106,18 +106,21 @@ export function DetailsView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Insights */}
|
{/* Insights + Variabilité côte à côte */}
|
||||||
{data.insights.length > 0 && (
|
<div className="grid grid-cols-3 gap-6 items-start">
|
||||||
<div className="space-y-2">
|
{data.insights.length > 0 && (
|
||||||
<h2 className="text-lg font-semibold text-text-primary">Insights</h2>
|
<div className="space-y-2">
|
||||||
{data.insights.map((insight, i) => (
|
<h2 className="text-lg font-semibold text-text-primary">Insights</h2>
|
||||||
<InsightCard key={i} insight={insight} />
|
{data.insights.map((insight, i) => (
|
||||||
))}
|
<InsightCard key={i} insight={insight} />
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Variabilité */}
|
<div className={data.insights.length > 0 ? 'col-span-2' : 'col-span-3'}>
|
||||||
<VariabilityPanel attributes={data.attributes} />
|
<VariabilityPanel attributes={data.attributes} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Bouton retour */}
|
{/* Bouton retour */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useDetections } from '../hooks/useDetections';
|
import { useDetections } from '../hooks/useDetections';
|
||||||
|
|
||||||
type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity';
|
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() {
|
export function DetectionsList() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const page = parseInt(searchParams.get('page') || '1');
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
@ -311,13 +312,13 @@ export function DetectionsList() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-background-card">
|
<tbody className="divide-y divide-background-card">
|
||||||
{processedData.items.map((detection) => (
|
{processedData.items.map((detection) => (
|
||||||
<tr
|
<tr
|
||||||
key={`${detection.src_ip}-${detection.detected_at}-${groupByIP ? 'grouped' : 'individual'}`}
|
key={`${detection.src_ip}-${detection.detected_at}-${groupByIP ? 'grouped' : 'individual'}`}
|
||||||
className="hover:bg-background-card/50 transition-colors cursor-pointer"
|
className="hover:bg-background-card/50 transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.href = `/detections/ip/${encodeURIComponent(detection.src_ip)}`;
|
navigate(`/detections/ip/${encodeURIComponent(detection.src_ip)}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{columns.filter(col => col.visible).map(col => {
|
{columns.filter(col => col.visible).map(col => {
|
||||||
if (col.key === 'ip_ja4') {
|
if (col.key === 'ip_ja4') {
|
||||||
const detectionAny = detection as any;
|
const detectionAny = detection as any;
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export function EntityInvestigationView() {
|
|||||||
const [data, setData] = useState<EntityInvestigationData | null>(null);
|
const [data, setData] = useState<EntityInvestigationData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showAllUA, setShowAllUA] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!type || !value) {
|
if (!type || !value) {
|
||||||
@ -90,10 +91,6 @@ export function EntityInvestigationView() {
|
|||||||
return flags[code] || code;
|
return flags[code] || code;
|
||||||
};
|
};
|
||||||
|
|
||||||
const truncateUA = (ua: string, maxLength: number = 150) => {
|
|
||||||
if (ua.length <= maxLength) return ua;
|
|
||||||
return ua.substring(0, maxLength) + '...';
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -227,10 +224,10 @@ export function EntityInvestigationView() {
|
|||||||
<div className="bg-background-secondary rounded-lg p-6 mb-6">
|
<div className="bg-background-secondary rounded-lg p-6 mb-6">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">3. User-Agents</h3>
|
<h3 className="text-lg font-medium text-text-primary mb-4">3. User-Agents</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.user_agents.slice(0, 10).map((ua, idx) => (
|
{(showAllUA ? data.user_agents : data.user_agents.slice(0, 10)).map((ua, idx) => (
|
||||||
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
||||||
<div className="text-xs text-text-primary font-mono break-all">
|
<div className="text-xs text-text-primary font-mono break-all leading-relaxed">
|
||||||
{truncateUA(ua.value)}
|
{ua.value}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-text-secondary text-xs">{ua.count} requêtes</div>
|
<div className="text-text-secondary text-xs">{ua.count} requêtes</div>
|
||||||
@ -243,9 +240,12 @@ export function EntityInvestigationView() {
|
|||||||
<div className="text-center text-text-secondary py-8">Aucun User-Agent</div>
|
<div className="text-center text-text-secondary py-8">Aucun User-Agent</div>
|
||||||
)}
|
)}
|
||||||
{data.user_agents.length > 10 && (
|
{data.user_agents.length > 10 && (
|
||||||
<div className="text-center text-text-secondary mt-4 text-sm">
|
<button
|
||||||
+{data.user_agents.length - 10} autres User-Agents
|
onClick={() => setShowAllUA(v => !v)}
|
||||||
</div>
|
className="mt-4 w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
{showAllUA ? '↑ Réduire' : `↓ Voir les ${data.user_agents.length - 10} autres`}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
1385
frontend/src/components/FingerprintsView.tsx
Normal file
1385
frontend/src/components/FingerprintsView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { QuickSearch } from './QuickSearch';
|
|
||||||
|
|
||||||
interface IncidentCluster {
|
interface IncidentCluster {
|
||||||
id: string;
|
id: string;
|
||||||
@ -124,10 +123,7 @@ export function IncidentsView() {
|
|||||||
<p className="text-text-secondary text-sm mt-1">
|
<p className="text-text-secondary text-sm mt-1">
|
||||||
Surveillance en temps réel - 24 dernières heures
|
Surveillance en temps réel - 24 dernières heures
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-auto">
|
|
||||||
<QuickSearch />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Critical Metrics */}
|
{/* Critical Metrics */}
|
||||||
@ -212,8 +208,10 @@ export function IncidentsView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Priority Incidents */}
|
{/* Main content: incidents list (2/3) + top threats table (1/3) */}
|
||||||
<div>
|
<div className="grid grid-cols-3 gap-6 items-start">
|
||||||
|
{/* Incidents list — 2/3 */}
|
||||||
|
<div className="col-span-2">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-xl font-semibold text-text-primary">
|
<h2 className="text-xl font-semibold text-text-primary">
|
||||||
Incidents Prioritaires
|
Incidents Prioritaires
|
||||||
@ -367,41 +365,35 @@ export function IncidentsView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>{/* end col-span-2 */}
|
||||||
|
|
||||||
{/* Top Active Threats */}
|
{/* Top threats sidebar — 1/3 */}
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="sticky top-4">
|
||||||
<h2 className="text-xl font-semibold text-text-primary mb-4">
|
<div className="bg-background-secondary rounded-lg overflow-hidden">
|
||||||
Top Menaces Actives
|
<div className="p-4 border-b border-background-card">
|
||||||
</h2>
|
<h3 className="text-base font-semibold text-text-primary">🔥 Top Menaces</h3>
|
||||||
<div className="overflow-x-auto">
|
</div>
|
||||||
<table className="w-full">
|
<div className="divide-y divide-background-card">
|
||||||
<thead className="bg-background-card">
|
{clusters.slice(0, 12).map((cluster, index) => (
|
||||||
<tr>
|
<div
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">#</th>
|
key={cluster.id}
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Entité</th>
|
className="px-4 py-3 flex items-center gap-3 hover:bg-background-card/50 transition-colors cursor-pointer"
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Type</th>
|
onClick={() => navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)}
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Score</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Pays</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">ASN</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Hits/s</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Tendance</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-background-card">
|
|
||||||
{clusters.slice(0, 10).map((cluster, index) => (
|
|
||||||
<tr
|
|
||||||
key={cluster.id}
|
|
||||||
className="hover:bg-background-card/50 transition-colors cursor-pointer"
|
|
||||||
onClick={() => navigate(`/investigation/${cluster.subnet?.split('/')[0] || ''}`)}
|
|
||||||
>
|
>
|
||||||
<td className="px-4 py-3 text-text-secondary">{index + 1}</td>
|
<span className="text-text-disabled text-xs w-4">{index + 1}</span>
|
||||||
<td className="px-4 py-3 font-mono text-sm text-text-primary">
|
<div className="flex-1 min-w-0">
|
||||||
{cluster.subnet?.split('/')[0] || 'Unknown'}
|
<div className="font-mono text-xs text-text-primary truncate">
|
||||||
</td>
|
{cluster.sample_ip || cluster.subnet?.split('/')[0] || 'Unknown'}
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">IP</td>
|
</div>
|
||||||
<td className="px-4 py-3">
|
<div className="text-xs text-text-secondary flex gap-2 mt-0.5">
|
||||||
<span className={`px-2 py-1 rounded text-xs font-bold ${
|
{cluster.countries[0] && (
|
||||||
|
<span>{getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code}</span>
|
||||||
|
)}
|
||||||
|
<span>AS{cluster.asn || '?'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<span className={`px-1.5 py-0.5 rounded text-xs font-bold ${
|
||||||
cluster.score > 80 ? 'bg-red-500 text-white' :
|
cluster.score > 80 ? 'bg-red-500 text-white' :
|
||||||
cluster.score > 60 ? 'bg-orange-500 text-white' :
|
cluster.score > 60 ? 'bg-orange-500 text-white' :
|
||||||
cluster.score > 40 ? 'bg-yellow-500 text-white' :
|
cluster.score > 40 ? 'bg-yellow-500 text-white' :
|
||||||
@ -409,33 +401,25 @@ export function IncidentsView() {
|
|||||||
}`}>
|
}`}>
|
||||||
{cluster.score}
|
{cluster.score}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
<span className={`text-xs font-bold ${
|
||||||
<td className="px-4 py-3 text-text-primary">
|
cluster.trend === 'up' ? 'text-red-500' :
|
||||||
{cluster.countries[0] && (
|
cluster.trend === 'down' ? 'text-green-500' :
|
||||||
<>
|
'text-gray-400'
|
||||||
{getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code}
|
}`}>
|
||||||
</>
|
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'}
|
||||||
)}
|
</span>
|
||||||
</td>
|
</div>
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">
|
</div>
|
||||||
AS{cluster.asn || '?'}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-text-primary font-bold">
|
|
||||||
{Math.round(cluster.total_detections / 24) || 0}
|
|
||||||
</td>
|
|
||||||
<td className={`px-4 py-3 font-bold ${
|
|
||||||
cluster.trend === 'up' ? 'text-red-500' :
|
|
||||||
cluster.trend === 'down' ? 'text-green-500' :
|
|
||||||
'text-gray-400'
|
|
||||||
}`}>
|
|
||||||
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} {cluster.trend_percentage}%
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
{clusters.length === 0 && (
|
||||||
</table>
|
<div className="px-4 py-8 text-center text-text-secondary text-sm">
|
||||||
|
Aucune menace active
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>{/* end grid */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi
|
|||||||
const [data, setData] = useState<EntityData | null>(null);
|
const [data, setData] = useState<EntityData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [classifying, setClassifying] = useState(false);
|
const [classifying, setClassifying] = useState(false);
|
||||||
|
const [showAllUA, setShowAllUA] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@ -193,9 +194,9 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi
|
|||||||
🤖 User-Agents ({data.attributes.user_agents.length})
|
🤖 User-Agents ({data.attributes.user_agents.length})
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{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) => (
|
||||||
<div key={idx} className="bg-background-card rounded-lg p-3">
|
<div key={idx} className="bg-background-card rounded-lg p-3">
|
||||||
<div className="text-xs text-text-primary font-mono break-all">
|
<div className="text-xs text-text-primary font-mono break-all leading-relaxed">
|
||||||
{ua.value}
|
{ua.value}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-text-secondary mt-1">
|
<div className="text-xs text-text-secondary mt-1">
|
||||||
@ -203,6 +204,14 @@ export function InvestigationPanel({ entityType, entityValue, onClose }: Investi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{data.attributes.user_agents.length > 5 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAllUA(v => !v)}
|
||||||
|
className="w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
{showAllUA ? '↑ Réduire' : `↓ Voir les ${data.attributes.user_agents.length - 5} autres`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,13 +1,156 @@
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { SubnetAnalysis } from './analysis/SubnetAnalysis';
|
import { SubnetAnalysis } from './analysis/SubnetAnalysis';
|
||||||
import { CountryAnalysis } from './analysis/CountryAnalysis';
|
import { CountryAnalysis } from './analysis/CountryAnalysis';
|
||||||
import { JA4Analysis } from './analysis/JA4Analysis';
|
import { JA4Analysis } from './analysis/JA4Analysis';
|
||||||
import { UserAgentAnalysis } from './analysis/UserAgentAnalysis';
|
import { UserAgentAnalysis } from './analysis/UserAgentAnalysis';
|
||||||
import { CorrelationSummary } from './analysis/CorrelationSummary';
|
import { CorrelationSummary } from './analysis/CorrelationSummary';
|
||||||
import { CorrelationGraph } from './CorrelationGraph';
|
import { CorrelationGraph } from './CorrelationGraph';
|
||||||
import { InteractiveTimeline } from './InteractiveTimeline';
|
|
||||||
import { ReputationPanel } from './ReputationPanel';
|
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<string, { cls: string; icon: string; label: string }> = {
|
||||||
|
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<CoherenceData | null>(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 (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-5">
|
||||||
|
<h3 className="text-base font-semibold text-text-primary mb-4">🎭 Cohérence JA4 / User-Agent</h3>
|
||||||
|
{loading && <div className="text-text-disabled text-sm">Analyse en cours…</div>}
|
||||||
|
{error && <div className="text-text-disabled text-sm">Données insuffisantes pour cette IP</div>}
|
||||||
|
{data && vs && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Verdict badge + score */}
|
||||||
|
<div className={`flex items-center gap-3 p-3 rounded-lg border ${vs.cls}`}>
|
||||||
|
<span className="text-2xl">{vs.icon}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold text-sm">{vs.label}</div>
|
||||||
|
<div className="text-xs opacity-75 mt-0.5">
|
||||||
|
Score de spoofing: <strong>{data.spoofing_score}/100</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-24">
|
||||||
|
<div className="h-2 bg-white/20 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full ${
|
||||||
|
data.spoofing_score >= 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}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Explanation */}
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{data.explanation.map((e, i) => (
|
||||||
|
<li key={i} className="text-xs text-text-secondary flex items-start gap-1.5">
|
||||||
|
<span className="text-text-disabled mt-0.5">•</span> {e}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Key indicators */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{[
|
||||||
|
{ 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) => (
|
||||||
|
<div key={ind.label} className={`rounded p-2 text-center ${ind.warn ? 'bg-threat-high/10' : 'bg-background-card'}`}>
|
||||||
|
<div className={`text-sm font-semibold ${ind.warn ? 'text-threat-high' : 'text-text-primary'}`}>{ind.value}</div>
|
||||||
|
<div className="text-xs text-text-disabled">{ind.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top UAs */}
|
||||||
|
{data.user_agents.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-disabled mb-1.5 font-medium uppercase tracking-wide">User-Agents observés</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{data.user_agents.slice(0, 4).map((u, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-xs">
|
||||||
|
<span className={`shrink-0 px-1.5 py-0.5 rounded text-xs ${
|
||||||
|
u.type === 'bot' ? 'bg-threat-critical/20 text-threat-critical' :
|
||||||
|
u.type === 'browser' ? 'bg-accent-primary/20 text-accent-primary' :
|
||||||
|
'bg-background-card text-text-secondary'
|
||||||
|
}`}>{u.type}</span>
|
||||||
|
<span className="truncate text-text-secondary font-mono" title={u.ua}>
|
||||||
|
{u.ua.length > 45 ? u.ua.slice(0, 45) + '…' : u.ua}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* JA4 links */}
|
||||||
|
{data.fingerprints.ja4_list.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-disabled mb-1 font-medium uppercase tracking-wide">JA4 utilisés</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{data.fingerprints.ja4_list.map((j4) => (
|
||||||
|
<button
|
||||||
|
key={j4}
|
||||||
|
onClick={() => navigate(`/investigation/ja4/${encodeURIComponent(j4)}`)}
|
||||||
|
className="text-xs font-mono text-accent-primary hover:underline truncate max-w-[140px]"
|
||||||
|
title={j4}
|
||||||
|
>
|
||||||
|
{j4.length > 18 ? `${j4.slice(0, 9)}…${j4.slice(-8)}` : j4}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function InvestigationView() {
|
export function InvestigationView() {
|
||||||
const { ip } = useParams<{ ip: string }>();
|
const { ip } = useParams<{ ip: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -45,41 +188,47 @@ export function InvestigationView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Panels d'analyse */}
|
{/* Ligne 1 : Réputation (1/3) + Graph de corrélations (2/3) */}
|
||||||
<div className="space-y-6">
|
<div className="grid grid-cols-3 gap-6 items-start">
|
||||||
{/* NOUVEAU: Réputation IP */}
|
<div className="bg-background-secondary rounded-lg p-6 h-full">
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<h3 className="text-lg font-medium text-text-primary mb-4">🌍 Réputation IP</h3>
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">🌍 Réputation IP (Bases publiques)</h3>
|
<ReputationPanel ip={ip} />
|
||||||
<ReputationPanel ip={ip || ''} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-2 bg-background-secondary rounded-lg p-6">
|
||||||
{/* NOUVEAU: Graph de corrélations */}
|
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">🕸️ Graph de Corrélations</h3>
|
<h3 className="text-lg font-medium text-text-primary mb-4">🕸️ Graph de Corrélations</h3>
|
||||||
<CorrelationGraph ip={ip || ''} height="500px" />
|
<CorrelationGraph ip={ip} height="600px" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* NOUVEAU: Timeline interactive */}
|
{/* Ligne 2 : Subnet / Country / JA4 (3 colonnes) */}
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="grid grid-cols-3 gap-6 items-start">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">📈 Timeline d'Activité</h3>
|
|
||||||
<InteractiveTimeline ip={ip || ''} hours={24} height="350px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Panel 1: Subnet/ASN */}
|
|
||||||
<SubnetAnalysis ip={ip} />
|
<SubnetAnalysis ip={ip} />
|
||||||
|
|
||||||
{/* Panel 2: Country (relatif à l'IP) */}
|
|
||||||
<CountryAnalysis ip={ip} />
|
<CountryAnalysis ip={ip} />
|
||||||
|
|
||||||
{/* Panel 3: JA4 */}
|
|
||||||
<JA4Analysis ip={ip} />
|
<JA4Analysis ip={ip} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Panel 4: User-Agents */}
|
{/* Ligne 3 : User-Agents (1/2) + Classification (1/2) */}
|
||||||
|
<div className="grid grid-cols-2 gap-6 items-start">
|
||||||
<UserAgentAnalysis ip={ip} />
|
<UserAgentAnalysis ip={ip} />
|
||||||
|
|
||||||
{/* Panel 5: Correlation Summary + Classification */}
|
|
||||||
<CorrelationSummary ip={ip} onClassify={handleClassify} />
|
<CorrelationSummary ip={ip} onClassify={handleClassify} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ligne 4 : Cohérence JA4/UA (spoofing) */}
|
||||||
|
<div className="grid grid-cols-3 gap-6 items-start">
|
||||||
|
<FingerprintCoherenceWidget ip={ip} />
|
||||||
|
<div className="col-span-2 bg-background-secondary rounded-lg p-5">
|
||||||
|
<h3 className="text-base font-semibold text-text-primary mb-3">🔏 JA4 Légitimes (baseline)</h3>
|
||||||
|
<p className="text-xs text-text-secondary mb-3">
|
||||||
|
Comparez les fingerprints de cette IP avec la baseline des JA4 légitimes pour évaluer le risque de spoofing.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/fingerprints?tab=spoofing')}
|
||||||
|
className="text-sm px-4 py-2 rounded-lg bg-accent-primary/20 text-accent-primary hover:bg-accent-primary/30 transition-colors"
|
||||||
|
>
|
||||||
|
🎭 Voir l'analyse de spoofing globale →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
@ -180,155 +176,127 @@ export function JA4InvestigationView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<StatBox
|
<StatBox label="IPs Uniques" value={data.unique_ips.toLocaleString()} />
|
||||||
label="IPs Uniques"
|
<StatBox label="Première détection" value={formatDate(data.first_seen)} />
|
||||||
value={data.unique_ips.toLocaleString()}
|
<StatBox label="Dernière détection" value={formatDate(data.last_seen)} />
|
||||||
/>
|
<StatBox label="User-Agents" value={data.user_agents.length.toString()} />
|
||||||
<StatBox
|
|
||||||
label="Première détection"
|
|
||||||
value={formatDate(data.first_seen)}
|
|
||||||
/>
|
|
||||||
<StatBox
|
|
||||||
label="Dernière détection"
|
|
||||||
value={formatDate(data.last_seen)}
|
|
||||||
/>
|
|
||||||
<StatBox
|
|
||||||
label="User-Agents"
|
|
||||||
value={data.user_agents.length.toString()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Panel 1: Top IPs */}
|
{/* Ligne 2: Top IPs (gauche) | Top Pays + Top ASNs (droite empilés) */}
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="grid grid-cols-2 gap-6 items-start">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">1. TOP IPs (Utilisant ce JA4)</h3>
|
{/* Top IPs */}
|
||||||
<div className="space-y-2">
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
{data.top_ips.length > 0 ? (
|
<h3 className="text-lg font-medium text-text-primary mb-4">📍 TOP IPs</h3>
|
||||||
data.top_ips.map((ipData, idx) => (
|
<div className="space-y-2">
|
||||||
<div
|
{data.top_ips.length > 0 ? (
|
||||||
key={idx}
|
data.top_ips.map((ipData, idx) => (
|
||||||
className="flex items-center justify-between bg-background-card rounded-lg p-3"
|
<div
|
||||||
>
|
key={idx}
|
||||||
<div className="flex items-center gap-3">
|
className="flex items-center justify-between bg-background-card rounded-lg p-3"
|
||||||
<span className="text-text-secondary text-sm w-6">{idx + 1}.</span>
|
>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => navigate(`/investigation/${ipData.ip}`)}
|
<span className="text-text-secondary text-sm w-6">{idx + 1}.</span>
|
||||||
className="font-mono text-sm text-accent-primary hover:text-accent-primary/80 transition-colors text-left"
|
<button
|
||||||
>
|
onClick={() => navigate(`/investigation/${ipData.ip}`)}
|
||||||
{ipData.ip}
|
className="font-mono text-sm text-accent-primary hover:text-accent-primary/80 transition-colors text-left"
|
||||||
</button>
|
>
|
||||||
</div>
|
{ipData.ip}
|
||||||
<div className="text-right">
|
</button>
|
||||||
<div className="text-text-primary font-medium">{ipData.count.toLocaleString()}</div>
|
</div>
|
||||||
<div className="text-text-secondary text-xs">{ipData.percentage.toFixed(1)}%</div>
|
<div className="text-right">
|
||||||
</div>
|
<div className="text-text-primary font-medium">{ipData.count.toLocaleString()}</div>
|
||||||
</div>
|
<div className="text-text-secondary text-xs">{ipData.percentage.toFixed(1)}%</div>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-text-secondary py-8">
|
|
||||||
Aucune IP trouvée
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{data.unique_ips > 10 && (
|
|
||||||
<p className="text-text-secondary text-sm mt-4 text-center">
|
|
||||||
... et {data.unique_ips - 10} autres IPs
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Panel 2: Top Pays */}
|
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">2. TOP Pays</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.top_countries.map((country, idx) => (
|
|
||||||
<div key={idx} className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-2xl">{getFlag(country.code)}</span>
|
|
||||||
<div className="text-text-primary font-medium text-sm">
|
|
||||||
{country.name} ({country.code})
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
))
|
||||||
<div className="text-text-primary font-bold">{country.count.toLocaleString()}</div>
|
) : (
|
||||||
<div className="text-text-secondary text-xs">{country.percentage.toFixed(1)}%</div>
|
<div className="text-center text-text-secondary py-8">Aucune IP trouvée</div>
|
||||||
|
)}
|
||||||
|
{data.unique_ips > 10 && (
|
||||||
|
<p className="text-text-secondary text-sm mt-4 text-center">
|
||||||
|
... et {data.unique_ips - 10} autres IPs
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Pays + Top ASNs empilés */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-text-primary mb-4">🌍 TOP Pays</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.top_countries.map((country, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl">{getFlag(country.code)}</span>
|
||||||
|
<span className="text-text-primary font-medium text-sm">{country.name} ({country.code})</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-text-primary font-bold">{country.count.toLocaleString()}</div>
|
||||||
|
<div className="text-text-secondary text-xs">{country.percentage.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-background-card rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full bg-accent-primary transition-all" style={{ width: `${Math.min(country.percentage, 100)}%` }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<div className="w-full bg-background-card rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-accent-primary transition-all"
|
|
||||||
style={{ width: `${Math.min(country.percentage, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-text-primary mb-4">🏢 TOP ASNs</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.top_asns.map((asn, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-text-primary font-medium text-sm">{asn.asn} - {asn.org}</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-text-primary font-bold">{asn.count.toLocaleString()}</div>
|
||||||
|
<div className="text-text-secondary text-xs">{asn.percentage.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-background-card rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full bg-accent-primary transition-all" style={{ width: `${Math.min(asn.percentage, 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Panel 3: Top ASN */}
|
{/* Ligne 3: Top Hosts (gauche) | User-Agents (droite) */}
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="grid grid-cols-2 gap-6 items-start">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">3. TOP ASN</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.top_asns.map((asn, idx) => (
|
|
||||||
<div key={idx} className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-text-primary font-medium text-sm">
|
|
||||||
{asn.asn} - {asn.org}
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-text-primary font-bold">{asn.count.toLocaleString()}</div>
|
|
||||||
<div className="text-text-secondary text-xs">{asn.percentage.toFixed(1)}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-background-card rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-accent-primary transition-all"
|
|
||||||
style={{ width: `${Math.min(asn.percentage, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Panel 4: Top Hosts */}
|
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">4. TOP Hosts Ciblés</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.top_hosts.map((host, idx) => (
|
|
||||||
<div key={idx} className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-text-primary font-medium text-sm truncate max-w-md">
|
|
||||||
{host.host}
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-text-primary font-bold">{host.count.toLocaleString()}</div>
|
|
||||||
<div className="text-text-secondary text-xs">{host.percentage.toFixed(1)}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-background-card rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-accent-primary transition-all"
|
|
||||||
style={{ width: `${Math.min(host.percentage, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Panel 5: User-Agents + Classification */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">5. User-Agents</h3>
|
<h3 className="text-lg font-medium text-text-primary mb-4">🖥️ TOP Hosts Ciblés</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.top_hosts.map((host, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-text-primary font-medium text-sm truncate max-w-xs">{host.host}</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-text-primary font-bold">{host.count.toLocaleString()}</div>
|
||||||
|
<div className="text-text-secondary text-xs">{host.percentage.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-background-card rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full bg-accent-primary transition-all" style={{ width: `${Math.min(host.percentage, 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-text-primary mb-4">🤖 User-Agents</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.user_agents.map((ua, idx) => (
|
{data.user_agents.map((ua, idx) => (
|
||||||
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="text-text-primary text-xs font-mono break-all flex-1">
|
<div className="text-text-primary text-xs font-mono break-all flex-1 leading-relaxed">{ua.ua}</div>
|
||||||
{truncateUA(ua.ua)}
|
|
||||||
</div>
|
|
||||||
{getClassificationBadge(ua.classification)}
|
{getClassificationBadge(ua.classification)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -338,16 +306,14 @@ export function JA4InvestigationView() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{data.user_agents.length === 0 && (
|
{data.user_agents.length === 0 && (
|
||||||
<div className="text-center text-text-secondary py-8">
|
<div className="text-center text-text-secondary py-8">Aucun User-Agent trouvé</div>
|
||||||
Aucun User-Agent trouvé
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Classification JA4 */}
|
|
||||||
<JA4CorrelationSummary ja4={ja4 || ''} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ligne 4: Classification JA4 (full width) */}
|
||||||
|
<JA4CorrelationSummary ja4={ja4 || ''} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
367
frontend/src/components/PivotView.tsx
Normal file
367
frontend/src/components/PivotView.tsx
Normal file
@ -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<EntityCol[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
|
||||||
|
const fetchEntity = useCallback(async (col: EntityCol): Promise<EntityCol> => {
|
||||||
|
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<string> => {
|
||||||
|
const loaded = cols.filter(c => c.data);
|
||||||
|
if (loaded.length < 2) return new Set();
|
||||||
|
const valueCounts = new Map<string, number>();
|
||||||
|
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<string>();
|
||||||
|
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<AttrKey, Set<string>>;
|
||||||
|
|
||||||
|
const totalCorrelations = ATTR_ROWS.reduce(
|
||||||
|
(sum, r) => sum + sharedByKey[r.key].size, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-text-primary">🔗 Pivot — Corrélation Multi-Entités</h1>
|
||||||
|
<p className="text-text-secondary text-sm mt-1">
|
||||||
|
Ajoutez des IPs ou JA4. Les valeurs partagées <span className="text-yellow-400 font-bold">★</span> révèlent des campagnes coordonnées.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input bar */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addEntity}
|
||||||
|
disabled={!input.trim()}
|
||||||
|
className="px-5 py-2.5 bg-accent-primary text-white rounded-lg text-sm font-medium hover:bg-accent-primary/80 disabled:opacity-40 transition-colors"
|
||||||
|
>
|
||||||
|
+ Ajouter
|
||||||
|
</button>
|
||||||
|
{cols.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setCols([])}
|
||||||
|
className="px-4 py-2.5 bg-background-card text-text-secondary rounded-lg text-sm hover:text-text-primary transition-colors"
|
||||||
|
>
|
||||||
|
Tout effacer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{cols.length === 0 && (
|
||||||
|
<div className="bg-background-secondary rounded-xl p-12 text-center space-y-3">
|
||||||
|
<div className="text-5xl">🔗</div>
|
||||||
|
<div className="text-lg font-medium text-text-primary">Aucune entité ajoutée</div>
|
||||||
|
<div className="text-sm text-text-secondary max-w-md mx-auto">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-disabled pt-2">
|
||||||
|
Exemple : <span className="font-mono text-text-secondary">1.2.3.4, 5.6.7.8, 9.10.11.12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Correlation summary badge */}
|
||||||
|
{cols.length >= 2 && cols.some(c => c.data) && (
|
||||||
|
<div className={`flex items-center gap-3 px-4 py-3 rounded-lg border ${
|
||||||
|
totalCorrelations > 0
|
||||||
|
? 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'
|
||||||
|
: 'bg-background-secondary border-background-card text-text-secondary'
|
||||||
|
}`}>
|
||||||
|
<span className="text-xl">{totalCorrelations > 0 ? '⚠️' : 'ℹ️'}</span>
|
||||||
|
<div>
|
||||||
|
{totalCorrelations > 0 ? (
|
||||||
|
<>
|
||||||
|
<span className="font-bold">{totalCorrelations} corrélation{totalCorrelations > 1 ? 's' : ''} détectée{totalCorrelations > 1 ? 's' : ''}</span>
|
||||||
|
{' '}— attributs partagés par 2+ entités. Possible campagne coordonnée.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Aucune corrélation détectée entre les entités analysées.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Matrix */}
|
||||||
|
{cols.length > 0 && (
|
||||||
|
<div className="overflow-x-auto rounded-xl border border-background-card">
|
||||||
|
<table className="w-full min-w-max">
|
||||||
|
{/* Column headers */}
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-background-secondary border-b border-background-card">
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-disabled uppercase tracking-wider w-40">
|
||||||
|
Attribut
|
||||||
|
</th>
|
||||||
|
{cols.map(col => (
|
||||||
|
<th key={col.id} className="px-4 py-3 text-left w-56">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-text-disabled">
|
||||||
|
{col.type === 'ip' ? '🌐' : '🔐'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(
|
||||||
|
col.type === 'ip'
|
||||||
|
? `/investigation/${col.value}`
|
||||||
|
: `/investigation/ja4/${col.value}`
|
||||||
|
)}
|
||||||
|
className="font-mono text-sm text-accent-primary hover:underline truncate max-w-[160px] text-left"
|
||||||
|
title={col.value}
|
||||||
|
>
|
||||||
|
{col.value.length > 20 ? col.value.slice(0, 20) + '…' : col.value}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{col.data && (
|
||||||
|
<div className="text-xs text-text-disabled mt-0.5">
|
||||||
|
{col.data.total_detections.toLocaleString()} det.
|
||||||
|
{col.type === 'ja4' && col.data.unique_ips !== undefined && (
|
||||||
|
<> · {col.data.unique_ips} IPs</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{col.loading && (
|
||||||
|
<div className="text-xs text-text-disabled mt-0.5 animate-pulse">Chargement…</div>
|
||||||
|
)}
|
||||||
|
{col.error && (
|
||||||
|
<div className="text-xs text-red-400 mt-0.5">⚠ {col.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeCol(col.id)}
|
||||||
|
className="text-text-disabled hover:text-red-400 transition-colors text-base leading-none shrink-0 mt-0.5"
|
||||||
|
title="Retirer"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
{/* Attribute rows */}
|
||||||
|
<tbody className="divide-y divide-background-card">
|
||||||
|
{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 (
|
||||||
|
<tr key={row.key} className="hover:bg-background-card/20 transition-colors">
|
||||||
|
<td className="px-4 py-3 align-top">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>{row.icon}</span>
|
||||||
|
<span className="text-xs font-medium text-text-secondary">{row.label}</span>
|
||||||
|
</div>
|
||||||
|
{shared.size > 0 && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||||
|
★ {shared.size} commun{shared.size > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{cols.map(col => {
|
||||||
|
const items = col.data?.attributes[row.key] ?? [];
|
||||||
|
return (
|
||||||
|
<td key={col.id} className="px-3 py-3 align-top">
|
||||||
|
{col.loading ? (
|
||||||
|
<div className="h-16 bg-background-card/30 rounded animate-pulse" />
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<span className="text-xs text-text-disabled">—</span>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{items.slice(0, MAX_VALUES_PER_CELL).map((item, i) => {
|
||||||
|
const isShared = shared.has(item.value);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`rounded px-2 py-1.5 text-xs ${
|
||||||
|
isShared
|
||||||
|
? 'bg-yellow-500/15 border border-yellow-500/30'
|
||||||
|
: 'bg-background-card/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-1">
|
||||||
|
<div className={`font-mono break-all leading-tight ${
|
||||||
|
isShared ? 'text-yellow-300' : 'text-text-primary'
|
||||||
|
}`}>
|
||||||
|
{isShared && <span className="mr-1 text-yellow-400">★</span>}
|
||||||
|
{row.key === 'countries'
|
||||||
|
? `${getCountryFlag(item.value)} ${item.value}`
|
||||||
|
: item.value.length > 60
|
||||||
|
? item.value.slice(0, 60) + '…'
|
||||||
|
: item.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-text-disabled mt-0.5 flex gap-2">
|
||||||
|
<span>{item.count.toLocaleString()}</span>
|
||||||
|
<span>{item.percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{items.length > MAX_VALUES_PER_CELL && (
|
||||||
|
<div className="text-xs text-text-disabled px-2">
|
||||||
|
+{items.length - MAX_VALUES_PER_CELL} autres
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
{cols.length >= 2 && (
|
||||||
|
<div className="flex items-center gap-4 text-xs text-text-disabled">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="w-3 h-3 rounded bg-yellow-500/20 border border-yellow-500/30 inline-block" />
|
||||||
|
Valeur partagée par 2+ entités ★
|
||||||
|
</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>Cliquer sur une entité → Investigation complète</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -235,7 +235,7 @@ export function QuickSearch({ onNavigate }: QuickSearchProps) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/investigate');
|
navigate('/detections');
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 bg-accent-primary/20 text-accent-primary rounded text-xs hover:bg-accent-primary/30 transition-colors"
|
className="px-3 py-1.5 bg-accent-primary/20 text-accent-primary rounded text-xs hover:bg-accent-primary/30 transition-colors"
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { QuickSearch } from './QuickSearch';
|
|
||||||
|
|
||||||
interface SubnetIP {
|
interface SubnetIP {
|
||||||
ip: string;
|
ip: string;
|
||||||
@ -127,13 +126,10 @@ export function SubnetInvestigation() {
|
|||||||
<p className="font-mono text-text-secondary">{subnet}</p>
|
<p className="font-mono text-text-secondary">{subnet}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-auto">
|
|
||||||
<QuickSearch />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Summary */}
|
{/* Stats Summary — 4 colonnes compact */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<div className="bg-background-card rounded-lg p-4">
|
<div className="bg-background-card rounded-lg p-4">
|
||||||
<div className="text-sm text-text-secondary mb-1">Total IPs</div>
|
<div className="text-sm text-text-secondary mb-1">Total IPs</div>
|
||||||
<div className="text-2xl font-bold text-text-primary">{stats.total_ips}</div>
|
<div className="text-2xl font-bold text-text-primary">{stats.total_ips}</div>
|
||||||
@ -150,6 +146,10 @@ export function SubnetInvestigation() {
|
|||||||
<div className="text-sm text-text-secondary mb-1">User-Agents Uniques</div>
|
<div className="text-sm text-text-secondary mb-1">User-Agents Uniques</div>
|
||||||
<div className="text-2xl font-bold text-text-primary">{stats.unique_ua}</div>
|
<div className="text-2xl font-bold text-text-primary">{stats.unique_ua}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Infos secondaires — 4 colonnes */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<div className="bg-background-card rounded-lg p-4">
|
<div className="bg-background-card rounded-lg p-4">
|
||||||
<div className="text-sm text-text-secondary mb-1">Hosts Uniques</div>
|
<div className="text-sm text-text-secondary mb-1">Hosts Uniques</div>
|
||||||
<div className="text-2xl font-bold text-text-primary">{stats.unique_hosts}</div>
|
<div className="text-2xl font-bold text-text-primary">{stats.unique_hosts}</div>
|
||||||
@ -166,8 +166,8 @@ export function SubnetInvestigation() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-background-card rounded-lg p-4">
|
<div className="bg-background-card rounded-lg p-4">
|
||||||
<div className="text-sm text-text-secondary mb-1">Période</div>
|
<div className="text-sm text-text-secondary mb-1">Période</div>
|
||||||
<div className="text-sm text-text-primary">
|
<div className="text-sm text-text-primary font-medium">
|
||||||
{formatDate(stats.first_seen)} - {formatDate(stats.last_seen)}
|
{formatDate(stats.first_seen)} – {formatDate(stats.last_seen)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { QuickSearch } from './QuickSearch';
|
|
||||||
|
|
||||||
interface Classification {
|
interface Classification {
|
||||||
ip?: string;
|
ip?: string;
|
||||||
@ -119,7 +118,6 @@ export function ThreatIntelView() {
|
|||||||
Base de connaissances des classifications SOC
|
Base de connaissances des classifications SOC
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<QuickSearch />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
@ -150,169 +148,140 @@ export function ThreatIntelView() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Main content: sidebar filtres (1/4) + table (3/4) */}
|
||||||
<div className="bg-background-secondary rounded-lg p-4">
|
<div className="grid grid-cols-4 gap-6 items-start">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
{/* Sidebar filtres + tags */}
|
||||||
{/* Search */}
|
<div className="space-y-4">
|
||||||
<input
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
type="text"
|
<h3 className="text-sm font-semibold text-text-primary mb-3">🔍 Recherche</h3>
|
||||||
value={search}
|
<input
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
type="text"
|
||||||
placeholder="Rechercher IP, JA4, tag, commentaire..."
|
value={search}
|
||||||
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"
|
onChange={(e) => 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"
|
||||||
{/* Label Filter */}
|
/>
|
||||||
<select
|
|
||||||
value={filterLabel}
|
|
||||||
onChange={(e) => setFilterLabel(e.target.value)}
|
|
||||||
className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary focus:outline-none focus:border-accent-primary"
|
|
||||||
>
|
|
||||||
<option value="all">Tous les labels</option>
|
|
||||||
<option value="malicious">🤖 Malicious</option>
|
|
||||||
<option value="suspicious">⚠️ Suspicious</option>
|
|
||||||
<option value="legitimate">✅ Légitime</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{/* Tag Filter */}
|
|
||||||
<select
|
|
||||||
value={filterTag}
|
|
||||||
onChange={(e) => setFilterTag(e.target.value)}
|
|
||||||
className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary focus:outline-none focus:border-accent-primary"
|
|
||||||
>
|
|
||||||
<option value="">Tous les tags</option>
|
|
||||||
{allTags.map(tag => (
|
|
||||||
<option key={tag} value={tag}>{tag}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(search || filterLabel !== 'all' || filterTag) && (
|
|
||||||
<div className="mt-4 flex items-center justify-between">
|
|
||||||
<div className="text-sm text-text-secondary">
|
|
||||||
{filteredClassifications.length} résultat(s)
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setSearch('');
|
|
||||||
setFilterLabel('all');
|
|
||||||
setFilterTag('');
|
|
||||||
}}
|
|
||||||
className="text-sm text-accent-primary hover:text-accent-primary/80"
|
|
||||||
>
|
|
||||||
Effacer filtres
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top Tags */}
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
<div className="bg-background-secondary rounded-lg p-4">
|
<h3 className="text-sm font-semibold text-text-primary mb-3">🏷️ Label</h3>
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">🏷️ Tags Populaires (30j)</h3>
|
<div className="space-y-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
{(['all', 'malicious', 'suspicious', 'legitimate'] as const).map(lbl => (
|
||||||
{allTags.slice(0, 20).map(tag => {
|
<button
|
||||||
const count = classifications.filter(c => c.tags.includes(tag)).length;
|
key={lbl}
|
||||||
return (
|
onClick={() => setFilterLabel(lbl)}
|
||||||
<button
|
className={`w-full text-left px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
||||||
key={tag}
|
filterLabel === lbl ? 'bg-accent-primary text-white' : 'text-text-secondary hover:text-text-primary hover:bg-background-card'
|
||||||
onClick={() => setFilterTag(filterTag === tag ? '' : tag)}
|
}`}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
>
|
||||||
filterTag === tag
|
{lbl === 'all' ? '🔹 Tous' : lbl === 'malicious' ? '❌ Malicious' : lbl === 'suspicious' ? '⚠️ Suspicious' : '✅ Légitime'}
|
||||||
? 'bg-accent-primary text-white'
|
</button>
|
||||||
: getTagColor(tag)
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tag} <span className="text-xs opacity-70">({count})</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Classifications Table */}
|
|
||||||
<div className="bg-background-secondary rounded-lg overflow-hidden">
|
|
||||||
<div className="p-4 border-b border-background-card">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary">
|
|
||||||
📋 Classifications Récentes
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-background-card">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Date</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Entité</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Label</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Tags</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Confiance</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Analyste</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-background-card">
|
|
||||||
{filteredClassifications.slice(0, 50).map((classification, idx) => (
|
|
||||||
<tr key={idx} className="hover:bg-background-card/50 transition-colors">
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">
|
|
||||||
{new Date(classification.created_at).toLocaleDateString('fr-FR', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="font-mono text-sm text-text-primary">
|
|
||||||
{classification.ip || classification.ja4}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className={`px-2 py-1 rounded text-xs font-bold ${getLabelColor(classification.label)}`}>
|
|
||||||
{getLabelIcon(classification.label)} {classification.label.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{classification.tags.slice(0, 5).map((tag, tagIdx) => (
|
|
||||||
<span
|
|
||||||
key={tagIdx}
|
|
||||||
className={`px-2 py-0.5 rounded text-xs ${getTagColor(tag)}`}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{classification.tags.length > 5 && (
|
|
||||||
<span className="text-xs text-text-secondary">
|
|
||||||
+{classification.tags.length - 5}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex-1 bg-background-secondary rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-accent-primary"
|
|
||||||
style={{ width: `${classification.confidence * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-text-primary font-bold">
|
|
||||||
{(classification.confidence * 100).toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">
|
|
||||||
{classification.analyst}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{filteredClassifications.length === 0 && (
|
|
||||||
<div className="text-center text-text-secondary py-12">
|
|
||||||
<div className="text-4xl mb-2">🔍</div>
|
|
||||||
<div className="text-sm">Aucune classification trouvée</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<div className="bg-background-secondary rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-text-primary mb-3">🏷️ Tags populaires</h3>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{allTags.slice(0, 20).map(tag => {
|
||||||
|
const count = classifications.filter(c => c.tags.includes(tag)).length;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
onClick={() => setFilterTag(filterTag === tag ? '' : tag)}
|
||||||
|
className={`px-2 py-1 rounded text-xs transition-colors ${
|
||||||
|
filterTag === tag ? 'bg-accent-primary text-white' : getTagColor(tag)
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag} <span className="opacity-70">({count})</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(search || filterLabel !== 'all' || filterTag) && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-text-secondary">{filteredClassifications.length} résultat(s)</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setSearch(''); setFilterLabel('all'); setFilterTag(''); }}
|
||||||
|
className="text-xs text-accent-primary hover:text-accent-primary/80"
|
||||||
|
>
|
||||||
|
Effacer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table classifications (3/4) */}
|
||||||
|
<div className="col-span-3 bg-background-secondary rounded-lg overflow-hidden">
|
||||||
|
<div className="p-4 border-b border-background-card">
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary">
|
||||||
|
📋 Classifications Récentes
|
||||||
|
<span className="ml-2 text-sm font-normal text-text-secondary">({filteredClassifications.length})</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-background-card">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Date</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Entité</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Label</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Tags</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Confiance</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Analyste</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-background-card">
|
||||||
|
{filteredClassifications.slice(0, 50).map((classification, idx) => (
|
||||||
|
<tr key={idx} className="hover:bg-background-card/50 transition-colors">
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">
|
||||||
|
{new Date(classification.created_at).toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="font-mono text-sm text-text-primary">
|
||||||
|
{classification.ip || classification.ja4}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-bold ${getLabelColor(classification.label)}`}>
|
||||||
|
{getLabelIcon(classification.label)} {classification.label.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{classification.tags.slice(0, 5).map((tag, tagIdx) => (
|
||||||
|
<span key={tagIdx} className={`px-2 py-0.5 rounded text-xs ${getTagColor(tag)}`}>{tag}</span>
|
||||||
|
))}
|
||||||
|
{classification.tags.length > 5 && (
|
||||||
|
<span className="text-xs text-text-secondary">+{classification.tags.length - 5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 bg-background-secondary rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full bg-accent-primary" style={{ width: `${classification.confidence * 100}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-text-primary font-bold">{(classification.confidence * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{classification.analyst}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{filteredClassifications.length === 0 && (
|
||||||
|
<div className="text-center text-text-secondary py-12">
|
||||||
|
<div className="text-4xl mb-2">🔍</div>
|
||||||
|
<div className="text-sm">Aucune classification trouvée</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -53,37 +53,7 @@ export function VariabilityPanel({ attributes }: VariabilityPanelProps) {
|
|||||||
|
|
||||||
{/* User-Agents */}
|
{/* User-Agents */}
|
||||||
{attributes.user_agents && attributes.user_agents.length > 0 && (
|
{attributes.user_agents && attributes.user_agents.length > 0 && (
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<UASection items={attributes.user_agents} />
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">
|
|
||||||
User-Agents ({attributes.user_agents.length})
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{attributes.user_agents.slice(0, 10).map((item, index) => (
|
|
||||||
<div key={index} className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-text-primary font-medium truncate max-w-lg text-sm">
|
|
||||||
{item.value}
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-text-primary font-medium">{item.count}</div>
|
|
||||||
<div className="text-text-secondary text-xs">{item.percentage?.toFixed(1)}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-background-card rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-threat-medium transition-all"
|
|
||||||
style={{ width: `${item.percentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{attributes.user_agents.length > 10 && (
|
|
||||||
<p className="text-text-secondary text-sm mt-4 text-center">
|
|
||||||
... et {attributes.user_agents.length - 10} autres (top 10 affiché)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pays */}
|
{/* 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 (
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-text-primary mb-4">
|
||||||
|
User-Agents ({items.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{displayed.map((item, index) => (
|
||||||
|
<div key={index} className="space-y-1">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="text-text-primary font-medium text-xs font-mono break-all leading-relaxed flex-1">
|
||||||
|
{item.value}
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<div className="text-text-primary font-medium">{item.count}</div>
|
||||||
|
<div className="text-text-secondary text-xs">{item.percentage?.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-background-card rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full bg-threat-medium transition-all"
|
||||||
|
style={{ width: `${item.percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{items.length > INITIAL && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(v => !v)}
|
||||||
|
className="mt-4 w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
{showAll ? '↑ Réduire' : `↓ Voir les ${items.length - INITIAL} autres`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Composant AttributeSection
|
// Composant AttributeSection
|
||||||
function AttributeSection({
|
function AttributeSection({
|
||||||
title,
|
title,
|
||||||
@ -284,7 +298,7 @@ function AttributeRow({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
to={getLink(value)}
|
to={getLink(value)}
|
||||||
className="text-text-primary hover:text-accent-primary transition-colors font-medium truncate max-w-md"
|
className="text-text-primary hover:text-accent-primary transition-colors font-medium break-all text-sm leading-relaxed flex-1"
|
||||||
>
|
>
|
||||||
{getValue(value)}
|
{getValue(value)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -22,6 +22,8 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
const [data, setData] = useState<UserAgentAnalysis | null>(null);
|
const [data, setData] = useState<UserAgentAnalysis | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showAllIpUA, setShowAllIpUA] = useState(false);
|
||||||
|
const [showAllJa4UA, setShowAllJa4UA] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserAgentAnalysis = async () => {
|
const fetchUserAgentAnalysis = async () => {
|
||||||
@ -60,20 +62,17 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
const getClassificationBadge = (classification: string) => {
|
const getClassificationBadge = (classification: string) => {
|
||||||
switch (classification) {
|
switch (classification) {
|
||||||
case 'normal':
|
case 'normal':
|
||||||
return <span className="bg-threat-low/20 text-threat-low px-2 py-0.5 rounded text-xs">✅ Normal</span>;
|
return <span className="bg-threat-low/20 text-threat-low px-2 py-0.5 rounded text-xs whitespace-nowrap">✅ Normal</span>;
|
||||||
case 'bot':
|
case 'bot':
|
||||||
return <span className="bg-threat-medium/20 text-threat-medium px-2 py-0.5 rounded text-xs">⚠️ Bot</span>;
|
return <span className="bg-threat-medium/20 text-threat-medium px-2 py-0.5 rounded text-xs whitespace-nowrap">⚠️ Bot</span>;
|
||||||
case 'script':
|
case 'script':
|
||||||
return <span className="bg-threat-high/20 text-threat-high px-2 py-0.5 rounded text-xs">❌ Script</span>;
|
return <span className="bg-threat-high/20 text-threat-high px-2 py-0.5 rounded text-xs whitespace-nowrap">❌ Script</span>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const truncateUA = (ua: string, maxLength = 80) => {
|
const INITIAL_COUNT = 5;
|
||||||
if (ua.length <= maxLength) return ua;
|
|
||||||
return ua.substring(0, maxLength) + '...';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
@ -86,18 +85,18 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* User-Agents pour cette IP */}
|
{/* User-Agents pour cette IP */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-text-secondary mb-3">
|
<div className="text-sm text-text-secondary mb-3">
|
||||||
User-Agents pour cette IP ({data.ip_user_agents.length})
|
User-Agents pour cette IP ({data.ip_user_agents.length})
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{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) => (
|
||||||
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="text-text-primary text-xs font-mono break-all flex-1">
|
<div className="text-text-primary text-xs font-mono break-all flex-1 leading-relaxed">
|
||||||
{truncateUA(ua.value)}
|
{ua.value}
|
||||||
</div>
|
</div>
|
||||||
{getClassificationBadge(ua.classification)}
|
{getClassificationBadge(ua.classification)}
|
||||||
</div>
|
</div>
|
||||||
@ -111,6 +110,16 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
<div className="text-text-secondary text-sm">Aucun User-Agent trouvé</div>
|
<div className="text-text-secondary text-sm">Aucun User-Agent trouvé</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{data.ip_user_agents.length > INITIAL_COUNT && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAllIpUA(v => !v)}
|
||||||
|
className="mt-3 w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
{showAllIpUA
|
||||||
|
? '↑ Réduire'
|
||||||
|
: `↓ Voir les ${data.ip_user_agents.length - INITIAL_COUNT} autres`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User-Agents pour le JA4 */}
|
{/* User-Agents pour le JA4 */}
|
||||||
@ -119,11 +128,11 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
User-Agents pour le JA4 (toutes IPs)
|
User-Agents pour le JA4 (toutes IPs)
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{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) => (
|
||||||
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="text-text-primary text-xs font-mono break-all flex-1">
|
<div className="text-text-primary text-xs font-mono break-all flex-1 leading-relaxed">
|
||||||
{truncateUA(ua.value)}
|
{ua.value}
|
||||||
</div>
|
</div>
|
||||||
{getClassificationBadge(ua.classification)}
|
{getClassificationBadge(ua.classification)}
|
||||||
</div>
|
</div>
|
||||||
@ -134,6 +143,16 @@ export function UserAgentAnalysis({ ip }: UserAgentAnalysisProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{data.ja4_user_agents.length > INITIAL_COUNT && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAllJa4UA(v => !v)}
|
||||||
|
className="mt-3 w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
{showAllJa4UA
|
||||||
|
? '↑ Réduire'
|
||||||
|
: `↓ Voir les ${data.ja4_user_agents.length - INITIAL_COUNT} autres`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { ThemeProvider } from './ThemeContext'
|
||||||
import './styles/globals.css'
|
import './styles/globals.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,63 +2,58 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
/* ── Dark theme (default, SOC standard) ── */
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
:root,
|
||||||
line-height: 1.5;
|
[data-theme="dark"] {
|
||||||
font-weight: 400;
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
font-synthesis: none;
|
--color-bg: 15 23 42; /* Slate 900 */
|
||||||
text-rendering: optimizeLegibility;
|
--color-bg-secondary: 30 41 59; /* Slate 800 */
|
||||||
-webkit-font-smoothing: antialiased;
|
--color-bg-card: 51 65 85; /* Slate 700 */
|
||||||
-moz-osx-font-smoothing: grayscale;
|
--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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
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: 6px; height: 6px; }
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
|
||||||
width: 8px;
|
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
|
||||||
height: 8px;
|
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
background: #1E293B;
|
@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 {
|
.animate-fade-in { animation: fadeIn 0.25s ease-in-out; }
|
||||||
background: #475569;
|
.animate-slide-up { animation: slideUp 0.35s ease-out; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -7,34 +7,34 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// Thème sombre Security Dashboard
|
// Backgrounds — CSS-variable driven for dark/light theming
|
||||||
background: {
|
background: {
|
||||||
DEFAULT: '#0F172A', // Slate 900
|
DEFAULT: 'rgb(var(--color-bg) / <alpha-value>)',
|
||||||
secondary: '#1E293B', // Slate 800
|
secondary: 'rgb(var(--color-bg-secondary) / <alpha-value>)',
|
||||||
card: '#334155', // Slate 700
|
card: 'rgb(var(--color-bg-card) / <alpha-value>)',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#F8FAFC', // Slate 50
|
primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
|
||||||
secondary: '#94A3B8', // Slate 400
|
secondary: 'rgb(var(--color-text-secondary)/ <alpha-value>)',
|
||||||
disabled: '#64748B', // Slate 500
|
disabled: 'rgb(var(--color-text-disabled) / <alpha-value>)',
|
||||||
},
|
},
|
||||||
// Menaces
|
// Threat levels — vivid, same in both themes
|
||||||
threat: {
|
threat: {
|
||||||
critical: '#EF4444', // Red 500
|
critical: '#EF4444',
|
||||||
critical_bg: '#7F1D1D',
|
critical_bg: '#7F1D1D',
|
||||||
high: '#F97316', // Orange 500
|
high: '#F97316',
|
||||||
high_bg: '#7C2D12',
|
high_bg: '#7C2D12',
|
||||||
medium: '#EAB308', // Yellow 500
|
medium: '#EAB308',
|
||||||
medium_bg: '#713F12',
|
medium_bg: '#713F12',
|
||||||
low: '#22C55E', // Green 500
|
low: '#22C55E',
|
||||||
low_bg: '#14532D',
|
low_bg: '#14532D',
|
||||||
},
|
},
|
||||||
// Accents
|
// Accents
|
||||||
accent: {
|
accent: {
|
||||||
primary: '#3B82F6', // Blue 500
|
primary: '#3B82F6',
|
||||||
success: '#10B981', // Emerald 500
|
success: '#10B981',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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;
|
|
||||||
@ -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
|
|
||||||
@ -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*
|
|
||||||
@ -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 "=========================================="
|
|
||||||
Reference in New Issue
Block a user