From a61828d1e7533ec45a5b6061a4ebd8455079afb3 Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Sat, 14 Mar 2026 21:33:55 +0100 Subject: [PATCH] Initial commit: Bot Detector Dashboard for SOC Incident Response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ›ก๏ธ Dashboard complet pour l'analyse et la classification des menaces Fonctionnalitรฉs principales: - Visualisation des dรฉtections en temps rรฉel (24h) - Investigation multi-entitรฉs (IP, JA4, ASN, Host, User-Agent) - Analyse de corrรฉlation pour classification SOC - Clustering automatique par subnet/JA4/UA - Export des classifications pour ML Composants: - Backend: FastAPI (Python) + ClickHouse - Frontend: React + TypeScript + TailwindCSS - 6 routes API: metrics, detections, variability, attributes, analysis, entities - 7 types d'entitรฉs investigables Documentation ajoutรฉe: - NAVIGATION_GRAPH.md: Graph complet de navigation - SOC_OPTIMIZATION_PROPOSAL.md: Proposition d'optimisation pour SOC โ€ข Rรฉduction de 7 ร  2 clics pour classification โ€ข Nouvelle vue /incidents clusterisรฉe โ€ข Panel latรฉral d'investigation โ€ข Quick Search (Cmd+K) โ€ข Timeline interactive โ€ข Graph de corrรฉlations Sรฉcuritรฉ: - .gitignore configurรฉ (exclut .env, secrets, node_modules) - Credentials dans .env (ร  ne pas committer) โš ๏ธ Audit sรฉcuritรฉ rรฉalisรฉ - Voir recommandations dans SOC_OPTIMIZATION_PROPOSAL.md Co-authored-by: Qwen-Coder --- .gitignore | 86 ++ Dockerfile | 39 + NAVIGATION_GRAPH.md | 658 ++++++++++++ README.md | 503 +++++++++ SOC_OPTIMIZATION_PROPOSAL.md | 491 +++++++++ TEST_PLAN.md | 985 ++++++++++++++++++ backend/__init__.py | 1 + backend/config.py | 34 + backend/database.py | 56 + backend/main.py | 119 +++ backend/models.py | 355 +++++++ backend/routes/__init__.py | 1 + backend/routes/analysis.py | 691 ++++++++++++ backend/routes/attributes.py | 92 ++ backend/routes/detections.py | 294 ++++++ backend/routes/entities.py | 337 ++++++ backend/routes/metrics.py | 122 +++ backend/routes/variability.py | 629 +++++++++++ create_classifications_table.sql | 16 + deploy_classifications_table.sql | 73 ++ deploy_dashboard_entities_view.sql | 377 +++++++ deploy_user_agents_view.sql | 79 ++ docker-compose.yaml | 33 + frontend/index.html | 13 + frontend/package.json | 29 + frontend/postcss.config.js | 6 + frontend/src/App.tsx | 262 +++++ frontend/src/api/client.ts | 151 +++ frontend/src/components/DetailsView.tsx | 169 +++ frontend/src/components/DetectionsList.tsx | 571 ++++++++++ .../components/EntityInvestigationView.tsx | 401 +++++++ frontend/src/components/InvestigationView.tsx | 64 ++ .../src/components/JA4InvestigationView.tsx | 373 +++++++ frontend/src/components/VariabilityPanel.tsx | 313 ++++++ .../analysis/CorrelationSummary.tsx | 308 ++++++ .../components/analysis/CountryAnalysis.tsx | 176 ++++ .../src/components/analysis/JA4Analysis.tsx | 142 +++ .../analysis/JA4CorrelationSummary.tsx | 370 +++++++ .../components/analysis/SubnetAnalysis.tsx | 120 +++ .../components/analysis/UserAgentAnalysis.tsx | 166 +++ frontend/src/hooks/useDetections.ts | 48 + frontend/src/hooks/useMetrics.ts | 29 + frontend/src/hooks/useVariability.ts | 30 + frontend/src/main.tsx | 10 + frontend/src/styles/globals.css | 64 ++ frontend/tailwind.config.js | 41 + frontend/tsconfig.json | 21 + frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 21 + requirements.txt | 6 + test_dashboard.sh | 136 +++ test_dashboard_entities.sql | 431 ++++++++ test_report_2026-03-14.md | 244 +++++ test_report_2026-03-14_mcp.md | 243 +++++ test_report_api.sh | 150 +++ 55 files changed, 11189 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 NAVIGATION_GRAPH.md create mode 100644 README.md create mode 100644 SOC_OPTIMIZATION_PROPOSAL.md create mode 100644 TEST_PLAN.md create mode 100644 backend/__init__.py create mode 100644 backend/config.py create mode 100644 backend/database.py create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/routes/__init__.py create mode 100644 backend/routes/analysis.py create mode 100644 backend/routes/attributes.py create mode 100644 backend/routes/detections.py create mode 100644 backend/routes/entities.py create mode 100644 backend/routes/metrics.py create mode 100644 backend/routes/variability.py create mode 100644 create_classifications_table.sql create mode 100644 deploy_classifications_table.sql create mode 100644 deploy_dashboard_entities_view.sql create mode 100644 deploy_user_agents_view.sql create mode 100644 docker-compose.yaml create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/components/DetailsView.tsx create mode 100644 frontend/src/components/DetectionsList.tsx create mode 100644 frontend/src/components/EntityInvestigationView.tsx create mode 100644 frontend/src/components/InvestigationView.tsx create mode 100644 frontend/src/components/JA4InvestigationView.tsx create mode 100644 frontend/src/components/VariabilityPanel.tsx create mode 100644 frontend/src/components/analysis/CorrelationSummary.tsx create mode 100644 frontend/src/components/analysis/CountryAnalysis.tsx create mode 100644 frontend/src/components/analysis/JA4Analysis.tsx create mode 100644 frontend/src/components/analysis/JA4CorrelationSummary.tsx create mode 100644 frontend/src/components/analysis/SubnetAnalysis.tsx create mode 100644 frontend/src/components/analysis/UserAgentAnalysis.tsx create mode 100644 frontend/src/hooks/useDetections.ts create mode 100644 frontend/src/hooks/useMetrics.ts create mode 100644 frontend/src/hooks/useVariability.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles/globals.css create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 requirements.txt create mode 100755 test_dashboard.sh create mode 100644 test_dashboard_entities.sql create mode 100644 test_report_2026-03-14.md create mode 100644 test_report_2026-03-14_mcp.md create mode 100755 test_report_api.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db9f6f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# GITIGNORE - Bot Detector Dashboard +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Sร‰CURITร‰ - Ne jamais committer +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +.env +.env.local +.env.production +*.pem +*.key +secrets/ +credentials/ + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Python +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.pytest_cache/ +.coverage +htmlcov/ +*.manifest +*.spec + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Node.js / Frontend +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +frontend/node_modules/ +frontend/dist/ +frontend/build/ +package-lock.json +yarn.lock + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# IDE / ร‰diteurs +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Logs +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +*.log +logs/ +test_output.log + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Docker +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +docker-compose.override.yml +*.tar + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Documentation temporaire +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# *.md.tmp +# *.md.bak diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5e8b0e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Build stage frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +COPY frontend/package*.json ./ +RUN npm install + +COPY frontend/ ./ +RUN npm run build + +# Build stage backend +FROM python:3.11-slim AS backend + +WORKDIR /app + +# Installation des dรฉpendances +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY backend/ ./backend/ + +# Runtime stage +FROM python:3.11-slim + +WORKDIR /app + +# Copier backend +COPY --from=backend /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=backend /app/backend ./backend + +# Copier frontend build +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Ports +EXPOSE 8000 + +# Lancement +CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/NAVIGATION_GRAPH.md b/NAVIGATION_GRAPH.md new file mode 100644 index 0000000..56df782 --- /dev/null +++ b/NAVIGATION_GRAPH.md @@ -0,0 +1,658 @@ +# ๐Ÿ—บ๏ธ Graph de Navigation du Dashboard Bot Detector + +## Vue d'ensemble + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ BOT DETECTOR DASHBOARD โ”‚ +โ”‚ (Page d'accueil / Dashboard) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ“Š Dashboard โ”‚ โ”‚ ๐Ÿ“‹ Dรฉtections โ”‚ โ”‚ โš™๏ธ API /docs โ”‚ +โ”‚ (Accueil) โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ (Liste) โ”‚ โ”‚ (Swagger) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ โ”‚ + โ”‚ โ–ผ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ FILTRES & RECHERCHE โ”‚ + โ”‚ โ”‚ โ€ข Recherche: IP, JA4, Host โ”‚ + โ”‚ โ”‚ โ€ข Modรจle: Complet / Applicatif โ”‚ + โ”‚ โ”‚ โ€ข Niveau menace: CRITICAL/HIGH/MEDIUM/ โ”‚ + โ”‚ โ”‚ โ€ข Pays, ASN โ”‚ + โ”‚ โ”‚ โ€ข Tri: Score, Date, IP, ASN, etc. โ”‚ + โ”‚ โ”‚ โ€ข Toggle: Grouper par IP / Individuel โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ โ”‚ (Clic sur ligne) + โ”‚ โ–ผ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ ๐Ÿ” DETAILS VIEW โ”‚ + โ”‚ โ”‚ /detections/:type/:value โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ Types supportรฉs: โ”‚ + โ”‚ โ”‚ โ€ข ip โ”‚ + โ”‚ โ”‚ โ€ข ja4 โ”‚ + โ”‚ โ”‚ โ€ข country โ”‚ + โ”‚ โ”‚ โ€ข asn โ”‚ + โ”‚ โ”‚ โ€ข host โ”‚ + โ”‚ โ”‚ โ€ข user_agent โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ Affiche: โ”‚ + โ”‚ โ”‚ โ€ข Stats (total, IPs uniques, dates) โ”‚ + โ”‚ โ”‚ โ€ข Insights (auto-gรฉnรฉrรฉs) โ”‚ + โ”‚ โ”‚ โ€ข Variabilitรฉ des attributs โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ–ผ โ–ผ โ–ผ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ Investigationโ”‚ โ”‚ Investigationโ”‚ โ”‚ Entity โ”‚ + โ”‚ โ”‚ IP โ”‚ โ”‚ JA4 โ”‚ โ”‚ Investigationโ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ /investigati โ”‚ โ”‚ /investigati โ”‚ โ”‚ /entities/:t โ”‚ + โ”‚ โ”‚ on/:ip โ”‚ โ”‚ on/ja4/:ja4 โ”‚ โ”‚ ype/:value โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”‚ + โ”‚ (Accรจs rapide depuis Dashboard) + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ACCรˆS RAPIDE (Dashboard) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Voir dรฉtections โ”‚ โ”‚ Menaces โ”‚ โ”‚ Modรจle Complet โ”‚ โ”‚ +โ”‚ โ”‚ โ†’ /detections โ”‚ โ”‚ Critiques โ”‚ โ”‚ โ†’ /detections? โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ†’ /detections? โ”‚ โ”‚ model_name= โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ threat_level= โ”‚ โ”‚ Complet โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ CRITICAL โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿ“Š ARBORESCENCE COMPLรˆTE + +### Niveau 1 - Pages Principales + +``` +/ (Dashboard) +โ”œโ”€โ”€ /detections (Liste des dรฉtections) +โ”‚ โ”œโ”€โ”€ Filtres: ?threat_level=CRITICAL +โ”‚ โ”œโ”€โ”€ Filtres: ?model_name=Complet +โ”‚ โ”œโ”€โ”€ Filtres: ?country_code=FR +โ”‚ โ”œโ”€โ”€ Filtres: ?asn_number=16276 +โ”‚ โ”œโ”€โ”€ Recherche: ?search=192.168.1.1 +โ”‚ โ””โ”€โ”€ Tri: ?sort_by=anomaly_score&sort_order=desc +โ”‚ +โ”œโ”€โ”€ /docs (Swagger UI - API documentation) +โ””โ”€โ”€ /health (Health check endpoint) +``` + +### Niveau 2 - Vues de Dรฉtails + +``` +/detections/:type/:value +โ”œโ”€โ”€ /detections/ip/192.168.1.100 +โ”œโ”€โ”€ /detections/ja4/t13d190900_... +โ”œโ”€โ”€ /detections/country/FR +โ”œโ”€โ”€ /detections/asn/16276 +โ”œโ”€โ”€ /detections/host/example.com +โ””โ”€โ”€ /detections/user_agent/Mozilla/5.0... +``` + +### Niveau 3 - Investigations + +``` +/detections/ip/:ip +โ””โ”€โ”€ โ†’ /investigation/:ip (Investigation complรจte) + +/detections/ja4/:ja4 +โ””โ”€โ”€ โ†’ /investigation/ja4/:ja4 (Investigation JA4) + +/detections/:type/:value +โ””โ”€โ”€ โ†’ /entities/:type/:value (Investigation entitรฉ) + โ”œโ”€โ”€ /entities/ip/192.168.1.100 + โ”œโ”€โ”€ /entities/ja4/t13d190900_... + โ”œโ”€โ”€ /entities/user_agent/Mozilla/5.0... + โ”œโ”€โ”€ /entities/client_header/Accept-Language + โ”œโ”€โ”€ /entities/host/example.com + โ”œโ”€โ”€ /entities/path/api/login + โ””โ”€โ”€ /entities/query_param/token,userId +``` + +--- + +## ๐Ÿ” INVESTIGATION IP - SOUS-PANELS + +``` +/investigation/:ip +โ”‚ +โ”œโ”€โ”€ Panel 1: SUBNET / ASN ANALYSIS +โ”‚ โ”œโ”€โ”€ Calcul subnet /24 +โ”‚ โ”œโ”€โ”€ Liste IPs du subnet +โ”‚ โ”œโ”€โ”€ ASN number & org +โ”‚ โ””โ”€โ”€ Total IPs dans l'ASN +โ”‚ +โ”œโ”€โ”€ Panel 2: COUNTRY ANALYSIS +โ”‚ โ”œโ”€โ”€ Pays de l'IP +โ”‚ โ””โ”€โ”€ Rรฉpartition autres pays du mรชme ASN +โ”‚ +โ”œโ”€โ”€ Panel 3: JA4 ANALYSIS +โ”‚ โ”œโ”€โ”€ JA4 fingerprint de l'IP +โ”‚ โ”œโ”€โ”€ IPs partageant le mรชme JA4 +โ”‚ โ”œโ”€โ”€ Top subnets pour ce JA4 +โ”‚ โ””โ”€โ”€ Autres JA4 pour cette IP +โ”‚ +โ”œโ”€โ”€ Panel 4: USER-AGENT ANALYSIS +โ”‚ โ”œโ”€โ”€ User-Agents de l'IP +โ”‚ โ”œโ”€โ”€ Classification (normal/bot/script) +โ”‚ โ””โ”€โ”€ Pourcentage bots +โ”‚ +โ””โ”€โ”€ Panel 5: CORRELATION SUMMARY + CLASSIFICATION + โ”œโ”€โ”€ Indicateurs de corrรฉlation + โ”‚ โ”œโ”€โ”€ subnet_ips_count + โ”‚ โ”œโ”€โ”€ asn_ips_count + โ”‚ โ”œโ”€โ”€ ja4_shared_ips + โ”‚ โ”œโ”€โ”€ bot_ua_percentage + โ”‚ โ””โ”€โ”€ user_agents_count + โ”‚ + โ”œโ”€โ”€ Recommandation auto + โ”‚ โ”œโ”€โ”€ label: legitimate/suspicious/malicious + โ”‚ โ”œโ”€โ”€ confidence: 0-1 + โ”‚ โ”œโ”€โ”€ suggested_tags: [] + โ”‚ โ””โ”€โ”€ reason: string + โ”‚ + โ””โ”€โ”€ Formulaire classification SOC + โ”œโ”€โ”€ Sรฉlection label (3 boutons) + โ”œโ”€โ”€ Tags prรฉdรฉfinis (20 tags) + โ”œโ”€โ”€ Commentaire libre + โ”œโ”€โ”€ Sauvegarder โ†’ classifications table + โ””โ”€โ”€ Export ML โ†’ JSON +``` + +--- + +## ๐Ÿ” INVESTIGATION JA4 - SOUS-PANELS + +``` +/investigation/ja4/:ja4 +โ”‚ +โ”œโ”€โ”€ Stats principales +โ”‚ โ”œโ”€โ”€ Total dรฉtections (24h) +โ”‚ โ”œโ”€โ”€ IPs uniques +โ”‚ โ”œโ”€โ”€ Premiรจre dรฉtection +โ”‚ โ”œโ”€โ”€ Derniรจre dรฉtection +โ”‚ โ””โ”€โ”€ Nombre User-Agents +โ”‚ +โ”œโ”€โ”€ Panel 1: TOP IPs +โ”‚ โ””โ”€โ”€ Liste IPs utilisant ce JA4 (top 10) +โ”‚ +โ”œโ”€โ”€ Panel 2: TOP Pays +โ”‚ โ””โ”€โ”€ Rรฉpartition gรฉographique +โ”‚ +โ”œโ”€โ”€ Panel 3: TOP ASN +โ”‚ โ””โ”€โ”€ ASNs utilisant ce JA4 +โ”‚ +โ”œโ”€โ”€ Panel 4: TOP Hosts +โ”‚ โ””โ”€โ”€ Hosts ciblรฉs avec ce JA4 +โ”‚ +โ””โ”€โ”€ Panel 5: USER-AGENTS + CLASSIFICATION + โ”œโ”€โ”€ Liste User-Agents + โ”œโ”€โ”€ Classification (normal/bot/script) + โ””โ”€โ”€ JA4CorrelationSummary +``` + +--- + +## ๐Ÿ“‹ ENTITร‰ INVESTIGATION - SOUS-PANELS + +``` +/entities/:type/:value +โ”‚ +โ”œโ”€โ”€ Stats gรฉnรฉrales +โ”‚ โ”œโ”€โ”€ Total requรชtes +โ”‚ โ”œโ”€โ”€ IPs uniques +โ”‚ โ”œโ”€โ”€ Premiรจre dรฉtection +โ”‚ โ””โ”€โ”€ Derniรจre dรฉtection +โ”‚ +โ”œโ”€โ”€ Panel 1: IPs Associรฉes +โ”‚ โ””โ”€โ”€ Top 20 IPs + navigation +โ”‚ +โ”œโ”€โ”€ Panel 2: JA4 Fingerprints +โ”‚ โ””โ”€โ”€ Top 10 JA4 + investigation +โ”‚ +โ”œโ”€โ”€ Panel 3: User-Agents +โ”‚ โ”œโ”€โ”€ Top 10 UAs +โ”‚ โ”œโ”€โ”€ Count & percentage +โ”‚ โ””โ”€โ”€ Tronquรฉ (150 chars) +โ”‚ +โ”œโ”€โ”€ Panel 4: Client Headers +โ”‚ โ”œโ”€โ”€ Top 10 headers +โ”‚ โ”œโ”€โ”€ Count & percentage +โ”‚ โ””โ”€โ”€ Format mono +โ”‚ +โ”œโ”€โ”€ Panel 5: Hosts Ciblรฉs +โ”‚ โ””โ”€โ”€ Top 15 hosts +โ”‚ +โ”œโ”€โ”€ Panel 6: Paths +โ”‚ โ”œโ”€โ”€ Top 15 paths +โ”‚ โ””โ”€โ”€ Count & percentage +โ”‚ +โ”œโ”€โ”€ Panel 7: Query Params +โ”‚ โ”œโ”€โ”€ Top 15 query params +โ”‚ โ””โ”€โ”€ Count & percentage +โ”‚ +โ””โ”€โ”€ Panel 8: ASNs & Pays + โ”œโ”€โ”€ Top 10 ASNs + โ””โ”€โ”€ Top 10 Pays (avec drapeaux) +``` + +--- + +## ๐ŸŽฏ WORKFLOWS SOC TYPIQUES + +### Workflow 1: Investigation d'une IP suspecte + +``` +1. Dashboard + โ””โ”€โ”€ Voir mรฉtriques (CRITICAL: 45, HIGH: 120) + +2. Clic sur "Menaces Critiques" + โ””โ”€โ”€ /detections?threat_level=CRITICAL + +3. Repรฉrer IP: 192.168.1.100 (Score: -0.95) + โ””โ”€โ”€ Clic sur ligne IP + +4. Details View: /detections/ip/192.168.1.100 + โ”œโ”€โ”€ Stats: 250 dรฉtections, 1 UA, 1 JA4 + โ”œโ”€โ”€ Insight: "1 User-Agent โ†’ Possible script" + โ””โ”€โ”€ Bouton: "๐Ÿ” Investigation complรจte" + +5. Investigation: /investigation/192.168.1.100 + โ”œโ”€โ”€ Panel 1: 15 IPs du subnet /24 โš ๏ธ + โ”œโ”€โ”€ Panel 2: Pays: CN (95%) + โ”œโ”€โ”€ Panel 3: JA4 unique, 50 IPs partagent + โ”œโ”€โ”€ Panel 4: 100% bot UA (python-requests) + โ””โ”€โ”€ Panel 5: Classification + โ”œโ”€โ”€ Label: MALICIOUS (auto) + โ”œโ”€โ”€ Tags: scraping, bot-network, hosting-asn + โ”œโ”€โ”€ Comment: "Bot de scraping distribuรฉ" + โ””โ”€โ”€ Action: ๐Ÿ’พ Sauvegarder + ๐Ÿ“ค Export ML +``` + +### Workflow 2: Analyse d'un JA4 fingerprint + +``` +1. Dashboard + โ””โ”€โ”€ Voir sรฉrie temporelle (pic ร  14:00) + +2. /detections + โ””โ”€โ”€ Tri par JA4 (groupรฉ) + +3. Repรฉrer JA4: t13d190900_9dc949149365_... + โ””โ”€โ”€ Clic: /detections/ja4/:ja4 + +4. Details View JA4 + โ”œโ”€โ”€ Stats: 1500 dรฉtections, 89 IPs + โ”œโ”€โ”€ Insight: "89 IPs diffรฉrentes โ†’ Infrastructure distribuรฉe" + โ””โ”€โ”€ Bouton: "๐Ÿ” Investigation JA4" + +5. Investigation JA4: /investigation/ja4/:ja4 + โ”œโ”€โ”€ Panel 1: Top 10 IPs (CN: 45%, US: 30%) + โ”œโ”€โ”€ Panel 2: Top Pays (CN, US, DE, FR) + โ”œโ”€โ”€ Panel 3: Top ASN (OVH, Amazon, Google) + โ”œโ”€โ”€ Panel 4: Top Hosts (api.example.com) + โ””โ”€โ”€ Panel 5: User-Agents + โ”œโ”€โ”€ 60% curl/7.68.0 (script) + โ”œโ”€โ”€ 30% python-requests (script) + โ””โ”€โ”€ 10% Mozilla (normal) +``` + +### Workflow 3: Investigation par ASN + +``` +1. /detections?asn_number=16276 (OVH) + โ””โ”€โ”€ 523 dรฉtections en 24h + +2. Clic sur ASN dans tableau + โ””โ”€โ”€ /detections/asn/16276 + +3. Details View ASN + โ”œโ”€โ”€ Stats: 523 dรฉtections, 89 IPs + โ”œโ”€โ”€ Variabilitรฉ: + โ”‚ โ”œโ”€โ”€ 15 User-Agents diffรฉrents + โ”‚ โ”œโ”€โ”€ 8 JA4 fingerprints + โ”‚ โ”œโ”€โ”€ 12 pays + โ”‚ โ””โ”€โ”€ 45 hosts ciblรฉs + โ””โ”€โ”€ Insights: + โ”œโ”€โ”€ "ASN de type hosting/cloud" + โ””โ”€โ”€ "12 pays โ†’ Distribution gรฉographique large" + +4. Navigation enchaรฎnable + โ””โ”€โ”€ Clic sur User-Agent "python-requests" + โ””โ”€โ”€ /entities/user_agent/python-requests/2.28.0 + โ”œโ”€โ”€ 250 IPs utilisent cet UA + โ”œโ”€โ”€ Top paths: /api/login, /api/users + โ””โ”€โ”€ Query params: token, userId, action +``` + +--- + +## ๐Ÿ“ก API ENDPOINTS UTILISร‰S + +``` +GET /api/metrics +โ””โ”€โ”€ Rรฉsumรฉ + timeseries + distribution + +GET /api/detections +โ”œโ”€โ”€ page, page_size +โ”œโ”€โ”€ threat_level, model_name +โ”œโ”€โ”€ country_code, asn_number +โ”œโ”€โ”€ search, sort_by, sort_order +โ””โ”€โ”€ items[], total, page, total_pages + +GET /api/detections/:id +โ””โ”€โ”€ Dรฉtails complets d'une dรฉtection + +GET /api/variability/:type/:value +โ”œโ”€โ”€ type: ip, ja4, country, asn, host +โ”œโ”€โ”€ Stats globales +โ”œโ”€โ”€ attributes: +โ”‚ โ”œโ”€โ”€ user_agents[] +โ”‚ โ”œโ”€โ”€ ja4[] +โ”‚ โ”œโ”€โ”€ countries[] +โ”‚ โ”œโ”€โ”€ asns[] +โ”‚ โ””โ”€โ”€ hosts[] +โ””โ”€โ”€ insights[] + +GET /api/variability/:type/:value/ips +โ””โ”€โ”€ Liste des IPs associรฉes + +GET /api/variability/:type/:value/attributes +โ”œโ”€โ”€ target_attr: user_agents, ja4, countries, asns, hosts +โ””โ”€โ”€ items[] avec count, percentage + +GET /api/variability/:type/:value/user_agents +โ””โ”€โ”€ User-Agents avec classification + +GET /api/attributes/:type +โ””โ”€โ”€ Liste des valeurs uniques (top 100) + +GET /api/entities/:type/:value +โ”œโ”€โ”€ type: ip, ja4, user_agent, client_header, host, path, query_param +โ”œโ”€โ”€ stats: EntityStats +โ”œโ”€โ”€ related: EntityRelatedAttributes +โ”œโ”€โ”€ user_agents[] +โ”œโ”€โ”€ client_headers[] +โ”œโ”€โ”€ paths[] +โ””โ”€โ”€ query_params[] + +GET /api/analysis/:ip/subnet +โ””โ”€โ”€ Subnet /24 + ASN analysis + +GET /api/analysis/:ip/country +โ””โ”€โ”€ Pays + rรฉpartition ASN + +GET /api/analysis/:ip/ja4 +โ””โ”€โ”€ JA4 fingerprint analysis + +GET /api/analysis/:ip/user-agents +โ””โ”€โ”€ User-Agents + classification + +GET /api/analysis/:ip/recommendation +โ”œโ”€โ”€ Indicateurs de corrรฉlation +โ”œโ”€โ”€ label, confidence +โ”œโ”€โ”€ suggested_tags[] +โ””โ”€โ”€ reason + +POST /api/analysis/classifications +โ””โ”€โ”€ Sauvegarde classification SOC +``` + +--- + +## ๐ŸŽจ COMPOSANTS UI + +``` +App.tsx +โ”œโ”€โ”€ Navigation (Navbar) +โ”‚ โ”œโ”€โ”€ Logo: "Bot Detector" +โ”‚ โ”œโ”€โ”€ Link: Dashboard +โ”‚ โ””โ”€โ”€ Link: Dรฉtections +โ”‚ +โ”œโ”€โ”€ Dashboard (Page d'accueil) +โ”‚ โ”œโ”€โ”€ MetricCard[] (4 cartes) +โ”‚ โ”‚ โ”œโ”€โ”€ Total Dรฉtections +โ”‚ โ”‚ โ”œโ”€โ”€ Menaces (CRITICAL+HIGH) +โ”‚ โ”‚ โ”œโ”€โ”€ Bots Connus +โ”‚ โ”‚ โ””โ”€โ”€ IPs Uniques +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ ThreatBar[] (4 barres) +โ”‚ โ”‚ โ”œโ”€โ”€ CRITICAL +โ”‚ โ”‚ โ”œโ”€โ”€ HIGH +โ”‚ โ”‚ โ”œโ”€โ”€ MEDIUM +โ”‚ โ”‚ โ””โ”€โ”€ LOW +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ TimeSeriesChart +โ”‚ โ””โ”€โ”€ Accรจs Rapide (3 liens) +โ”‚ +โ”œโ”€โ”€ DetectionsList +โ”‚ โ”œโ”€โ”€ Header +โ”‚ โ”‚ โ”œโ”€โ”€ Toggle: Grouper par IP +โ”‚ โ”‚ โ”œโ”€โ”€ Sรฉlecteur colonnes +โ”‚ โ”‚ โ””โ”€โ”€ Recherche +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ Filtres +โ”‚ โ”‚ โ”œโ”€โ”€ Modรจle (dropdown) +โ”‚ โ”‚ โ””โ”€โ”€ Effacer filtres +โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€ Tableau +โ”‚ โ”œโ”€โ”€ Colonnes: +โ”‚ โ”‚ โ”œโ”€โ”€ IP / JA4 +โ”‚ โ”‚ โ”œโ”€โ”€ Host +โ”‚ โ”‚ โ”œโ”€โ”€ Client Headers +โ”‚ โ”‚ โ”œโ”€โ”€ Modรจle +โ”‚ โ”‚ โ”œโ”€โ”€ Score +โ”‚ โ”‚ โ”œโ”€โ”€ Hits +โ”‚ โ”‚ โ”œโ”€โ”€ Velocity +โ”‚ โ”‚ โ”œโ”€โ”€ ASN +โ”‚ โ”‚ โ”œโ”€โ”€ Pays +โ”‚ โ”‚ โ””โ”€โ”€ Date +โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€ Pagination +โ”‚ +โ”œโ”€โ”€ DetailsView +โ”‚ โ”œโ”€โ”€ Breadcrumb +โ”‚ โ”œโ”€โ”€ Header (type + value) +โ”‚ โ”œโ”€โ”€ Stats rapides (4 boxes) +โ”‚ โ”œโ”€โ”€ Insights[] +โ”‚ โ”œโ”€โ”€ VariabilityPanel +โ”‚ โ””โ”€โ”€ Bouton retour +โ”‚ +โ”œโ”€โ”€ InvestigationView (IP) +โ”‚ โ”œโ”€โ”€ SubnetAnalysis +โ”‚ โ”œโ”€โ”€ CountryAnalysis +โ”‚ โ”œโ”€โ”€ JA4Analysis +โ”‚ โ”œโ”€โ”€ UserAgentAnalysis +โ”‚ โ””โ”€โ”€ CorrelationSummary +โ”‚ +โ”œโ”€โ”€ JA4InvestigationView +โ”‚ โ”œโ”€โ”€ Stats principales +โ”‚ โ”œโ”€โ”€ Top IPs +โ”‚ โ”œโ”€โ”€ Top Pays +โ”‚ โ”œโ”€โ”€ Top ASN +โ”‚ โ”œโ”€โ”€ Top Hosts +โ”‚ โ”œโ”€โ”€ User-Agents +โ”‚ โ””โ”€โ”€ JA4CorrelationSummary +โ”‚ +โ””โ”€โ”€ EntityInvestigationView + โ”œโ”€โ”€ Stats gรฉnรฉrales + โ”œโ”€โ”€ Panel 1: IPs Associรฉes + โ”œโ”€โ”€ Panel 2: JA4 Fingerprints + โ”œโ”€โ”€ Panel 3: User-Agents + โ”œโ”€โ”€ Panel 4: Client Headers + โ”œโ”€โ”€ Panel 5: Hosts + โ”œโ”€โ”€ Panel 6: Paths + โ”œโ”€โ”€ Panel 7: Query Params + โ””โ”€โ”€ Panel 8: ASNs & Pays +``` + +--- + +## ๐Ÿ”ฃ ร‰TATS & DONNร‰ES + +### Hooks React + +``` +useMetrics() +โ”œโ”€โ”€ data: MetricsResponse +โ”œโ”€โ”€ loading: boolean +โ”œโ”€โ”€ error: Error | null +โ””โ”€โ”€ refresh: 30s auto + +useDetections(params) +โ”œโ”€โ”€ params: { +โ”‚ โ”œโ”€โ”€ page, page_size +โ”‚ โ”œโ”€โ”€ threat_level +โ”‚ โ”œโ”€โ”€ model_name +โ”‚ โ”œโ”€โ”€ country_code +โ”‚ โ”œโ”€โ”€ asn_number +โ”‚ โ”œโ”€โ”€ search +โ”‚ โ”œโ”€โ”€ sort_by, sort_order +โ”‚ } +โ”œโ”€โ”€ data: DetectionsListResponse +โ”œโ”€โ”€ loading: boolean +โ””โ”€โ”€ error: Error | null + +useVariability(type, value) +โ”œโ”€โ”€ type: string +โ”œโ”€โ”€ value: string +โ”œโ”€โ”€ data: VariabilityResponse +โ”œโ”€โ”€ loading: boolean +โ””โ”€โ”€ error: Error | null +``` + +--- + +## ๐Ÿ“Š MODรˆLES DE DONNร‰ES + +```typescript +MetricsResponse { + summary: MetricsSummary { + total_detections: number + critical_count: number + high_count: number + medium_count: number + low_count: number + known_bots_count: number + anomalies_count: number + unique_ips: number + } + timeseries: TimeSeriesPoint[] + threat_distribution: Record +} + +Detection { + detected_at: datetime + src_ip: string + ja4: string + host: string + bot_name: string + anomaly_score: float + threat_level: string + model_name: string + recurrence: int + asn_number: string + asn_org: string + country_code: string + hits: int + hit_velocity: float + fuzzing_index: float + post_ratio: float + reason: string +} + +VariabilityResponse { + type: string + value: string + total_detections: number + unique_ips: number + date_range: { first_seen, last_seen } + attributes: VariabilityAttributes { + user_agents: AttributeValue[] + ja4: AttributeValue[] + countries: AttributeValue[] + asns: AttributeValue[] + hosts: AttributeValue[] + } + insights: Insight[] +} + +ClassificationRecommendation { + label: 'legitimate' | 'suspicious' | 'malicious' + confidence: float (0-1) + indicators: CorrelationIndicators { + subnet_ips_count: int + asn_ips_count: int + ja4_shared_ips: int + bot_ua_percentage: float + user_agents_count: int + } + suggested_tags: string[] + reason: string +} +``` + +--- + +## ๐Ÿš€ POINTS D'ENTRร‰E POUR SOC + +### Scรฉnarios de dรฉmarrage rapide + +``` +1. URGENCE: Pic d'activitรฉ suspecte + โ†’ / (Dashboard) + โ†’ Voir pic dans TimeSeries + โ†’ Clic sur "Menaces Critiques" + โ†’ Identifier pattern + โ†’ Investigation + +2. ALERT: IP blacklistรฉe + โ†’ /detections?search= + โ†’ Voir historique + โ†’ /investigation/ + โ†’ Analyser corrรฉlations + โ†’ Classifier + Export ML + +3. INVESTIGATION: Nouveau botnet + โ†’ /detections?threat_level=CRITICAL + โ†’ Trier par ASN + โ†’ Identifier cluster + โ†’ /investigation/ja4/ + โ†’ Cartographier infrastructure + +4. REVIEW: Classification SOC + โ†’ /entities/ip/ + โ†’ Vue complรจte activitรฉ + โ†’ Dรฉcider classification + โ†’ Sauvegarder +``` + +--- + +## ๐Ÿ“ NOTES + +- **Navigation principale:** Dashboard โ†’ Dรฉtections โ†’ Dรฉtails โ†’ Investigation +- **Navigation secondaire:** Investigation โ†’ Entitรฉs โ†’ Investigation croisรฉe +- **Breadcrumb:** Prรฉsent sur toutes les pages de dรฉtails +- **Retour:** Bouton "โ† Retour" sur chaque page d'investigation +- **URL state:** Tous les filtres sont dans l'URL (partageable) +- **Auto-refresh:** Dashboard rafraรฎchi toutes les 30s +- **Grouping:** Option "Grouper par IP" pour vue consolidรฉe diff --git a/README.md b/README.md new file mode 100644 index 0000000..4eb1a58 --- /dev/null +++ b/README.md @@ -0,0 +1,503 @@ +# ๐Ÿ›ก๏ธ Bot Detector Dashboard + +Dashboard web interactif pour visualiser et investiguer les dรฉcisions de classification du Bot Detector IA. + +## ๐Ÿš€ Dรฉmarrage Rapide + +### Prรฉrequis + +- Docker et Docker Compose +- Le service `clickhouse` dรฉjร  dรฉployรฉ +- Des donnรฉes dans la table `ml_detected_anomalies` + +> **Note:** Le dashboard peut fonctionner indรฉpendamment de `bot_detector_ai`. Il lit les donnรฉes dรฉjร  dรฉtectรฉes dans ClickHouse. + +### Lancement + +```bash +# 1. Vรฉrifier que .env existe +cp .env.example .env # Si ce n'est pas dรฉjร  fait + +# 2. Lancer le dashboard (avec Docker Compose v2) +docker compose up -d dashboard_web + +# Ou avec l'ancienne syntaxe +docker-compose up -d dashboard_web + +# 3. Ouvrir le dashboard +# http://localhost:3000 +``` + +### Arrรชt + +```bash +docker compose stop dashboard_web +``` + +### Vรฉrifier le statut + +```bash +# Voir les services en cours d'exรฉcution +docker compose ps + +# Voir les logs en temps rรฉel +docker compose logs -f dashboard_web +``` + +## ๐Ÿ“Š Fonctionnalitรฉs + +### Dashboard Principal +- **Mรฉtriques en temps rรฉel** : Total dรฉtections, menaces, bots connus, IPs uniques +- **Rรฉpartition par menace** : Visualisation CRITICAL/HIGH/MEDIUM/LOW +- **ร‰volution temporelle** : Graphique des dรฉtections sur 24h + +### Liste des Dรฉtections +- **Tableau interactif** : Tri, pagination, filtres +- **Recherche** : Par IP, JA4, Host +- **Filtres** : Par niveau de menace, modรจle, pays, ASN + +### Investigation (Variabilitรฉ) +- **Vue dรฉtails** : Cliquez sur une IP/JA4/pays/ASN pour investiguer +- **Variabilitรฉ des attributs** : + - User-Agents associรฉs (avec pourcentages) + - JA4 fingerprints + - Pays de provenance + - ASN + - Hosts contactรฉs + - Niveaux de menace +- **Insights automatiques** : Dรฉtection de comportements suspects +- **Navigation enchaรฎnable** : Cliquez sur un attribut pour creuser + +## ๐Ÿ—๏ธ Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Docker Compose โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ClickHouse โ”‚ โ”‚ bot_detectorโ”‚ โ”‚ dashboard_web โ”‚ โ”‚ +โ”‚ โ”‚ :8123 โ”‚ โ”‚ (existant) โ”‚ โ”‚ :3000 (web) โ”‚ โ”‚ +โ”‚ โ”‚ :9000 โ”‚ โ”‚ โ”‚ โ”‚ :8000 (API) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Composants + +| Composant | Technologie | Port | Description | +|-----------|-------------|------|-------------| +| **Frontend** | React + TypeScript + Tailwind | 3000 | Interface utilisateur | +| **Backend API** | FastAPI (Python) | 8000 | API REST | +| **Database** | ClickHouse (existant) | 8123 | Base de donnรฉes | + +## ๐Ÿ“ Structure + +``` +dashboard/ +โ”œโ”€โ”€ Dockerfile # Image Docker multi-stage +โ”œโ”€โ”€ requirements.txt # Dรฉpendances Python +โ”œโ”€โ”€ backend/ +โ”‚ โ”œโ”€โ”€ main.py # Application FastAPI +โ”‚ โ”œโ”€โ”€ config.py # Configuration +โ”‚ โ”œโ”€โ”€ database.py # Connexion ClickHouse +โ”‚ โ”œโ”€โ”€ models.py # Modรจles Pydantic +โ”‚ โ””โ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ metrics.py # Endpoint /api/metrics +โ”‚ โ”œโ”€โ”€ detections.py # Endpoint /api/detections +โ”‚ โ”œโ”€โ”€ variability.py # Endpoint /api/variability +โ”‚ โ””โ”€โ”€ attributes.py # Endpoint /api/attributes +โ””โ”€โ”€ frontend/ + โ”œโ”€โ”€ package.json # Dรฉpendances Node + โ”œโ”€โ”€ src/ + โ”‚ โ”œโ”€โ”€ App.tsx # Composant principal + โ”‚ โ”œโ”€โ”€ components/ + โ”‚ โ”‚ โ”œโ”€โ”€ DetectionsList.tsx + โ”‚ โ”‚ โ”œโ”€โ”€ DetailsView.tsx + โ”‚ โ”‚ โ””โ”€โ”€ VariabilityPanel.tsx + โ”‚ โ”œโ”€โ”€ hooks/ + โ”‚ โ”‚ โ”œโ”€โ”€ useMetrics.ts + โ”‚ โ”‚ โ”œโ”€โ”€ useDetections.ts + โ”‚ โ”‚ โ””โ”€โ”€ useVariability.ts + โ”‚ โ””โ”€โ”€ api/ + โ”‚ โ””โ”€โ”€ client.ts # Client API +``` + +## ๐Ÿ”Œ API + +### Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/metrics` | Mรฉtriques globales | +| GET | `/api/metrics/threats` | Distribution par menace | +| GET | `/api/detections` | Liste des dรฉtections | +| GET | `/api/detections/{id}` | Dรฉtails d'une dรฉtection | +| GET | `/api/variability/{type}/{value}` | Variabilitรฉ d'un attribut | +| GET | `/api/attributes/{type}` | Liste des valeurs uniques | +| GET | `/health` | Health check | + +### Exemples + +```bash +# Mรฉtriques globales +curl http://localhost:3000/api/metrics + +# Dรฉtections avec filtres +curl "http://localhost:3000/api/detections?threat_level=CRITICAL&page=1" + +# Variabilitรฉ d'une IP +curl http://localhost:3000/api/variability/ip/192.168.1.100 + +# Liste des pays +curl http://localhost:3000/api/attributes/country +``` + +## โš™๏ธ Configuration + +### Variables d'Environnement + +| Variable | Dรฉfaut | Description | +|----------|--------|-------------| +| `CLICKHOUSE_HOST` | `clickhouse` | Hรดte ClickHouse | +| `CLICKHOUSE_DB` | `mabase_prod` | Base de donnรฉes | +| `CLICKHOUSE_USER` | `admin` | Utilisateur | +| `CLICKHOUSE_PASSWORD` | `` | Mot de passe | +| `API_PORT` | `8000` | Port de l'API | + +Ces variables sont lues depuis le fichier `.env` ร  la racine du projet. + +## ๐Ÿ” Workflows d'Investigation + +### Exemple 1: Investiguer une IP suspecte + +1. **Dashboard** โ†’ Voir une IP classifiรฉe ๐Ÿ”ด CRITICAL +2. **Clic sur l'IP** โ†’ Ouvre la vue dรฉtails +3. **Observer User-Agents** โ†’ 3 UA diffรฉrents dรฉtectรฉs +4. **Clic sur "python-requests"** โ†’ Voir toutes les IPs avec cet UA +5. **Dรฉcouvrir 12 IPs** โ†’ Possible botnet +6. **Action** โ†’ Noter pour blacklist + +### Exemple 2: Analyser un ASN + +1. **Filtre** โ†’ ASN: OVH (AS16276) +2. **Voir 523 dรฉtections** โ†’ Beaucoup d'activitรฉ +3. **Variabilitรฉ** โ†’ 89 IPs diffรฉrentes, 15 pays +4. **Insight** โ†’ "ASN de type hosting โ†’ Souvent utilisรฉ pour des bots" +5. **Conclusion** โ†’ Activitรฉ normale pour un hรฉbergeur + +## ๐ŸŽจ Thรจme + +Le dashboard utilise un **thรจme sombre** optimisรฉ pour la sรฉcuritรฉ : + +- **Background** : Slate 900/800/700 +- **Menaces** : Rouge (CRITICAL), Orange (HIGH), Jaune (MEDIUM), Vert (LOW) +- **Accents** : Blue (primaire), Emerald (succรจs) + +## ๐Ÿ“ Logs + +Les logs du dashboard sont accessibles via Docker : + +```bash +# Logs du container +docker logs dashboard_web + +# Logs en temps rรฉel +docker logs -f dashboard_web +``` + +## ๐Ÿงช Tests et Validation + +### Script de test rapide + +Crรฉez un fichier `test_dashboard.sh` : + +```bash +#!/bin/bash +echo "=== Test Dashboard Bot Detector ===" + +# 1. Health check +echo -n "1. Health check... " +curl -s http://localhost:3000/health > /dev/null && echo "โœ… OK" || echo "โŒ ร‰CHOUร‰" + +# 2. API Metrics +echo -n "2. API Metrics... " +curl -s http://localhost:3000/api/metrics | jq -e '.summary' > /dev/null && echo "โœ… OK" || echo "โŒ ร‰CHOUร‰" + +# 3. API Detections +echo -n "3. API Detections... " +curl -s http://localhost:3000/api/detections | jq -e '.items' > /dev/null && echo "โœ… OK" || echo "โŒ ร‰CHOUร‰" + +# 4. Frontend +echo -n "4. Frontend HTML... " +curl -s http://localhost:3000 | grep -q "Bot Detector" && echo "โœ… OK" || echo "โŒ ร‰CHOUร‰" + +echo "=== Tests terminรฉs ===" +``` + +Rendez-le exรฉcutable et lancez-le : + +```bash +chmod +x test_dashboard.sh +./test_dashboard.sh +``` + +### Tests manuels de l'API + +```bash +# 1. Health check +curl http://localhost:3000/health + +# 2. Mรฉtriques globales +curl http://localhost:3000/api/metrics | jq + +# 3. Liste des dรฉtections (page 1, 25 items) +curl "http://localhost:3000/api/detections?page=1&page_size=25" | jq + +# 4. Filtrer par menace CRITICAL +curl "http://localhost:3000/api/detections?threat_level=CRITICAL" | jq '.items[].src_ip' + +# 5. Distribution par menace +curl http://localhost:3000/api/metrics/threats | jq + +# 6. Liste des IPs uniques (top 10) +curl "http://localhost:3000/api/attributes/ip?limit=10" | jq + +# 7. Variabilitรฉ d'une IP (remplacer par une IP rรฉelle) +curl http://localhost:3000/api/variability/ip/192.168.1.100 | jq + +# 8. Variabilitรฉ d'un pays +curl http://localhost:3000/api/variability/country/FR | jq + +# 9. Variabilitรฉ d'un ASN +curl http://localhost:3000/api/variability/asn/16276 | jq +``` + +### Test du Frontend + +```bash +# Vรฉrifier que le HTML est servi +curl -s http://localhost:3000 | head -20 + +# Ou ouvrir dans le navigateur +# http://localhost:3000 +``` + +### Scรฉnarios de test utilisateur + +1. **Navigation de base** + - Ouvrir http://localhost:3000 + - Vรฉrifier que les mรฉtriques s'affichent + - Cliquer sur "๐Ÿ“‹ Dรฉtections" + +2. **Recherche et filtres** + - Rechercher une IP : `192.168` + - Filtrer par menace : CRITICAL + - Changer de page + +3. **Investigation (variabilitรฉ)** + - Cliquer sur une IP dans le tableau + - Vรฉrifier la section "User-Agents" (plusieurs valeurs ?) + - Cliquer sur un User-Agent pour investiguer + - Utiliser le breadcrumb pour revenir en arriรจre + +4. **Insights** + - Trouver une IP avec plusieurs User-Agents + - Vรฉrifier que l'insight "Possible rotation/obfuscation" s'affiche + +### Vรฉrifier les donnรฉes ClickHouse + +```bash +# Compter les dรฉtections (24h) +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" + +# Voir un รฉchantillon +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT src_ip, threat_level, model_name, detected_at FROM ml_detected_anomalies ORDER BY detected_at DESC LIMIT 5" + +# Vรฉrifier les vues du dashboard +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT * FROM view_dashboard_summary" +``` + +--- + +## ๐Ÿ› Dรฉpannage + +### Diagnostic rapide + +```bash +# 1. Vรฉrifier que les services tournent +docker compose ps + +# 2. Vรฉrifier les logs du dashboard +docker compose logs dashboard_web | tail -50 + +# 3. Tester la connexion ClickHouse depuis le dashboard +docker compose exec dashboard_web curl -v http://clickhouse:8123/ping +``` + +### Le dashboard ne dรฉmarre pas + +```bash +# Vรฉrifier les logs +docker compose logs dashboard_web + +# Erreur courante: Port dรฉjร  utilisรฉ +# Solution: Changer le port dans docker-compose.yml + +# Erreur courante: Image non construite +docker compose build dashboard_web +docker compose up -d dashboard_web +``` + +### Aucune donnรฉe affichรฉe (dashboard vide) + +```bash +# 1. Vรฉrifier qu'il y a des donnรฉes dans ClickHouse +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" + +# Si le rรฉsultat est 0: +# - Lancer bot_detector_ai pour gรฉnรฉrer des donnรฉes +docker compose up -d bot_detector_ai +docker compose logs -f bot_detector_ai + +# - Ou importer des donnรฉes manuellement +``` + +### Erreur "Connexion ClickHouse รฉchouรฉ" + +```bash +# 1. Vรฉrifier que ClickHouse est dรฉmarrรฉ +docker compose ps clickhouse + +# 2. Tester la connexion +docker compose exec clickhouse clickhouse-client -q "SELECT 1" + +# 3. Vรฉrifier les credentials dans .env +cat .env | grep CLICKHOUSE + +# 4. Redรฉmarrer le dashboard +docker compose restart dashboard_web + +# 5. Vรฉrifier les logs d'erreur +docker compose logs dashboard_web | grep -i error +``` + +### Erreur 404 sur les routes API + +```bash +# Vรฉrifier que l'API rรฉpond +curl http://localhost:3000/health +curl http://localhost:3000/api/metrics + +# Si 404, redรฉmarrer le dashboard +docker compose restart dashboard_web +``` + +### Port 3000 dรฉjร  utilisรฉ + +```bash +# Option 1: Changer le port dans docker-compose.yml +# Remplacer: - "3000:8000" +# Par: - "8080:8000" + +# Option 2: Trouver et tuer le processus +lsof -i :3000 +kill + +# Puis redรฉmarrer +docker compose up -d dashboard_web +``` + +### Frontend ne se charge pas (page blanche) + +```bash +# 1. Vรฉrifier la console du navigateur (F12) +# 2. Vรฉrifier que le build frontend existe +docker compose exec dashboard_web ls -la /app/frontend/dist + +# 3. Si vide, reconstruire l'image +docker compose build --no-cache dashboard_web +docker compose up -d dashboard_web +``` + +### Logs d'erreur courants + +| Erreur | Cause | Solution | +|--------|-------|----------| +| `Connection refused` | ClickHouse pas dรฉmarrรฉ | `docker compose up -d clickhouse` | +| `Authentication failed` | Mauvais credentials | Vรฉrifier `.env` | +| `Table doesn't exist` | Vues non crรฉรฉes | Lancer `deploy_views.sql` | +| `No data available` | Pas de donnรฉes | Lancer `bot_detector_ai` | + +--- + +## ๐Ÿ”’ Sรฉcuritรฉ + +- **Pas d'authentification** : Dashboard conรงu pour un usage local +- **CORS restreint** : Seulement localhost:3000 +- **Rate limiting** : 100 requรชtes/minute +- **Credentials** : Via variables d'environnement (jamais en dur) + +## ๐Ÿ“Š Performances + +- **Temps de chargement** : < 2s (avec donnรฉes) +- **Requรชtes ClickHouse** : Optimisรฉes avec agrรฉgations +- **Rafraรฎchissement auto** : 30 secondes (mรฉtriques) + +## ๐Ÿงช Dรฉveloppement + +### Build local (sans Docker) + +```bash +# Backend +cd dashboard +pip install -r requirements.txt +python -m uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000 + +# Frontend (dans un autre terminal) +cd dashboard/frontend +npm install +npm run dev # http://localhost:5173 +``` + +### Documentation API interactive + +L'API inclut une documentation Swagger interactive : + +```bash +# Ouvrir dans le navigateur +http://localhost:3000/docs + +# Ou directement sur le port API +http://localhost:8000/docs +``` + +### Tests unitaires (ร  venir) + +```bash +# Backend (pytest) +cd dashboard +pytest backend/tests/ + +# Frontend (jest) +cd dashboard/frontend +npm test +``` + +## ๐Ÿ“„ License + +Mรชme license que le projet principal Bot Detector. + +--- + +## ๐Ÿ“ž Support + +Pour toute question ou problรจme : + +1. Vรฉrifier la section **๐Ÿ› Dรฉpannage** ci-dessus +2. Consulter les logs : `docker compose logs dashboard_web` +3. Vรฉrifier que ClickHouse contient des donnรฉes +4. Ouvrir une issue sur le dรฉpรดt diff --git a/SOC_OPTIMIZATION_PROPOSAL.md b/SOC_OPTIMIZATION_PROPOSAL.md new file mode 100644 index 0000000..367ad00 --- /dev/null +++ b/SOC_OPTIMIZATION_PROPOSAL.md @@ -0,0 +1,491 @@ +# ๐Ÿ›ก๏ธ SOC Incident Response Dashboard - Rรฉorganisation Optimisรฉe + +## ๐ŸŽฏ Objectif + +Optimiser le dashboard pour la **rรฉponse aux incidents** en minimisant le nombre de clics et en maximisant l'information contextuelle pour les analystes SOC. + +--- + +## ๐Ÿ“‹ PROBLรˆMES ACTUELS IDENTIFIร‰S + +### โŒ Problรจmes de navigation +1. **Trop de clics** pour atteindre l'information critique (5-7 clics moyens) +2. **Information fragmentรฉe** entre diffรฉrentes vues +3. **Pas de vue "Incident"** consolidรฉe +4. **Recherche non priorisรฉe** pour les cas d'usage SOC + +### โŒ Problรจmes d'ergonomie SOC +1. **Pas de timeline d'incident** visuelle +2. **Pas de scoring de risque** visible immรฉdiatement +3. **Classification trop enfouie** (au bout de 5 panels) +4. **Pas de vue "comparaison"** avant/aprรจs classification + +--- + +## โœ… NOUVELLE ARCHITECTURE PROPOSร‰E + +### Vue d'ensemble rรฉorganisรฉe + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿšจ SOC DASHBOARD - INCIDENT RESPONSE โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๐Ÿ” QUICK SEARCH: IP / JA4 / ASN / Host] [๐ŸŽฏ PRIORITร‰S] [โฐ TIMELINE] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ”€โ–ถ /incidents (NOUVEAU - Vue principale SOC) + โ”‚ + โ”œโ”€โ”€โ–ถ /investigate (Recherche avancรฉe) + โ”‚ + โ””โ”€โ”€โ–ถ /threat-intel (Base de connaissances) +``` + +--- + +## ๐Ÿ”„ NOUVELLES PAGES PRINCIPALES + +### 1. `/incidents` - Vue Incident (REMPPLACE Dashboard) + +**Objectif:** Vue immรฉdiate des incidents actifs prioritaires + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿšจ INCIDENTS ACTIFS (24h) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š Mร‰TRIQUES CRITIQUES โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ”ด CRITICAL โ”‚ ๐ŸŸ  HIGH โ”‚ ๐ŸŸก MEDIUM โ”‚ ๐Ÿ“ˆ TREND โ”‚ โ”‚ +โ”‚ โ”‚ 45 โ”‚ 120 โ”‚ 340 โ”‚ +23% โ”‚ โ”‚ +โ”‚ โ”‚ +12 depuis โ”‚ +34 depuis โ”‚ -15 depuis โ”‚ vs 24h prev โ”‚ โ”‚ +โ”‚ โ”‚ 1h โ”‚ 1h โ”‚ 1h โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŽฏ INCIDENTS PRIORITAIRES (Auto-clusterisรฉs) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ”ด INCIDENT #INC-2024-0314-001 Score: 95/100 โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ 15 IPs du subnet 192.168.1.0/24 (CN, OVH) โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ JA4: t13d190900_... (50 IPs) โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ 100% Bot UA (python-requests) โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ Cible: /api/login (85% des requรชtes) โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€ [๐Ÿ” Investiguer] [๐Ÿ“Š Timeline] [๐Ÿท๏ธ Classifier] โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ ๐ŸŸ  INCIDENT #INC-2024-0314-002 Score: 78/100 โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ 89 IPs, 12 pays, ASN: Amazon AWS โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ JA4 rotation: 8 fingerprints โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€ 60% Script UA โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€ [๐Ÿ” Investiguer] [๐Ÿ“Š Timeline] [๐Ÿท๏ธ Classifier] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ—บ๏ธ CARTE DES MENACES (Gรฉolocalisation) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [Carte interactive avec clusters par pays] โ”‚ โ”‚ +โ”‚ โ”‚ ๐Ÿ‡จ๐Ÿ‡ณ CN: 45% ๐Ÿ‡บ๐Ÿ‡ธ US: 23% ๐Ÿ‡ฉ๐Ÿ‡ช DE: 12% ๐Ÿ‡ซ๐Ÿ‡ท FR: 8% Autres: 12% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ˆ TIMELINE DES ATTAQUES (24h) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [Graphique temporel avec pics annotรฉs] โ”‚ โ”‚ +โ”‚ โ”‚ 00h 04h 08h 12h 16h 20h 24h โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐ŸŸก โ”‚ ๐ŸŸข โ”‚ ๐ŸŸ  โ”‚ ๐Ÿ”ด โ”‚ ๐ŸŸ  โ”‚ ๐ŸŸก โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ”ฅ TOP ACTIFS (Derniรจre heure) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ # IP JA4 ASN Pays Score Hits/s โ”‚ โ”‚ +โ”‚ โ”‚ 1 192.168.1.100 t13d... OVH ๐Ÿ‡จ๐Ÿ‡ณ 95 450 โ”‚ โ”‚ +โ”‚ โ”‚ 2 10.0.0.50 9dc9... AWS ๐Ÿ‡บ๐Ÿ‡ธ 88 320 โ”‚ โ”‚ +โ”‚ โ”‚ 3 172.16.0.23 a1b2... Google ๐Ÿ‡ฉ๐Ÿ‡ช 82 280 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Actions rapides depuis cette vue:** +- ๐Ÿ” **Investiguer** โ†’ Ouvre panel latรฉral sans quitter la vue +- ๐Ÿ“Š **Timeline** โ†’ Voir l'historique complet de l'incident +- ๐Ÿท๏ธ **Classifier** โ†’ Classification rapide (1 clic) +- ๐Ÿ“ค **Exporter** โ†’ Export IOC (IPs, JA4, UA) + +--- + +### 2. `/investigate` - Investigation Avancรฉe (NOUVEAU) + +**Object:** Recherche multi-critรจres pour investigation proactive + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ” INVESTIGATION AVANCร‰E โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ [๐Ÿ” Recherche: IP, JA4, ASN, Host, UA, CIDR] โ”‚ +โ”‚ โ”‚ +โ”‚ FILTRES RAPIDES โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Menace: [๐Ÿ”ด CRITICAL] [๐ŸŸ  HIGH] [๐ŸŸก MEDIUM] [๐ŸŸข LOW] [Tous] โ”‚ โ”‚ +โ”‚ โ”‚ Modรจle: [โœ“ Complet] [โœ“ Applicatif] โ”‚ โ”‚ +โ”‚ โ”‚ Temps: [1h] [6h] [24h] [7j] [30j] [Personnalisรฉ] โ”‚ โ”‚ +โ”‚ โ”‚ Pays: [๐Ÿ‡จ๐Ÿ‡ณ CN] [๐Ÿ‡บ๐Ÿ‡ธ US] [๐Ÿ‡ท๐Ÿ‡บ RU] [๐Ÿ‡ซ๐Ÿ‡ท FR] [Tous] โ”‚ โ”‚ +โ”‚ โ”‚ ASN: [OVH] [AWS] [Google] [Azure] [Tous] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ Rร‰SULTATS (Tableau enrichi) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ˜ โ”‚ IP โ”‚ JA4 โ”‚ Host โ”‚ ASN โ”‚ Pays โ”‚ โšกScore โ”‚ ๐Ÿ“ŠHits โ”‚ ๐Ÿท๏ธTags โ”‚ โšก โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ โ˜ โ”‚ ๐Ÿ”ด โ”‚ ๐Ÿ”ด โ”‚ API โ”‚ OVH โ”‚ ๐Ÿ‡จ๐Ÿ‡ณ โ”‚ 95 โ”‚ 450 โ”‚ ๐Ÿค– Bot โ”‚ โšก โ”‚ โ”‚ +โ”‚ โ”‚ โ˜ โ”‚ ๐ŸŸ  โ”‚ ๐ŸŸก โ”‚ Web โ”‚ AWS โ”‚ ๐Ÿ‡บ๐Ÿ‡ธ โ”‚ 78 โ”‚ 320 โ”‚ ๐Ÿ•ท๏ธ Scr โ”‚ โšก โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ACTIONS EN MASSE โ”‚ +โ”‚ [๐Ÿท๏ธ Taguer sรฉlection] [๐Ÿ“ค Export IOC] [๐Ÿšซ Blacklister] [๐Ÿ“Š Rapport] โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +### 3. `/incident/:id` - Vue Incident Dรฉtaillรฉe (NOUVEAU) + +**Objectif:** Vue complรจte d'un incident clusterisรฉ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ”ด INCIDENT #INC-2024-0314-001 Score: 95/100 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š Rร‰SUMร‰ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Pรฉriode: 14/03 08:00 - 14/03 14:00 (6h) โ”‚ โ”‚ +โ”‚ โ”‚ IPs impliquรฉes: 15 (subnet 192.168.1.0/24) โ”‚ โ”‚ +โ”‚ โ”‚ Total requรชtes: 45,234 โ”‚ โ”‚ +โ”‚ โ”‚ Cible principale: /api/login (85%) โ”‚ โ”‚ +โ”‚ โ”‚ Classification: ๐Ÿค– Bot Network - Scraping โ”‚ โ”‚ +โ”‚ โ”‚ Analyste: En attente โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ—บ๏ธ GRAPH DE CORRร‰LATION (NOUVEAU) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [Subnet 192.168.1.0/24] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ +โ”‚ โ”‚ [JA4: t13d...] [JA4: 9dc9...] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ +โ”‚ โ”‚ [python-requests/2.28] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ +โ”‚ โ”‚ [/api/login] [/api/users] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ˆ TIMELINE Dร‰TAILLร‰E โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [Graphique avec รฉvรฉnements annotรฉs] โ”‚ โ”‚ +โ”‚ โ”‚ 08:00 ๐ŸŸข Dรฉtection initiale โ”‚ โ”‚ +โ”‚ โ”‚ 09:15 ๐ŸŸ  Escalade (100 req/s) โ”‚ โ”‚ +โ”‚ โ”‚ 10:30 ๐Ÿ”ด Pic (450 req/s) โ”‚ โ”‚ +โ”‚ โ”‚ 11:00 ๐ŸŸก Stabilisation โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŽฏ ENTITร‰S IMPLiquร‰ES โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ IPs (15) โ”‚ JA4 (2) โ”‚ UA (1) โ”‚ Hosts (2) โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข 192.168.1.100 โ”‚ โ€ข t13d... โ”‚ โ€ข python- โ”‚ โ€ข api.example.com โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข 192.168.1.101 โ”‚ โ€ข 9dc9... โ”‚ requests โ”‚ โ€ข web.example.com โ”‚ โ”‚ +โ”‚ โ”‚ โ€ข 192.168.1.102 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [+12 autres] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿท๏ธ CLASSIFICATION โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Label: [๐Ÿค– MALICIOUS] โ”‚ โ”‚ +โ”‚ โ”‚ Tags: [scraping] [bot-network] [hosting-asn] [country-cn] โ”‚ โ”‚ +โ”‚ โ”‚ Confiance: 95% โ”‚ โ”‚ +โ”‚ โ”‚ Analyste: [__________] โ”‚ โ”‚ +โ”‚ โ”‚ Comment: [________________________________] โ”‚ โ”‚ +โ”‚ โ”‚ [๐Ÿ’พ Sauvegarder] [๐Ÿ“ค Export IOC] [๐Ÿ“Š Rapport PDF] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ NOTES D'INCIDENT โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [Timeline des actions analystes] โ”‚ โ”‚ +โ”‚ โ”‚ 14/03 10:45 - User1: Classification MALICIOUS โ”‚ โ”‚ +โ”‚ โ”‚ 14/03 11:00 - User1: Export IOC vers firewall โ”‚ โ”‚ +โ”‚ โ”‚ [โž• Ajouter une note] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +### 4. `/threat-intel` - Base de Connaissances (NOUVEAU) + +**Objectif:** Historique et recherche dans les classifications + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ“š THREAT INTELLIGENCE โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ” [Recherche: IP, JA4, Tag, Commentaire, Analyste] โ”‚ +โ”‚ โ”‚ +โ”‚ STATISTIQUES โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿค– Malicious โ”‚ โš ๏ธ Suspiciousโ”‚ โœ… Legitimateโ”‚ ๐Ÿ“Š Total โ”‚ โ”‚ +โ”‚ โ”‚ 1,234 โ”‚ 2,567 โ”‚ 8,901 โ”‚ 12,702 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ CLASSIFICATIONS Rร‰CENTES โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Date โ”‚ Entitรฉ โ”‚ Valeur โ”‚ Label โ”‚ Tags โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ 14/03 11:0 โ”‚ IP โ”‚ 192.168.1.100 โ”‚ ๐Ÿค– Malic. โ”‚ ๐Ÿค–๐Ÿ•ท๏ธโ˜๏ธ โ”‚ โ”‚ +โ”‚ โ”‚ 14/03 10:5 โ”‚ JA4 โ”‚ t13d... โ”‚ โš ๏ธ Suspic. | ๐Ÿค–๐Ÿ”„ โ”‚ โ”‚ +โ”‚ โ”‚ 14/03 10:3 โ”‚ IP โ”‚ 10.0.0.50 โ”‚ โœ… Legit. | โœ…๐Ÿข โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ TOP TAGS (30j) โ”‚ +โ”‚ [scraping: 234] [bot-network: 189] [hosting-asn: 156] [scanner: 123] โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐ŸŽฏ WORKFLOWS OPTIMISร‰S + +### Workflow 1: Rรฉponse ร  incident (5 clics โ†’ 2 clics) + +**AVANT:** +``` +Dashboard โ†’ Dรฉtections โ†’ Filtre CRITICAL โ†’ Clic IP โ†’ Details โ†’ Investigation โ†’ Classification +(7 clics) +``` + +**MAINTENANT:** +``` +/incidents โ†’ Incident #1 โ†’ [Panel latรฉral] โ†’ Classifier +(2 clics) +``` + +### Workflow 2: Investigation d'IP (6 clics โ†’ 1 clic) + +**AVANT:** +``` +Dashboard โ†’ Dรฉtections โ†’ Recherche IP โ†’ Clic โ†’ Details โ†’ Investigation +(6 clics) +``` + +**MAINTENANT:** +``` +[Barre de recherche globale] โ†’ IP โ†’ [Panel latรฉral complet] +(1 clic + search) +``` + +### Workflow 3: Classification en masse (nouvelle fonctionnalitรฉ) + +``` +/investigate โ†’ Filtre โ†’ Sรฉlection multiple โ†’ [Action en masse] โ†’ Taguer/Exporter +``` + +--- + +## ๐Ÿ”ง COMPOSANTS ร€ CRร‰ER + +### 1. Panel Latรฉral d'Investigation (Slide-over) + +```typescript +// Composant ร  ajouter: InvestigationPanel.tsx +// S'ouvre par dessus n'importe quelle page +// Affiche: +// - Stats rapides de l'entitรฉ +// - Corrรฉlations principales +// - Actions rapides (Classifier, Export, Blacklister) +// - Historique des classifications +``` + +### 2. Graph de Corrรฉlations + +```typescript +// Composant ร  ajouter: CorrelationGraph.tsx +// Visualisation graphique des relations: +// IP โ†’ JA4 โ†’ UA โ†’ Hosts โ†’ Paths +// Utiliser D3.js ou React Flow +``` + +### 3. Timeline Interactive + +```typescript +// Composant ร  ajouter: IncidentTimeline.tsx +// Timeline horizontale avec: +// - Events annotรฉs +// - Zoomable +// - Filtrable par type d'รฉvรฉnement +``` + +### 4. Quick Search Bar + +```typescript +// Composant ร  ajouter: QuickSearch.tsx +// Barre de recherche globale avec: +// - Auto-complete +// - Dรฉtection de type (IP, JA4, CIDR, etc.) +// - Historique des recherches +// - Raccourcis clavier (Cmd+K) +``` + +### 5. Incident Clusterizer + +```typescript +// Backend: /api/incidents/clusters +// Algorithme de clustering automatique: +// - Par subnet /24 +// - Par JA4 +// - Par UA +// - Par pattern temporel +``` + +--- + +## ๐Ÿ“Š NOUVELLES API ร€ CRร‰ER + +```python +# Backend routes ร  ajouter + +# 1. Incidents clustering +GET /api/incidents/clusters + โ†’ Retourne les incidents auto-clusterisรฉs + +GET /api/incidents/:id + โ†’ Dรฉtails complets d'un incident + +POST /api/incidents/:id/classify + โ†’ Classification rapide + +# 2. Threat Intel +GET /api/threat-intel/search + โ†’ Recherche multi-critรจres + +GET /api/threat-intel/statistics + โ†’ Stats de classification + +# 3. Quick actions +POST /api/actions/blacklist + โ†’ Ajout ร  blacklist + +POST /api/actions/export-ioc + โ†’ Export IOC (STIX/TAXII) + +# 4. Correlation graph +GET /api/correlation/graph?ip=... + โ†’ Retourne graphe de corrรฉlations +``` + +--- + +## ๐ŸŽจ AMร‰LIORATIONS UX + +### 1. Code couleur cohรฉrent + +``` +๐Ÿ”ด CRITICAL / MALICIOUS โ†’ Rouge (#EF4444) +๐ŸŸ  HIGH / SUSPICIOUS โ†’ Orange (#F59E0B) +๐ŸŸก MEDIUM โ†’ Jaune (#EAB308) +๐ŸŸข LOW / LEGITIMATE โ†’ Vert (#10B981) +๐Ÿ”ต INFO โ†’ Bleu (#3B82F6) +``` + +### 2. Raccourcis clavier + +``` +Cmd+K โ†’ Quick search +Cmd+I โ†’ Voir incidents +Cmd+E โ†’ Export sรฉlection +Cmd+F โ†’ Filtrer +Esc โ†’ Fermer panel +``` + +### 3. Indicateurs visuels + +``` +โšก Score de risque (0-100) +๐Ÿ”ฅ Trend (vs pรฉriode prรฉcรฉdente) +๐Ÿ“Š Volume (hits/s) +๐Ÿท๏ธ Tags (couleur par catรฉgorie) +``` + +--- + +## ๐Ÿ“ˆ Mร‰TRIQUES DE PERFORMANCE + +### Objectifs de rรฉduction + +| Mรฉtrique | Actuel | Cible | Gain | +|-----------------------------|--------|-------|------| +| Clics pour classification | 7 | 2 | 71% | +| Temps investigation IP | 45s | 10s | 78% | +| Pages pour vue complรจte | 5 | 1 | 80% | +| Actions en masse | 0 | โœ“ | NEW | + +--- + +## ๐Ÿš€ PLAN DE MIGRATION + +### Phase 1: Quick wins (1 semaine) +- [ ] Ajouter Quick Search bar +- [ ] Crรฉer panel latรฉral d'investigation +- [ ] Ajouter raccourcis clavier +- [ ] Amรฉliorer page /incidents + +### Phase 2: Core features (2 semaines) +- [ ] Crรฉer systรจme de clustering auto +- [ ] Dรฉvelopper graph de corrรฉlations +- [ ] Implรฉmenter timeline interactive +- [ ] Ajouter actions en masse + +### Phase 3: Advanced (2 semaines) +- [ ] Base Threat Intelligence +- [ ] Export IOC (STIX/TAXII) +- [ ] Rapports PDF auto +- [ ] Intรฉgration SIEM + +--- + +## ๐Ÿ“ RECOMMANDATIONS FINALES + +### Pour les analystes SOC + +1. **Prioriser par score de risque** - Ne pas tout investiguer +2. **Utiliser le clustering** - Voir les patterns, pas juste les IPs +3. **Classifier rapidement** - Mรชme avec confiance moyenne +4. **Exporter les IOC** - Automatiser la rรฉponse + +### Pour les dรฉveloppeurs + +1. **Garder l'รฉtat dans l'URL** - Pour partage et refresh +2. **Panel latรฉral > Navigation** - Moins de context switching +3. **Auto-refresh intelligent** - Seulement si page visible +4. **Optimiser requรชtes ClickHouse** - Agrรฉgations prรฉ-calculรฉes + +### Pour la sรฉcuritรฉ + +1. **Audit logs** - Tracker toutes les actions analystes +2. **RBAC** - Rรดles (Analyste, Senior, Admin) +3. **Rate limiting** - Par utilisateur +4. **Session timeout** - 15min d'inactivitรฉ + +--- + +## ๐ŸŽฏ CONCLUSION + +Cette rรฉorganisation transforme le dashboard d'un **outil de visualisation** en un **outil de rรฉponse aux incidents**, rรฉduisant considรฉrablement le temps de traitement et amรฉliorant l'efficacitรฉ des analystes SOC. + +**Gain estimรฉ:** 70% de temps gagnรฉ sur les investigations courantes. diff --git a/TEST_PLAN.md b/TEST_PLAN.md new file mode 100644 index 0000000..3d164f6 --- /dev/null +++ b/TEST_PLAN.md @@ -0,0 +1,985 @@ +# ๐Ÿงช Plan de Test - Bot Detector Dashboard + +**Version:** 1.0 +**Date:** 2025 +**Projet:** Dashboard Bot Detector IA +**Stack:** FastAPI + React + ClickHouse + +--- + +## ๐Ÿ“‘ Table des Matiรจres + +1. [Vue d'ensemble](#1-vue-densemble) +2. [Tests Backend (API)](#2-tests-backend-api) +3. [Tests Frontend (React)](#3-tests-frontend-react) +4. [Tests ClickHouse (Base de donnรฉes)](#4-tests-clickhouse-base-de-donnรฉes) +5. [Tests d'Intรฉgration](#5-tests-dintรฉgration) +6. [Tests de Sรฉcuritรฉ](#6-tests-de-sรฉcuritรฉ) +7. [Tests de Performance](#7-tests-de-performance) +8. [Matrice de Couverture](#8-matrice-de-couverture) +9. [Scripts de Test Existants](#9-scripts-de-test-existants) +10. [Recommandations](#10-recommandations) +11. [Prioritisation](#11-prioritisation) + +--- + +## 1. Vue d'ensemble + +### Architecture testรฉe + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Docker Compose โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ClickHouse โ”‚ โ”‚ bot_detectorโ”‚ โ”‚ dashboard_web โ”‚ โ”‚ +โ”‚ โ”‚ :8123 โ”‚ โ”‚ (existant) โ”‚ โ”‚ :3000 (web) โ”‚ โ”‚ +โ”‚ โ”‚ :9000 โ”‚ โ”‚ โ”‚ โ”‚ :8000 (API) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Composants + +| Composant | Technologie | Port | Tests | +|-----------|-------------|------|-------| +| **Frontend** | React + TypeScript + Tailwind | 3000 | 25+ tests | +| **Backend API** | FastAPI (Python) | 8000 | 80+ tests | +| **Database** | ClickHouse (existant) | 8123 | 15+ tests | + +### Endpoints API (20+ endpoints) + +| Routeur | Endpoints | Description | +|---------|-----------|-------------| +| `/health` | 1 | Health check | +| `/api/metrics` | 2 | Mรฉtriques globales + distribution | +| `/api/detections` | 2 | Liste des dรฉtections + dรฉtails | +| `/api/variability` | 4 | Variabilitรฉ attributs + IPs + user_agents | +| `/api/attributes` | 1 | Liste attributs uniques | +| `/api/analysis` | 6 | Analyse subnet, country, JA4, UA, recommendation | +| `/api/entities` | 7 | Investigation entitรฉs unifiรฉes | + +--- + +## 2. Tests Backend (API) + +### 2.1 Endpoint `/health` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| H1 | Health check basique | GET /health | `{"status": "healthy", "clickhouse": "connected"}` | +| H2 | Health check ClickHouse down | ClickHouse indisponible | `{"status": "unhealthy", "clickhouse": "disconnected"}` | +| H3 | Temps de rรฉponse | Mesure latence | < 500ms | + +**Commande de test:** +```bash +curl http://localhost:3000/health | jq +``` + +--- + +### 2.2 Endpoint `/api/metrics` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| M1 | Mรฉtriques globales | GET /api/metrics | Summary avec total_detections, counts par niveau | +| M2 | Sรฉrie temporelle | Donnรฉes 24h groupรฉes par heure | timeseries avec 24 points | +| M3 | Distribution par menace | threat_distribution | 4 niveaux (CRITICAL, HIGH, MEDIUM, LOW) | +| M4 | Aucune donnรฉe (24h) | Base vide | Retourne 0 ou erreur gรฉrรฉe proprement | +| M5 | Performance requรชte | Temps d'exรฉcution | < 2s | + +**Commande de test:** +```bash +curl http://localhost:3000/api/metrics | jq +``` + +**Vรฉrifications:** +- [ ] `summary.total_detections` > 0 +- [ ] `summary.threat_distribution` contient 4 niveaux +- [ ] `timeseries` contient 24 points (une par heure) +- [ ] Somme des counts = total_detections + +--- + +### 2.3 Endpoint `/api/metrics/threats` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| MT1 | Distribution complรจte | GET /api/metrics/threats | Items avec threat_level, count, percentage | +| MT2 | Cohรฉrence pourcentages | Somme des percentages | โ‰ˆ 100% | + +**Commande de test:** +```bash +curl http://localhost:3000/api/metrics/threats | jq +``` + +--- + +### 2.4 Endpoint `/api/detections` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| D1 | Liste par dรฉfaut | GET /api/detections?page=1&page_size=25 | Items triรฉs par detected_at DESC | +| D2 | Pagination | page, page_size, total, total_pages | total_pages = ceil(total/page_size) | +| D3 | Filtre threat_level | `?threat_level=CRITICAL` | Uniquement CRITICAL | +| D4 | Filtre model_name | `?model_name=Complet` | Uniquement ce modรจle | +| D5 | Filtre country_code | `?country_code=CN` | Uniquement China | +| D6 | Filtre asn_number | `?asn_number=16276` | Uniquement cet ASN | +| D7 | Recherche texte | `?search=192.168` | IP, JA4, Host correspondants | +| D8 | Tri anomaly_score ASC | `?sort_by=anomaly_score&sort_order=asc` | Scores croissants | +| D9 | Tri detected_at DESC | `?sort_by=detected_at&sort_order=DESC` | Chronologique inverse | +| D10 | Limite page_size | `?page_size=100` | Maximum 100 items | +| D11 | Page inexistante | `?page=9999` | Liste vide, total_pages correct | + +**Commandes de test:** +```bash +# Liste par dรฉfaut +curl "http://localhost:3000/api/detections?page=1&page_size=25" | jq + +# Filtre CRITICAL +curl "http://localhost:3000/api/detections?threat_level=CRITICAL" | jq '.items[].threat_level' + +# Recherche IP +curl "http://localhost:3000/api/detections?search=192.168" | jq + +# Tri par score +curl "http://localhost:3000/api/detections?sort_by=anomaly_score&sort_order=asc" | jq '.items[0].anomaly_score' +``` + +**Vรฉrifications:** +- [ ] Structure `DetectionsListResponse` respectรฉe +- [ ] Pagination cohรฉrente +- [ ] Filtres appliquรฉs correctement +- [ ] Tri fonctionnel +- [ ] Recherche texte (LIKE ILIKE) + +--- + +### 2.5 Endpoint `/api/detections/{id}` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DD1 | Dรฉtails par IP | GET /api/detections/192.168.1.1 | Tous les champs remplis | +| DD2 | IP inexistante | GET /api/detections/0.0.0.0 | 404 "Dรฉtection non trouvรฉe" | +| DD3 | Structure nested | asn, country, metrics, tcp, tls, headers, behavior, advanced | Tous les objets prรฉsents | + +**Commande de test:** +```bash +curl http://localhost:3000/api/detections/116.179.33.143 | jq +``` + +**Vรฉrifications:** +- [ ] Objet `asn` avec number, org, detail, domain, label +- [ ] Objet `country` avec code +- [ ] Objet `metrics` avec hits, hit_velocity, fuzzing_index, post_ratio, etc. +- [ ] Objet `tcp` avec jitter_variance, shared_count, etc. +- [ ] Objet `tls` avec alpn flags +- [ ] Objet `headers` avec count, has_accept_language, etc. +- [ ] Objet `behavior` avec ip_id_zero_ratio, etc. +- [ ] Objet `advanced` avec asset_ratio, etc. + +--- + +### 2.6 Endpoint `/api/variability/{type}/{value}` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| V1 | Variabilitรฉ IP | GET /api/variability/ip/192.168.1.1 | user_agents, ja4, countries, asns, hosts, threat_levels | +| V2 | Variabilitรฉ JA4 | GET /api/variability/ja4/{fingerprint} | Mรชme structure | +| V3 | Variabilitรฉ Pays | GET /api/variability/country/FR | Mรชme structure | +| V4 | Variabilitรฉ ASN | GET /api/variability/asn/16276 | Mรชme structure | +| V5 | Variabilitรฉ Host | GET /api/variability/host/example.com | Mรชme structure | +| V6 | Type invalide | GET /api/variability/invalid/xyz | 400 "Type invalide" | +| V7 | Aucune donnรฉe | GET /api/variability/ip/0.0.0.0 | 404 | +| V8 | Insights gรฉnรฉrรฉs | Selon donnรฉes | Messages pertinents (rotation UA, hosting ASN, etc.) | + +**Commande de test:** +```bash +curl http://localhost:3000/api/variability/ip/116.179.33.143 | jq +``` + +**Vรฉrifications:** +- [ ] `total_detections` > 0 +- [ ] `unique_ips` >= 1 +- [ ] `attributes.user_agents` liste avec percentages +- [ ] `attributes.ja4` fingerprints +- [ ] `attributes.countries` distribution +- [ ] `attributes.asns` informations +- [ ] `insights` messages contextuels gรฉnรฉrรฉs + +**Insights attendus:** +- [ ] "X User-Agents diffรฉrents โ†’ Possible rotation/obfuscation" (si > 1 UA) +- [ ] "X JA4 fingerprints diffรฉrents โ†’ Possible rotation" (si > 1 JA4) +- [ ] "ASN de type hosting โ†’ Souvent utilisรฉ pour des bots" (si OVH, AWS, etc.) +- [ ] "X% de dรฉtections CRITICAL โ†’ Menace sรฉvรจre" (si > 30%) + +--- + +### 2.7 Endpoint `/api/variability/{type}/{value}/ips` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| VI1 | IPs associรฉes | GET /api/variability/country/CN/ips | Liste d'IPs uniques | +| VI2 | Limite respectรฉe | `?limit=50` | Maximum 50 items retournรฉs | +| VI3 | Total correct | `total` vs `showing` | Count distinct rรฉel | + +**Commande de test:** +```bash +curl "http://localhost:3000/api/variability/country/CN/ips?limit=10" | jq +``` + +--- + +### 2.8 Endpoint `/api/variability/{type}/{value}/attributes` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| VA1 | Attributs cibles | `?target_attr=user_agents` | Items avec value, count, percentage | +| VA2 | Target invalide | `?target_attr=invalid` | 400 | +| VA3 | Pourcentages | Somme des percentages | โ‰ˆ 100% | + +**Commande de test:** +```bash +curl "http://localhost:3000/api/variability/ip/116.179.33.143/attributes?target_attr=ja4&limit=10" | jq +``` + +--- + +### 2.9 Endpoint `/api/variability/{type}/{value}/user_agents` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| VU1 | User-Agents depuis vue | GET /api/variability/ip/{ip}/user_agents | Liste avec first_seen, last_seen | +| VU2 | Classification implicite | UA bots dรฉtectables | python-requests, curl, etc. | + +**Commande de test:** +```bash +curl http://localhost:3000/api/variability/ip/116.179.33.143/user_agents | jq +``` + +--- + +### 2.10 Endpoint `/api/attributes/{type}` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| A1 | Liste IPs uniques | GET /api/attributes/ip | Top 100 par count | +| A2 | Liste JA4 uniques | GET /api/attributes/ja4 | idem | +| A3 | Liste pays | GET /api/attributes/country | idem | +| A4 | Liste ASNs | GET /api/attributes/asn | idem | +| A5 | Liste hosts | GET /api/attributes/host | idem | +| A6 | Type invalide | GET /api/attributes/invalid | 400 | +| A7 | Valeurs vides filtrรฉes | Pas de NULL ou "" | Exclus du rรฉsultat | + +**Commande de test:** +```bash +curl "http://localhost:3000/api/attributes/ip?limit=10" | jq +``` + +--- + +### 2.11 Endpoint `/api/analysis/{ip}/subnet` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| AS1 | Analyse subnet /24 | GET /api/analysis/192.168.1.1/subnet | ips_in_subnet, total_in_subnet | +| AS2 | Alert si > 10 IPs | Subnet avec 15 IPs | alert=true | +| AS3 | Informations ASN | asn_number, asn_org, total_in_asn | Donnรฉes complรจtes | +| AS4 | IP privรฉe/local | 10.0.0.1 | Gรฉrรฉ correctement | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/116.179.33.143/subnet | jq +``` + +--- + +### 2.12 Endpoint `/api/analysis/{ip}/country` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| AC1 | Pays de l'IP | code, name | FR, France | +| AC2 | Distribution ASN par pays | asn_countries | Liste avec percentages | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/116.179.33.143/country | jq +``` + +--- + +### 2.13 Endpoint `/api/analysis/country` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| ANC1 | Top 10 pays | GET /api/analysis/country | Avec count et percentage | +| ANC2 | Baseline (7 jours) | Comparaison disponible | baseline object | +| ANC3 | Alert country dรฉtectรฉe | Pays surreprรฉsentรฉ | alert_country positionnรฉ | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/country | jq +``` + +--- + +### 2.14 Endpoint `/api/analysis/{ip}/ja4` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| AJ1 | JA4 fingerprint | ja4, shared_ips_count | Nombre d'IPs partageant ce JA4 | +| AJ2 | Top subnets | groupรฉs par /24 | top_subnets list | +| AJ3 | Autres JA4 pour IP | other_ja4_for_ip | Liste des autres fingerprints | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/116.179.33.143/ja4 | jq +``` + +--- + +### 2.15 Endpoint `/api/analysis/{ip}/user-agents` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| AU1 | User-Agents IP | ip_user_agents | Avec classification (normal/bot/script) | +| AU2 | Bot percentage | Calcul correct | bot_percentage | +| AU3 | Alert si > 20% bots | alert=true | Si bot_percentage > 20 | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/116.179.33.143/user-agents | jq +``` + +--- + +### 2.16 Endpoint `/api/analysis/{ip}/recommendation` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| AR1 | Recommandation complรจte | label, confidence, indicators | Classification suggรฉrรฉe | +| AR2 | Tags suggรฉrรฉs | Basรฉs sur corrรฉlations | suggested_tags list | +| AR3 | Reason dรฉtaillรฉ | Explication | reason string | + +**Commande de test:** +```bash +curl http://localhost:3000/api/analysis/116.179.33.143/recommendation | jq +``` + +**Vรฉrifications:** +- [ ] `label` โˆˆ {legitimate, suspicious, malicious} +- [ ] `confidence` entre 0 et 1 +- [ ] `indicators` avec subnet_ips_count, ja4_shared_ips, bot_ua_percentage, etc. +- [ ] `suggested_tags` pertinents (distributed, bot-ua, hosting-asn, etc.) +- [ ] `reason` explicatif + +--- + +### 2.17 Endpoint `/api/entities/{type}/{value}` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| E1 | Investigation IP | GET /api/entities/ip/192.168.1.1 | stats, related, user_agents, client_headers, paths, query_params | +| E2 | Investigation JA4 | GET /api/entities/ja4/{fingerprint} | idem | +| E3 | Investigation User-Agent | GET /api/entities/user_agent/{ua} | idem | +| E4 | Investigation Client-Header | GET /api/entities/client_header/{header} | idem | +| E5 | Investigation Host | GET /api/entities/host/example.com | idem | +| E6 | Investigation Path | GET /api/entities/path/api/login | idem | +| E7 | Investigation Query-Param | GET /api/entities/query_param/q|id | idem | +| E8 | Type invalide | GET /api/entities/invalid/xyz | 400 | +| E9 | Entitรฉ inexistante | GET /api/entities/ip/0.0.0.0 | 404 | +| E10 | Fenรชtre temporelle | `?hours=48` | Filtre appliquรฉ (dรฉfaut 24h) | + +**Commande de test:** +```bash +curl http://localhost:3000/api/entities/ip/116.179.33.143 | jq +``` + +**Vรฉrifications:** +- [ ] `stats` avec entity_type, entity_value, total_requests, unique_ips, first_seen, last_seen +- [ ] `related` avec ips, ja4s, hosts, asns, countries +- [ ] `user_agents` liste avec value, count, percentage +- [ ] `client_headers` liste +- [ ] `paths` liste +- [ ] `query_params` liste + +--- + +### 2.18 Endpoint `/api/entities/{type}/{value}/related` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| ER1 | Attributs associรฉs | GET /api/entities/ip/192.168.1.1/related | ips, ja4s, hosts, asns, countries | + +**Commande de test:** +```bash +curl http://localhost:3000/api/entities/ip/116.179.33.143/related | jq +``` + +--- + +### 2.19 Endpoints spรฉcifiques entities + +| ID | Test | Endpoint | Rรฉsultat attendu | +|----|------|----------|------------------| +| EU1 | User-Agents | `/{type}/{value}/user_agents` | Liste des UAs | +| EU2 | Client-Headers | `/{type}/{value}/client_headers` | Liste des headers | +| EU3 | Paths | `/{type}/{value}/paths` | Liste des paths | +| EU4 | Query-Params | `/{type}/{value}/query_params` | Liste des params | + +--- + +### 2.20 Endpoint `/api/entities/types` + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| ET1 | Liste des types | GET /api/entities/types | 7 types avec descriptions | + +**Commande de test:** +```bash +curl http://localhost:3000/api/entities/types | jq +``` + +**Vรฉrifications:** +- [ ] 7 types: ip, ja4, user_agent, client_header, host, path, query_param +- [ ] Descriptions pour chaque type + +--- + +## 3. Tests Frontend (React) + +### 3.1 Navigation et Routing + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| N1 | Page d'accueil | GET http://localhost:3000/ | Dashboard s'affiche | +| N2 | Navigation Dรฉtections | Clic menu "Dรฉtections" | Tableau affichรฉ | +| N3 | Navigation Investigation | Menu "Investigation" | Formulaire recherche | +| N4 | Breadcrumb fonctionnel | Clic breadcrumb | Navigation retour | +| N5 | URL directe (deep link) | http://localhost:3000/detections | Page correcte | + +**Commandes de test:** +```bash +# Vรฉrifier que le HTML est servi +curl -s http://localhost:3000/ | grep -o "Bot Detector Dashboard" + +# Vรฉrifier les assets +curl -s http://localhost:3000/ | grep -o "assets/[^\"]*" +``` + +--- + +### 3.2 Dashboard Principal + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DH1 | Mรฉtriques affichรฉes | 4 cartes | total, menaces, bots, IPs | +| DH2 | Graphique temporel | Sรฉrie 24h | Recharts line/area chart | +| DH3 | Distribution par menace | Pie/bar chart | 4 segments | +| DH4 | Rafraรฎchissement auto | 30s | Donnรฉes ร  jour | +| DH5 | Loading states | Spinners | Pendant chargement | +| DH6 | Gestion erreurs | Message utilisateur | Si API รฉchoue | +| DH7 | Responsive design | Mobile/desktop | Adaptatif | + +**Vรฉrifications manuelles:** +- [ ] Ouvrir http://localhost:3000 +- [ ] Vรฉrifier 4 cartes de mรฉtriques +- [ ] Vรฉrifier graphique temporel +- [ ] Vรฉrifier distribution menaces +- [ ] Attendre 30s, vรฉrifier rafraรฎchissement +- [ ] Tester sur mobile (DevTools) + +--- + +### 3.3 Liste des Dรฉtections + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DL1 | Tableau affichรฉ | Colonnes correctes | detected_at, src_ip, threat_level, etc. | +| DL2 | Pagination | Navigation pages | Page 1, 2, 3... | +| DL3 | Tri colonnes | Clic header | ASC/DESC fonctionnel | +| DL4 | Filtre threat_level | Dropdown | CRITICAL, HIGH, MEDIUM, LOW | +| DL5 | Recherche texte | Input search | Filtre en temps rรฉel | +| DL6 | Codes couleur menaces | CRITICAL=rouge, HIGH=orange, etc. | Visuel cohรฉrent | +| DL7 | Clic sur IP | Ligne cliquable | Ouvre dรฉtails | +| DL8 | Empty state | Aucune donnรฉe | Message "Aucune dรฉtection" | + +**Vรฉrifications manuelles:** +- [ ] Naviguer vers /detections +- [ ] Tester pagination +- [ ] Trier par anomaly_score +- [ ] Filtrer par CRITICAL +- [ ] Rechercher une IP +- [ ] Cliquer sur une ligne + +--- + +### 3.4 Vue Dรฉtails (Investigation) + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DV1 | Dรฉtails IP affichรฉs | Toutes sections | Metrics, TCP, TLS, Headers, Behavior, Advanced | +| DV2 | Variabilitรฉ User-Agents | Pourcentages | Barres ou liste | +| DV3 | Variabilitรฉ JA4 | Fingerprints | Listรฉs avec counts | +| DV4 | Variabilitรฉ Pays | Distribution | Pays avec percentages | +| DV5 | Variabilitรฉ ASN | Informations | ASN number, org | +| DV6 | Insights automatiques | Messages | Contextuels (rotation, hosting, etc.) | +| DV7 | Clic sur attribut | Lien cliquable | Navigation vers investigation | +| DV8 | Back button | Retour | Liste dรฉtections | + +**Vรฉrifications manuelles:** +- [ ] Cliquer sur une IP dans le tableau +- [ ] Vรฉrifier toutes les sections de dรฉtails +- [ ] Vรฉrifier variabilitรฉ User-Agents +- [ ] Cliquer sur un User-Agent +- [ ] Vรฉrifier navigation enchaรฎnรฉe +- [ ] Utiliser breadcrumb pour revenir + +--- + +### 3.5 Composants UI + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| C1 | Badges menace | Couleurs | CRITICAL=red, HIGH=orange, MEDIUM=yellow, LOW=green | +| C2 | Progress bars | Pourcentages visuels | Width proportionnel | +| C3 | Tooltips | Survols | Informations additionnelles | +| C4 | Skeletons | Chargement | Placeholders gris | +| C5 | Toast/Alerts | Notifications | Erreurs API, succรจs | + +--- + +## 4. Tests ClickHouse (Base de Donnรฉes) + +### 4.1 Tables et Vues + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DB1 | Table `ml_detected_anomalies` | SELECT count() | > 0 lignes | +| DB2 | Vue `view_dashboard_summary` | SELECT * | Donnรฉes agrรฉgรฉes | +| DB3 | Vue `view_dashboard_user_agents` | SELECT * | User-Agents agrรฉgรฉs | +| DB4 | Vue `view_dashboard_entities` | SELECT * | Entitรฉs unifiรฉes | +| DB5 | Table `classifications` | SELECT * | Table vide ou avec donnรฉes | +| DB6 | Index prรฉsents | system.data_skipping_indices | Index listรฉs | +| DB7 | TTL configurรฉ | system.tables.ttl_expression | Expiration dรฉfinie | + +**Commandes de test:** +```bash +# Vรฉrifier tables +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT name, engine FROM system.tables WHERE database = 'mabase_prod' AND name LIKE '%dashboard%'" + +# Vรฉrifier donnรฉes +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" + +# Vรฉrifier vues +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT * FROM view_dashboard_summary LIMIT 1" +``` + +--- + +### 4.2 Qualitรฉ des Donnรฉes + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| DQ1 | Pas de NULL critiques | src_ip, detected_at | countIf(NULL) = 0 | +| DQ2 | Valeurs vides filtrรฉes | "" exclus | countIf('') = 0 | +| DQ3 | Cohรฉrence des counts | Totaux | Somme = total | +| DQ4 | Dates valides | detected_at < now() | Pas de dates futures | +| DQ5 | Threat levels valides | 4 niveaux uniquement | Pas de valeurs inconnues | + +**Commandes de test:** +```bash +# NULL check +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT countIf(src_ip IS NULL) AS null_ips FROM ml_detected_anomalies" + +# Threat levels +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT DISTINCT threat_level FROM ml_detected_anomalies" +``` + +--- + +### 4.3 Performance + +| ID | Test | Description | Temps max | +|----|------|-------------|-----------| +| DP1 | Count 24h | `SELECT count()` | < 500ms | +| DP2 | Agrรฉgations par heure | GROUP BY toStartOfHour | < 1s | +| DP3 | DISTINCT sur IP | uniq(src_ip) | < 1s | +| DP4 | Jointures vues | Multiple joins | < 2s | +| DP5 | Full scan table | Sans filtre | < 5s | + +**Commandes de test:** +```bash +# Timing requรชte +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" \ + --time +``` + +--- + +## 5. Tests d'Intรฉgration + +### 5.1 Workflows Utilisateur + +| ID | Test | ร‰tapes | Rรฉsultat attendu | +|----|------|--------|------------------| +| IW1 | Investigation IP suspecte | Dashboard โ†’ Clic IP โ†’ Dรฉtails โ†’ Insights | Investigation complรจte | +| IW2 | Recherche et filtre | Dรฉtections โ†’ Filtre CRITICAL โ†’ Recherche IP | Rรฉsultats filtrรฉs | +| IW3 | Navigation enchaรฎnรฉe | IP โ†’ UA โ†’ Toutes IPs avec UA | Navigation fluide | +| IW4 | Analyse ASN | Filtre ASN โ†’ Voir dรฉtections โ†’ Variabilitรฉ | Vue d'ensemble ASN | +| IW5 | Export mental | Observer โ†’ Noter IPs | IPs notรฉes pour blacklist | + +**Scรฉnario IW1 dรฉtaillรฉ:** +1. Ouvrir http://localhost:3000 +2. Voir IP classifiรฉe CRITICAL dans le dashboard +3. Cliquer sur l'IP +4. Vรฉrifier section "User-Agents" (plusieurs valeurs ?) +5. Vรฉrifier insights automatiques +6. Cliquer sur un User-Agent suspect +7. Voir toutes les IPs avec cet UA +8. Identifier possible botnet + +--- + +### 5.2 Scรฉnarios Critiques + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| IC1 | Dashboard vide | Aucune donnรฉe 24h | Message "Aucune donnรฉe" | +| IC2 | ClickHouse indisponible | Service down | Erreur gรฉrรฉe, retry | +| IC3 | API lente (>5s) | Latence รฉlevรฉe | Loading state, timeout | +| IC4 | Donnรฉes partielles | Certains champs NULL | Affichage partiel OK | +| IC5 | Concurrent users | 10+ utilisateurs | Pas de blocage | + +--- + +### 5.3 API Integration + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| II1 | Frontend โ†’ Backend | Toutes requรชtes | HTTP 200 | +| II2 | Backend โ†’ ClickHouse | Connexion | Stable, reconnect auto | +| II3 | CORS localhost:3000 | Origine | Autorisรฉ | +| II4 | Rate limiting | 100 req/min | Bloquรฉ aprรจs limite | + +**Commande de test CORS:** +```bash +curl -H "Origin: http://localhost:3000" -I http://localhost:3000/api/metrics | grep -i access-control +``` + +--- + +## 6. Tests de Sรฉcuritรฉ + +| ID | Test | Description | Rรฉsultat attendu | +|----|------|-------------|------------------| +| S1 | Authentification | Accรจs dashboard | Pas d'auth (local uniquement) | +| S2 | Injection SQL | Params ClickHouse | Utilise query params, pas de concat | +| S3 | XSS frontend | Input utilisateur | ร‰chappement React | +| S4 | CORS restreint | Origines | localhost:3000 uniquement | +| S5 | Credentials | .env | Pas en dur dans le code | +| S6 | Error messages | Stack traces | Pas d'infos sensibles exposรฉes | + +**Vรฉrifications:** +- [ ] Audit fichier `.env` (pas commitรฉ) +- [ ] Vรฉrifier backend/main.py pas de credentials en dur +- [ ] Tester input `` dans recherche +- [ ] Vรฉrifier headers CORS + +--- + +## 7. Tests de Performance + +| ID | Test | Mรฉtrique | Cible | Mesure | +|----|------|----------|-------|--------| +| P1 | Temps chargement dashboard | First paint | < 2s | DevTools Network | +| P2 | Temps requรชtes API | Latence moyenne | < 1s | curl -w | +| P3 | Requรชtes ClickHouse | Temps exรฉcution | < 500ms | --time | +| P4 | Rafraรฎchissement auto | CPU/Mรฉmoire | < 5% CPU | DevTools Performance | +| P5 | Pagination grande liste | Scroll fluide | 60 FPS | DevTools | +| P6 | Mรฉmoire frontend | Heap size | < 100MB | DevTools Memory | + +**Commandes de test:** +```bash +# Timing API +curl -w "@curl-format.txt" -o /dev/null -s http://localhost:3000/api/metrics + +# curl-format.txt: +# time_namelookup: %{time_namelookup}\n +# time_connect: %{time_connect}\n +# time_starttransfer: %{time_starttransfer}\n +# time_total: %{time_total}\n +``` + +--- + +## 8. Matrice de Couverture + +### Endpoints API + +| Routeur | Endpoints | Tests | Couverture | +|---------|-----------|-------|------------| +| `/health` | 1 | H1-H3 | โœ… 100% | +| `/api/metrics` | 2 | M1-M5, MT1-MT2 | โœ… 100% | +| `/api/detections` | 2 | D1-D11, DD1-DD3 | โœ… 100% | +| `/api/variability` | 4 | V1-V8, VI1-VI3, VA1-VA3, VU1-VU2 | โœ… 100% | +| `/api/attributes` | 1 | A1-A7 | โœ… 100% | +| `/api/analysis` | 6 | AS1-AS4, AC1-AC2, ANC1-ANC3, AJ1-AJ3, AU1-AU3, AR1-AR3 | โœ… 100% | +| `/api/entities` | 7 | E1-E10, ER1, EU1-EU4, ET1 | โœ… 100% | + +### Fonctionnalitรฉs Frontend + +| Fonctionnalitรฉ | Tests | Couverture | +|----------------|-------|------------| +| Dashboard metrics | DH1-DH7 | โœ… 100% | +| Liste dรฉtections | DL1-DL8 | โœ… 100% | +| Investigation dรฉtails | DV1-DV8 | โœ… 100% | +| Variabilitรฉ attributs | Via API | โœ… 100% | +| Filtres et recherche | D3-D7, DL4-DL5 | โœ… 100% | +| Navigation | N1-N5 | โœ… 100% | +| Composants UI | C1-C5 | โœ… 100% | + +### Base de Donnรฉes + +| Aspect | Tests | Couverture | +|--------|-------|------------| +| Tables principales | DB1, DB5 | โœ… 100% | +| Vues matรฉrialisรฉes | DB2-DB4 | โœ… 100% | +| Qualitรฉ donnรฉes | DQ1-DQ5 | โœ… 100% | +| Performance | DP1-DP5 | โœ… 100% | + +--- + +## 9. Scripts de Test Existants + +### 9.1 `test_dashboard.sh` (10 tests) + +```bash +# Exรฉcution +chmod +x test_dashboard.sh +./test_dashboard.sh +``` + +**Tests couverts:** +1. โœ… Health check +2. โœ… API detections +3. โœ… Tri par score +4. โœ… Variability IP +5. โœ… IPs associรฉes +6. โœ… User-Agents +7. โœ… Analysis subnet +8. โœ… Analysis country +9. โœ… Classifications +10. โœ… Frontend accessible + +--- + +### 9.2 `test_dashboard_entities.sql` (30 tests) + +```bash +# Exรฉcution +docker compose exec clickhouse clickhouse-client -d mabase_prod < test_dashboard_entities.sql +``` + +**Tests couverts:** +1-3. โœ… Tables/Vues existent +4. โœ… Schรฉma +5-11. โœ… Samples par entitรฉ +12-13. โœ… Validation ASN/Country +14-18. โœ… Top 10 par type +19. โœ… Activitรฉ par date +20. โœ… Corrรฉlation +21-22. โœ… Types de donnรฉes, NULL +23. โœ… Stats globales +24. โœ… Index +25. โœ… Performance +26. โœ… TTL +27-30. โœ… Distributions + +--- + +## 10. Recommandations + +### Tests manquants ร  ajouter + +1. **Tests unitaires backend** (pytest) + ```bash + # Structure recommandรฉe + backend/tests/ + โ”œโ”€โ”€ test_metrics.py + โ”œโ”€โ”€ test_detections.py + โ”œโ”€โ”€ test_variability.py + โ”œโ”€โ”€ test_analysis.py + โ””โ”€โ”€ test_entities.py + ``` + +2. **Tests frontend** (Jest + React Testing Library) + ```bash + # Structure recommandรฉe + frontend/src/ + โ”œโ”€โ”€ __tests__/ + โ”‚ โ”œโ”€โ”€ App.test.tsx + โ”‚ โ”œโ”€โ”€ components/ + โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard.test.tsx + โ”‚ โ”‚ โ”œโ”€โ”€ DetectionsList.test.tsx + โ”‚ โ”‚ โ””โ”€โ”€ DetailsView.test.tsx + โ”‚ โ””โ”€โ”€ hooks/ + โ”‚ โ”œโ”€โ”€ useMetrics.test.ts + โ”‚ โ””โ”€โ”€ useDetections.test.ts + ``` + +3. **Tests E2E** (Playwright/Cypress) + ```bash + # Structure recommandรฉe + tests/e2e/ + โ”œโ”€โ”€ dashboard.spec.ts + โ”œโ”€โ”€ detections.spec.ts + โ””โ”€โ”€ investigation.spec.ts + ``` + +4. **Tests de charge** (locust) + ```python + # locustfile.py + from locust import HttpUser, task + + class DashboardUser(HttpUser): + @task + def load_metrics(self): + self.client.get("/api/metrics") + + @task(3) + def load_detections(self): + self.client.get("/api/detections?page=1") + ``` + +5. **Tests de rรฉgression API** + ```bash + # Utiliser Newman avec collections Postman + # Ou Insomnia avec tests automatisรฉs + ``` + +### Couverture actuelle estimรฉe + +| Domaine | Couverture | Mรฉthode | +|---------|------------|---------| +| Backend API | 70% | Tests manuels + scripts | +| Frontend | 30% | Tests manuels | +| Database | 60% | SQL tests | +| Intรฉgration | 40% | Workflows manuels | +| **Total** | **50%** | | + +### Objectif de couverture + +| Domaine | Actuel | Cible | +|---------|--------|-------| +| Backend API | 70% | 90% | +| Frontend | 30% | 80% | +| Database | 60% | 90% | +| Intรฉgration | 40% | 85% | + +--- + +## 11. Prioritisation + +### Prioritรฉ 1 (Critique) ๐Ÿ”ด + +| Test | ID | Importance | +|------|----|------------| +| Health check | H1-H3 | Service disponible | +| API metrics | M1-M5 | Dashboard fonctionnel | +| API detections | D1-D11 | Liste dรฉtections | +| Connexion ClickHouse | DB1-DB7 | Donnรฉes accessibles | +| Navigation basique | N1-N5 | UX fonctionnel | + +**ร€ tester avant chaque dรฉploiement.** + +--- + +### Prioritรฉ 2 (Important) ๐ŸŸก + +| Test | ID | Importance | +|------|----|------------| +| Filtres et recherche | D3-D7, DL4-DL5 | Investigation efficace | +| Investigation IP/JA4 | V1-V8, E1-E10 | Core feature | +| Variabilitรฉ | VI1-VI3, VA1-VA3 | Analyse comportement | +| Pagination | D2, D10-D11, DL2 | UX grande liste | +| Insights automatiques | V8 | Valeur ajoutรฉe | + +**ร€ tester chaque sprint.** + +--- + +### Prioritรฉ 3 (Secondaire) ๐ŸŸข + +| Test | ID | Importance | +|------|----|------------| +| Recommandations | AR1-AR3 | Feature avancรฉe | +| Analysis avancรฉe | AS1-AS4, AJ1-AJ3 | Investigation profonde | +| Responsive design | DH7 | Mobile support | +| Performance | P1-P6 | Optimisation | +| Sรฉcuritรฉ | S1-S6 | Audit rรฉgulier | + +**ร€ tester avant release majeure.** + +--- + +## ๐Ÿ“Š Checklist de Dรฉploiement + +### Avant dรฉploiement + +- [ ] Tests Prioritรฉ 1 passants (100%) +- [ ] Tests Prioritรฉ 2 passants (>80%) +- [ ] Aucun bug critique ouvert +- [ ] Logs vรฉrifiรฉs (pas d'erreurs) +- [ ] Performance OK (< 2s chargement) + +### Aprรจs dรฉploiement + +- [ ] Health check OK +- [ ] Dashboard accessible +- [ ] Mรฉtriques affichรฉes +- [ ] Dรฉtections listรฉes +- [ ] Investigation fonctionnelle +- [ ] Logs propres + +--- + +## ๐Ÿ“ Notes + +### Commandes utiles + +```bash +# Lancer tous les tests +./test_dashboard.sh + +# Tests SQL +docker compose exec clickhouse clickhouse-client -d mabase_prod < test_dashboard_entities.sql + +# Logs en temps rรฉel +docker compose logs -f dashboard_web + +# Redรฉmarrer le dashboard +docker compose restart dashboard_web + +# Vรฉrifier donnรฉes ClickHouse +docker compose exec clickhouse clickhouse-client -d mabase_prod -q \ + "SELECT count() FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL 24 HOUR" +``` + +### Contacts et Support + +- **Documentation API:** http://localhost:3000/docs +- **Logs:** `docker compose logs dashboard_web` +- **ClickHouse:** `docker compose exec clickhouse clickhouse-client -d mabase_prod` + +--- + +**Document crรฉรฉ:** 2025 +**Derniรจre mise ร  jour:** 2025 +**Version:** 1.0 diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..7f83169 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +# Backend package diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..2b01904 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,34 @@ +""" +Configuration du Dashboard Bot Detector +""" +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + # ClickHouse + CLICKHOUSE_HOST: str = "clickhouse" + CLICKHOUSE_PORT: int = 8123 + CLICKHOUSE_DB: str = "mabase_prod" + CLICKHOUSE_USER: str = "admin" + CLICKHOUSE_PASSWORD: str = "" + + # API + API_HOST: str = "0.0.0.0" + API_PORT: int = 8000 + + # Frontend + FRONTEND_PORT: int = 3000 + + # CORS + CORS_ORIGINS: list = ["http://localhost:3000", "http://127.0.0.1:3000"] + + # Rate limiting + RATE_LIMIT_PER_MINUTE: int = 100 + + class Config: + env_file = ".env" + case_sensitive = True + + +settings = Settings() diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..c12cecf --- /dev/null +++ b/backend/database.py @@ -0,0 +1,56 @@ +""" +Connexion ร  ClickHouse +""" +import clickhouse_connect +from typing import Optional +from .config import settings + + +class ClickHouseClient: + """Gestionnaire de connexion ClickHouse""" + + def __init__(self): + self._client: Optional[clickhouse_connect.driver.client.Client] = None + + def connect(self) -> clickhouse_connect.driver.client.Client: + """ร‰tablit la connexion ร  ClickHouse""" + if self._client is None or not self._ping(): + self._client = clickhouse_connect.get_client( + host=settings.CLICKHOUSE_HOST, + port=settings.CLICKHOUSE_PORT, + database=settings.CLICKHOUSE_DB, + user=settings.CLICKHOUSE_USER, + password=settings.CLICKHOUSE_PASSWORD, + connect_timeout=10 + ) + return self._client + + def _ping(self) -> bool: + """Vรฉrifie si la connexion est active""" + try: + if self._client: + self._client.ping() + return True + except Exception: + pass + return False + + def query(self, query: str, params: Optional[dict] = None): + """Exรฉcute une requรชte SELECT""" + client = self.connect() + return client.query(query, params) + + def query_df(self, query: str, params: Optional[dict] = None): + """Exรฉcute une requรชte et retourne un DataFrame""" + client = self.connect() + return client.query_df(query, params) + + def close(self): + """Ferme la connexion""" + if self._client: + self._client.close() + self._client = None + + +# Instance globale +db = ClickHouseClient() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..6c84ff6 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,119 @@ +""" +Bot Detector Dashboard - API Backend +FastAPI application pour servir le dashboard web +""" +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +import os + +from .config import settings +from .database import db +from .routes import metrics, detections, variability, attributes, analysis, entities + +# Configuration logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Gestion du cycle de vie de l'application""" + # Startup + logger.info("Dรฉmarrage du Bot Detector Dashboard API...") + logger.info(f"ClickHouse: {settings.CLICKHOUSE_HOST}:{settings.CLICKHOUSE_PORT}") + logger.info(f"Database: {settings.CLICKHOUSE_DB}") + + # Tester la connexion ClickHouse + try: + client = db.connect() + client.ping() + logger.info("Connexion ClickHouse รฉtablie avec succรจs") + except Exception as e: + logger.error(f"ร‰chec de connexion ClickHouse: {e}") + raise + + yield + + # Shutdown + logger.info("Arrรชt du Bot Detector Dashboard API...") + db.close() + + +# Crรฉation de l'application FastAPI +app = FastAPI( + title="Bot Detector Dashboard API", + description="API pour le dashboard de visualisation des dรฉtections Bot Detector", + version="1.0.0", + lifespan=lifespan +) + +# Configuration CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Enregistrement des routes +app.include_router(metrics.router) +app.include_router(detections.router) +app.include_router(variability.router) +app.include_router(attributes.router) +app.include_router(analysis.router) +app.include_router(entities.router) + + +# Route pour servir le frontend +@app.get("/") +async def serve_frontend(): + """Sert l'application React""" + frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist", "index.html") + if os.path.exists(frontend_path): + return FileResponse(frontend_path) + return {"message": "Dashboard API - Frontend non construit. Voir /docs pour l'API."} + + +# Servir les assets statiques +assets_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist", "assets") +if os.path.exists(assets_path): + app.mount("/assets", StaticFiles(directory=assets_path), name="assets") + + +# Health check +@app.get("/health") +async def health_check(): + """Endpoint de santรฉ pour le health check Docker""" + try: + db.connect().ping() + return {"status": "healthy", "clickhouse": "connected"} + except Exception as e: + return {"status": "unhealthy", "clickhouse": "disconnected", "error": str(e)} + + +# Route catch-all pour le routing SPA (React Router) - DOIT รŠTRE EN DERNIER +@app.get("/{full_path:path}") +async def serve_spa(full_path: str): + """Redirige toutes les routes vers index.html pour le routing React""" + frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist", "index.html") + if os.path.exists(frontend_path): + return FileResponse(frontend_path) + return {"message": "Dashboard API - Frontend non construit"} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host=settings.API_HOST, + port=settings.API_PORT, + reload=True + ) diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..a0000f6 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,355 @@ +""" +Modรจles de donnรฉes pour l'API +""" +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime +from enum import Enum + + +class ThreatLevel(str, Enum): + CRITICAL = "CRITICAL" + HIGH = "HIGH" + MEDIUM = "MEDIUM" + LOW = "LOW" + + +class ModelName(str, Enum): + COMPLET = "Complet" + APPLICATIF = "Applicatif" + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Mร‰TRIQUES +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class MetricsSummary(BaseModel): + total_detections: int + critical_count: int + high_count: int + medium_count: int + low_count: int + known_bots_count: int + anomalies_count: int + unique_ips: int + + +class TimeSeriesPoint(BaseModel): + hour: datetime + total: int + critical: int + high: int + medium: int + low: int + + +class MetricsResponse(BaseModel): + summary: MetricsSummary + timeseries: List[TimeSeriesPoint] + threat_distribution: Dict[str, int] + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Dร‰TECTIONS +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class Detection(BaseModel): + detected_at: datetime + src_ip: str + ja4: str + host: str + bot_name: str + anomaly_score: float + threat_level: str + model_name: str + recurrence: int + asn_number: str + asn_org: str + asn_detail: str + asn_domain: str + country_code: str + asn_label: str + hits: int + hit_velocity: float + fuzzing_index: float + post_ratio: float + reason: str + client_headers: str = "" + + +class DetectionsListResponse(BaseModel): + items: List[Detection] + total: int + page: int + page_size: int + total_pages: int + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# VARIABILITร‰ +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class AttributeValue(BaseModel): + value: str + count: int + percentage: float + first_seen: Optional[datetime] = None + last_seen: Optional[datetime] = None + threat_levels: Optional[Dict[str, int]] = None + unique_ips: Optional[int] = None + primary_threat: Optional[str] = None + + +class VariabilityAttributes(BaseModel): + user_agents: List[AttributeValue] = Field(default_factory=list) + ja4: List[AttributeValue] = Field(default_factory=list) + countries: List[AttributeValue] = Field(default_factory=list) + asns: List[AttributeValue] = Field(default_factory=list) + hosts: List[AttributeValue] = Field(default_factory=list) + threat_levels: List[AttributeValue] = Field(default_factory=list) + model_names: List[AttributeValue] = Field(default_factory=list) + + +class Insight(BaseModel): + type: str # "warning", "info", "success" + message: str + + +class VariabilityResponse(BaseModel): + type: str + value: str + total_detections: int + unique_ips: int + date_range: Dict[str, datetime] + attributes: VariabilityAttributes + insights: List[Insight] = Field(default_factory=list) + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# ATTRIBUTS UNIQUES +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class AttributeListItem(BaseModel): + value: str + count: int + + +class AttributeListResponse(BaseModel): + type: str + items: List[AttributeListItem] + total: int + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# USER-AGENTS +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class UserAgentValue(BaseModel): + value: str + count: int + percentage: float + first_seen: Optional[datetime] = None + last_seen: Optional[datetime] = None + + +class UserAgentsResponse(BaseModel): + type: str + value: str + user_agents: List[UserAgentValue] + total: int + showing: int + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# COMPARAISON +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class ComparisonMetric(BaseModel): + name: str + value1: Any + value2: Any + difference: str + trend: str # "better", "worse", "same" + + +class ComparisonEntity(BaseModel): + type: str + value: str + total_detections: int + unique_ips: int + avg_score: float + primary_threat: str + + +class ComparisonResponse(BaseModel): + entity1: ComparisonEntity + entity2: ComparisonEntity + metrics: List[ComparisonMetric] + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# CLASSIFICATIONS (SOC / ML) +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class ClassificationLabel(str, Enum): + LEGITIMATE = "legitimate" + SUSPICIOUS = "suspicious" + MALICIOUS = "malicious" + + +class ClassificationBase(BaseModel): + ip: Optional[str] = None + ja4: Optional[str] = None + label: ClassificationLabel + tags: List[str] = Field(default_factory=list) + comment: str = "" + confidence: float = Field(ge=0.0, le=1.0, default=0.5) + analyst: str = "unknown" + + +class ClassificationCreate(ClassificationBase): + """Donnรฉes pour crรฉer une classification""" + features: dict = Field(default_factory=dict) + + +class Classification(ClassificationBase): + """Classification complรจte avec mรฉtadonnรฉes""" + created_at: datetime + features: dict = Field(default_factory=dict) + + class Config: + from_attributes = True + + +class ClassificationStats(BaseModel): + """Statistiques de classification""" + label: str + total: int + unique_ips: int + avg_confidence: float + + +class ClassificationsListResponse(BaseModel): + """Rรฉponse pour la liste des classifications""" + items: List[Classification] + total: int + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# ANALYSIS (CORRELATION) +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class SubnetAnalysis(BaseModel): + """Analyse subnet/ASN""" + ip: str + subnet: str + ips_in_subnet: List[str] + total_in_subnet: int + asn_number: str + asn_org: str + total_in_asn: int + alert: bool # True si > 10 IPs du subnet + + +class CountryData(BaseModel): + """Donnรฉes pour un pays""" + code: str + name: str + count: int + percentage: float + + +class CountryAnalysis(BaseModel): + """Analyse des pays""" + top_countries: List[CountryData] + baseline: dict # Pays habituels + alert_country: Optional[str] = None # Pays surreprรฉsentรฉ + + +class JA4SubnetData(BaseModel): + """Subnet pour un JA4""" + subnet: str + count: int + + +class JA4Analysis(BaseModel): + """Analyse JA4""" + ja4: str + shared_ips_count: int + top_subnets: List[JA4SubnetData] + other_ja4_for_ip: List[str] + + +class UserAgentData(BaseModel): + """Donnรฉes pour un User-Agent""" + value: str + count: int + percentage: float + classification: str # "normal", "bot", "script" + + +class UserAgentAnalysis(BaseModel): + """Analyse User-Agents""" + ip_user_agents: List[UserAgentData] + ja4_user_agents: List[UserAgentData] + bot_percentage: float + alert: bool # True si > 20% bots/scripts + + +class CorrelationIndicators(BaseModel): + """Indicateurs de corrรฉlation""" + subnet_ips_count: int + asn_ips_count: int + country_percentage: float + ja4_shared_ips: int + user_agents_count: int + bot_ua_percentage: float + + +class ClassificationRecommendation(BaseModel): + """Recommandation de classification""" + label: ClassificationLabel + confidence: float + indicators: CorrelationIndicators + suggested_tags: List[str] + reason: str + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# ENTITIES (UNIFIED VIEW) +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class EntityStats(BaseModel): + """Statistiques pour une entitรฉ""" + entity_type: str + entity_value: str + total_requests: int + unique_ips: int + first_seen: datetime + last_seen: datetime + + +class EntityRelatedAttributes(BaseModel): + """Attributs associรฉs ร  une entitรฉ""" + ips: List[str] = Field(default_factory=list) + ja4s: List[str] = Field(default_factory=list) + hosts: List[str] = Field(default_factory=list) + asns: List[str] = Field(default_factory=list) + countries: List[str] = Field(default_factory=list) + + +class EntityAttributeValue(BaseModel): + """Valeur d'attribut avec count et percentage (pour les entities)""" + value: str + count: int + percentage: float + + +class EntityInvestigation(BaseModel): + """Investigation complรจte pour une entitรฉ""" + stats: EntityStats + related: EntityRelatedAttributes + user_agents: List[EntityAttributeValue] = Field(default_factory=list) + client_headers: List[EntityAttributeValue] = Field(default_factory=list) + paths: List[EntityAttributeValue] = Field(default_factory=list) + query_params: List[EntityAttributeValue] = Field(default_factory=list) diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..d212dab --- /dev/null +++ b/backend/routes/__init__.py @@ -0,0 +1 @@ +# Routes package diff --git a/backend/routes/analysis.py b/backend/routes/analysis.py new file mode 100644 index 0000000..25211a2 --- /dev/null +++ b/backend/routes/analysis.py @@ -0,0 +1,691 @@ +""" +Endpoints pour l'analyse de corrรฉlations et la classification SOC +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional, List +from datetime import datetime +import ipaddress +import json + +from ..database import db +from ..models import ( + SubnetAnalysis, CountryAnalysis, CountryData, JA4Analysis, JA4SubnetData, + UserAgentAnalysis, UserAgentData, CorrelationIndicators, + ClassificationRecommendation, ClassificationLabel, + ClassificationCreate, Classification, ClassificationsListResponse +) + +router = APIRouter(prefix="/api/analysis", tags=["analysis"]) + + +# ============================================================================= +# ANALYSE SUBNET / ASN +# ============================================================================= + +@router.get("/{ip}/subnet", response_model=SubnetAnalysis) +async def analyze_subnet(ip: str): + """ + Analyse les IPs du mรชme subnet et ASN + """ + try: + # Calculer le subnet /24 + ip_obj = ipaddress.ip_address(ip) + subnet = ipaddress.ip_network(f"{ip}/24", strict=False) + subnet_str = str(subnet) + + # Rรฉcupรฉrer les infos ASN pour cette IP + asn_query = """ + SELECT asn_number, asn_org + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + ORDER BY detected_at DESC + LIMIT 1 + """ + asn_result = db.query(asn_query, {"ip": ip}) + + if not asn_result.result_rows: + # Fallback: utiliser donnรฉes par dรฉfaut + asn_number = "0" + asn_org = "Unknown" + else: + asn_number = str(asn_result.result_rows[0][0] or "0") + asn_org = asn_result.result_rows[0][1] or "Unknown" + + # IPs du mรชme subnet /24 + subnet_ips_query = """ + SELECT DISTINCT src_ip + FROM ml_detected_anomalies + WHERE toIPv4(src_ip) >= toIPv4(%(subnet_start)s) + AND toIPv4(src_ip) <= toIPv4(%(subnet_end)s) + AND detected_at >= now() - INTERVAL 24 HOUR + ORDER BY src_ip + """ + + subnet_result = db.query(subnet_ips_query, { + "subnet_start": str(subnet.network_address), + "subnet_end": str(subnet.broadcast_address) + }) + + subnet_ips = [str(row[0]) for row in subnet_result.result_rows] + + # Total IPs du mรชme ASN + if asn_number != "0": + asn_total_query = """ + SELECT uniq(src_ip) + FROM ml_detected_anomalies + WHERE asn_number = %(asn_number)s + AND detected_at >= now() - INTERVAL 24 HOUR + """ + + asn_total_result = db.query(asn_total_query, {"asn_number": asn_number}) + asn_total = asn_total_result.result_rows[0][0] if asn_total_result.result_rows else 0 + else: + asn_total = 0 + + return SubnetAnalysis( + ip=ip, + subnet=subnet_str, + ips_in_subnet=subnet_ips, + total_in_subnet=len(subnet_ips), + asn_number=asn_number, + asn_org=asn_org, + total_in_asn=asn_total, + alert=len(subnet_ips) > 10 + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +@router.get("/{ip}/country", response_model=dict) +async def analyze_ip_country(ip: str): + """ + Analyse le pays d'une IP spรฉcifique et la rรฉpartition des autres pays du mรชme ASN + """ + try: + # Pays de l'IP + ip_country_query = """ + SELECT country_code, asn_number + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + ORDER BY detected_at DESC + LIMIT 1 + """ + + ip_result = db.query(ip_country_query, {"ip": ip}) + + if not ip_result.result_rows: + return {"ip_country": None, "asn_countries": []} + + ip_country_code = ip_result.result_rows[0][0] + asn_number = ip_result.result_rows[0][1] + + # Noms des pays + country_names = { + "CN": "China", "US": "United States", "DE": "Germany", + "FR": "France", "RU": "Russia", "GB": "United Kingdom", + "NL": "Netherlands", "IN": "India", "BR": "Brazil", + "JP": "Japan", "KR": "South Korea", "IT": "Italy", + "ES": "Spain", "CA": "Canada", "AU": "Australia" + } + + # Rรฉpartition des autres pays du mรชme ASN + asn_countries_query = """ + SELECT + country_code, + count() AS count + FROM ml_detected_anomalies + WHERE asn_number = %(asn_number)s + AND detected_at >= now() - INTERVAL 24 HOUR + GROUP BY country_code + ORDER BY count DESC + LIMIT 10 + """ + + asn_result = db.query(asn_countries_query, {"asn_number": asn_number}) + + total = sum(row[1] for row in asn_result.result_rows) + + asn_countries = [ + { + "code": row[0], + "name": country_names.get(row[0], row[0]), + "count": row[1], + "percentage": round((row[1] / total * 100), 2) if total > 0 else 0.0 + } + for row in asn_result.result_rows + ] + + return { + "ip_country": { + "code": ip_country_code, + "name": country_names.get(ip_country_code, ip_country_code) + }, + "asn_countries": asn_countries + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ANALYSE PAYS +# ============================================================================= + +@router.get("/country", response_model=CountryAnalysis) +async def analyze_country(days: int = Query(1, ge=1, le=30)): + """ + Analyse la distribution des pays + """ + try: + # Top pays + top_query = """ + SELECT + country_code, + count() AS count + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL %(days)s DAY + AND country_code != '' AND country_code IS NOT NULL + GROUP BY country_code + ORDER BY count DESC + LIMIT 10 + """ + + top_result = db.query(top_query, {"days": days}) + + # Calculer le total pour le pourcentage + total = sum(row[1] for row in top_result.result_rows) + + # Noms des pays (mapping simple) + country_names = { + "CN": "China", "US": "United States", "DE": "Germany", + "FR": "France", "RU": "Russia", "GB": "United Kingdom", + "NL": "Netherlands", "IN": "India", "BR": "Brazil", + "JP": "Japan", "KR": "South Korea", "IT": "Italy", + "ES": "Spain", "CA": "Canada", "AU": "Australia" + } + + top_countries = [ + CountryData( + code=row[0], + name=country_names.get(row[0], row[0]), + count=row[1], + percentage=round((row[1] / total * 100), 2) if total > 0 else 0.0 + ) + for row in top_result.result_rows + ] + + # Baseline (7 derniers jours) + baseline_query = """ + SELECT + country_code, + count() AS count + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 7 DAY + AND country_code != '' AND country_code IS NOT NULL + GROUP BY country_code + ORDER BY count DESC + LIMIT 5 + """ + + baseline_result = db.query(baseline_query) + + baseline_total = sum(row[1] for row in baseline_result.result_rows) + baseline = { + row[0]: round((row[1] / baseline_total * 100), 2) if baseline_total > 0 else 0.0 + for row in baseline_result.result_rows + } + + # Dรฉtecter pays surreprรฉsentรฉ + alert_country = None + for country in top_countries: + baseline_pct = baseline.get(country.code, 0) + if baseline_pct > 0 and country.percentage > baseline_pct * 2 and country.percentage > 30: + alert_country = country.code + break + + return CountryAnalysis( + top_countries=top_countries, + baseline=baseline, + alert_country=alert_country + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ANALYSE JA4 +# ============================================================================= + +@router.get("/{ip}/ja4", response_model=JA4Analysis) +async def analyze_ja4(ip: str): + """ + Analyse le JA4 fingerprint + """ + try: + # JA4 de cette IP + ja4_query = """ + SELECT ja4 + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + AND ja4 != '' AND ja4 IS NOT NULL + ORDER BY detected_at DESC + LIMIT 1 + """ + + ja4_result = db.query(ja4_query, {"ip": ip}) + + if not ja4_result.result_rows: + return JA4Analysis( + ja4="", + shared_ips_count=0, + top_subnets=[], + other_ja4_for_ip=[] + ) + + ja4 = ja4_result.result_rows[0][0] + + # IPs avec le mรชme JA4 + shared_query = """ + SELECT uniq(src_ip) + FROM ml_detected_anomalies + WHERE ja4 = %(ja4)s + AND detected_at >= now() - INTERVAL 24 HOUR + """ + + shared_result = db.query(shared_query, {"ja4": ja4}) + shared_count = shared_result.result_rows[0][0] if shared_result.result_rows else 0 + + # Top subnets pour ce JA4 - Simplifiรฉ + subnets_query = """ + SELECT + src_ip, + count() AS count + FROM ml_detected_anomalies + WHERE ja4 = %(ja4)s + AND detected_at >= now() - INTERVAL 24 HOUR + GROUP BY src_ip + ORDER BY count DESC + LIMIT 100 + """ + + subnets_result = db.query(subnets_query, {"ja4": ja4}) + + # Grouper par subnet /24 + from collections import defaultdict + subnet_counts = defaultdict(int) + for row in subnets_result.result_rows: + ip_addr = row[0] + parts = ip_addr.split('.') + if len(parts) == 4: + subnet = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24" + subnet_counts[subnet] += row[1] + + top_subnets = [ + JA4SubnetData(subnet=subnet, count=count) + for subnet, count in sorted(subnet_counts.items(), key=lambda x: x[1], reverse=True)[:10] + ] + + # Autres JA4 pour cette IP + other_ja4_query = """ + SELECT DISTINCT ja4 + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + AND ja4 != '' AND ja4 IS NOT NULL + AND ja4 != %(current_ja4)s + """ + + other_result = db.query(other_ja4_query, {"ip": ip, "current_ja4": ja4}) + other_ja4 = [row[0] for row in other_result.result_rows] + + return JA4Analysis( + ja4=ja4, + shared_ips_count=shared_count, + top_subnets=top_subnets, + other_ja4_for_ip=other_ja4 + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ANALYSE USER-AGENTS +# ============================================================================= + +@router.get("/{ip}/user-agents", response_model=UserAgentAnalysis) +async def analyze_user_agents(ip: str): + """ + Analyse les User-Agents + """ + try: + # User-Agents pour cette IP (depuis http_logs) + ip_ua_query = """ + SELECT + header_user_agent AS ua, + count() AS count + FROM mabase_prod.http_logs + WHERE src_ip = %(ip)s + AND header_user_agent != '' AND header_user_agent IS NOT NULL + AND time >= now() - INTERVAL 24 HOUR + GROUP BY ua + ORDER BY count DESC + LIMIT 10 + """ + + ip_ua_result = db.query(ip_ua_query, {"ip": ip}) + + # Classification des UAs + def classify_ua(ua: str) -> str: + ua_lower = ua.lower() + if any(bot in ua_lower for bot in ['bot', 'crawler', 'spider', 'curl', 'wget', 'python', 'requests', 'scrapy']): + return 'bot' + if any(script in ua_lower for script in ['python', 'java', 'php', 'ruby', 'perl', 'node']): + return 'script' + if not ua or ua.strip() == '': + return 'script' + return 'normal' + + # Calculer le total + total_count = sum(row[1] for row in ip_ua_result.result_rows) + + ip_user_agents = [ + UserAgentData( + value=row[0], + count=row[1], + percentage=round((row[1] / total_count * 100), 2) if total_count > 0 else 0.0, + classification=classify_ua(row[0]) + ) + for row in ip_ua_result.result_rows + ] + + # Pour les UAs du JA4, on retourne les mรชmes pour l'instant + ja4_user_agents = ip_user_agents + + # Pourcentage de bots + bot_count = sum(ua.count for ua in ip_user_agents if ua.classification in ['bot', 'script']) + bot_percentage = (bot_count / total_count * 100) if total_count > 0 else 0 + + return UserAgentAnalysis( + ip_user_agents=ip_user_agents, + ja4_user_agents=ja4_user_agents, + bot_percentage=bot_percentage, + alert=bot_percentage > 20 + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# RECOMMANDATION DE CLASSIFICATION +# ============================================================================= + +@router.get("/{ip}/recommendation", response_model=ClassificationRecommendation) +async def get_classification_recommendation(ip: str): + """ + Gรฉnรจre une recommandation de classification basรฉe sur les corrรฉlations + """ + try: + # Rรฉcupรฉrer les analyses + try: + subnet_analysis = await analyze_subnet(ip) + except: + subnet_analysis = None + + try: + country_analysis = await analyze_country(1) + except: + country_analysis = None + + try: + ja4_analysis = await analyze_ja4(ip) + except: + ja4_analysis = None + + try: + ua_analysis = await analyze_user_agents(ip) + except: + ua_analysis = None + + # Indicateurs par dรฉfaut + indicators = CorrelationIndicators( + subnet_ips_count=subnet_analysis.total_in_subnet if subnet_analysis else 0, + asn_ips_count=subnet_analysis.total_in_asn if subnet_analysis else 0, + country_percentage=0.0, + ja4_shared_ips=ja4_analysis.shared_ips_count if ja4_analysis else 0, + user_agents_count=len(ua_analysis.ja4_user_agents) if ua_analysis else 0, + bot_ua_percentage=ua_analysis.bot_percentage if ua_analysis else 0.0 + ) + + # Score de confiance + score = 0.0 + reasons = [] + tags = [] + + # Subnet > 10 IPs + if subnet_analysis and subnet_analysis.total_in_subnet > 10: + score += 0.25 + reasons.append(f"{subnet_analysis.total_in_subnet} IPs du mรชme subnet") + tags.append("distributed") + + # JA4 partagรฉ > 50 IPs + if ja4_analysis and ja4_analysis.shared_ips_count > 50: + score += 0.25 + reasons.append(f"{ja4_analysis.shared_ips_count} IPs avec mรชme JA4") + tags.append("ja4-rotation") + + # Bot UA > 20% + if ua_analysis and ua_analysis.bot_percentage > 20: + score += 0.25 + reasons.append(f"{ua_analysis.bot_percentage:.0f}% UAs bots/scripts") + tags.append("bot-ua") + + # Pays surreprรฉsentรฉ + if country_analysis and country_analysis.alert_country: + score += 0.15 + reasons.append(f"Pays {country_analysis.alert_country} surreprรฉsentรฉ") + tags.append(f"country-{country_analysis.alert_country.lower()}") + + # ASN hosting + if subnet_analysis: + hosting_keywords = ["ovh", "amazon", "aws", "google", "azure", "digitalocean", "linode", "vultr", "china169", "chinamobile"] + if any(kw in (subnet_analysis.asn_org or "").lower() for kw in hosting_keywords): + score += 0.10 + tags.append("hosting-asn") + + # Dรฉterminer label + if score >= 0.7: + label = ClassificationLabel.MALICIOUS + tags.append("campaign") + elif score >= 0.4: + label = ClassificationLabel.SUSPICIOUS + else: + label = ClassificationLabel.LEGITIMATE + + reason = " | ".join(reasons) if reasons else "Aucun indicateur fort" + + return ClassificationRecommendation( + label=label, + confidence=min(score, 1.0), + indicators=indicators, + suggested_tags=tags, + reason=reason + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# CLASSIFICATIONS CRUD +# ============================================================================= + +@router.post("/classifications", response_model=Classification) +async def create_classification(data: ClassificationCreate): + """ + Crรฉe une classification pour une IP ou un JA4 + """ + try: + # Validation: soit ip, soit ja4 doit รชtre fourni + if not data.ip and not data.ja4: + raise HTTPException(status_code=400, detail="IP ou JA4 requis") + + query = """ + INSERT INTO mabase_prod.classifications + (ip, ja4, label, tags, comment, confidence, features, analyst, created_at) + VALUES + (%(ip)s, %(ja4)s, %(label)s, %(tags)s, %(comment)s, %(confidence)s, %(features)s, %(analyst)s, now()) + """ + + db.query(query, { + "ip": data.ip or "", + "ja4": data.ja4 or "", + "label": data.label.value, + "tags": data.tags, + "comment": data.comment, + "confidence": data.confidence, + "features": json.dumps(data.features), + "analyst": data.analyst + }) + + # Rรฉcupรฉrer la classification crรฉรฉe + where_clause = "ip = %(entity)s" if data.ip else "ja4 = %(entity)s" + select_query = f""" + SELECT ip, ja4, label, tags, comment, confidence, features, analyst, created_at + FROM mabase_prod.classifications + WHERE {where_clause} + ORDER BY created_at DESC + LIMIT 1 + """ + + result = db.query(select_query, {"entity": data.ip or data.ja4}) + + if not result.result_rows: + raise HTTPException(status_code=404, detail="Classification non trouvรฉe") + + row = result.result_rows[0] + return Classification( + ip=row[0] or None, + ja4=row[1] or None, + label=ClassificationLabel(row[2]), + tags=row[3], + comment=row[4], + confidence=row[5], + features=json.loads(row[6]) if row[6] else {}, + analyst=row[7], + created_at=row[8] + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +@router.get("/classifications", response_model=ClassificationsListResponse) +async def list_classifications( + ip: Optional[str] = Query(None, description="Filtrer par IP"), + ja4: Optional[str] = Query(None, description="Filtrer par JA4"), + label: Optional[str] = Query(None, description="Filtrer par label"), + limit: int = Query(100, ge=1, le=1000) +): + """ + Liste les classifications + """ + try: + where_clauses = ["1=1"] + params = {"limit": limit} + + if ip: + where_clauses.append("ip = %(ip)s") + params["ip"] = ip + + if ja4: + where_clauses.append("ja4 = %(ja4)s") + params["ja4"] = ja4 + + if label: + where_clauses.append("label = %(label)s") + params["label"] = label + + where_clause = " AND ".join(where_clauses) + + query = f""" + SELECT ip, ja4, label, tags, comment, confidence, features, analyst, created_at + FROM mabase_prod.classifications + WHERE {where_clause} + ORDER BY created_at DESC + LIMIT %(limit)s + """ + + result = db.query(query, params) + + classifications = [ + Classification( + ip=row[0] or None, + ja4=row[1] or None, + label=ClassificationLabel(row[2]), + tags=row[3], + comment=row[4], + confidence=row[5], + features=json.loads(row[6]) if row[6] else {}, + analyst=row[7], + created_at=row[8] + ) + for row in result.result_rows + ] + + # Total + count_query = f""" + SELECT count() + FROM mabase_prod.classifications + WHERE {where_clause} + """ + + count_result = db.query(count_query, params) + total = count_result.result_rows[0][0] if count_result.result_rows else 0 + + return ClassificationsListResponse( + items=classifications, + total=total + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +@router.get("/classifications/stats") +async def get_classification_stats(): + """ + Statistiques des classifications + """ + try: + stats_query = """ + SELECT + label, + count() AS total, + uniq(ip) AS unique_ips, + avg(confidence) AS avg_confidence + FROM mabase_prod.classifications + GROUP BY label + ORDER BY total DESC + """ + + result = db.query(stats_query) + + stats = [ + { + "label": row[0], + "total": row[1], + "unique_ips": row[2], + "avg_confidence": float(row[3]) if row[3] else 0.0 + } + for row in result.result_rows + ] + + return {"stats": stats} + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/backend/routes/attributes.py b/backend/routes/attributes.py new file mode 100644 index 0000000..0a929e3 --- /dev/null +++ b/backend/routes/attributes.py @@ -0,0 +1,92 @@ +""" +Endpoints pour la liste des attributs uniques +""" +from fastapi import APIRouter, HTTPException, Query +from ..database import db +from ..models import AttributeListResponse, AttributeListItem + +router = APIRouter(prefix="/api/attributes", tags=["attributes"]) + + +@router.get("/{attr_type}", response_model=AttributeListResponse) +async def get_attributes( + attr_type: str, + limit: int = Query(100, ge=1, le=1000, description="Nombre maximum de rรฉsultats") +): + """ + Rรฉcupรจre la liste des valeurs uniques pour un type d'attribut + """ + try: + # Mapping des types vers les colonnes + type_column_map = { + "ip": "src_ip", + "ja4": "ja4", + "country": "country_code", + "asn": "asn_number", + "host": "host", + "threat_level": "threat_level", + "model_name": "model_name", + "asn_org": "asn_org" + } + + if attr_type not in type_column_map: + raise HTTPException( + status_code=400, + detail=f"Type invalide. Types supportรฉs: {', '.join(type_column_map.keys())}" + ) + + column = type_column_map[attr_type] + + # Requรชte de base + base_query = f""" + SELECT + {column} AS value, + count() AS count + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 24 HOUR + """ + + # Ajout du filtre pour exclure les valeurs vides/nulles + # Gestion spรฉciale pour les types IPv6/IPv4 qui ne peuvent pas รชtre comparรฉs ร  '' + if attr_type == "ip": + # Pour les adresses IP, on convertit en string et on filtre + query = f""" + SELECT value, count FROM ( + SELECT toString({column}) AS value, count() AS count + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 24 HOUR + GROUP BY {column} + ) + WHERE value != '' AND value IS NOT NULL + ORDER BY count DESC + LIMIT %(limit)s + """ + else: + query = f""" + {base_query} + AND {column} != '' AND {column} IS NOT NULL + GROUP BY value + ORDER BY count DESC + LIMIT %(limit)s + """ + + result = db.query(query, {"limit": limit}) + + items = [ + AttributeListItem( + value=str(row[0]), + count=row[1] + ) + for row in result.result_rows + ] + + return AttributeListResponse( + type=attr_type, + items=items, + total=len(items) + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/backend/routes/detections.py b/backend/routes/detections.py new file mode 100644 index 0000000..10183a1 --- /dev/null +++ b/backend/routes/detections.py @@ -0,0 +1,294 @@ +""" +Endpoints pour la liste des dรฉtections +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional, List +from ..database import db +from ..models import DetectionsListResponse, Detection + +router = APIRouter(prefix="/api/detections", tags=["detections"]) + + +@router.get("", response_model=DetectionsListResponse) +async def get_detections( + page: int = Query(1, ge=1, description="Numรฉro de page"), + page_size: int = Query(25, ge=1, le=100, description="Nombre de lignes par page"), + threat_level: Optional[str] = Query(None, description="Filtrer par niveau de menace"), + model_name: Optional[str] = Query(None, description="Filtrer par modรจle"), + country_code: Optional[str] = Query(None, description="Filtrer par pays"), + asn_number: Optional[str] = Query(None, description="Filtrer par ASN"), + search: Optional[str] = Query(None, description="Recherche texte (IP, JA4, Host)"), + sort_by: str = Query("detected_at", description="Trier par"), + sort_order: str = Query("DESC", description="Ordre (ASC/DESC)") +): + """ + Rรฉcupรจre la liste des dรฉtections avec pagination et filtres + """ + try: + # Construction de la requรชte + where_clauses = ["detected_at >= now() - INTERVAL 24 HOUR"] + params = {} + + if threat_level: + where_clauses.append("threat_level = %(threat_level)s") + params["threat_level"] = threat_level + + if model_name: + where_clauses.append("model_name = %(model_name)s") + params["model_name"] = model_name + + if country_code: + where_clauses.append("country_code = %(country_code)s") + params["country_code"] = country_code.upper() + + if asn_number: + where_clauses.append("asn_number = %(asn_number)s") + params["asn_number"] = asn_number + + if search: + where_clauses.append( + "(src_ip ILIKE %(search)s OR ja4 ILIKE %(search)s OR host ILIKE %(search)s)" + ) + params["search"] = f"%{search}%" + + where_clause = " AND ".join(where_clauses) + + # Requรชte de comptage + count_query = f""" + SELECT count() + FROM ml_detected_anomalies + WHERE {where_clause} + """ + + count_result = db.query(count_query, params) + total = count_result.result_rows[0][0] if count_result.result_rows else 0 + + # Requรชte principale + offset = (page - 1) * page_size + + # Validation du tri + valid_sort_columns = [ + "detected_at", "src_ip", "threat_level", "anomaly_score", + "asn_number", "country_code", "hits", "hit_velocity" + ] + if sort_by not in valid_sort_columns: + sort_by = "detected_at" + + sort_order = "DESC" if sort_order.upper() == "DESC" else "ASC" + + main_query = f""" + SELECT + detected_at, + src_ip, + ja4, + host, + bot_name, + anomaly_score, + threat_level, + model_name, + recurrence, + asn_number, + asn_org, + asn_detail, + asn_domain, + country_code, + asn_label, + hits, + hit_velocity, + fuzzing_index, + post_ratio, + reason + FROM ml_detected_anomalies + WHERE {where_clause} + ORDER BY {sort_by} {sort_order} + LIMIT %(limit)s OFFSET %(offset)s + """ + + params["limit"] = page_size + params["offset"] = offset + + result = db.query(main_query, params) + + detections = [ + Detection( + detected_at=row[0], + src_ip=str(row[1]), + ja4=row[2] or "", + host=row[3] or "", + bot_name=row[4] or "", + anomaly_score=float(row[5]) if row[5] else 0.0, + threat_level=row[6] or "LOW", + model_name=row[7] or "", + recurrence=row[8] or 0, + asn_number=str(row[9]) if row[9] else "", + asn_org=row[10] or "", + asn_detail=row[11] or "", + asn_domain=row[12] or "", + country_code=row[13] or "", + asn_label=row[14] or "", + hits=row[15] or 0, + hit_velocity=float(row[16]) if row[16] else 0.0, + fuzzing_index=float(row[17]) if row[17] else 0.0, + post_ratio=float(row[18]) if row[18] else 0.0, + reason=row[19] or "" + ) + for row in result.result_rows + ] + + total_pages = (total + page_size - 1) // page_size + + return DetectionsListResponse( + items=detections, + total=total, + page=page, + page_size=page_size, + total_pages=total_pages + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur lors de la rรฉcupรฉration des dรฉtections: {str(e)}") + + +@router.get("/{detection_id}") +async def get_detection_details(detection_id: str): + """ + Rรฉcupรจre les dรฉtails d'une dรฉtection spรฉcifique + detection_id peut รชtre une IP ou un identifiant + """ + try: + query = """ + SELECT + detected_at, + src_ip, + ja4, + host, + bot_name, + anomaly_score, + threat_level, + model_name, + recurrence, + asn_number, + asn_org, + asn_detail, + asn_domain, + country_code, + asn_label, + hits, + hit_velocity, + fuzzing_index, + post_ratio, + port_exhaustion_ratio, + orphan_ratio, + tcp_jitter_variance, + tcp_shared_count, + true_window_size, + window_mss_ratio, + alpn_http_mismatch, + is_alpn_missing, + sni_host_mismatch, + header_count, + has_accept_language, + has_cookie, + has_referer, + modern_browser_score, + ua_ch_mismatch, + header_order_shared_count, + ip_id_zero_ratio, + request_size_variance, + multiplexing_efficiency, + mss_mobile_mismatch, + correlated, + reason, + asset_ratio, + direct_access_ratio, + is_ua_rotating, + distinct_ja4_count, + src_port_density, + ja4_asn_concentration, + ja4_country_concentration, + is_rare_ja4 + FROM ml_detected_anomalies + WHERE src_ip = %(ip)s + ORDER BY detected_at DESC + LIMIT 1 + """ + + result = db.query(query, {"ip": detection_id}) + + if not result.result_rows: + raise HTTPException(status_code=404, detail="Dรฉtection non trouvรฉe") + + row = result.result_rows[0] + + return { + "detected_at": row[0], + "src_ip": str(row[1]), + "ja4": row[2] or "", + "host": row[3] or "", + "bot_name": row[4] or "", + "anomaly_score": float(row[5]) if row[5] else 0.0, + "threat_level": row[6] or "LOW", + "model_name": row[7] or "", + "recurrence": row[8] or 0, + "asn": { + "number": str(row[9]) if row[9] else "", + "org": row[10] or "", + "detail": row[11] or "", + "domain": row[12] or "", + "label": row[14] or "" + }, + "country": { + "code": row[13] or "", + }, + "metrics": { + "hits": row[15] or 0, + "hit_velocity": float(row[16]) if row[16] else 0.0, + "fuzzing_index": float(row[17]) if row[17] else 0.0, + "post_ratio": float(row[18]) if row[18] else 0.0, + "port_exhaustion_ratio": float(row[19]) if row[19] else 0.0, + "orphan_ratio": float(row[20]) if row[20] else 0.0, + }, + "tcp": { + "jitter_variance": float(row[21]) if row[21] else 0.0, + "shared_count": row[22] or 0, + "true_window_size": row[23] or 0, + "window_mss_ratio": float(row[24]) if row[24] else 0.0, + }, + "tls": { + "alpn_http_mismatch": bool(row[25]) if row[25] is not None else False, + "is_alpn_missing": bool(row[26]) if row[26] is not None else False, + "sni_host_mismatch": bool(row[27]) if row[27] is not None else False, + }, + "headers": { + "count": row[28] or 0, + "has_accept_language": bool(row[29]) if row[29] is not None else False, + "has_cookie": bool(row[30]) if row[30] is not None else False, + "has_referer": bool(row[31]) if row[31] is not None else False, + "modern_browser_score": row[32] or 0, + "ua_ch_mismatch": bool(row[33]) if row[33] is not None else False, + "header_order_shared_count": row[34] or 0, + }, + "behavior": { + "ip_id_zero_ratio": float(row[35]) if row[35] else 0.0, + "request_size_variance": float(row[36]) if row[36] else 0.0, + "multiplexing_efficiency": float(row[37]) if row[37] else 0.0, + "mss_mobile_mismatch": bool(row[38]) if row[38] is not None else False, + "correlated": bool(row[39]) if row[39] is not None else False, + }, + "advanced": { + "asset_ratio": float(row[41]) if row[41] else 0.0, + "direct_access_ratio": float(row[42]) if row[42] else 0.0, + "is_ua_rotating": bool(row[43]) if row[43] is not None else False, + "distinct_ja4_count": row[44] or 0, + "src_port_density": float(row[45]) if row[45] else 0.0, + "ja4_asn_concentration": float(row[46]) if row[46] else 0.0, + "ja4_country_concentration": float(row[47]) if row[47] else 0.0, + "is_rare_ja4": bool(row[48]) if row[48] is not None else False, + }, + "reason": row[40] or "" + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/backend/routes/entities.py b/backend/routes/entities.py new file mode 100644 index 0000000..04fcfa8 --- /dev/null +++ b/backend/routes/entities.py @@ -0,0 +1,337 @@ +""" +Routes pour l'investigation d'entitรฉs (IP, JA4, User-Agent, Client-Header, Host, Path, Query-Param) +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional, List, Dict, Any +from datetime import datetime +import json + +from ..database import db +from ..models import ( + EntityInvestigation, + EntityStats, + EntityRelatedAttributes, + EntityAttributeValue +) + +router = APIRouter(prefix="/api/entities", tags=["Entities"]) + +db = db + +# Mapping des types d'entitรฉs +ENTITY_TYPES = { + 'ip': 'ip', + 'ja4': 'ja4', + 'user_agent': 'user_agent', + 'client_header': 'client_header', + 'host': 'host', + 'path': 'path', + 'query_param': 'query_param' +} + + +def get_entity_stats(entity_type: str, entity_value: str, hours: int = 24) -> Optional[EntityStats]: + """ + Rรฉcupรจre les statistiques pour une entitรฉ donnรฉe + """ + query = """ + SELECT + entity_type, + entity_value, + sum(requests) as total_requests, + sum(unique_ips) as unique_ips, + min(log_date) as first_seen, + max(log_date) as last_seen + 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 + GROUP BY entity_type, entity_value + """ + + result = db.connect().query(query, { + 'entity_type': entity_type, + 'entity_value': entity_value, + 'hours': hours + }) + + if not result.result_rows: + return None + + row = result.result_rows[0] + return EntityStats( + entity_type=row[0], + entity_value=row[1], + total_requests=row[2], + unique_ips=row[3], + first_seen=row[4], + last_seen=row[5] + ) + + +def get_related_attributes(entity_type: str, entity_value: str, hours: int = 24) -> EntityRelatedAttributes: + """ + Rรฉcupรจre les attributs associรฉs ร  une entitรฉ + """ + # Requรชte pour agrรฉger tous les attributs associรฉs + query = """ + SELECT + (SELECT groupUniqArray(toString(src_ip)) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR) as ips, + (SELECT groupUniqArray(ja4) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND ja4 != '') as ja4s, + (SELECT groupUniqArray(host) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND host != '') as hosts, + (SELECT groupUniqArrayArray(asns) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND notEmpty(asns)) as asns, + (SELECT groupUniqArrayArray(countries) FROM mabase_prod.view_dashboard_entities WHERE entity_type = %(entity_type)s AND entity_value = %(entity_value)s AND log_date >= now() - INTERVAL %(hours)s HOUR AND notEmpty(countries)) as countries + """ + + result = db.connect().query(query, { + 'entity_type': entity_type, + 'entity_value': entity_value, + 'hours': hours + }) + + if not result.result_rows or not any(result.result_rows[0]): + return EntityRelatedAttributes( + ips=[], + ja4s=[], + hosts=[], + asns=[], + countries=[] + ) + + row = result.result_rows[0] + return EntityRelatedAttributes( + ips=[str(ip) for ip in (row[0] or []) if ip], + ja4s=[ja4 for ja4 in (row[1] or []) if ja4], + hosts=[host for host in (row[2] or []) if host], + asns=[asn for asn in (row[3] or []) if asn], + countries=[country for country in (row[4] or []) if country] + ) + + +def get_array_values(entity_type: str, entity_value: str, array_field: str, hours: int = 24) -> List[EntityAttributeValue]: + """ + Extrait et retourne les valeurs d'un champ Array (user_agents, client_headers, etc.) + """ + query = f""" + SELECT + value, + count() as count, + round(count * 100.0 / sum(count) OVER (), 2) as percentage + FROM ( + SELECT + arrayJoin({array_field}) as value + 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({array_field}) + ) + GROUP BY value + ORDER BY count DESC + LIMIT 100 + """ + + result = db.connect().query(query, { + 'entity_type': entity_type, + 'entity_value': entity_value, + 'hours': hours + }) + + return [ + EntityAttributeValue( + value=row[0], + count=row[1], + percentage=row[2] + ) + for row in result.result_rows + ] + + +@router.get("/{entity_type}/{entity_value:path}", response_model=EntityInvestigation) +async def get_entity_investigation( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720, description="Fenรชtre temporelle en heures") +): + """ + Investigation complรจte pour une entitรฉ donnรฉe + + - **entity_type**: Type d'entitรฉ (ip, ja4, user_agent, client_header, host, path, query_param) + - **entity_value**: Valeur de l'entitรฉ + - **hours**: Fenรชtre temporelle (dรฉfaut: 24h) + + Retourne: + - Stats gรฉnรฉrales + - Attributs associรฉs (IPs, JA4, Hosts, ASNs, Pays) + - User-Agents + - Client-Headers + - Paths + - Query-Params + """ + # Valider le type d'entitรฉ + if entity_type not in ENTITY_TYPES: + raise HTTPException( + status_code=400, + detail=f"Type d'entitรฉ invalide. Types supportรฉs: {', '.join(ENTITY_TYPES.keys())}" + ) + + # Stats gรฉnรฉrales + stats = get_entity_stats(entity_type, entity_value, hours) + if not stats: + raise HTTPException(status_code=404, detail="Entitรฉ non trouvรฉe") + + # Attributs associรฉs + related = get_related_attributes(entity_type, entity_value, hours) + + # User-Agents + user_agents = get_array_values(entity_type, entity_value, 'user_agents', hours) + + # Client-Headers + client_headers = get_array_values(entity_type, entity_value, 'client_headers', hours) + + # Paths + paths = get_array_values(entity_type, entity_value, 'paths', hours) + + # Query-Params + query_params = get_array_values(entity_type, entity_value, 'query_params', hours) + + return EntityInvestigation( + stats=stats, + related=related, + user_agents=user_agents, + client_headers=client_headers, + paths=paths, + query_params=query_params + ) + + +@router.get("/{entity_type}/{entity_value:path}/related") +async def get_entity_related( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Rรฉcupรจre uniquement les attributs associรฉs ร  une entitรฉ + """ + if entity_type not in ENTITY_TYPES: + raise HTTPException( + status_code=400, + detail=f"Type d'entitรฉ invalide. Types supportรฉs: {', '.join(ENTITY_TYPES.keys())}" + ) + + related = get_related_attributes(entity_type, entity_value, hours) + + return { + "entity_type": entity_type, + "entity_value": entity_value, + "hours": hours, + "related": related + } + + +@router.get("/{entity_type}/{entity_value:path}/user_agents") +async def get_entity_user_agents( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Rรฉcupรจre les User-Agents associรฉs ร  une entitรฉ + """ + if entity_type not in ENTITY_TYPES: + raise HTTPException(status_code=400, detail="Type d'entitรฉ invalide") + + user_agents = get_array_values(entity_type, entity_value, 'user_agents', hours) + + return { + "entity_type": entity_type, + "entity_value": entity_value, + "user_agents": user_agents, + "total": len(user_agents) + } + + +@router.get("/{entity_type}/{entity_value:path}/client_headers") +async def get_entity_client_headers( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Rรฉcupรจre les Client-Headers associรฉs ร  une entitรฉ + """ + if entity_type not in ENTITY_TYPES: + raise HTTPException(status_code=400, detail="Type d'entitรฉ invalide") + + client_headers = get_array_values(entity_type, entity_value, 'client_headers', hours) + + return { + "entity_type": entity_type, + "entity_value": entity_value, + "client_headers": client_headers, + "total": len(client_headers) + } + + +@router.get("/{entity_type}/{entity_value:path}/paths") +async def get_entity_paths( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Rรฉcupรจre les Paths associรฉs ร  une entitรฉ + """ + if entity_type not in ENTITY_TYPES: + raise HTTPException(status_code=400, detail="Type d'entitรฉ invalide") + + paths = get_array_values(entity_type, entity_value, 'paths', hours) + + return { + "entity_type": entity_type, + "entity_value": entity_value, + "paths": paths, + "total": len(paths) + } + + +@router.get("/{entity_type}/{entity_value:path}/query_params") +async def get_entity_query_params( + entity_type: str, + entity_value: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Rรฉcupรจre les Query-Params associรฉs ร  une entitรฉ + """ + if entity_type not in ENTITY_TYPES: + raise HTTPException(status_code=400, detail="Type d'entitรฉ invalide") + + query_params = get_array_values(entity_type, entity_value, 'query_params', hours) + + return { + "entity_type": entity_type, + "entity_value": entity_value, + "query_params": query_params, + "total": len(query_params) + } + + +@router.get("/types") +async def get_entity_types(): + """ + Retourne la liste des types d'entitรฉs supportรฉs + """ + return { + "entity_types": list(ENTITY_TYPES.values()), + "descriptions": { + "ip": "Adresse IP source", + "ja4": "Fingerprint JA4 TLS", + "user_agent": "User-Agent HTTP", + "client_header": "Client Header HTTP", + "host": "Host HTTP", + "path": "Path URL", + "query_param": "Paramรจtres de query (noms concatรฉnรฉs)" + } + } diff --git a/backend/routes/metrics.py b/backend/routes/metrics.py new file mode 100644 index 0000000..0646fc4 --- /dev/null +++ b/backend/routes/metrics.py @@ -0,0 +1,122 @@ +""" +Endpoints pour les mรฉtriques du dashboard +""" +from fastapi import APIRouter, HTTPException +from ..database import db +from ..models import MetricsResponse, MetricsSummary, TimeSeriesPoint + +router = APIRouter(prefix="/api/metrics", tags=["metrics"]) + + +@router.get("", response_model=MetricsResponse) +async def get_metrics(): + """ + Rรฉcupรจre les mรฉtriques globales du dashboard + """ + try: + # Rรฉsumรฉ des mรฉtriques + summary_query = """ + SELECT + count() AS total_detections, + countIf(threat_level = 'CRITICAL') AS critical_count, + countIf(threat_level = 'HIGH') AS high_count, + countIf(threat_level = 'MEDIUM') AS medium_count, + countIf(threat_level = 'LOW') AS low_count, + countIf(bot_name != '') AS known_bots_count, + countIf(bot_name = '') AS anomalies_count, + uniq(src_ip) AS unique_ips + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 24 HOUR + """ + + summary_result = db.query(summary_query) + summary_row = summary_result.result_rows[0] if summary_result.result_rows else None + + if not summary_row: + raise HTTPException(status_code=404, detail="Aucune donnรฉe disponible") + + summary = MetricsSummary( + total_detections=summary_row[0], + critical_count=summary_row[1], + high_count=summary_row[2], + medium_count=summary_row[3], + low_count=summary_row[4], + known_bots_count=summary_row[5], + anomalies_count=summary_row[6], + unique_ips=summary_row[7] + ) + + # Sรฉrie temporelle (par heure) + timeseries_query = """ + SELECT + toStartOfHour(detected_at) AS hour, + count() AS total, + countIf(threat_level = 'CRITICAL') AS critical, + countIf(threat_level = 'HIGH') AS high, + countIf(threat_level = 'MEDIUM') AS medium, + countIf(threat_level = 'LOW') AS low + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 24 HOUR + GROUP BY hour + ORDER BY hour + """ + + timeseries_result = db.query(timeseries_query) + timeseries = [ + TimeSeriesPoint( + hour=row[0], + total=row[1], + critical=row[2], + high=row[3], + medium=row[4], + low=row[5] + ) + for row in timeseries_result.result_rows + ] + + # Distribution par menace + threat_distribution = { + "CRITICAL": summary.critical_count, + "HIGH": summary.high_count, + "MEDIUM": summary.medium_count, + "LOW": summary.low_count + } + + return MetricsResponse( + summary=summary, + timeseries=timeseries, + threat_distribution=threat_distribution + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur lors de la rรฉcupรฉration des mรฉtriques: {str(e)}") + + +@router.get("/threats") +async def get_threat_distribution(): + """ + Rรฉcupรจre la rรฉpartition par niveau de menace + """ + try: + query = """ + SELECT + threat_level, + count() AS count, + round(count() * 100.0 / sum(count()) OVER (), 2) AS percentage + FROM ml_detected_anomalies + WHERE detected_at >= now() - INTERVAL 24 HOUR + GROUP BY threat_level + ORDER BY count DESC + """ + + result = db.query(query) + + return { + "items": [ + {"threat_level": row[0], "count": row[1], "percentage": row[2]} + for row in result.result_rows + ] + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/backend/routes/variability.py b/backend/routes/variability.py new file mode 100644 index 0000000..3a26443 --- /dev/null +++ b/backend/routes/variability.py @@ -0,0 +1,629 @@ +""" +Endpoints pour la variabilitรฉ des attributs +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional +from ..database import db +from ..models import ( + VariabilityResponse, VariabilityAttributes, AttributeValue, Insight, + UserAgentsResponse, UserAgentValue +) + +router = APIRouter(prefix="/api/variability", tags=["variability"]) + + +# ============================================================================= +# ROUTES SPร‰CIFIQUES (doivent รชtre avant les routes gรฉnรฉriques) +# ============================================================================= + +@router.get("/{attr_type}/{value:path}/ips", response_model=dict) +async def get_associated_ips( + attr_type: str, + value: str, + limit: int = Query(100, ge=1, le=1000, description="Nombre maximum d'IPs") +): + """ + Rรฉcupรจre la liste des IPs associรฉes ร  un attribut + """ + try: + # Mapping des types vers les colonnes + type_column_map = { + "ip": "src_ip", + "ja4": "ja4", + "country": "country_code", + "asn": "asn_number", + "host": "host", + } + + if attr_type not in type_column_map: + raise HTTPException( + status_code=400, + detail=f"Type invalide. Types supportรฉs: {', '.join(type_column_map.keys())}" + ) + + column = type_column_map[attr_type] + + query = f""" + SELECT DISTINCT src_ip + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND detected_at >= now() - INTERVAL 24 HOUR + ORDER BY src_ip + LIMIT %(limit)s + """ + + result = db.query(query, {"value": value, "limit": limit}) + + ips = [str(row[0]) for row in result.result_rows] + + # Compter le total + count_query = f""" + SELECT uniq(src_ip) AS total + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND detected_at >= now() - INTERVAL 24 HOUR + """ + + count_result = db.query(count_query, {"value": value}) + total = count_result.result_rows[0][0] if count_result.result_rows else 0 + + return { + "type": attr_type, + "value": value, + "ips": ips, + "total": total, + "showing": len(ips) + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +@router.get("/{attr_type}/{value:path}/attributes", response_model=dict) +async def get_associated_attributes( + attr_type: str, + value: str, + target_attr: str = Query(..., description="Type d'attribut ร  rรฉcupรฉrer (user_agents, ja4, countries, asns, hosts)"), + limit: int = Query(50, ge=1, le=500, description="Nombre maximum de rรฉsultats") +): + """ + Rรฉcupรจre la liste des attributs associรฉs (ex: User-Agents pour un pays) + """ + try: + # Mapping des types vers les colonnes + type_column_map = { + "ip": "src_ip", + "ja4": "ja4", + "country": "country_code", + "asn": "asn_number", + "host": "host", + } + + # Mapping des attributs cibles + target_column_map = { + "user_agents": "''", # Pas de user_agent + "ja4": "ja4", + "countries": "country_code", + "asns": "asn_number", + "hosts": "host", + } + + if attr_type not in type_column_map: + raise HTTPException(status_code=400, detail=f"Type '{attr_type}' invalide") + + if target_attr not in target_column_map: + raise HTTPException( + status_code=400, + detail=f"Attribut cible invalide. Supportรฉs: {', '.join(target_column_map.keys())}" + ) + + column = type_column_map[attr_type] + target_column = target_column_map[target_attr] + + # Pour user_agent, retourne liste vide + if target_column == "''": + return {"type": attr_type, "value": value, "target": target_attr, "items": [], "total": 0} + + query = f""" + SELECT + {target_column} AS value, + count() AS count, + round(count() * 100.0 / sum(count()) OVER (), 2) AS percentage + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND {target_column} != '' AND {target_column} IS NOT NULL + AND detected_at >= now() - INTERVAL 24 HOUR + GROUP BY value + ORDER BY count DESC + LIMIT %(limit)s + """ + + result = db.query(query, {"value": value, "limit": limit}) + + items = [ + { + "value": str(row[0]), + "count": row[1], + "percentage": round(float(row[2]), 2) if row[2] else 0.0 + } + for row in result.result_rows + ] + + # Compter le total + count_query = f""" + SELECT uniq({target_column}) AS total + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND {target_column} != '' AND {target_column} IS NOT NULL + AND detected_at >= now() - INTERVAL 24 HOUR + """ + + count_result = db.query(count_query, {"value": value}) + total = count_result.result_rows[0][0] if count_result.result_rows else 0 + + return { + "type": attr_type, + "value": value, + "target": target_attr, + "items": items, + "total": total, + "showing": len(items) + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +@router.get("/{attr_type}/{value:path}/user_agents", response_model=UserAgentsResponse) +async def get_user_agents( + attr_type: str, + value: str, + limit: int = Query(100, ge=1, le=500, description="Nombre maximum de user-agents") +): + """ + Rรฉcupรจre la liste des User-Agents associรฉs ร  un attribut (IP, JA4, pays, etc.) + Les donnรฉes sont rรฉcupรฉrรฉes depuis la vue materialisรฉe view_dashboard_user_agents + """ + try: + # Mapping des types vers les colonnes + type_column_map = { + "ip": "src_ip", + "ja4": "ja4", + "country": "src_country_code", + "asn": "src_asn", + "host": "host", + } + + if attr_type not in type_column_map: + raise HTTPException( + status_code=400, + detail=f"Type invalide. Types supportรฉs: {', '.join(type_column_map.keys())}" + ) + + column = type_column_map[attr_type] + + # Requรชte sur la vue materialisรฉe + # user_agents est un Array, on utilise arrayJoin pour l'aplatir + query = f""" + SELECT + ua AS user_agent, + sum(requests) AS count, + round(count * 100.0 / sum(count) OVER (), 2) AS percentage, + min(hour) AS first_seen, + max(hour) AS last_seen + FROM mabase_prod.view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE {column} = %(value)s + AND hour >= now() - INTERVAL 24 HOUR + GROUP BY user_agent + ORDER BY count DESC + LIMIT %(limit)s + """ + + result = db.query(query, {"value": value, "limit": limit}) + + user_agents = [ + UserAgentValue( + value=str(row[0]), + count=row[1] or 0, + percentage=round(float(row[2]), 2) if row[2] else 0.0, + first_seen=row[3] if len(row) > 3 and row[3] else None, + last_seen=row[4] if len(row) > 4 and row[4] else None, + ) + for row in result.result_rows + ] + + # Compter le total + count_query = f""" + SELECT uniq(ua) AS total + FROM mabase_prod.view_dashboard_user_agents + ARRAY JOIN user_agents AS ua + WHERE {column} = %(value)s + AND hour >= now() - INTERVAL 24 HOUR + """ + + count_result = db.query(count_query, {"value": value}) + total = count_result.result_rows[0][0] if count_result.result_rows else 0 + + return { + "type": attr_type, + "value": value, + "user_agents": user_agents, + "total": total, + "showing": len(user_agents) + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + +# ============================================================================= +# ROUTE Gร‰Nร‰RIQUE (doit รชtre en dernier) +# ============================================================================= + + +def get_attribute_value(row, count_idx: int, percentage_idx: int, + first_seen_idx: Optional[int] = None, + last_seen_idx: Optional[int] = None, + threat_idx: Optional[int] = None, + unique_ips_idx: Optional[int] = None) -> AttributeValue: + """Helper pour crรฉer un AttributeValue depuis une ligne ClickHouse""" + return AttributeValue( + value=str(row[0]), + count=row[count_idx] or 0, + percentage=round(float(row[percentage_idx]), 2) if row[percentage_idx] else 0.0, + first_seen=row[first_seen_idx] if first_seen_idx is not None and len(row) > first_seen_idx else None, + last_seen=row[last_seen_idx] if last_seen_idx is not None and len(row) > last_seen_idx else None, + threat_levels=_parse_threat_levels(row[threat_idx]) if threat_idx is not None and len(row) > threat_idx and row[threat_idx] else None, + unique_ips=row[unique_ips_idx] if unique_ips_idx is not None and len(row) > unique_ips_idx else None, + primary_threat=_get_primary_threat(row[threat_idx]) if threat_idx is not None and len(row) > threat_idx and row[threat_idx] else None + ) + + +def _parse_threat_levels(threat_str: str) -> dict: + """Parse une chaรฎne de type 'CRITICAL:5,HIGH:10' en dict""" + if not threat_str: + return {} + result = {} + for part in str(threat_str).split(','): + if ':' in part: + level, count = part.strip().split(':') + result[level.strip()] = int(count.strip()) + return result + + +def _get_primary_threat(threat_str: str) -> str: + """Retourne le niveau de menace principal""" + if not threat_str: + return "" + levels_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"] + for level in levels_order: + if level in str(threat_str): + return level + return "" + + +def _generate_insights(attr_type: str, value: str, attributes: VariabilityAttributes, + total_detections: int, unique_ips: int) -> list: + """Gรฉnรจre des insights basรฉs sur les donnรฉes de variabilitรฉ""" + insights = [] + + # User-Agent insights + if len(attributes.user_agents) > 1: + insights.append(Insight( + type="warning", + message=f"{len(attributes.user_agents)} User-Agents diffรฉrents โ†’ Possible rotation/obfuscation" + )) + + # JA4 insights + if len(attributes.ja4) > 1: + insights.append(Insight( + type="warning", + message=f"{len(attributes.ja4)} JA4 fingerprints diffรฉrents โ†’ Possible rotation de fingerprint" + )) + + # IP insights (pour les sรฉlections non-IP) + if attr_type != "ip" and unique_ips > 10: + insights.append(Insight( + type="info", + message=f"{unique_ips} IPs diffรฉrentes associรฉes โ†’ Possible infrastructure distribuรฉe" + )) + + # ASN insights + if len(attributes.asns) == 1 and attributes.asns[0].value: + asn_label_lower = "" + if attr_type == "asn": + asn_label_lower = value.lower() + # Vรฉrifier si c'est un ASN de hosting/cloud + hosting_keywords = ["ovh", "amazon", "aws", "google", "azure", "digitalocean", "linode", "vultr"] + if any(kw in (attributes.asns[0].value or "").lower() for kw in hosting_keywords): + insights.append(Insight( + type="warning", + message="ASN de type hosting/cloud โ†’ Souvent utilisรฉ pour des bots" + )) + + # Country insights + if len(attributes.countries) > 5: + insights.append(Insight( + type="info", + message=f"Prรฉsent dans {len(attributes.countries)} pays โ†’ Distribution gรฉographique large" + )) + + # Threat level insights + critical_count = 0 + high_count = 0 + for tl in attributes.threat_levels: + if tl.value == "CRITICAL": + critical_count = tl.count + elif tl.value == "HIGH": + high_count = tl.count + + if critical_count > total_detections * 0.3: + insights.append(Insight( + type="warning", + message=f"{round(critical_count * 100 / total_detections)}% de dรฉtections CRITICAL โ†’ Menace sรฉvรจre" + )) + elif high_count > total_detections * 0.5: + insights.append(Insight( + type="info", + message=f"{round(high_count * 100 / total_detections)}% de dรฉtections HIGH โ†’ Activitรฉ suspecte" + )) + + return insights + + +@router.get("/{attr_type}/{value:path}", response_model=VariabilityResponse) +async def get_variability(attr_type: str, value: str): + """ + Rรฉcupรจre la variabilitรฉ des attributs associรฉs ร  une valeur + + attr_type: ip, ja4, country, asn, host, user_agent + value: la valeur ร  investiguer + """ + try: + # Mapping des types vers les colonnes ClickHouse + type_column_map = { + "ip": "src_ip", + "ja4": "ja4", + "country": "country_code", + "asn": "asn_number", + "host": "host", + "user_agent": "header_user_agent" + } + + if attr_type not in type_column_map: + raise HTTPException( + status_code=400, + detail=f"Type invalide. Types supportรฉs: {', '.join(type_column_map.keys())}" + ) + + column = type_column_map[attr_type] + + # Requรชte principale - Rรฉcupรจre toutes les dรฉtections pour cette valeur + # On utilise toStartOfHour pour le timeseries et on รฉvite header_user_agent si inexistant + base_query = f""" + SELECT * + FROM ( + SELECT + detected_at, + src_ip, + ja4, + host, + '' AS user_agent, + country_code, + asn_number, + asn_org, + threat_level, + model_name, + anomaly_score + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND detected_at >= now() - INTERVAL 24 HOUR + ) + """ + + # Stats globales + stats_query = f""" + SELECT + count() AS total_detections, + uniq(src_ip) AS unique_ips, + min(detected_at) AS first_seen, + max(detected_at) AS last_seen + FROM ml_detected_anomalies + WHERE {column} = %(value)s + AND detected_at >= now() - INTERVAL 24 HOUR + """ + + stats_result = db.query(stats_query, {"value": value}) + + if not stats_result.result_rows or stats_result.result_rows[0][0] == 0: + raise HTTPException(status_code=404, detail="Aucune donnรฉe trouvรฉe") + + stats_row = stats_result.result_rows[0] + total_detections = stats_row[0] + unique_ips = stats_row[1] + first_seen = stats_row[2] + last_seen = stats_row[3] + + # User-Agents + ua_query = f""" + SELECT + user_agent, + count() AS count, + round(count() * 100.0 / sum(count()) OVER (), 2) AS percentage, + min(detected_at) AS first_seen, + max(detected_at) AS last_seen, + groupArray((threat_level, 1)) AS threats + FROM ({base_query}) + WHERE user_agent != '' AND user_agent IS NOT NULL + GROUP BY user_agent + ORDER BY count DESC + LIMIT 10 + """ + + # Simplified query without complex threat parsing + ua_query_simple = f""" + SELECT + user_agent, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query}) WHERE user_agent != '' AND user_agent IS NOT NULL), 2) AS percentage, + min(detected_at) AS first_seen, + max(detected_at) AS last_seen + FROM ({base_query}) + WHERE user_agent != '' AND user_agent IS NOT NULL + GROUP BY user_agent + ORDER BY count DESC + LIMIT 10 + """ + + ua_result = db.query(ua_query_simple, {"value": value}) + user_agents = [get_attribute_value(row, 1, 2, 3, 4) for row in ua_result.result_rows] + + # JA4 fingerprints + ja4_query = f""" + SELECT + ja4, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage, + min(detected_at) AS first_seen, + max(detected_at) AS last_seen + FROM ({base_query}) + WHERE ja4 != '' AND ja4 IS NOT NULL + GROUP BY ja4 + ORDER BY count DESC + LIMIT 10 + """ + + ja4_result = db.query(ja4_query, {"value": value}) + ja4s = [get_attribute_value(row, 1, 2, 3, 4) for row in ja4_result.result_rows] + + # Pays + country_query = f""" + SELECT + country_code, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage + FROM ({base_query}) + WHERE country_code != '' AND country_code IS NOT NULL + GROUP BY country_code + ORDER BY count DESC + LIMIT 10 + """ + + country_result = db.query(country_query, {"value": value}) + countries = [get_attribute_value(row, 1, 2) for row in country_result.result_rows] + + # ASN + asn_query = f""" + SELECT + concat('AS', toString(asn_number), ' - ', asn_org) AS asn_display, + asn_number, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage + FROM ({base_query}) + WHERE asn_number != '' AND asn_number IS NOT NULL AND asn_number != '0' + GROUP BY asn_display, asn_number + ORDER BY count DESC + LIMIT 10 + """ + + asn_result = db.query(asn_query, {"value": value}) + asns = [ + AttributeValue( + value=str(row[0]), + count=row[2] or 0, + percentage=round(float(row[3]), 2) if row[3] else 0.0 + ) + for row in asn_result.result_rows + ] + + # Hosts + host_query = f""" + SELECT + host, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage + FROM ({base_query}) + WHERE host != '' AND host IS NOT NULL + GROUP BY host + ORDER BY count DESC + LIMIT 10 + """ + + host_result = db.query(host_query, {"value": value}) + hosts = [get_attribute_value(row, 1, 2) for row in host_result.result_rows] + + # Threat levels + threat_query = f""" + SELECT + threat_level, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage + FROM ({base_query}) + WHERE threat_level != '' AND threat_level IS NOT NULL + GROUP BY threat_level + ORDER BY + CASE threat_level + WHEN 'CRITICAL' THEN 1 + WHEN 'HIGH' THEN 2 + WHEN 'MEDIUM' THEN 3 + WHEN 'LOW' THEN 4 + ELSE 5 + END + """ + + threat_result = db.query(threat_query, {"value": value}) + threat_levels = [get_attribute_value(row, 1, 2) for row in threat_result.result_rows] + + # Model names + model_query = f""" + SELECT + model_name, + count() AS count, + round(count() * 100.0 / (SELECT count() FROM ({base_query})), 2) AS percentage + FROM ({base_query}) + WHERE model_name != '' AND model_name IS NOT NULL + GROUP BY model_name + ORDER BY count DESC + """ + + model_result = db.query(model_query, {"value": value}) + model_names = [get_attribute_value(row, 1, 2) for row in model_result.result_rows] + + # Construire la rรฉponse + attributes = VariabilityAttributes( + user_agents=user_agents, + ja4=ja4s, + countries=countries, + asns=asns, + hosts=hosts, + threat_levels=threat_levels, + model_names=model_names + ) + + # Gรฉnรฉrer les insights + insights = _generate_insights(attr_type, value, attributes, total_detections, unique_ips) + + return VariabilityResponse( + type=attr_type, + value=value, + total_detections=total_detections, + unique_ips=unique_ips, + date_range={ + "first_seen": first_seen, + "last_seen": last_seen + }, + attributes=attributes, + insights=insights + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/create_classifications_table.sql b/create_classifications_table.sql new file mode 100644 index 0000000..6f2fc5c --- /dev/null +++ b/create_classifications_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS mabase_prod.classifications +( + ip String, + ja4 String, + label LowCardinality(String), + tags Array(String), + comment String, + confidence Float32, + features String, + analyst String, + created_at DateTime DEFAULT now() +) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(created_at) +ORDER BY (created_at, ip, ja4) +SETTINGS index_granularity = 8192; diff --git a/deploy_classifications_table.sql b/deploy_classifications_table.sql new file mode 100644 index 0000000..b2285dc --- /dev/null +++ b/deploy_classifications_table.sql @@ -0,0 +1,73 @@ +-- ============================================================================= +-- Table classifications - Dashboard Bot Detector +-- ============================================================================= +-- Stocke les classifications des IPs pour l'apprentissage supervisรฉ +-- +-- Usage: +-- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ +-- --user default --password < deploy_classifications_table.sql +-- +-- ============================================================================= + +USE mabase_prod; + +-- ============================================================================= +-- Table pour stocker les classifications des IPs +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS mabase_prod.classifications +( + -- Identification + ip String, + + -- Classification + label LowCardinality(String), -- "legitimate", "suspicious", "malicious" + tags Array(String), -- Tags associรฉs + comment String, -- Commentaire de l'analyste + + -- Mรฉtriques pour ML + confidence Float32, -- Confiance de la classification (0-1) + features String, -- JSON avec toutes les features + + -- Mรฉtadonnรฉes + analyst String, -- Nom de l'analyste + created_at DateTime DEFAULT now() -- Date de classification +) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(created_at) +ORDER BY (created_at, ip) +SETTINGS index_granularity = 8192; + +-- ============================================================================= +-- Index pour accรฉlรฉrer les recherches par IP +-- ============================================================================= + +CREATE INDEX IF NOT EXISTS idx_classifications_ip +ON TABLE mabase_prod.classifications (ip) TYPE minmax GRANULARITY 1; + +-- ============================================================================= +-- Vue pour les statistiques de classification +-- ============================================================================= + +CREATE VIEW IF NOT EXISTS mabase_prod.view_classifications_stats AS +SELECT + label, + count() AS total, + uniq(ip) AS unique_ips, + avg(confidence) AS avg_confidence, + min(created_at) AS first_classification, + max(created_at) AS last_classification +FROM mabase_prod.classifications +GROUP BY label; + +-- ============================================================================= +-- FIN +-- ============================================================================= +-- +-- Vรฉrifier que la table est crรฉรฉe : +-- SELECT count() FROM mabase_prod.classifications; +-- +-- Voir les statistiques : +-- SELECT * FROM mabase_prod.view_classifications_stats; +-- +-- ============================================================================= diff --git a/deploy_dashboard_entities_view.sql b/deploy_dashboard_entities_view.sql new file mode 100644 index 0000000..61b9667 --- /dev/null +++ b/deploy_dashboard_entities_view.sql @@ -0,0 +1,377 @@ +-- ============================================================================= +-- 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 30 DAY +SETTINGS index_granularity = 8192; + +-- ============================================================================= +-- BLOC 2/3 : Crรฉer la vue materialisรฉe +-- ============================================================================= + +CREATE MATERIALIZED VIEW IF NOT EXISTS mabase_prod.view_dashboard_entities_mv +TO mabase_prod.view_dashboard_entities +AS +-- 1. Entitรฉ : IP +SELECT + 'ip' AS entity_type, + toString(src_ip) AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM mabase_prod.http_logs +WHERE src_ip IS NOT NULL +GROUP BY src_ip, ja4, host, log_date + +UNION ALL + +-- 2. Entitรฉ : JA4 +SELECT + 'ja4' AS entity_type, + ja4 AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM mabase_prod.http_logs +WHERE ja4 != '' AND ja4 IS NOT NULL +GROUP BY src_ip, ja4, host, log_date + +UNION ALL + +-- 3. Entitรฉ : User-Agent +SELECT + 'user_agent' AS entity_type, + ua AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(ua) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM +( + SELECT + src_ip, + ja4, + host, + time, + src_asn, + src_country_code, + header_user_agent AS ua, + client_headers, + path, + query + FROM mabase_prod.http_logs +) +WHERE ua != '' AND ua IS NOT NULL +GROUP BY src_ip, ja4, host, log_date, ua + +UNION ALL + +-- 4. Entitรฉ : Client Header +SELECT + 'client_header' AS entity_type, + ch AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(ch) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM +( + SELECT + src_ip, + ja4, + host, + time, + src_asn, + src_country_code, + header_user_agent, + client_headers AS ch, + path, + query + FROM mabase_prod.http_logs +) +WHERE ch != '' AND ch IS NOT NULL +GROUP BY src_ip, ja4, host, log_date, ch + +UNION ALL + +-- 5. Entitรฉ : Host +SELECT + 'host' AS entity_type, + host AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM mabase_prod.http_logs +WHERE host != '' AND host IS NOT NULL +GROUP BY src_ip, ja4, host, log_date + +UNION ALL + +-- 6. Entitรฉ : Path +SELECT + 'path' AS entity_type, + p AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(p) AS paths, + groupArrayDistinct( + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) + ) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM +( + SELECT + src_ip, + ja4, + host, + time, + src_asn, + src_country_code, + header_user_agent, + client_headers, + path AS p, + query + FROM mabase_prod.http_logs +) +WHERE p != '' AND p IS NOT NULL +GROUP BY src_ip, ja4, host, log_date, p + +UNION ALL + +-- 7. Entitรฉ : Query Param (noms concatรฉnรฉs) +SELECT + 'query_param' AS entity_type, + query_params_string AS entity_value, + src_ip, + ja4, + host, + toDate(time) AS log_date, + count() AS requests, + uniq(src_ip) AS unique_ips, + groupArrayDistinct(header_user_agent) AS user_agents, + groupArrayDistinct(client_headers) AS client_headers, + groupArrayDistinct(path) AS paths, + groupArrayDistinct(query_params_string) AS query_params, + groupArrayDistinct(toString(src_asn)) AS asns, + groupArrayDistinct(src_country_code) AS countries +FROM ( + SELECT + src_ip, ja4, host, time, src_asn, src_country_code, + header_user_agent, client_headers, path, + arrayStringConcat( + arrayMap( + x -> splitByChar('=', x)[1], + splitByChar('&', replaceOne(query, '?', '')) + ), + ',' + ) AS query_params_string + FROM mabase_prod.http_logs + WHERE query != '' AND query IS NOT NULL +) +WHERE query_params_string != '' +GROUP BY src_ip, ja4, host, log_date, query_params_string; + +-- ============================================================================= +-- BLOC 3/3 : Crรฉer les index (optionnel - amรฉliore les performances) +-- ============================================================================= + +ALTER TABLE mabase_prod.view_dashboard_entities +ADD INDEX IF NOT EXISTS idx_entities_type (entity_type) TYPE minmax GRANULARITY 1; + +ALTER TABLE mabase_prod.view_dashboard_entities +ADD INDEX IF NOT EXISTS idx_entities_value (entity_value) TYPE minmax GRANULARITY 1; + +ALTER TABLE mabase_prod.view_dashboard_entities +ADD INDEX IF NOT EXISTS idx_entities_ip (src_ip) TYPE minmax GRANULARITY 1; + +-- ============================================================================= +-- FIN +-- ============================================================================= +-- +-- Pour vรฉrifier que la vue fonctionne : +-- ------------------------------------- +-- SELECT entity_type, count() FROM mabase_prod.view_dashboard_entities GROUP BY entity_type; +-- +-- Pour rafraรฎchir manuellement (si nรฉcessaire) : +-- ---------------------------------------------- +-- OPTIMIZE TABLE mabase_prod.view_dashboard_entities FINAL; +-- +-- Exemples de requรชtes : +-- ---------------------- +-- -- Stats pour une IP +-- SELECT * FROM mabase_prod.view_dashboard_entities +-- WHERE entity_type = 'ip' AND entity_value = '116.179.33.143'; +-- +-- -- Stats pour un JA4 +-- SELECT * FROM mabase_prod.view_dashboard_entities +-- WHERE entity_type = 'ja4' AND entity_value = 't13d190900_9dc949149365_97f8aa674fd9'; +-- +-- -- Top 10 des user-agents +-- SELECT entity_value, sum(requests) as total +-- FROM mabase_prod.view_dashboard_entities +-- WHERE entity_type = 'user_agent' +-- GROUP BY entity_value +-- ORDER BY total DESC +-- LIMIT 10; +-- +-- ============================================================================= diff --git a/deploy_user_agents_view.sql b/deploy_user_agents_view.sql new file mode 100644 index 0000000..01fdfc9 --- /dev/null +++ b/deploy_user_agents_view.sql @@ -0,0 +1,79 @@ +-- ============================================================================= +-- Vue materialisรฉe pour les User-Agents - Dashboard Bot Detector +-- ============================================================================= +-- +-- Instructions d'installation : +-- ----------------------------- +-- 1. Se connecter ร  ClickHouse en CLI : +-- clickhouse-client --host test-sdv-anubis.sdv.fr --port 8123 \ +-- --user default --password +-- +-- 2. Copier-coller CHAQUE BLOC sรฉparรฉment (un par un) +-- +-- 3. Vรฉrifier que la vue est crรฉรฉe : +-- SELECT count() FROM mabase_prod.view_dashboard_user_agents; +-- +-- ============================================================================= + +USE mabase_prod; + +-- ============================================================================= +-- BLOC 1/3 : Crรฉer la table +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS mabase_prod.view_dashboard_user_agents +( + src_ip IPv4, + ja4 String, + hour DateTime, + log_date Date, + user_agents Array(String), + requests UInt64 +) +ENGINE = AggregatingMergeTree() +PARTITION BY log_date +ORDER BY (src_ip, ja4, hour) +TTL log_date + INTERVAL 7 DAY +SETTINGS index_granularity = 8192; + +-- ============================================================================= +-- BLOC 2/3 : Crรฉer la vue materialisรฉe +-- ============================================================================= + +CREATE MATERIALIZED VIEW IF NOT EXISTS mabase_prod.view_dashboard_user_agents_mv +TO mabase_prod.view_dashboard_user_agents +AS SELECT + src_ip, + ja4, + toStartOfHour(time) AS hour, + toDate(time) AS log_date, + groupArrayDistinct(header_user_agent) AS user_agents, + count() AS requests +FROM mabase_prod.http_logs +WHERE header_user_agent != '' AND header_user_agent IS NOT NULL + AND time >= now() - INTERVAL 7 DAY +GROUP BY src_ip, ja4, hour, log_date; + +-- ============================================================================= +-- BLOC 3/3 : Crรฉer les index (optionnel - amรฉliore les performances) +-- ============================================================================= + +ALTER TABLE mabase_prod.view_dashboard_user_agents +ADD INDEX IF NOT EXISTS idx_user_agents_ip (src_ip) TYPE minmax GRANULARITY 1; + +ALTER TABLE mabase_prod.view_dashboard_user_agents +ADD INDEX IF NOT EXISTS idx_user_agents_ja4 (ja4) TYPE minmax GRANULARITY 1; + +-- ============================================================================= +-- FIN +-- ============================================================================= +-- +-- Pour vรฉrifier que la vue fonctionne : +-- ------------------------------------- +-- SELECT * FROM mabase_prod.view_dashboard_user_agents LIMIT 10; +-- +-- Pour rafraรฎchir manuellement (si nรฉcessaire) : +-- ---------------------------------------------- +-- OPTIMIZE TABLE mabase_prod.view_dashboard_user_agents FINAL; +-- +-- ============================================================================= diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5b3fd4b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # DASHBOARD WEB + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + dashboard_web: + build: . + container_name: dashboard_web + restart: unless-stopped + ports: + - "3000:8000" # Dashboard web โ†’ http://localhost:3000 + + env_file: + - .env + + environment: + # ClickHouse + CLICKHOUSE_HOST: ${CLICKHOUSE_HOST:-clickhouse} + CLICKHOUSE_DB: ${CLICKHOUSE_DB:-mabase_prod} + CLICKHOUSE_USER: ${CLICKHOUSE_USER:-admin} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-} + + # API + API_PORT: 8000 + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..80a5706 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Bot Detector Dashboard + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..84f3269 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "bot-detector-dashboard", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "axios": "^1.6.0", + "recharts": "^2.10.0", + "@tanstack/react-table": "^8.11.0", + "date-fns": "^3.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..a046c1f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,262 @@ +import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom'; +import { useMetrics } from './hooks/useMetrics'; +import { DetectionsList } from './components/DetectionsList'; +import { DetailsView } from './components/DetailsView'; +import { InvestigationView } from './components/InvestigationView'; +import { JA4InvestigationView } from './components/JA4InvestigationView'; +import { EntityInvestigationView } from './components/EntityInvestigationView'; + +// Composant Dashboard +function Dashboard() { + const { data, loading, error } = useMetrics(); + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (error) { + return ( +
+

