Add score_type filter and detection attributes section
- Backend: Add score_type query parameter to filter detections by threat level (BOT, REGLE, BOT_REGLE, SCORE) - Frontend: Add score_type dropdown filter in DetectionsList component - Frontend: Add IP detection route redirect (/detections/ip/:ip → /investigation/:ip) - Frontend: Add DetectionAttributesSection component showing variability metrics - API client: Update detectionsApi to support score_type parameter Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -302,6 +302,12 @@ function InvestigateRoute() {
|
||||
return <Navigate to={`/detections/${type}/${encodeURIComponent(decodedValue)}`} replace />;
|
||||
}
|
||||
|
||||
/** Redirige /detections/ip/:ip → /investigation/:ip */
|
||||
function IpDetectionPageRedirect() {
|
||||
const { ip } = useParams<{ ip: string }>();
|
||||
return <Navigate to={`/investigation/${encodeURIComponent(ip || '')}`} replace />;
|
||||
}
|
||||
|
||||
/** Redirige /investigation/ip/:ip → /investigation/:ip */
|
||||
function IpInvestigationRedirect() {
|
||||
const { ip } = useParams<{ ip: string }>();
|
||||
@ -377,6 +383,7 @@ function MainContent({ counts: _counts }: { counts: AlertCounts | null }) {
|
||||
<Route path="/rotation" element={<Navigate to="/fingerprints" replace />} />
|
||||
<Route path="/ml-features" element={<MLFeaturesView />} />
|
||||
<Route path="/detections" element={<DetectionsList />} />
|
||||
<Route path="/detections/ip/:ip" element={<IpDetectionPageRedirect />} />
|
||||
<Route path="/detections/:type/:value" element={<DetailsView />} />
|
||||
<Route path="/investigate" element={<DetectionsList />} />
|
||||
<Route path="/investigate/:type/:value" element={<InvestigateRoute />} />
|
||||
|
||||
@ -140,6 +140,7 @@ export const detectionsApi = {
|
||||
sort_by?: string;
|
||||
sort_order?: string;
|
||||
group_by_ip?: boolean;
|
||||
score_type?: string;
|
||||
}) => api.get<DetectionsListResponse>('/detections', { params }),
|
||||
|
||||
getDetails: (id: string) => api.get(`/detections/${encodeURIComponent(id)}`),
|
||||
|
||||
@ -52,6 +52,7 @@ export function DetectionsList() {
|
||||
const search = searchParams.get('search') || undefined;
|
||||
const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'detected_at') as SortField;
|
||||
const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'desc') as SortOrder;
|
||||
const scoreType = searchParams.get('score_type') || undefined;
|
||||
|
||||
const [groupByIP, setGroupByIP] = useState(true);
|
||||
|
||||
@ -63,6 +64,7 @@ export function DetectionsList() {
|
||||
sort_by: sortField,
|
||||
sort_order: sortOrder,
|
||||
group_by_ip: groupByIP,
|
||||
score_type: scoreType,
|
||||
});
|
||||
|
||||
const [searchInput, setSearchInput] = useState(search || '');
|
||||
@ -468,7 +470,7 @@ export function DetectionsList() {
|
||||
|
||||
{/* Filtres */}
|
||||
<div className="bg-background-secondary rounded-lg p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<select
|
||||
value={modelName || ''}
|
||||
onChange={(e) => handleFilterChange('model_name', e.target.value)}
|
||||
@ -479,7 +481,19 @@ export function DetectionsList() {
|
||||
<option value="Applicatif">Applicatif</option>
|
||||
</select>
|
||||
|
||||
{(modelName || search || sortField) && (
|
||||
<select
|
||||
value={scoreType || ''}
|
||||
onChange={(e) => handleFilterChange('score_type', e.target.value)}
|
||||
className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary focus:outline-none focus:border-accent-primary"
|
||||
>
|
||||
<option value="">Tous types de score</option>
|
||||
<option value="BOT">🟢 BOT seulement</option>
|
||||
<option value="REGLE">🔴 RÈGLE seulement</option>
|
||||
<option value="BOT_REGLE">BOT + RÈGLE</option>
|
||||
<option value="SCORE">Score numérique seulement</option>
|
||||
</select>
|
||||
|
||||
{(modelName || scoreType || search || sortField !== 'detected_at') && (
|
||||
<button
|
||||
onClick={() => setSearchParams({})}
|
||||
className="bg-background-card hover:bg-background-card/80 border border-background-card rounded-lg px-4 py-2 text-text-secondary hover:text-text-primary transition-colors"
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useVariability } from '../hooks/useVariability';
|
||||
import { VariabilityPanel } from './VariabilityPanel';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
import { SubnetAnalysis } from './analysis/SubnetAnalysis';
|
||||
import { CountryAnalysis } from './analysis/CountryAnalysis';
|
||||
import { JA4Analysis } from './analysis/JA4Analysis';
|
||||
@ -312,6 +315,93 @@ function FingerprintCoherenceWidget({ ip }: { ip: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Section "Attributs détectés" (données de variabilité, ex-DetailsView) ───
|
||||
|
||||
function Metric({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
|
||||
return (
|
||||
<div className="bg-background-card rounded-xl p-3">
|
||||
<p className="text-[10px] font-semibold text-text-secondary uppercase tracking-wider mb-1">{label}</p>
|
||||
<p className={`text-xl font-bold ${accent ? 'text-accent-primary' : 'text-text-primary'}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetectionAttributesSection({ ip }: { ip: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data, loading } = useVariability('ip', ip);
|
||||
|
||||
const first = data?.date_range.first_seen ? new Date(data.date_range.first_seen) : null;
|
||||
const last = data?.date_range.last_seen ? new Date(data.date_range.last_seen) : null;
|
||||
const sameDate = first && last && first.getTime() === last.getTime();
|
||||
const fmt = (d: Date) => formatDateShort(d.toISOString());
|
||||
|
||||
return (
|
||||
<div className="bg-background-secondary rounded-lg border border-border">
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 hover:bg-background-card/50 transition-colors"
|
||||
>
|
||||
<span className="font-semibold text-text-primary flex items-center gap-2">
|
||||
📋 Attributs détectés
|
||||
{data && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-accent-primary/20 text-accent-primary font-normal">
|
||||
{data.total_detections} détections · {data.attributes.user_agents?.length ?? 0} UA · {data.attributes.ja4?.length ?? 0} JA4
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-text-secondary">{open ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="px-5 pb-5 space-y-4">
|
||||
{loading && <div className="text-text-disabled text-sm py-4">Chargement…</div>}
|
||||
{data && (
|
||||
<>
|
||||
{/* Métriques */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<Metric label="Détections (24h)" value={data.total_detections.toLocaleString()} accent />
|
||||
<Metric label="User-Agents" value={(data.attributes.user_agents?.length ?? 0).toString()} />
|
||||
{first && last && (
|
||||
sameDate ? (
|
||||
<Metric label="Détecté le" value={fmt(last!)} />
|
||||
) : (
|
||||
<div className="bg-background-card rounded-xl p-3 col-span-2">
|
||||
<p className="text-[10px] font-semibold text-text-secondary uppercase tracking-wider mb-1">Période</p>
|
||||
<p className="text-xs text-text-primary font-medium">{fmt(first)}</p>
|
||||
<p className="text-[10px] text-text-secondary">→ {fmt(last!)}</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Insights */}
|
||||
{data.insights.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{data.insights.map((ins, i) => {
|
||||
const s: Record<string, string> = {
|
||||
warning: 'bg-yellow-500/10 border-yellow-500/40 text-yellow-400',
|
||||
info: 'bg-blue-500/10 border-blue-500/40 text-blue-400',
|
||||
success: 'bg-green-500/10 border-green-500/40 text-green-400',
|
||||
};
|
||||
return (
|
||||
<div key={i} className={`${s[ins.type] ?? s.info} border rounded-xl p-3 text-sm`}>
|
||||
{ins.message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attributs (JA4, hosts, ASN, pays, UA…) */}
|
||||
<VariabilityPanel attributes={data.attributes} hideAssociatedIPs />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InvestigationView() {
|
||||
const { ip } = useParams<{ ip: string }>();
|
||||
const navigate = useNavigate();
|
||||
@ -331,12 +421,21 @@ export function InvestigationView() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-xs text-text-secondary">
|
||||
<Link to="/" className="hover:text-text-primary">Dashboard</Link>
|
||||
<span>/</span>
|
||||
<Link to="/detections" className="hover:text-text-primary">Détections</Link>
|
||||
<span>/</span>
|
||||
<span className="text-text-primary font-mono">{ip}</span>
|
||||
</nav>
|
||||
|
||||
{/* En-tête */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<button
|
||||
onClick={() => navigate('/detections')}
|
||||
onClick={() => navigate(-1)}
|
||||
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
← Retour
|
||||
@ -349,6 +448,9 @@ export function InvestigationView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attributs détectés (ex-DetailsView) */}
|
||||
<DetectionAttributesSection ip={ip} />
|
||||
|
||||
{/* Ligne 0 : Synthèse multi-sources */}
|
||||
<IPActivitySummary ip={ip} />
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ interface UseDetectionsParams {
|
||||
sort_by?: string;
|
||||
sort_order?: string;
|
||||
group_by_ip?: boolean;
|
||||
score_type?: string;
|
||||
}
|
||||
|
||||
export function useDetections(params: UseDetectionsParams = {}) {
|
||||
@ -44,6 +45,7 @@ export function useDetections(params: UseDetectionsParams = {}) {
|
||||
params.sort_by,
|
||||
params.sort_order,
|
||||
params.group_by_ip,
|
||||
params.score_type,
|
||||
]);
|
||||
|
||||
return { data, loading, error };
|
||||
|
||||
Reference in New Issue
Block a user