fix: correction de 7 bugs UI/API sur les dashboards avancés
BruteForce:
- Attaquants: strip ::ffff: des IPs (replaceRegexpAll dans SQL)
- Cibles: 'Voir détails' remplacé par expansion inline avec top IPs par host
+ nouveau endpoint GET /api/bruteforce/host/{host}/attackers
+ interface BruteForceTarget: top_ja4 → top_ja4s (cohérence avec API)
Header Fingerprint:
- Détail cluster: data.ips → data.items (clé API incorrecte)
Heatmap Temporelle:
- Top hosts ciblés: data.hosts → data.items (clé API incorrecte)
- Type annotation corrigé: { hosts: TopHost[] } → { items: TopHost[] }
Botnets Distribués:
- Clic sur ligne: data.countries → data.items (clé API incorrecte)
Rotation & Persistance:
- IPs rotateurs: strip ::ffff: (replaceRegexpAll dans SQL)
- IPs menaces persistantes: strip ::ffff: (replaceRegexpAll dans SQL)
- Historique JA4: data.history → data.ja4_history (clé API incorrecte)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -104,8 +104,8 @@ function BotnetRow({
|
||||
try {
|
||||
const res = await fetch(`/api/botnets/ja4/${encodeURIComponent(item.ja4)}/countries?limit=30`);
|
||||
if (!res.ok) throw new Error('Erreur chargement des pays');
|
||||
const data: { countries: CountryEntry[] } = await res.json();
|
||||
setCountries(data.countries ?? []);
|
||||
const data: { items: CountryEntry[] } = await res.json();
|
||||
setCountries(data.items ?? []);
|
||||
setCountriesLoaded(true);
|
||||
} catch (err) {
|
||||
setCountriesError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||
|
||||
@ -9,7 +9,7 @@ interface BruteForceTarget {
|
||||
total_hits: number;
|
||||
total_params: number;
|
||||
attack_type: string;
|
||||
top_ja4: string[];
|
||||
top_ja4s: string[];
|
||||
}
|
||||
|
||||
interface BruteForceAttacker {
|
||||
@ -63,6 +63,116 @@ function ErrorMessage({ message }: { message: string }) {
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
interface HostAttacker { ip: string; total_hits: number; total_params: number; ja4: string; attack_type: string; }
|
||||
|
||||
function TargetRow({ t, navigate }: { t: BruteForceTarget; navigate: (path: string) => void }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [hostAttackers, setHostAttackers] = useState<HostAttacker[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const toggle = async () => {
|
||||
setExpanded(prev => !prev);
|
||||
if (!loaded && !expanded) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bruteforce/host/${encodeURIComponent(t.host)}/attackers?limit=20`);
|
||||
if (!res.ok) throw new Error('Erreur chargement');
|
||||
const data: { items: HostAttacker[] } = await res.json();
|
||||
setHostAttackers(data.items ?? []);
|
||||
setLoaded(true);
|
||||
} catch { /* ignore */ }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className="border-b border-border hover:bg-background-card transition-colors cursor-pointer" onClick={toggle}>
|
||||
<td className="px-4 py-3 font-mono text-text-primary text-xs flex items-center gap-2">
|
||||
<span className="text-accent-primary">{expanded ? '▾' : '▸'}</span>
|
||||
{t.host}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.unique_ips)}</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.total_hits)}</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.total_params)}</td>
|
||||
<td className="px-4 py-3">
|
||||
{t.attack_type === 'credential_stuffing' ? (
|
||||
<span className="bg-threat-critical/20 text-threat-critical text-xs px-2 py-1 rounded-full">💳 Credential Stuffing</span>
|
||||
) : (
|
||||
<span className="bg-threat-high/20 text-threat-high text-xs px-2 py-1 rounded-full">🔍 Énumération</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(t.top_ja4s ?? []).slice(0, 2).map((ja4, i) => (
|
||||
<span key={i} className="font-mono text-xs bg-background-card px-1.5 py-0.5 rounded border border-border text-text-secondary">
|
||||
{ja4.slice(0, 12)}…
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{expanded && (
|
||||
<tr className="border-b border-border bg-background-card">
|
||||
<td colSpan={6} className="px-6 py-3">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 text-text-secondary text-sm py-2">
|
||||
<div className="w-4 h-4 border-2 border-accent-primary border-t-transparent rounded-full animate-spin" />
|
||||
Chargement des attaquants…
|
||||
</div>
|
||||
) : hostAttackers.length === 0 ? (
|
||||
<p className="text-text-disabled text-sm py-2">Aucun attaquant trouvé.</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-text-secondary text-xs mb-2 font-medium">
|
||||
Top {hostAttackers.length} IP attaquant <span className="text-accent-primary font-mono">{t.host}</span>
|
||||
</p>
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-text-disabled border-b border-border">
|
||||
<th className="text-left py-1 pr-4">IP</th>
|
||||
<th className="text-left py-1 pr-4">Hits</th>
|
||||
<th className="text-left py-1 pr-4">Params</th>
|
||||
<th className="text-left py-1 pr-4">JA4</th>
|
||||
<th className="text-left py-1 pr-4">Type</th>
|
||||
<th className="text-left py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hostAttackers.map((a) => (
|
||||
<tr key={a.ip} className="border-b border-border/50 hover:bg-background-secondary transition-colors">
|
||||
<td className="py-1.5 pr-4 font-mono text-text-primary">{a.ip}</td>
|
||||
<td className="py-1.5 pr-4 text-text-primary">{formatNumber(a.total_hits)}</td>
|
||||
<td className="py-1.5 pr-4 text-text-secondary">{formatNumber(a.total_params)}</td>
|
||||
<td className="py-1.5 pr-4 font-mono text-text-secondary">{a.ja4 ? a.ja4.slice(0, 16) + '…' : '—'}</td>
|
||||
<td className="py-1.5 pr-4">
|
||||
{a.attack_type === 'credential_stuffing'
|
||||
? <span className="text-threat-critical">💳</span>
|
||||
: <span className="text-threat-medium">🔍</span>
|
||||
}
|
||||
</td>
|
||||
<td className="py-1.5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); navigate(`/investigation/${a.ip}`); }}
|
||||
className="text-accent-primary hover:underline text-xs"
|
||||
>
|
||||
Investiguer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BruteForceView() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -202,51 +312,17 @@ export function BruteForceView() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-text-secondary text-left">
|
||||
<th className="px-4 py-3">Host</th>
|
||||
<th className="px-4 py-3">Host (cliquer pour détails)</th>
|
||||
<th className="px-4 py-3">IPs distinctes</th>
|
||||
<th className="px-4 py-3">Total hits</th>
|
||||
<th className="px-4 py-3">Params combos</th>
|
||||
<th className="px-4 py-3">Type d'attaque</th>
|
||||
<th className="px-4 py-3">Top JA4</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{targets.map((t) => (
|
||||
<tr key={t.host} className="border-b border-border hover:bg-background-card transition-colors">
|
||||
<td className="px-4 py-3 font-mono text-text-primary text-xs">{t.host}</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.unique_ips)}</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.total_hits)}</td>
|
||||
<td className="px-4 py-3 text-text-primary">{formatNumber(t.total_params)}</td>
|
||||
<td className="px-4 py-3">
|
||||
{t.attack_type === 'credential_stuffing' ? (
|
||||
<span className="bg-threat-critical/20 text-threat-critical text-xs px-2 py-1 rounded-full">
|
||||
💳 Credential Stuffing
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-threat-high/20 text-threat-high text-xs px-2 py-1 rounded-full">
|
||||
🔍 Énumération
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(t.top_ja4 ?? []).slice(0, 3).map((ja4) => (
|
||||
<span key={ja4} className="font-mono text-xs bg-background-card px-1.5 py-0.5 rounded border border-border text-text-secondary">
|
||||
{ja4.slice(0, 12)}…
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => navigate(`/investigation/${t.host}`)}
|
||||
className="text-xs bg-accent-primary/10 text-accent-primary px-3 py-1 rounded hover:bg-accent-primary/20 transition-colors"
|
||||
>
|
||||
Voir détails
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<TargetRow key={t.host} t={t} navigate={navigate} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -103,8 +103,8 @@ function ClusterRow({
|
||||
try {
|
||||
const res = await fetch(`/api/headers/cluster/${cluster.hash}/ips?limit=50`);
|
||||
if (!res.ok) throw new Error('Erreur chargement IPs');
|
||||
const data: { ips: ClusterIP[] } = await res.json();
|
||||
setClusterIPs(data.ips ?? []);
|
||||
const data: { items: ClusterIP[] } = await res.json();
|
||||
setClusterIPs(data.items ?? []);
|
||||
setIpsLoaded(true);
|
||||
} catch (err) {
|
||||
setIpsError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||
|
||||
@ -116,8 +116,8 @@ export function HeatmapView() {
|
||||
try {
|
||||
const res = await fetch('/api/heatmap/top-hosts?limit=20');
|
||||
if (!res.ok) throw new Error('Erreur chargement top hosts');
|
||||
const data: { hosts: TopHost[] } = await res.json();
|
||||
setTopHosts(data.hosts ?? []);
|
||||
const data: { items: TopHost[] } = await res.json();
|
||||
setTopHosts(data.items ?? []);
|
||||
} catch (err) {
|
||||
setTopHostsError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||
} finally {
|
||||
|
||||
@ -96,8 +96,8 @@ function RotatorRow({ item }: { item: JA4Rotator }) {
|
||||
try {
|
||||
const res = await fetch(`/api/rotation/ip/${encodeURIComponent(item.ip)}/ja4-history`);
|
||||
if (!res.ok) throw new Error('Erreur chargement historique JA4');
|
||||
const data: { history: JA4HistoryEntry[] } = await res.json();
|
||||
setHistory(data.history ?? []);
|
||||
const data: { ja4_history: JA4HistoryEntry[] } = await res.json();
|
||||
setHistory(data.ja4_history ?? []);
|
||||
setHistoryLoaded(true);
|
||||
} catch (err) {
|
||||
setHistoryError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||
|
||||
Reference in New Issue
Block a user