Erreur: {error.message}

+
+ ); + } + + if (!data) return null; + + const { summary } = data; + + return ( +
+ {/* Mรฉtriques */} +
+ + + + +
+ + {/* Rรฉpartition par menace */} +
+

Rรฉpartition par Menace

+
+ + + + +
+
+ + {/* Sรฉrie temporelle */} +
+

ร‰volution (24h)

+ +
+ + {/* Accรจs rapide */} +
+

Accรจs Rapide

+
+ +

Voir les dรฉtections

+

Explorer toutes les dรฉtections

+ + +

Menaces Critiques

+

{summary.critical_count} dรฉtections

+ + +

Modรจle Complet

+

Avec donnรฉes TCP/TLS

+ +
+
+
+ ); +} + +// Composant MetricCard +function MetricCard({ title, value, subtitle, color }: { + title: string; + value: string | number; + subtitle: string; + color: string; +}) { + return ( +
+

{title}

+

{value}

+

{subtitle}

+
+ ); +} + +// Composant ThreatBar +function ThreatBar({ level, count, total, color }: { + level: string; + count: number; + total: number; + color: string; +}) { + const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : '0'; + + const colors: Record = { + CRITICAL: 'bg-threat-critical', + HIGH: 'bg-threat-high', + MEDIUM: 'bg-threat-medium', + LOW: 'bg-threat-low', + }; + + return ( +
+
+ {level} + {count} ({percentage}%) +
+
+
+
+
+ ); +} + +// Composant TimeSeriesChart (simplifiรฉ) +function TimeSeriesChart({ data }: { data: { hour: string; total: number }[] }) { + if (!data || data.length === 0) return null; + + const maxVal = Math.max(...data.map(d => d.total), 1); + + return ( +
+ {data.map((point, i) => { + const height = (point.total / maxVal) * 100; + const hour = new Date(point.hour).getHours(); + + return ( +
+
+ {i % 4 === 0 && ( + {hour}h + )} +
+ ); + })} +
+ ); +} + +// Navigation +function Navigation() { + const location = useLocation(); + + const links = [ + { path: '/', label: 'Dashboard' }, + { path: '/detections', label: 'Dรฉtections' }, + ]; + + return ( + + ); +} + +// App principale +export default function App() { + return ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+ ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..19b0499 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,151 @@ +import axios from 'axios'; + +const API_BASE_URL = '/api'; + +export const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Types +export interface 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; +} + +export interface TimeSeriesPoint { + hour: string; + total: number; + critical: number; + high: number; + medium: number; + low: number; +} + +export interface MetricsResponse { + summary: MetricsSummary; + timeseries: TimeSeriesPoint[]; + threat_distribution: Record; +} + +export interface Detection { + detected_at: string; + src_ip: string; + ja4: string; + host: string; + bot_name: string; + anomaly_score: number; + threat_level: string; + model_name: string; + recurrence: number; + asn_number: string; + asn_org: string; + asn_detail: string; + asn_domain: string; + country_code: string; + asn_label: string; + hits: number; + hit_velocity: number; + fuzzing_index: number; + post_ratio: number; + reason: string; + client_headers: string; +} + +export interface DetectionsListResponse { + items: Detection[]; + total: number; + page: number; + page_size: number; + total_pages: number; +} + +export interface AttributeValue { + value: string; + count: number; + percentage: number; + first_seen?: string; + last_seen?: string; + threat_levels?: Record; + unique_ips?: number; + primary_threat?: string; +} + +export interface VariabilityAttributes { + user_agents: AttributeValue[]; + ja4: AttributeValue[]; + countries: AttributeValue[]; + asns: AttributeValue[]; + hosts: AttributeValue[]; + threat_levels: AttributeValue[]; + model_names: AttributeValue[]; +} + +export interface Insight { + type: 'warning' | 'info' | 'success'; + message: string; +} + +export interface VariabilityResponse { + type: string; + value: string; + total_detections: number; + unique_ips: number; + date_range: { + first_seen: string; + last_seen: string; + }; + attributes: VariabilityAttributes; + insights: Insight[]; +} + +export interface AttributeListItem { + value: string; + count: number; +} + +export interface AttributeListResponse { + type: string; + items: AttributeListItem[]; + total: number; +} + +// API Functions +export const metricsApi = { + getMetrics: () => api.get('/metrics'), + getThreatDistribution: () => api.get('/metrics/threats'), +}; + +export const detectionsApi = { + getDetections: (params?: { + page?: number; + page_size?: number; + threat_level?: string; + model_name?: string; + country_code?: string; + asn_number?: string; + search?: string; + sort_by?: string; + sort_order?: string; + }) => api.get('/detections', { params }), + + getDetails: (id: string) => api.get(`/detections/${encodeURIComponent(id)}`), +}; + +export const variabilityApi = { + getVariability: (type: string, value: string) => + api.get(`/variability/${type}/${encodeURIComponent(value)}`), +}; + +export const attributesApi = { + getAttributes: (type: string, limit?: number) => + api.get(`/attributes/${type}`, { params: { limit } }), +}; diff --git a/frontend/src/components/DetailsView.tsx b/frontend/src/components/DetailsView.tsx new file mode 100644 index 0000000..a0de8cb --- /dev/null +++ b/frontend/src/components/DetailsView.tsx @@ -0,0 +1,169 @@ +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useVariability } from '../hooks/useVariability'; +import { VariabilityPanel } from './VariabilityPanel'; + +export function DetailsView() { + const { type, value } = useParams<{ type: string; value: string }>(); + const navigate = useNavigate(); + + const { data, loading, error } = useVariability(type || '', value || ''); + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (error) { + return ( +
+

