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:
SOC Analyst
2026-03-15 18:15:01 +01:00
parent 776aa52241
commit 05d21ae8fb
7 changed files with 662 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import { UserAgentAnalysis } from './analysis/UserAgentAnalysis';
import { CorrelationSummary } from './analysis/CorrelationSummary';
import { CorrelationGraph } from './CorrelationGraph';
import { InteractiveTimeline } from './InteractiveTimeline';
import { ReputationPanel } from './ReputationPanel';
export function InvestigationView() {
const { ip } = useParams<{ ip: string }>();
@ -46,6 +47,12 @@ export function InvestigationView() {
{/* Panels d'analyse */}
<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 */}
<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>

View 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>
);
}