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:
SOC Analyst
2026-03-20 09:09:17 +01:00
parent ee54034ffd
commit dbb9bb3f94
6 changed files with 144 additions and 6 deletions

View File

@ -20,7 +20,8 @@ async def get_detections(
search: Optional[str] = Query(None, description="Recherche texte (IP, JA4, Host)"), search: Optional[str] = Query(None, description="Recherche texte (IP, JA4, Host)"),
sort_by: str = Query("detected_at", description="Trier par"), sort_by: str = Query("detected_at", description="Trier par"),
sort_order: str = Query("DESC", description="Ordre (ASC/DESC)"), sort_order: str = Query("DESC", description="Ordre (ASC/DESC)"),
group_by_ip: bool = Query(False, description="Grouper par IP (first_seen/last_seen agrégés)") group_by_ip: bool = Query(False, description="Grouper par IP (first_seen/last_seen agrégés)"),
score_type: Optional[str] = Query(None, description="Filtrer par type de score: BOT, REGLE, BOT_REGLE, SCORE")
): ):
""" """
Récupère la liste des détections avec pagination et filtres Récupère la liste des détections avec pagination et filtres
@ -52,6 +53,17 @@ async def get_detections(
) )
params["search"] = f"%{search}%" params["search"] = f"%{search}%"
if score_type:
st = score_type.upper()
if st == "BOT":
where_clauses.append("threat_level = 'KNOWN_BOT'")
elif st == "REGLE":
where_clauses.append("threat_level = 'ANUBIS_DENY'")
elif st == "BOT_REGLE":
where_clauses.append("threat_level IN ('KNOWN_BOT', 'ANUBIS_DENY')")
elif st == "SCORE":
where_clauses.append("threat_level NOT IN ('KNOWN_BOT', 'ANUBIS_DENY')")
where_clause = " AND ".join(where_clauses) where_clause = " AND ".join(where_clauses)
# Requête de comptage # Requête de comptage

View File

@ -302,6 +302,12 @@ function InvestigateRoute() {
return <Navigate to={`/detections/${type}/${encodeURIComponent(decodedValue)}`} replace />; 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 */ /** Redirige /investigation/ip/:ip → /investigation/:ip */
function IpInvestigationRedirect() { function IpInvestigationRedirect() {
const { ip } = useParams<{ ip: string }>(); 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="/rotation" element={<Navigate to="/fingerprints" replace />} />
<Route path="/ml-features" element={<MLFeaturesView />} /> <Route path="/ml-features" element={<MLFeaturesView />} />
<Route path="/detections" element={<DetectionsList />} /> <Route path="/detections" element={<DetectionsList />} />
<Route path="/detections/ip/:ip" element={<IpDetectionPageRedirect />} />
<Route path="/detections/:type/:value" element={<DetailsView />} /> <Route path="/detections/:type/:value" element={<DetailsView />} />
<Route path="/investigate" element={<DetectionsList />} /> <Route path="/investigate" element={<DetectionsList />} />
<Route path="/investigate/:type/:value" element={<InvestigateRoute />} /> <Route path="/investigate/:type/:value" element={<InvestigateRoute />} />

View File

@ -140,6 +140,7 @@ export const detectionsApi = {
sort_by?: string; sort_by?: string;
sort_order?: string; sort_order?: string;
group_by_ip?: boolean; group_by_ip?: boolean;
score_type?: string;
}) => api.get<DetectionsListResponse>('/detections', { params }), }) => api.get<DetectionsListResponse>('/detections', { params }),
getDetails: (id: string) => api.get(`/detections/${encodeURIComponent(id)}`), getDetails: (id: string) => api.get(`/detections/${encodeURIComponent(id)}`),

View File

@ -52,6 +52,7 @@ export function DetectionsList() {
const search = searchParams.get('search') || undefined; const search = searchParams.get('search') || undefined;
const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'detected_at') as SortField; 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 sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'desc') as SortOrder;
const scoreType = searchParams.get('score_type') || undefined;
const [groupByIP, setGroupByIP] = useState(true); const [groupByIP, setGroupByIP] = useState(true);
@ -63,6 +64,7 @@ export function DetectionsList() {
sort_by: sortField, sort_by: sortField,
sort_order: sortOrder, sort_order: sortOrder,
group_by_ip: groupByIP, group_by_ip: groupByIP,
score_type: scoreType,
}); });
const [searchInput, setSearchInput] = useState(search || ''); const [searchInput, setSearchInput] = useState(search || '');
@ -468,7 +470,7 @@ export function DetectionsList() {
{/* Filtres */} {/* Filtres */}
<div className="bg-background-secondary rounded-lg p-4"> <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 <select
value={modelName || ''} value={modelName || ''}
onChange={(e) => handleFilterChange('model_name', e.target.value)} onChange={(e) => handleFilterChange('model_name', e.target.value)}
@ -479,7 +481,19 @@ export function DetectionsList() {
<option value="Applicatif">Applicatif</option> <option value="Applicatif">Applicatif</option>
</select> </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 <button
onClick={() => setSearchParams({})} 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" 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"

View File

@ -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 { useState, useEffect } from 'react';
import { useVariability } from '../hooks/useVariability';
import { VariabilityPanel } from './VariabilityPanel';
import { formatDateShort } from '../utils/dateUtils';
import { SubnetAnalysis } from './analysis/SubnetAnalysis'; import { SubnetAnalysis } from './analysis/SubnetAnalysis';
import { CountryAnalysis } from './analysis/CountryAnalysis'; import { CountryAnalysis } from './analysis/CountryAnalysis';
import { JA4Analysis } from './analysis/JA4Analysis'; 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() { export function InvestigationView() {
const { ip } = useParams<{ ip: string }>(); const { ip } = useParams<{ ip: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
@ -331,12 +421,21 @@ export function InvestigationView() {
return ( return (
<div className="space-y-6 animate-fade-in"> <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 */} {/* En-tête */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<div className="flex items-center gap-4 mb-2"> <div className="flex items-center gap-4 mb-2">
<button <button
onClick={() => navigate('/detections')} onClick={() => navigate(-1)}
className="text-text-secondary hover:text-text-primary transition-colors" className="text-text-secondary hover:text-text-primary transition-colors"
> >
← Retour ← Retour
@ -349,6 +448,9 @@ export function InvestigationView() {
</div> </div>
</div> </div>
{/* Attributs détectés (ex-DetailsView) */}
<DetectionAttributesSection ip={ip} />
{/* Ligne 0 : Synthèse multi-sources */} {/* Ligne 0 : Synthèse multi-sources */}
<IPActivitySummary ip={ip} /> <IPActivitySummary ip={ip} />

View File

@ -12,6 +12,7 @@ interface UseDetectionsParams {
sort_by?: string; sort_by?: string;
sort_order?: string; sort_order?: string;
group_by_ip?: boolean; group_by_ip?: boolean;
score_type?: string;
} }
export function useDetections(params: UseDetectionsParams = {}) { export function useDetections(params: UseDetectionsParams = {}) {
@ -44,6 +45,7 @@ export function useDetections(params: UseDetectionsParams = {}) {
params.sort_by, params.sort_by,
params.sort_order, params.sort_order,
params.group_by_ip, params.group_by_ip,
params.score_type,
]); ]);
return { data, loading, error }; return { data, loading, error };