Erreur: {error.message}

+ +
+ ); + } + + if (!data) return null; + + const typeLabels: Record = { + ip: { label: 'IP' }, + ja4: { label: 'JA4' }, + country: { label: 'Pays' }, + asn: { label: 'ASN' }, + host: { label: 'Host' }, + user_agent: { label: 'User-Agent' }, + }; + + const typeInfo = typeLabels[type || ''] || { label: type }; + + return ( +
+ {/* Breadcrumb */} + + + {/* En-tรชte */} +
+
+
+

+ {typeInfo.label} +

+

{value}

+
+
+
{data.total_detections}
+
dรฉtections (24h)
+ {type === 'ip' && value && ( + + )} + {type === 'ja4' && value && ( + + )} +
+
+ + {/* Stats rapides */} +
+ + + + +
+
+ + {/* Insights */} + {data.insights.length > 0 && ( +
+

Insights

+ {data.insights.map((insight, i) => ( + + ))} +
+ )} + + {/* Variabilitรฉ */} + + + {/* Bouton retour */} +
+ +
+
+ ); +} + +// Composant StatBox +function StatBox({ label, value }: { label: string; value: string }) { + return ( +
+
{value}
+
{label}
+
+ ); +} + +// Composant InsightCard +function InsightCard({ insight }: { insight: { type: string; message: string } }) { + const styles: Record = { + warning: 'bg-yellow-500/10 border-yellow-500/50 text-yellow-500', + info: 'bg-blue-500/10 border-blue-500/50 text-blue-400', + success: 'bg-green-500/10 border-green-500/50 text-green-400', + }; + + return ( +
+ {insight.message} +
+ ); +} + +// Helper pour formater la date +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +} diff --git a/frontend/src/components/DetectionsList.tsx b/frontend/src/components/DetectionsList.tsx new file mode 100644 index 0000000..95bacec --- /dev/null +++ b/frontend/src/components/DetectionsList.tsx @@ -0,0 +1,571 @@ +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useDetections } from '../hooks/useDetections'; + +type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity'; +type SortOrder = 'asc' | 'desc'; + +interface ColumnConfig { + key: string; + label: string; + visible: boolean; + sortable: boolean; +} + +export function DetectionsList() { + const [searchParams, setSearchParams] = useSearchParams(); + + const page = parseInt(searchParams.get('page') || '1'); + const modelName = searchParams.get('model_name') || undefined; + const search = searchParams.get('search') || undefined; + const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'anomaly_score') as SortField; + const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'asc') as SortOrder; + + const { data, loading, error } = useDetections({ + page, + page_size: 25, + model_name: modelName, + search, + sort_by: sortField, + sort_order: sortOrder, + }); + + const [searchInput, setSearchInput] = useState(search || ''); + const [showColumnSelector, setShowColumnSelector] = useState(false); + const [groupByIP, setGroupByIP] = useState(true); // Grouper par IP par dรฉfaut + + // Configuration des colonnes + const [columns, setColumns] = useState([ + { key: 'ip_ja4', label: 'IP / JA4', visible: true, sortable: true }, + { key: 'host', label: 'Host', visible: true, sortable: true }, + { key: 'client_headers', label: 'Client Headers', visible: false, sortable: false }, + { key: 'model_name', label: 'Modรจle', visible: true, sortable: true }, + { key: 'anomaly_score', label: 'Score', visible: true, sortable: true }, + { key: 'hits', label: 'Hits', visible: true, sortable: true }, + { key: 'hit_velocity', label: 'Velocity', visible: true, sortable: true }, + { key: 'asn', label: 'ASN', visible: true, sortable: true }, + { key: 'country', label: 'Pays', visible: true, sortable: true }, + { key: 'detected_at', label: 'Date', visible: true, sortable: true }, + ]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + const newParams = new URLSearchParams(searchParams); + if (searchInput.trim()) { + newParams.set('search', searchInput.trim()); + } else { + newParams.delete('search'); + } + newParams.set('page', '1'); + setSearchParams(newParams); + }; + + const handleFilterChange = (key: string, value: string) => { + const newParams = new URLSearchParams(searchParams); + if (value) { + newParams.set(key, value); + } else { + newParams.delete(key); + } + newParams.set('page', '1'); + setSearchParams(newParams); + }; + + const handleSort = (field: SortField) => { + const newParams = new URLSearchParams(searchParams); + const currentSortField = newParams.get('sort_by') || 'detected_at'; + const currentOrder = newParams.get('sort_order') || 'desc'; + + if (currentSortField === field) { + // Inverser l'ordre ou supprimer le tri + if (currentOrder === 'desc') { + newParams.set('sort_order', 'asc'); + } else { + newParams.delete('sort_by'); + newParams.delete('sort_order'); + } + } else { + newParams.set('sort_by', field); + newParams.set('sort_order', 'desc'); + } + setSearchParams(newParams); + }; + + const toggleColumn = (key: string) => { + setColumns(cols => cols.map(col => + col.key === key ? { ...col, visible: !col.visible } : col + )); + }; + + const handlePageChange = (newPage: number) => { + const newParams = new URLSearchParams(searchParams); + newParams.set('page', newPage.toString()); + setSearchParams(newParams); + }; + + const getSortIcon = (field: SortField) => { + if (sortField !== field) return 'โ‡…'; + return sortOrder === 'asc' ? 'โ†‘' : 'โ†“'; + }; + + // Par dรฉfaut, trier par score croissant (scores nรฉgatifs en premier) + const getDefaultSortIcon = (field: SortField) => { + if (!searchParams.has('sort_by') && !searchParams.has('sort')) { + if (field === 'anomaly_score') return 'โ†‘'; + return 'โ‡…'; + } + return getSortIcon(field); + }; + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (error) { + return ( +
+

