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:
@ -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>
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user