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