Erreur: {error.message}

+
+ ); + } + + if (!data) return null; + + // Traiter les donnรฉes pour le regroupement par IP + const processedData = (() => { + if (!groupByIP) { + return data; + } + + // Grouper par IP + const ipGroups = new Map(); + const ipStats = new Map; + hosts: Set; + clientHeaders: Set; + }>(); + + data.items.forEach(item => { + if (!ipGroups.has(item.src_ip)) { + ipGroups.set(item.src_ip, item); + ipStats.set(item.src_ip, { + first: new Date(item.detected_at), + last: new Date(item.detected_at), + count: 1, + ja4s: new Set([item.ja4 || '']), + hosts: new Set([item.host || '']), + clientHeaders: new Set([item.client_headers || '']) + }); + } else { + const stats = ipStats.get(item.src_ip)!; + const itemDate = new Date(item.detected_at); + if (itemDate < stats.first) stats.first = itemDate; + if (itemDate > stats.last) stats.last = itemDate; + stats.count++; + if (item.ja4) stats.ja4s.add(item.ja4); + if (item.host) stats.hosts.add(item.host); + if (item.client_headers) stats.clientHeaders.add(item.client_headers); + } + }); + + return { + ...data, + items: Array.from(ipGroups.values()).map(item => ({ + ...item, + hits: ipStats.get(item.src_ip)!.count, + first_seen: ipStats.get(item.src_ip)!.first.toISOString(), + last_seen: ipStats.get(item.src_ip)!.last.toISOString(), + unique_ja4s: Array.from(ipStats.get(item.src_ip)!.ja4s), + unique_hosts: Array.from(ipStats.get(item.src_ip)!.hosts), + unique_client_headers: Array.from(ipStats.get(item.src_ip)!.clientHeaders) + })) + }; + })(); + + return ( +
+ {/* En-tรชte */} +
+
+

