feat: Réputation IP depuis bases publiques (sans clé API)
- Nouveau service backend/services/reputation_ip.py - IP-API.com: Géolocalisation + détection Proxy/Hosting - IPinfo.io: ASN + Organisation - Agrégation des sources avec score de menace 0-100 - Niveaux: clean/low/medium/high/critical - Nouvelle route API GET /api/reputation/ip/:ip - Validation IPv4 - Version complète et summary - Timeout 10s par source - Nouveau composant frontend ReputationPanel.tsx - Badge de niveau de menace (code couleur) - 4 badges détection: Proxy 🌐, Hosting ☁️, VPN 🔒, Tor 🧅 - Infos géographiques: pays, ville, ASN, organisation - Liste des avertissements - Sources et timestamp - Intégration dans InvestigationView - Panel affiché en premier (avant Graph de corrélations) - Chargement asynchrone au montage du composant - Dépendance: httpx==0.26.0 (requêtes HTTP async) Testé avec 141.98.11.209 (Lithuania, AS209605) → 🟢 CLEAN (0/100) Aucun proxy/hosting/VPN/Tor détecté Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -12,7 +12,7 @@ import os
|
|||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import db
|
from .database import db
|
||||||
from .routes import metrics, detections, variability, attributes, analysis, entities, incidents, audit
|
from .routes import metrics, detections, variability, attributes, analysis, entities, incidents, audit, reputation
|
||||||
|
|
||||||
# Configuration logging
|
# Configuration logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -72,6 +72,7 @@ app.include_router(analysis.router)
|
|||||||
app.include_router(entities.router)
|
app.include_router(entities.router)
|
||||||
app.include_router(incidents.router)
|
app.include_router(incidents.router)
|
||||||
app.include_router(audit.router)
|
app.include_router(audit.router)
|
||||||
|
app.include_router(reputation.router)
|
||||||
|
|
||||||
|
|
||||||
# Route pour servir le frontend
|
# Route pour servir le frontend
|
||||||
|
|||||||
125
backend/routes/reputation.py
Normal file
125
backend/routes/reputation.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Routes pour la réputation IP (bases de données publiques)
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException, Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..services.reputation_ip import get_reputation_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/reputation", tags=["Reputation"])
|
||||||
|
|
||||||
|
# Pattern de validation d'IP (IPv4)
|
||||||
|
IP_PATTERN = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$')
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_ipv4(ip: str) -> bool:
|
||||||
|
"""Valide qu'une chaîne est une adresse IPv4 valide"""
|
||||||
|
if not IP_PATTERN.match(ip):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Vérifie que chaque octet est entre 0 et 255
|
||||||
|
parts = ip.split('.')
|
||||||
|
for part in parts:
|
||||||
|
try:
|
||||||
|
num = int(part)
|
||||||
|
if num < 0 or num > 255:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ip/{ip_address}")
|
||||||
|
async def get_ip_reputation(
|
||||||
|
ip_address: str = Path(..., description="Adresse IP à vérifier")
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Récupère la réputation d'une adresse IP depuis les bases de données publiques
|
||||||
|
|
||||||
|
Sources utilisées (sans clé API):
|
||||||
|
- IP-API.com: Géolocalisation + Proxy/Hosting detection
|
||||||
|
- IPinfo.io: ASN + Organisation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict avec:
|
||||||
|
- ip: Adresse IP vérifiée
|
||||||
|
- timestamp: Date de la vérification
|
||||||
|
- sources: Détails par source
|
||||||
|
- aggregated: Résultats agrégés
|
||||||
|
- is_proxy: bool
|
||||||
|
- is_hosting: bool
|
||||||
|
- is_vpn: bool
|
||||||
|
- is_tor: bool
|
||||||
|
- threat_score: 0-100
|
||||||
|
- threat_level: clean/low/medium/high/critical
|
||||||
|
- country: Pays
|
||||||
|
- asn: Numéro ASN
|
||||||
|
- asn_org: Organisation ASN
|
||||||
|
- org: ISP/Organisation
|
||||||
|
- warnings: Liste des alertes
|
||||||
|
"""
|
||||||
|
# Valide l'adresse IP
|
||||||
|
if not is_valid_ipv4(ip_address):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Adresse IP invalide: {ip_address}. Format attendu: x.x.x.x"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Récupère le service de réputation
|
||||||
|
reputation_service = get_reputation_service()
|
||||||
|
|
||||||
|
# Interroge les sources
|
||||||
|
results = await reputation_service.get_reputation(ip_address)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Erreur lors de la vérification de réputation: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ip/{ip_address}/summary")
|
||||||
|
async def get_ip_reputation_summary(
|
||||||
|
ip_address: str = Path(..., description="Adresse IP à vérifier")
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Version simplifiée de la réputation IP (juste les infos essentielles)
|
||||||
|
|
||||||
|
Utile pour affichage rapide dans les tableaux
|
||||||
|
"""
|
||||||
|
if not is_valid_ipv4(ip_address):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Adresse IP invalide: {ip_address}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reputation_service = get_reputation_service()
|
||||||
|
full_results = await reputation_service.get_reputation(ip_address)
|
||||||
|
|
||||||
|
# Retourne juste l'essentiel
|
||||||
|
aggregated = full_results.get('aggregated', {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ip': ip_address,
|
||||||
|
'threat_level': aggregated.get('threat_level', 'unknown'),
|
||||||
|
'threat_score': aggregated.get('threat_score', 0),
|
||||||
|
'is_proxy': aggregated.get('is_proxy', False),
|
||||||
|
'is_hosting': aggregated.get('is_hosting', False),
|
||||||
|
'country': aggregated.get('country'),
|
||||||
|
'country_code': aggregated.get('country_code'),
|
||||||
|
'asn': aggregated.get('asn'),
|
||||||
|
'org': aggregated.get('org'),
|
||||||
|
'warnings_count': len(aggregated.get('warnings', []))
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Erreur: {str(e)}"
|
||||||
|
)
|
||||||
0
backend/services/__init__.py
Normal file
0
backend/services/__init__.py
Normal file
312
backend/services/reputation_ip.py
Normal file
312
backend/services/reputation_ip.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
"""
|
||||||
|
Services de réputation IP - Bases de données publiques sans clé API
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Timeout pour les requêtes HTTP
|
||||||
|
HTTP_TIMEOUT = 10.0
|
||||||
|
|
||||||
|
|
||||||
|
class IPReputationService:
|
||||||
|
"""
|
||||||
|
Service de réputation IP utilisant des bases de données publiques gratuites
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.http_client = httpx.AsyncClient(timeout=HTTP_TIMEOUT)
|
||||||
|
|
||||||
|
# Sources de réputation (sans clé API)
|
||||||
|
self.sources = {
|
||||||
|
'ip_api': 'http://ip-api.com/json/{ip}',
|
||||||
|
'ipinfo': 'https://ipinfo.io/{ip}/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_reputation(self, ip: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Récupère la réputation d'une IP depuis toutes les sources disponibles
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip: Adresse IP à vérifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict avec les informations de réputation agrégées
|
||||||
|
"""
|
||||||
|
results = {
|
||||||
|
'ip': ip,
|
||||||
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
|
'sources': {},
|
||||||
|
'aggregated': {
|
||||||
|
'is_proxy': False,
|
||||||
|
'is_hosting': False,
|
||||||
|
'is_vpn': False,
|
||||||
|
'is_tor': False,
|
||||||
|
'threat_score': 0,
|
||||||
|
'threat_level': 'unknown',
|
||||||
|
'country': None,
|
||||||
|
'asn': None,
|
||||||
|
'org': None,
|
||||||
|
'warnings': []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Interroge chaque source
|
||||||
|
for source_name, url_template in self.sources.items():
|
||||||
|
try:
|
||||||
|
url = url_template.format(ip=ip)
|
||||||
|
response = await self.http_client.get(url)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
results['sources'][source_name] = self._parse_source_data(source_name, data)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Source {source_name} returned status {response.status_code}")
|
||||||
|
results['sources'][source_name] = {'error': f'Status {response.status_code}'}
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
logger.warning(f"Timeout for source {source_name}")
|
||||||
|
results['sources'][source_name] = {'error': 'Timeout'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching from {source_name}: {str(e)}")
|
||||||
|
results['sources'][source_name] = {'error': str(e)}
|
||||||
|
|
||||||
|
# Agrège les résultats
|
||||||
|
results['aggregated'] = self._aggregate_results(results['sources'])
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _parse_source_data(self, source: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parse les données d'une source spécifique
|
||||||
|
"""
|
||||||
|
if source == 'ip_api':
|
||||||
|
return self._parse_ip_api(data)
|
||||||
|
elif source == 'ipinfo':
|
||||||
|
return self._parse_ipinfo(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _parse_ip_api(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parse les données de IP-API.com
|
||||||
|
|
||||||
|
Response example:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"country": "France",
|
||||||
|
"countryCode": "FR",
|
||||||
|
"region": "IDF",
|
||||||
|
"regionName": "Île-de-France",
|
||||||
|
"city": "Paris",
|
||||||
|
"zip": "75001",
|
||||||
|
"lat": 48.8534,
|
||||||
|
"lon": 2.3488,
|
||||||
|
"timezone": "Europe/Paris",
|
||||||
|
"isp": "OVH SAS",
|
||||||
|
"org": "OVH SAS",
|
||||||
|
"as": "AS16276 OVH SAS",
|
||||||
|
"asname": "OVH",
|
||||||
|
"mobile": false,
|
||||||
|
"proxy": false,
|
||||||
|
"hosting": true,
|
||||||
|
"query": "51.15.0.1"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if data.get('status') != 'success':
|
||||||
|
return {'error': data.get('message', 'Unknown error')}
|
||||||
|
|
||||||
|
# Extraire l'ASN
|
||||||
|
asn_full = data.get('as', '')
|
||||||
|
asn_number = None
|
||||||
|
asn_org = None
|
||||||
|
|
||||||
|
if asn_full:
|
||||||
|
parts = asn_full.split(' ', 1)
|
||||||
|
if len(parts) >= 1:
|
||||||
|
asn_number = parts[0].replace('AS', '')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
asn_org = parts[1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'country': data.get('country'),
|
||||||
|
'country_code': data.get('countryCode'),
|
||||||
|
'region': data.get('regionName'),
|
||||||
|
'city': data.get('city'),
|
||||||
|
'isp': data.get('isp'),
|
||||||
|
'org': data.get('org'),
|
||||||
|
'asn': asn_number,
|
||||||
|
'asn_org': asn_org,
|
||||||
|
'is_proxy': data.get('proxy', False),
|
||||||
|
'is_hosting': data.get('hosting', False),
|
||||||
|
'is_mobile': data.get('mobile', False),
|
||||||
|
'timezone': data.get('timezone'),
|
||||||
|
'lat': data.get('lat'),
|
||||||
|
'lon': data.get('lon'),
|
||||||
|
'query': data.get('query')
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_ipinfo(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parse les données de IPinfo.io
|
||||||
|
|
||||||
|
Response example:
|
||||||
|
{
|
||||||
|
"ip": "51.15.0.1",
|
||||||
|
"city": "Paris",
|
||||||
|
"region": "Île-de-France",
|
||||||
|
"country": "FR",
|
||||||
|
"loc": "48.8534,2.3488",
|
||||||
|
"org": "AS16276 OVH SAS",
|
||||||
|
"postal": "75001",
|
||||||
|
"timezone": "Europe/Paris",
|
||||||
|
"readme": "https://ipinfo.io/missingauth"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Extraire l'ASN
|
||||||
|
org_full = data.get('org', '')
|
||||||
|
asn_number = None
|
||||||
|
asn_org = None
|
||||||
|
|
||||||
|
if org_full:
|
||||||
|
parts = org_full.split(' ', 1)
|
||||||
|
if len(parts) >= 1:
|
||||||
|
asn_number = parts[0].replace('AS', '')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
asn_org = parts[1]
|
||||||
|
|
||||||
|
# Extraire lat/lon
|
||||||
|
loc = data.get('loc', '')
|
||||||
|
lat = None
|
||||||
|
lon = None
|
||||||
|
if loc:
|
||||||
|
coords = loc.split(',')
|
||||||
|
if len(coords) == 2:
|
||||||
|
lat = float(coords[0])
|
||||||
|
lon = float(coords[1])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ip': data.get('ip'),
|
||||||
|
'city': data.get('city'),
|
||||||
|
'region': data.get('region'),
|
||||||
|
'country': data.get('country'),
|
||||||
|
'postal': data.get('postal'),
|
||||||
|
'timezone': data.get('timezone'),
|
||||||
|
'asn': asn_number,
|
||||||
|
'asn_org': asn_org,
|
||||||
|
'org': data.get('org'),
|
||||||
|
'lat': lat,
|
||||||
|
'lon': lon
|
||||||
|
}
|
||||||
|
|
||||||
|
def _aggregate_results(self, sources: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Agrège les résultats de toutes les sources
|
||||||
|
|
||||||
|
Logique d'agrégation:
|
||||||
|
- is_proxy: true si au moins une source le détecte
|
||||||
|
- is_hosting: true si au moins une source le détecte
|
||||||
|
- threat_score: basé sur les détections proxy/hosting/vpn/tor
|
||||||
|
- threat_level: low/medium/high/critical basé sur le score
|
||||||
|
"""
|
||||||
|
aggregated = {
|
||||||
|
'is_proxy': False,
|
||||||
|
'is_hosting': False,
|
||||||
|
'is_vpn': False,
|
||||||
|
'is_tor': False,
|
||||||
|
'threat_score': 0,
|
||||||
|
'threat_level': 'unknown',
|
||||||
|
'country': None,
|
||||||
|
'country_code': None,
|
||||||
|
'asn': None,
|
||||||
|
'asn_org': None,
|
||||||
|
'org': None,
|
||||||
|
'city': None,
|
||||||
|
'warnings': []
|
||||||
|
}
|
||||||
|
|
||||||
|
threat_score = 0
|
||||||
|
|
||||||
|
for source_name, source_data in sources.items():
|
||||||
|
if 'error' in source_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Proxy detection
|
||||||
|
if source_data.get('is_proxy'):
|
||||||
|
aggregated['is_proxy'] = True
|
||||||
|
threat_score += 30
|
||||||
|
aggregated['warnings'].append(f'{source_name}: Proxy détecté')
|
||||||
|
|
||||||
|
# Hosting detection
|
||||||
|
if source_data.get('is_hosting'):
|
||||||
|
aggregated['is_hosting'] = True
|
||||||
|
threat_score += 20
|
||||||
|
aggregated['warnings'].append(f'{source_name}: Hébergement cloud/datacenter')
|
||||||
|
|
||||||
|
# VPN detection (si disponible)
|
||||||
|
if source_data.get('is_vpn'):
|
||||||
|
aggregated['is_vpn'] = True
|
||||||
|
threat_score += 40
|
||||||
|
aggregated['warnings'].append(f'{source_name}: VPN détecté')
|
||||||
|
|
||||||
|
# Tor detection (si disponible)
|
||||||
|
if source_data.get('is_tor'):
|
||||||
|
aggregated['is_tor'] = True
|
||||||
|
threat_score += 50
|
||||||
|
aggregated['warnings'].append(f'{source_name}: Exit node Tor détecté')
|
||||||
|
|
||||||
|
# Infos géographiques (prend la première disponible)
|
||||||
|
if not aggregated['country'] and source_data.get('country'):
|
||||||
|
aggregated['country'] = source_data.get('country')
|
||||||
|
|
||||||
|
if not aggregated['country_code'] and source_data.get('country_code'):
|
||||||
|
aggregated['country_code'] = source_data.get('country_code')
|
||||||
|
|
||||||
|
# ASN (prend la première disponible)
|
||||||
|
if not aggregated['asn'] and source_data.get('asn'):
|
||||||
|
aggregated['asn'] = source_data.get('asn')
|
||||||
|
|
||||||
|
if not aggregated['asn_org'] and source_data.get('asn_org'):
|
||||||
|
aggregated['asn_org'] = source_data.get('asn_org')
|
||||||
|
|
||||||
|
# Organisation/ISP
|
||||||
|
if not aggregated['org'] and source_data.get('org'):
|
||||||
|
aggregated['org'] = source_data.get('org')
|
||||||
|
|
||||||
|
# Ville
|
||||||
|
if not aggregated['city'] and source_data.get('city'):
|
||||||
|
aggregated['city'] = source_data.get('city')
|
||||||
|
|
||||||
|
# Calcul du niveau de menace
|
||||||
|
aggregated['threat_score'] = min(100, threat_score)
|
||||||
|
|
||||||
|
if threat_score >= 80:
|
||||||
|
aggregated['threat_level'] = 'critical'
|
||||||
|
elif threat_score >= 60:
|
||||||
|
aggregated['threat_level'] = 'high'
|
||||||
|
elif threat_score >= 40:
|
||||||
|
aggregated['threat_level'] = 'medium'
|
||||||
|
elif threat_score >= 20:
|
||||||
|
aggregated['threat_level'] = 'low'
|
||||||
|
else:
|
||||||
|
aggregated['threat_level'] = 'clean'
|
||||||
|
|
||||||
|
return aggregated
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Ferme le client HTTP"""
|
||||||
|
await self.http_client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton pour réutiliser le service
|
||||||
|
_reputation_service: Optional[IPReputationService] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_reputation_service() -> IPReputationService:
|
||||||
|
"""Retourne l'instance singleton du service de réputation"""
|
||||||
|
global _reputation_service
|
||||||
|
if _reputation_service is None:
|
||||||
|
_reputation_service = IPReputationService()
|
||||||
|
return _reputation_service
|
||||||
@ -6,6 +6,7 @@ import { UserAgentAnalysis } from './analysis/UserAgentAnalysis';
|
|||||||
import { CorrelationSummary } from './analysis/CorrelationSummary';
|
import { CorrelationSummary } from './analysis/CorrelationSummary';
|
||||||
import { CorrelationGraph } from './CorrelationGraph';
|
import { CorrelationGraph } from './CorrelationGraph';
|
||||||
import { InteractiveTimeline } from './InteractiveTimeline';
|
import { InteractiveTimeline } from './InteractiveTimeline';
|
||||||
|
import { ReputationPanel } from './ReputationPanel';
|
||||||
|
|
||||||
export function InvestigationView() {
|
export function InvestigationView() {
|
||||||
const { ip } = useParams<{ ip: string }>();
|
const { ip } = useParams<{ ip: string }>();
|
||||||
@ -46,6 +47,12 @@ export function InvestigationView() {
|
|||||||
|
|
||||||
{/* Panels d'analyse */}
|
{/* Panels d'analyse */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* NOUVEAU: Réputation IP */}
|
||||||
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-text-primary mb-4">🌍 Réputation IP (Bases publiques)</h3>
|
||||||
|
<ReputationPanel ip={ip || ''} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* NOUVEAU: Graph de corrélations */}
|
{/* NOUVEAU: Graph de corrélations */}
|
||||||
<div className="bg-background-secondary rounded-lg p-6">
|
<div className="bg-background-secondary rounded-lg p-6">
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-4">🕸️ Graph de Corrélations</h3>
|
<h3 className="text-lg font-medium text-text-primary mb-4">🕸️ Graph de Corrélations</h3>
|
||||||
|
|||||||
215
frontend/src/components/ReputationPanel.tsx
Normal file
215
frontend/src/components/ReputationPanel.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ReputationData {
|
||||||
|
ip: string;
|
||||||
|
timestamp: string;
|
||||||
|
sources: {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
aggregated: {
|
||||||
|
is_proxy: boolean;
|
||||||
|
is_hosting: boolean;
|
||||||
|
is_vpn: boolean;
|
||||||
|
is_tor: boolean;
|
||||||
|
threat_score: number;
|
||||||
|
threat_level: 'unknown' | 'clean' | 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
country: string | null;
|
||||||
|
country_code: string | null;
|
||||||
|
asn: string | null;
|
||||||
|
asn_org: string | null;
|
||||||
|
org: string | null;
|
||||||
|
city: string | null;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReputationPanelProps {
|
||||||
|
ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReputationPanel({ ip }: ReputationPanelProps) {
|
||||||
|
const [reputation, setReputation] = useState<ReputationData | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchReputation = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/reputation/ip/${encodeURIComponent(ip)}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setReputation(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ip) {
|
||||||
|
fetchReputation();
|
||||||
|
}
|
||||||
|
}, [ip]);
|
||||||
|
|
||||||
|
const getThreatLevelColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'critical': return 'text-red-500 bg-red-500/10 border-red-500';
|
||||||
|
case 'high': return 'text-orange-500 bg-orange-500/10 border-orange-500';
|
||||||
|
case 'medium': return 'text-yellow-500 bg-yellow-500/10 border-yellow-500';
|
||||||
|
case 'low': return 'text-blue-500 bg-blue-500/10 border-blue-500';
|
||||||
|
case 'clean': return 'text-green-500 bg-green-500/10 border-green-500';
|
||||||
|
default: return 'text-gray-500 bg-gray-500/10 border-gray-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThreatLevelLabel = (level: string) => {
|
||||||
|
const labels: { [key: string]: string } = {
|
||||||
|
'critical': '🔴 Critique',
|
||||||
|
'high': '🟠 Élevé',
|
||||||
|
'medium': '🟡 Moyen',
|
||||||
|
'low': '🔵 Faible',
|
||||||
|
'clean': '🟢 Propre',
|
||||||
|
'unknown': '⚪ Inconnu'
|
||||||
|
};
|
||||||
|
return labels[level] || level;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-text-secondary">
|
||||||
|
Vérification de la réputation...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-red-500">
|
||||||
|
Erreur: {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reputation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { aggregated } = reputation;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Threat Level Badge */}
|
||||||
|
<div className={`p-4 rounded-lg border ${getThreatLevelColor(aggregated.threat_level)}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium mb-1">Niveau de menace</div>
|
||||||
|
<div className="text-2xl font-bold">{getThreatLevelLabel(aggregated.threat_level)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-medium mb-1">Score</div>
|
||||||
|
<div className="text-2xl font-bold">{aggregated.threat_score}/100</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="mt-3 w-full bg-background-secondary rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all ${
|
||||||
|
aggregated.threat_score >= 80 ? 'bg-red-500' :
|
||||||
|
aggregated.threat_score >= 60 ? 'bg-orange-500' :
|
||||||
|
aggregated.threat_score >= 40 ? 'bg-yellow-500' :
|
||||||
|
aggregated.threat_score >= 20 ? 'bg-blue-500' :
|
||||||
|
'bg-green-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${aggregated.threat_score}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detection Badges */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<DetectionBadge
|
||||||
|
label="Proxy"
|
||||||
|
detected={aggregated.is_proxy}
|
||||||
|
icon="🌐"
|
||||||
|
/>
|
||||||
|
<DetectionBadge
|
||||||
|
label="Hosting"
|
||||||
|
detected={aggregated.is_hosting}
|
||||||
|
icon="☁️"
|
||||||
|
/>
|
||||||
|
<DetectionBadge
|
||||||
|
label="VPN"
|
||||||
|
detected={aggregated.is_vpn}
|
||||||
|
icon="🔒"
|
||||||
|
/>
|
||||||
|
<DetectionBadge
|
||||||
|
label="Tor"
|
||||||
|
detected={aggregated.is_tor}
|
||||||
|
icon="🧅"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<InfoField label="Pays" value={aggregated.country || '-'} />
|
||||||
|
<InfoField label="Ville" value={aggregated.city || '-'} />
|
||||||
|
<InfoField label="ASN" value={aggregated.asn ? `AS${aggregated.asn}` : '-'} />
|
||||||
|
<InfoField label="Organisation" value={aggregated.org || aggregated.asn_org || '-'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{aggregated.warnings.length > 0 && (
|
||||||
|
<div className="bg-orange-500/10 border border-orange-500 rounded-lg p-3">
|
||||||
|
<div className="text-sm font-medium text-orange-500 mb-2">
|
||||||
|
⚠️ Avertissements ({aggregated.warnings.length})
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{aggregated.warnings.map((warning, index) => (
|
||||||
|
<li key={index} className="text-xs text-text-secondary">
|
||||||
|
• {warning}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sources */}
|
||||||
|
<div className="text-xs text-text-secondary text-center">
|
||||||
|
Sources: {Object.keys(reputation.sources).join(', ')} • {new Date(reputation.timestamp).toLocaleString('fr-FR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetectionBadge({ label, detected, icon }: { label: string; detected: boolean; icon: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`p-2 rounded-lg border text-center ${
|
||||||
|
detected
|
||||||
|
? 'bg-red-500/10 border-red-500 text-red-500'
|
||||||
|
: 'bg-background-card border-background-card text-text-secondary'
|
||||||
|
}`}>
|
||||||
|
<div className="text-lg mb-1">{icon}</div>
|
||||||
|
<div className="text-xs font-medium">{label}</div>
|
||||||
|
<div className={`text-xs font-bold ${detected ? 'text-red-500' : 'text-text-secondary'}`}>
|
||||||
|
{detected ? 'DÉTECTÉ' : 'Non'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoField({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-background-card rounded-lg p-2">
|
||||||
|
<div className="text-xs text-text-secondary mb-1">{label}</div>
|
||||||
|
<div className="text-sm text-text-primary font-medium truncate" title={value}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,3 +4,4 @@ clickhouse-connect==0.8.0
|
|||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
|
httpx==0.26.0
|
||||||
|
|||||||
Reference in New Issue
Block a user