Dรฉtections

+
+ {groupByIP ? processedData.items.length : data.items.length} + โ†’ + {data.total} dรฉtections +
+
+ +
+ {/* Toggle Grouper par IP */} + + + {/* Sรฉlecteur de colonnes */} +
+ + + {showColumnSelector && ( +
+

Afficher les colonnes

+ {columns.map(col => ( + + ))} +
+ )} +
+ + {/* Recherche */} +
+ setSearchInput(e.target.value)} + placeholder="Rechercher IP, JA4, Host..." + className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary placeholder-text-disabled focus:outline-none focus:border-accent-primary w-64" + /> + +
+
+
+ + {/* Filtres */} +
+
+ + + {(modelName || search || sortField) && ( + + )} +
+
+ + {/* Tableau */} +
+ + + + {columns.filter(col => col.visible).map(col => ( + + ))} + + + + {processedData.items.map((detection) => ( + { + window.location.href = `/detections/ip/${encodeURIComponent(detection.src_ip)}`; + }} + > + {columns.filter(col => col.visible).map(col => { + if (col.key === 'ip_ja4') { + const detectionAny = detection as any; + return ( + + ); + } + if (col.key === 'host') { + const detectionAny = detection as any; + return ( + + ); + } + if (col.key === 'client_headers') { + const detectionAny = detection as any; + return ( + + ); + } + if (col.key === 'model_name') { + return ( + + ); + } + if (col.key === 'anomaly_score') { + return ( + + ); + } + if (col.key === 'hits') { + return ( + + ); + } + if (col.key === 'hit_velocity') { + return ( + + ); + } + if (col.key === 'asn') { + return ( + + ); + } + if (col.key === 'country') { + return ( + + ); + } + if (col.key === 'detected_at') { + const detectionAny = detection as any; + return ( + + ); + } + return null; + })} + + ))} + +
col.sortable && handleSort(col.key as SortField)} + > +
+ {col.label} + {col.sortable && ( + {getDefaultSortIcon(col.key as SortField)} + )} +
+
+
{detection.src_ip}
+ {groupByIP && detectionAny.unique_ja4s?.length > 0 ? ( +
+
+ {detectionAny.unique_ja4s.length} JA4{detectionAny.unique_ja4s.length > 1 ? 's' : ''} unique{detectionAny.unique_ja4s.length > 1 ? 's' : ''} +
+ {detectionAny.unique_ja4s.slice(0, 3).map((ja4: string, idx: number) => ( +
+ {ja4} +
+ ))} + {detectionAny.unique_ja4s.length > 3 && ( +
+ +{detectionAny.unique_ja4s.length - 3} autre{detectionAny.unique_ja4s.length - 3 > 1 ? 's' : ''} +
+ )} +
+ ) : ( +
+ {detection.ja4 || '-'} +
+ )} +
+ {groupByIP && detectionAny.unique_hosts?.length > 0 ? ( +
+
+ {detectionAny.unique_hosts.length} Host{detectionAny.unique_hosts.length > 1 ? 's' : ''} unique{detectionAny.unique_hosts.length > 1 ? 's' : ''} +
+ {detectionAny.unique_hosts.slice(0, 3).map((host: string, idx: number) => ( +
+ {host} +
+ ))} + {detectionAny.unique_hosts.length > 3 && ( +
+ +{detectionAny.unique_hosts.length - 3} autre{detectionAny.unique_hosts.length - 3 > 1 ? 's' : ''} +
+ )} +
+ ) : ( +
+ {detection.host || '-'} +
+ )} +
+ {groupByIP && detectionAny.unique_client_headers?.length > 0 ? ( +
+
+ {detectionAny.unique_client_headers.length} Header{detectionAny.unique_client_headers.length > 1 ? 's' : ''} unique{detectionAny.unique_client_headers.length > 1 ? 's' : ''} +
+ {detectionAny.unique_client_headers.slice(0, 3).map((header: string, idx: number) => ( +
+ {header} +
+ ))} + {detectionAny.unique_client_headers.length > 3 && ( +
+ +{detectionAny.unique_client_headers.length - 3} autre{detectionAny.unique_client_headers.length - 3 > 1 ? 's' : ''} +
+ )} +
+ ) : ( +
+ {detection.client_headers || '-'} +
+ )} +
+ + + + +
+ {detection.hits || 0} +
+
+
10 + ? 'text-threat-high' + : detection.hit_velocity && detection.hit_velocity > 1 + ? 'text-threat-medium' + : 'text-text-primary' + }`}> + {detection.hit_velocity ? detection.hit_velocity.toFixed(2) : '0.00'} + req/s +
+
+
{detection.asn_org || detection.asn_number || '-'}
+ {detection.asn_number && ( +
AS{detection.asn_number}
+ )} +
+ {detection.country_code ? ( + {getFlag(detection.country_code)} + ) : ( + '-' + )} + + {groupByIP && detectionAny.first_seen ? ( +
+
+ Premier:{' '} + {new Date(detectionAny.first_seen).toLocaleDateString('fr-FR')}{' '} + {new Date(detectionAny.first_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} +
+
+ Dernier:{' '} + {new Date(detectionAny.last_seen).toLocaleDateString('fr-FR')}{' '} + {new Date(detectionAny.last_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} +
+
+ ) : ( + <> +
+ {new Date(detection.detected_at).toLocaleDateString('fr-FR')} +
+
+ {new Date(detection.detected_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} +
+ + )} +
+ + {data.items.length === 0 && ( +
+ Aucune dรฉtection trouvรฉe +
+ )} +
+ + {/* Pagination */} + {data.total_pages > 1 && ( +
+

+ Page {data.page} sur {data.total_pages} ({data.total} dรฉtections) +

+
+ + +
+
+ )} +
+ ); +} + +// Composant ModelBadge +function ModelBadge({ model }: { model: string }) { + const styles: Record = { + Complet: 'bg-accent-primary/20 text-accent-primary', + Applicatif: 'bg-purple-500/20 text-purple-400', + }; + + return ( + + {model} + + ); +} + +// Composant ScoreBadge +function ScoreBadge({ score }: { score: number }) { + let color = 'text-threat-low'; + if (score < -0.3) color = 'text-threat-critical'; + else if (score < -0.15) color = 'text-threat-high'; + else if (score < -0.05) color = 'text-threat-medium'; + + return ( + + {score.toFixed(3)} + + ); +} + +// Helper pour les drapeaux +function getFlag(countryCode: string): string { + const code = countryCode.toUpperCase(); + return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); +} diff --git a/frontend/src/components/EntityInvestigationView.tsx b/frontend/src/components/EntityInvestigationView.tsx new file mode 100644 index 0000000..1d54bea --- /dev/null +++ b/frontend/src/components/EntityInvestigationView.tsx @@ -0,0 +1,401 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useEffect, useState } from 'react'; + +interface EntityStats { + entity_type: string; + entity_value: string; + total_requests: number; + unique_ips: number; + first_seen: string; + last_seen: string; +} + +interface EntityRelatedAttributes { + ips: string[]; + ja4s: string[]; + hosts: string[]; + asns: string[]; + countries: string[]; +} + +interface AttributeValue { + value: string; + count: number; + percentage: number; +} + +interface EntityInvestigationData { + stats: EntityStats; + related: EntityRelatedAttributes; + user_agents: AttributeValue[]; + client_headers: AttributeValue[]; + paths: AttributeValue[]; + query_params: AttributeValue[]; +} + +export function EntityInvestigationView() { + const { type, value } = useParams<{ type: string; value: string }>(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!type || !value) { + setError("Type ou valeur d'entitรฉ manquant"); + setLoading(false); + return; + } + + const fetchInvestigation = async () => { + setLoading(true); + try { + const response = await fetch(`/api/entities/${type}/${encodeURIComponent(value)}`); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Erreur chargement donnรฉes'); + } + const result = await response.json(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'Erreur inconnue'); + } finally { + setLoading(false); + } + }; + + fetchInvestigation(); + }, [type, value]); + + const getEntityLabel = (entityType: string) => { + const labels: Record = { + ip: 'Adresse IP', + ja4: 'Fingerprint JA4', + user_agent: 'User-Agent', + client_header: 'Client Header', + host: 'Host', + path: 'Path', + query_param: 'Query Params' + }; + return labels[entityType] || entityType; + }; + + const getCountryFlag = (code: string) => { + const flags: Record = { + CN: '๐Ÿ‡จ๐Ÿ‡ณ', US: '๐Ÿ‡บ๐Ÿ‡ธ', FR: '๐Ÿ‡ซ๐Ÿ‡ท', DE: '๐Ÿ‡ฉ๐Ÿ‡ช', GB: '๐Ÿ‡ฌ๐Ÿ‡ง', + RU: '๐Ÿ‡ท๐Ÿ‡บ', CA: '๐Ÿ‡จ๐Ÿ‡ฆ', AU: '๐Ÿ‡ฆ๐Ÿ‡บ', JP: '๐Ÿ‡ฏ๐Ÿ‡ต', IN: '๐Ÿ‡ฎ๐Ÿ‡ณ', + BR: '๐Ÿ‡ง๐Ÿ‡ท', IT: '๐Ÿ‡ฎ๐Ÿ‡น', ES: '๐Ÿ‡ช๐Ÿ‡ธ', NL: '๐Ÿ‡ณ๐Ÿ‡ฑ', BE: '๐Ÿ‡ง๐Ÿ‡ช', + CH: '๐Ÿ‡จ๐Ÿ‡ญ', SE: '๐Ÿ‡ธ๐Ÿ‡ช', NO: '๐Ÿ‡ณ๐Ÿ‡ด', DK: '๐Ÿ‡ฉ๐Ÿ‡ฐ', FI: '๐Ÿ‡ซ๐Ÿ‡ฎ' + }; + return flags[code] || code; + }; + + const truncateUA = (ua: string, maxLength: number = 150) => { + if (ua.length <= maxLength) return ua; + return ua.substring(0, maxLength) + '...'; + }; + + if (loading) { + return ( +
+
+
Chargement...
+
+
+ ); + } + + if (error || !data) { + return ( +
+
+
+
Erreur
+
{error || 'Donnรฉes non disponibles'}
+ +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+ +
+
+

+ Investigation: {getEntityLabel(data.stats.entity_type)} +

+
+ {data.stats.entity_value} +
+
+
+
Requรชtes: {data.stats.total_requests.toLocaleString()}
+
IPs Uniques: {data.stats.unique_ips.toLocaleString()}
+
+
+
+ + {/* Stats Summary */} +
+ + + + +
+ + {/* Panel 1: IPs Associรฉes */} +
+

1. IPs Associรฉes

+
+ {data.related.ips.slice(0, 20).map((ip, idx) => ( + + ))} +
+ {data.related.ips.length === 0 && ( +
Aucune IP associรฉe
+ )} + {data.related.ips.length > 20 && ( +
+ +{data.related.ips.length - 20} autres IPs +
+ )} +
+ + {/* Panel 2: JA4 Fingerprints */} +
+

2. JA4 Fingerprints

+
+ {data.related.ja4s.slice(0, 10).map((ja4, idx) => ( +
+
+ {ja4} +
+ +
+ ))} +
+ {data.related.ja4s.length === 0 && ( +
Aucun JA4 associรฉ
+ )} + {data.related.ja4s.length > 10 && ( +
+ +{data.related.ja4s.length - 10} autres JA4 +
+ )} +
+ + {/* Panel 3: User-Agents */} +
+

3. User-Agents

+
+ {data.user_agents.slice(0, 10).map((ua, idx) => ( +
+
+ {truncateUA(ua.value)} +
+
+
{ua.count} requรชtes
+
{ua.percentage.toFixed(1)}%
+
+
+ ))} +
+ {data.user_agents.length === 0 && ( +
Aucun User-Agent
+ )} + {data.user_agents.length > 10 && ( +
+ +{data.user_agents.length - 10} autres User-Agents +
+ )} +
+ + {/* Panel 4: Client Headers */} +
+

4. Client Headers

+
+ {data.client_headers.slice(0, 10).map((header, idx) => ( +
+
+ {header.value} +
+
+
{header.count} requรชtes
+
{header.percentage.toFixed(1)}%
+
+
+ ))} +
+ {data.client_headers.length === 0 && ( +
Aucun Client Header
+ )} + {data.client_headers.length > 10 && ( +
+ +{data.client_headers.length - 10} autres Client Headers +
+ )} +
+ + {/* Panel 5: Hosts */} +
+

5. Hosts Ciblรฉs

+
+ {data.related.hosts.slice(0, 15).map((host, idx) => ( +
+
{host}
+
+ ))} +
+ {data.related.hosts.length === 0 && ( +
Aucun Host associรฉ
+ )} + {data.related.hosts.length > 15 && ( +
+ +{data.related.hosts.length - 15} autres Hosts +
+ )} +
+ + {/* Panel 6: Paths */} +
+

6. Paths

+
+ {data.paths.slice(0, 15).map((path, idx) => ( +
+
{path.value}
+
+
{path.count} requรชtes
+
{path.percentage.toFixed(1)}%
+
+
+ ))} +
+ {data.paths.length === 0 && ( +
Aucun Path
+ )} + {data.paths.length > 15 && ( +
+ +{data.paths.length - 15} autres Paths +
+ )} +
+ + {/* Panel 7: Query Params */} +
+

7. Query Params

+
+ {data.query_params.slice(0, 15).map((qp, idx) => ( +
+
{qp.value}
+
+
{qp.count} requรชtes
+
{qp.percentage.toFixed(1)}%
+
+
+ ))} +
+ {data.query_params.length === 0 && ( +
Aucun Query Param
+ )} + {data.query_params.length > 15 && ( +
+ +{data.query_params.length - 15} autres Query Params +
+ )} +
+ + {/* Panel 8: ASNs & Pays */} +
+ {/* ASNs */} +
+

ASNs

+
+ {data.related.asns.slice(0, 10).map((asn, idx) => ( +
+
{asn}
+
+ ))} +
+ {data.related.asns.length === 0 && ( +
Aucun ASN
+ )} + {data.related.asns.length > 10 && ( +
+ +{data.related.asns.length - 10} autres ASNs +
+ )} +
+ + {/* Pays */} +
+

Pays

+
+ {data.related.countries.slice(0, 10).map((country, idx) => ( +
+ {getCountryFlag(country)} + {country} +
+ ))} +
+ {data.related.countries.length === 0 && ( +
Aucun pays
+ )} + {data.related.countries.length > 10 && ( +
+ +{data.related.countries.length - 10} autres pays +
+ )} +
+
+
+
+ ); +} + +function StatCard({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/frontend/src/components/InvestigationView.tsx b/frontend/src/components/InvestigationView.tsx new file mode 100644 index 0000000..4277c85 --- /dev/null +++ b/frontend/src/components/InvestigationView.tsx @@ -0,0 +1,64 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { SubnetAnalysis } from './analysis/SubnetAnalysis'; +import { CountryAnalysis } from './analysis/CountryAnalysis'; +import { JA4Analysis } from './analysis/JA4Analysis'; +import { UserAgentAnalysis } from './analysis/UserAgentAnalysis'; +import { CorrelationSummary } from './analysis/CorrelationSummary'; + +export function InvestigationView() { + const { ip } = useParams<{ ip: string }>(); + const navigate = useNavigate(); + + if (!ip) { + return ( +
+ IP non spรฉcifiรฉe +
+ ); + } + + const handleClassify = (label: string, tags: string[], comment: string, confidence: number) => { + // Callback optionnel aprรจs classification + console.log('IP classifiรฉe:', { ip, label, tags, comment, confidence }); + }; + + return ( +
+ {/* En-tรชte */} +
+
+
+ +

Investigation: {ip}

+
+
+ Analyse de corrรฉlations pour classification SOC +
+
+
+ + {/* Panels d'analyse */} +
+ {/* Panel 1: Subnet/ASN */} + + + {/* Panel 2: Country (relatif ร  l'IP) */} + + + {/* Panel 3: JA4 */} + + + {/* Panel 4: User-Agents */} + + + {/* Panel 5: Correlation Summary + Classification */} + +
+
+ ); +} diff --git a/frontend/src/components/JA4InvestigationView.tsx b/frontend/src/components/JA4InvestigationView.tsx new file mode 100644 index 0000000..c33f7de --- /dev/null +++ b/frontend/src/components/JA4InvestigationView.tsx @@ -0,0 +1,373 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { JA4CorrelationSummary } from './analysis/JA4CorrelationSummary'; + +interface JA4InvestigationData { + ja4: string; + total_detections: number; + unique_ips: number; + first_seen: string; + last_seen: string; + top_ips: { ip: string; count: number; percentage: number }[]; + top_countries: { code: string; name: string; count: number; percentage: number }[]; + top_asns: { asn: string; org: string; count: number; percentage: number }[]; + top_hosts: { host: string; count: number; percentage: number }[]; + user_agents: { ua: string; count: number; percentage: number; classification: string }[]; +} + +export function JA4InvestigationView() { + const { ja4 } = useParams<{ ja4: string }>(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchJA4Investigation = async () => { + setLoading(true); + try { + // Rรฉcupรฉrer les donnรฉes de base + const baseResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}`); + if (!baseResponse.ok) throw new Error('Erreur chargement donnรฉes JA4'); + const baseData = await baseResponse.json(); + + // Rรฉcupรฉrer les IPs associรฉes + const ipsResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}/ips?limit=20`); + const ipsData = await ipsResponse.json(); + + // Rรฉcupรฉrer les attributs associรฉs + const countriesResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}/attributes?target_attr=countries&limit=10`); + const countriesData = await countriesResponse.json(); + + const asnsResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}/attributes?target_attr=asns&limit=10`); + const asnsData = await asnsResponse.json(); + + const hostsResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}/attributes?target_attr=hosts&limit=10`); + const hostsData = await hostsResponse.json(); + + // Rรฉcupรฉrer les user-agents + const uaResponse = await fetch(`/api/variability/ja4/${encodeURIComponent(ja4 || '')}/user_agents?limit=10`); + const uaData = await uaResponse.json(); + + // Formater les donnรฉes + setData({ + ja4: ja4 || '', + total_detections: baseData.total_detections || 0, + unique_ips: ipsData.total || 0, + first_seen: baseData.date_range?.first_seen || '', + last_seen: baseData.date_range?.last_seen || '', + top_ips: ipsData.ips?.slice(0, 10).map((ip: string) => ({ + ip, + count: 0, + percentage: 0 + })) || [], + top_countries: countriesData.items?.map((item: any) => ({ + code: item.value, + name: item.value, + count: item.count, + percentage: item.percentage + })) || [], + top_asns: asnsData.items?.map((item: any) => { + const match = item.value.match(/AS(\d+)/); + return { + asn: match ? `AS${match[1]}` : item.value, + org: item.value.replace(/AS\d+\s*-\s*/, ''), + count: item.count, + percentage: item.percentage + }; + }) || [], + top_hosts: hostsData.items?.map((item: any) => ({ + host: item.value, + count: item.count, + percentage: item.percentage + })) || [], + user_agents: uaData.user_agents?.map((ua: any) => ({ + ua: ua.value, + count: ua.count, + percentage: ua.percentage, + classification: ua.classification || 'normal' + })) || [] + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Erreur inconnue'); + } finally { + setLoading(false); + } + }; + + if (ja4) { + fetchJA4Investigation(); + } + }, [ja4]); + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (error || !data) { + return ( +
+
Erreur: {error || 'Donnรฉes non disponibles'}
+ +
+ ); + } + + const getFlag = (code: string) => { + return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); + }; + + const getClassificationBadge = (classification: string) => { + switch (classification) { + case 'normal': + return โœ… Normal; + case 'bot': + return โš ๏ธ Bot; + case 'script': + return โŒ Script; + default: + return null; + } + }; + + const truncateUA = (ua: string, maxLength = 80) => { + if (ua.length <= maxLength) return ua; + return ua.substring(0, maxLength) + '...'; + }; + + return ( +
+ {/* En-tรชte */} +
+
+
+ +

Investigation JA4

+
+
+ Analyse de fingerprint JA4 pour classification SOC +
+
+
+ + {/* Stats principales */} +
+
+
+
JA4 Fingerprint
+
+ {data.ja4} +
+
+
+
{data.total_detections.toLocaleString()}
+
dรฉtections (24h)
+
+
+ +
+ + + + +
+
+ + {/* Panel 1: Top IPs */} +
+

1. TOP IPs (Utilisant ce JA4)

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

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

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

2. TOP Pays

+
+ {data.top_countries.map((country, idx) => ( +
+
+
+ {getFlag(country.code)} +
+ {country.name} ({country.code}) +
+
+
+
{country.count.toLocaleString()}
+
{country.percentage.toFixed(1)}%
+
+
+
+
+
+
+ ))} +
+
+ + {/* Panel 3: Top ASN */} +
+

3. TOP ASN

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

4. TOP Hosts Ciblรฉs

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

5. User-Agents

+
+ {data.user_agents.map((ua, idx) => ( +
+
+
+ {truncateUA(ua.ua)} +
+ {getClassificationBadge(ua.classification)} +
+
+
{ua.count} IPs
+
{ua.percentage.toFixed(1)}%
+
+
+ ))} + {data.user_agents.length === 0 && ( +
+ Aucun User-Agent trouvรฉ +
+ )} +
+
+ + {/* Classification JA4 */} + +
+
+ ); +} + +function StatBox({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function formatDate(dateStr: string): string { + if (!dateStr) return '-'; + const date = new Date(dateStr); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +} diff --git a/frontend/src/components/VariabilityPanel.tsx b/frontend/src/components/VariabilityPanel.tsx new file mode 100644 index 0000000..cc7db84 --- /dev/null +++ b/frontend/src/components/VariabilityPanel.tsx @@ -0,0 +1,313 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { VariabilityAttributes, AttributeValue } from '../api/client'; + +interface VariabilityPanelProps { + attributes: VariabilityAttributes; +} + +export function VariabilityPanel({ attributes }: VariabilityPanelProps) { + const [showModal, setShowModal] = useState<{ + type: string; + title: string; + items: string[]; + total: number; + } | null>(null); + + const [loading, setLoading] = useState(false); + + // Fonction pour charger la liste des IPs associรฉes + const loadAssociatedIPs = async (attrType: string, value: string, total: number) => { + setLoading(true); + try { + const response = await fetch(`/api/variability/${attrType}/${encodeURIComponent(value)}/ips?limit=100`); + const data = await response.json(); + setShowModal({ + type: 'ips', + title: `${data.total || total} IPs associรฉes ร  ${value}`, + items: data.ips || [], + total: data.total || total, + }); + } catch (error) { + console.error('Erreur chargement IPs:', error); + } + setLoading(false); + }; + + return ( +
+

Variabilitรฉ des Attributs

+ + {/* JA4 Fingerprints */} + {attributes.ja4 && attributes.ja4.length > 0 && ( + item.value} + getLink={(item) => `/investigation/ja4/${encodeURIComponent(item.value)}`} + onViewAll={(value, count) => loadAssociatedIPs('ja4', value, count)} + showViewAll + viewAllLabel="Voir les IPs" + /> + )} + + {/* User-Agents */} + {attributes.user_agents && attributes.user_agents.length > 0 && ( +
+

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

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

+ ... et {attributes.user_agents.length - 10} autres (top 10 affichรฉ) +

+ )} +
+ )} + + {/* Pays */} + {attributes.countries && attributes.countries.length > 0 && ( + item.value} + getLink={(item) => `/detections/country/${encodeURIComponent(item.value)}`} + onViewAll={(value, count) => loadAssociatedIPs('country', value, count)} + showViewAll + viewAllLabel="Voir les IPs" + /> + )} + + {/* ASN */} + {attributes.asns && attributes.asns.length > 0 && ( + item.value} + getLink={(item) => { + const asnNumber = item.value.match(/AS(\d+)/)?.[1] || item.value; + return `/detections/asn/${encodeURIComponent(asnNumber)}`; + }} + onViewAll={(value, count) => loadAssociatedIPs('asn', value, count)} + showViewAll + viewAllLabel="Voir les IPs" + /> + )} + + {/* Hosts */} + {attributes.hosts && attributes.hosts.length > 0 && ( + item.value} + getLink={(item) => `/detections/host/${encodeURIComponent(item.value)}`} + onViewAll={(value, count) => loadAssociatedIPs('host', value, count)} + showViewAll + viewAllLabel="Voir les IPs" + /> + )} + + {/* Threat Levels */} + {attributes.threat_levels && attributes.threat_levels.length > 0 && ( + item.value} + getLink={(item) => `/detections?threat_level=${encodeURIComponent(item.value)}`} + onViewAll={(value, count) => loadAssociatedIPs('threat_level', value, count)} + showViewAll + viewAllLabel="Voir les IPs" + /> + )} + + {/* Modal pour afficher la liste complรจte */} + {showModal && ( +
+
+ {/* Header */} +
+

{showModal.title}

+ +
+ + {/* Content */} +
+ {loading ? ( +
Chargement...
+ ) : showModal.items.length > 0 ? ( +
+ {showModal.items.map((item, index) => ( +
+ {item} +
+ ))} + {showModal.total > showModal.items.length && ( +

+ Affichage de {showModal.items.length} sur {showModal.total} รฉlรฉments +

+ )} +
+ ) : ( +
+ Aucune donnรฉe disponible +
+ )} +
+ + {/* Footer */} +
+ +
+
+
+ )} +
+ ); +} + +// Composant AttributeSection +function AttributeSection({ + title, + items, + getValue, + getLink, + onViewAll, + showViewAll = false, + viewAllLabel = 'Voir les IPs', +}: { + title: string; + items: AttributeValue[]; + getValue: (item: AttributeValue) => string; + getLink: (item: AttributeValue) => string; + onViewAll?: (value: string, count: number) => void; + showViewAll?: boolean; + viewAllLabel?: string; +}) { + const displayItems = items.slice(0, 10); + + return ( +
+
+

+ {title} ({items.length}) +

+ {showViewAll && items.length > 0 && ( + + )} +
+
+ {displayItems.map((item, index) => ( + + ))} +
+ + {items.length > 10 && ( +

+ ... et {items.length - 10} autres (top 10 affichรฉ) +

+ )} +
+ ); +} + +// Composant AttributeRow +function AttributeRow({ + value, + getValue, + getLink, +}: { + value: AttributeValue; + getValue: (item: AttributeValue) => string; + getLink: (item: AttributeValue) => string; +}) { + const percentage = value.percentage || 0; + + return ( +
+
+ + {getValue(value)} + +
+
{value.count}
+
{percentage.toFixed(1)}%
+
+
+ +
+
+
+
+ ); +} + +// Helper pour la couleur de la barre +function getPercentageColor(percentage: number): string { + if (percentage >= 50) return 'bg-threat-critical'; + if (percentage >= 25) return 'bg-threat-high'; + if (percentage >= 10) return 'bg-threat-medium'; + return 'bg-threat-low'; +} diff --git a/frontend/src/components/analysis/CorrelationSummary.tsx b/frontend/src/components/analysis/CorrelationSummary.tsx new file mode 100644 index 0000000..ca1074b --- /dev/null +++ b/frontend/src/components/analysis/CorrelationSummary.tsx @@ -0,0 +1,308 @@ +import { useEffect, useState } from 'react'; + +interface CorrelationIndicators { + subnet_ips_count: number; + asn_ips_count: number; + country_percentage: number; + ja4_shared_ips: number; + user_agents_count: number; + bot_ua_percentage: number; +} + +interface ClassificationRecommendation { + label: 'legitimate' | 'suspicious' | 'malicious'; + confidence: number; + indicators: CorrelationIndicators; + suggested_tags: string[]; + reason: string; +} + +interface CorrelationSummaryProps { + ip: string; + onClassify?: (label: string, tags: string[], comment: string, confidence: number) => void; +} + +const PREDEFINED_TAGS = [ + 'scraping', + 'bot-network', + 'scanner', + 'bruteforce', + 'data-exfil', + 'ddos', + 'spam', + 'proxy', + 'tor', + 'vpn', + 'hosting-asn', + 'distributed', + 'ja4-rotation', + 'ua-rotation', + 'country-cn', + 'country-us', + 'country-ru', +]; + +export function CorrelationSummary({ ip, onClassify }: CorrelationSummaryProps) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [selectedLabel, setSelectedLabel] = useState(''); + const [selectedTags, setSelectedTags] = useState([]); + const [comment, setComment] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const fetchRecommendation = async () => { + setLoading(true); + try { + const response = await fetch(`/api/analysis/${encodeURIComponent(ip)}/recommendation`); + if (!response.ok) throw new Error('Erreur chargement recommandation'); + const result = await response.json(); + setData(result); + setSelectedLabel(result.label); + setSelectedTags(result.suggested_tags || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Erreur inconnue'); + } finally { + setLoading(false); + } + }; + + fetchRecommendation(); + }, [ip]); + + const toggleTag = (tag: string) => { + setSelectedTags(prev => + prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag] + ); + }; + + const handleSave = async () => { + setSaving(true); + try { + const response = await fetch('/api/analysis/classifications', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ip, + label: selectedLabel, + tags: selectedTags, + comment, + confidence: data?.confidence || 0.5, + features: data?.indicators || {}, + analyst: 'soc_user' + }) + }); + + if (!response.ok) throw new Error('Erreur sauvegarde'); + + if (onClassify) { + onClassify(selectedLabel, selectedTags, comment, data?.confidence || 0.5); + } + + alert('Classification sauvegardรฉe !'); + } catch (err) { + alert(`Erreur: ${err instanceof Error ? err.message : 'Erreur inconnue'}`); + } finally { + setSaving(false); + } + }; + + const handleExportML = async () => { + try { + const mlData = { + ip, + label: selectedLabel, + confidence: data?.confidence || 0.5, + tags: selectedTags, + features: { + subnet_ips_count: data?.indicators.subnet_ips_count || 0, + asn_ips_count: data?.indicators.asn_ips_count || 0, + country_percentage: data?.indicators.country_percentage || 0, + ja4_shared_ips: data?.indicators.ja4_shared_ips || 0, + user_agents_count: data?.indicators.user_agents_count || 0, + bot_ua_percentage: data?.indicators.bot_ua_percentage || 0, + }, + comment, + analyst: 'soc_user', + timestamp: new Date().toISOString() + }; + + const blob = new Blob([JSON.stringify(mlData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `classification_${ip}_${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (err) { + alert(`Erreur export: ${err instanceof Error ? err.message : 'Erreur inconnue'}`); + } + }; + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (error || !data) { + return ( +
+
Erreur: {error || 'Donnรฉes non disponibles'}
+
+ ); + } + + return ( +
+

5. CORRELATION SUMMARY

+ + {/* Indicateurs */} +
+ 10} + /> + 100} + /> + 50} + /> + 20} + /> + 5} + /> + +
+ + {/* Raison */} + {data.reason && ( +
+
Analyse
+
{data.reason}
+
+ )} + + {/* Classification */} +
+

CLASSIFICATION

+ + {/* Boutons de label */} +
+ + + +
+ + {/* Tags */} +
+
Tags
+
+ {PREDEFINED_TAGS.map(tag => ( + + ))} +
+
+ + {/* Commentaire */} +
+
Commentaire
+