feat(dashboard): thème auto, config centralisée, dates UTC→TZ navigateur, tooltip Anubis
- ThemeContext: thème par défaut 'auto' (suit prefers-color-scheme du navigateur) - config.ts: fichier de configuration centrale (API_BASE_URL, DEFAULT_THEME, PAGE_SIZES, seuils, description du mécanisme d'identification Anubis) - dateUtils.ts: utilitaire partagé formatDate/formatDateShort/formatDateOnly/ formatTimeOnly/formatNumber — convertit les dates UTC ClickHouse dans le fuseau horaire et la locale du navigateur (plus de 'fr-FR' hardcodé) - tooltips.ts: ajout TIPS.anubis_identification — explique que les bots sont identifiés par UA (regex), IP/CIDR, ASN, pays via les règles Anubis - DetectionsList: colonne Anubis avec icône ⓘ affichant le tooltip explicatif - DataTable: Column.label étendu à React.ReactNode (pour JSX dans les headers) - 24 composants mis à jour: fr-FR remplacé par locale navigateur partout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { CONFIG } from './config';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'auto';
|
||||
|
||||
@ -9,12 +10,12 @@ interface ThemeContextValue {
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
theme: 'dark',
|
||||
theme: CONFIG.DEFAULT_THEME,
|
||||
resolved: 'dark',
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
const STORAGE_KEY = 'soc_theme';
|
||||
const STORAGE_KEY = CONFIG.THEME_STORAGE_KEY;
|
||||
|
||||
function resolveTheme(theme: Theme): 'dark' | 'light' {
|
||||
if (theme === 'auto') {
|
||||
@ -26,11 +27,11 @@ function resolveTheme(theme: Theme): 'dark' | 'light' {
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||
return stored ?? 'dark'; // SOC default: dark
|
||||
return stored ?? CONFIG.DEFAULT_THEME;
|
||||
});
|
||||
|
||||
const [resolved, setResolved] = useState<'dark' | 'light'>(() => resolveTheme(
|
||||
(localStorage.getItem(STORAGE_KEY) as Theme | null) ?? 'dark'
|
||||
(localStorage.getItem(STORAGE_KEY) as Theme | null) ?? CONFIG.DEFAULT_THEME
|
||||
));
|
||||
|
||||
const applyTheme = (t: Theme) => {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
import { CONFIG } from '../config';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: CONFIG.API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@ -60,6 +59,9 @@ export interface Detection {
|
||||
client_headers: string;
|
||||
asn_score?: number | null;
|
||||
asn_rep_label?: string;
|
||||
anubis_bot_name?: string;
|
||||
anubis_bot_action?: string;
|
||||
anubis_bot_category?: string;
|
||||
}
|
||||
|
||||
export interface DetectionsListResponse {
|
||||
|
||||
@ -30,7 +30,7 @@ type SortField = 'unique_ips' | 'unique_countries' | 'targeted_hosts';
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function getCountryFlag(code: string): string {
|
||||
|
||||
@ -34,7 +34,7 @@ type ActiveTab = 'targets' | 'attackers' | 'timeline';
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
@ -855,7 +855,7 @@ interface BotnetCountryEntry {
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function getCountryFlag(code: string): string {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useVariability } from '../hooks/useVariability';
|
||||
import { VariabilityPanel } from './VariabilityPanel';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
export function DetailsView() {
|
||||
const { type, value } = useParams<{ type: string; value: string }>();
|
||||
@ -48,10 +49,7 @@ export function DetailsView() {
|
||||
const last = data.date_range.last_seen ? new Date(data.date_range.last_seen) : null;
|
||||
const sameDate = first && last && first.getTime() === last.getTime();
|
||||
|
||||
const fmtDate = (d: Date) =>
|
||||
d.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }) +
|
||||
' ' +
|
||||
d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||
const fmtDate = (d: Date) => formatDateShort(d.toISOString());
|
||||
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
|
||||
@ -2,6 +2,9 @@ import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useDetections } from '../hooks/useDetections';
|
||||
import DataTable, { Column } from './ui/DataTable';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDate, formatDateOnly, formatTimeOnly } from '../utils/dateUtils';
|
||||
|
||||
type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity';
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
@ -33,6 +36,9 @@ interface DetectionRow {
|
||||
unique_ja4s?: string[];
|
||||
unique_hosts?: string[];
|
||||
unique_client_headers?: string[];
|
||||
anubis_bot_name?: string;
|
||||
anubis_bot_action?: string;
|
||||
anubis_bot_category?: string;
|
||||
}
|
||||
|
||||
export function DetectionsList() {
|
||||
@ -65,6 +71,7 @@ export function DetectionsList() {
|
||||
{ key: 'client_headers', label: 'Client Headers', visible: false, sortable: false },
|
||||
{ key: 'model_name', label: 'Modèle', visible: true, sortable: true },
|
||||
{ key: 'anomaly_score', label: 'Score', visible: true, sortable: true },
|
||||
{ key: 'anubis', label: '🤖 Anubis', visible: true, sortable: false },
|
||||
{ key: 'hits', label: 'Hits', visible: true, sortable: true },
|
||||
{ key: 'hit_velocity', label: 'Velocity', visible: true, sortable: true },
|
||||
{ key: 'asn', label: 'ASN', visible: true, sortable: true },
|
||||
@ -236,6 +243,38 @@ export function DetectionsList() {
|
||||
sortable: true,
|
||||
render: (_, row) => <ModelBadge model={row.model_name} />,
|
||||
};
|
||||
case 'anubis':
|
||||
return {
|
||||
key: 'anubis_bot_name',
|
||||
label: (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
🤖 Anubis
|
||||
<InfoTip content={TIPS.anubis_identification} />
|
||||
</span>
|
||||
),
|
||||
sortable: false,
|
||||
render: (_, row) => {
|
||||
const name = row.anubis_bot_name;
|
||||
const action = row.anubis_bot_action;
|
||||
const category = row.anubis_bot_category;
|
||||
if (!name) return <span className="text-text-disabled text-xs">—</span>;
|
||||
const actionColor =
|
||||
action === 'ALLOW' ? 'bg-green-500/15 text-green-400 border-green-500/30' :
|
||||
action === 'DENY' ? 'bg-red-500/15 text-red-400 border-red-500/30' :
|
||||
'bg-yellow-500/15 text-yellow-400 border-yellow-500/30';
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${actionColor}`}>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{action && <span className="text-[10px] text-text-secondary">{action}</span>}
|
||||
{category && <span className="text-[10px] text-text-disabled">· {category}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
case 'anomaly_score':
|
||||
return {
|
||||
key: 'anomaly_score',
|
||||
@ -313,8 +352,7 @@ export function DetectionsList() {
|
||||
const first = new Date(row.first_seen!);
|
||||
const last = new Date(row.last_seen!);
|
||||
const sameTime = first.getTime() === last.getTime();
|
||||
const fmt = (d: Date) =>
|
||||
`${d.toLocaleDateString('fr-FR')} ${d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
const fmt = (d: Date) => formatDate(d.toISOString());
|
||||
return sameTime ? (
|
||||
<div className="text-xs text-text-secondary">{fmt(last)}</div>
|
||||
) : (
|
||||
@ -330,10 +368,10 @@ export function DetectionsList() {
|
||||
})() : (
|
||||
<>
|
||||
<div className="text-sm text-text-primary">
|
||||
{new Date(row.detected_at).toLocaleDateString('fr-FR')}
|
||||
{formatDateOnly(row.detected_at)}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">
|
||||
{new Date(row.detected_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
{formatTimeOnly(row.detected_at)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
@ -2,6 +2,7 @@ import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDateOnly } from '../utils/dateUtils';
|
||||
|
||||
interface EntityStats {
|
||||
entity_type: string;
|
||||
@ -162,11 +163,11 @@ export function EntityInvestigationView() {
|
||||
/>
|
||||
<StatCard
|
||||
label="Première Détection"
|
||||
value={new Date(data.stats.first_seen).toLocaleDateString('fr-FR')}
|
||||
value={formatDateOnly(data.stats.first_seen)}
|
||||
/>
|
||||
<StatCard
|
||||
label="Dernière Détection"
|
||||
value={new Date(data.stats.last_seen).toLocaleDateString('fr-FR')}
|
||||
value={formatDateOnly(data.stats.last_seen)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import DataTable, { Column } from './ui/DataTable';
|
||||
import ThreatBadge from './ui/ThreatBadge';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatNumber as fmtNum, formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -129,7 +130,7 @@ function getCountryFlag(code: string): string {
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return fmtNum(n);
|
||||
}
|
||||
|
||||
function botUaPercentage(userAgents: AttributeValue[]): number {
|
||||
@ -1463,7 +1464,7 @@ type RotationSubTab = 'rotators' | 'persistent' | 'sophistication' | 'hunt';
|
||||
function formatDate(iso: string): string {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
return formatDateShort(iso);
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ interface ClusterIP {
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function mismatchColor(pct: number): string {
|
||||
|
||||
@ -25,7 +25,7 @@ interface HeatmapMatrix {
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function heatmapCellStyle(value: number, maxValue: number): React.CSSProperties {
|
||||
|
||||
@ -161,8 +161,8 @@ export function IncidentsView() {
|
||||
<span className="text-xl">{icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-text-disabled uppercase tracking-wide flex items-center gap-1">{label}<InfoTip content={tip} /></div>
|
||||
<div className="text-xl font-bold text-text-primary">{m.today.toLocaleString('fr-FR')}</div>
|
||||
<div className="text-xs text-text-secondary">hier: {m.yesterday.toLocaleString('fr-FR')}</div>
|
||||
<div className="text-xl font-bold text-text-primary">{m.today.toLocaleString(navigator.language || undefined)}</div>
|
||||
<div className="text-xs text-text-secondary">hier: {m.yesterday.toLocaleString(navigator.language || undefined)}</div>
|
||||
</div>
|
||||
<div className={`text-sm font-bold px-2 py-1 rounded ${
|
||||
neutral ? 'text-text-disabled' :
|
||||
|
||||
@ -131,7 +131,7 @@ function IPActivitySummary({ ip }: { ip: string }) {
|
||||
{data.bruteforce.active && (
|
||||
<div className="bg-background-card rounded p-2">
|
||||
<div className="text-text-disabled mb-1">Brute Force</div>
|
||||
<div className="text-threat-high font-medium">{data.bruteforce.total_hits.toLocaleString('fr-FR')} hits</div>
|
||||
<div className="text-threat-high font-medium">{data.bruteforce.total_hits.toLocaleString(navigator.language || undefined)} hits</div>
|
||||
<div className="text-text-secondary truncate" title={data.bruteforce.top_hosts.join(', ')}>
|
||||
{data.bruteforce.top_hosts[0] ?? '—'}
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
import { JA4CorrelationSummary } from './analysis/JA4CorrelationSummary';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
interface JA4InvestigationData {
|
||||
ja4: string;
|
||||
@ -331,11 +332,5 @@ function StatBox({ label, value, tip }: { label: string; value: string; tip?: st
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return formatDateShort(dateStr);
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ interface ScatterPoint {
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function attackTypeEmoji(type: string): string {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatDate } from '../utils/dateUtils';
|
||||
|
||||
interface ReputationData {
|
||||
ip: string;
|
||||
@ -181,7 +182,7 @@ export function ReputationPanel({ ip }: ReputationPanelProps) {
|
||||
|
||||
{/* Sources */}
|
||||
<div className="text-xs text-text-secondary text-center">
|
||||
Sources: {Object.keys(reputation.sources).join(', ')} • {new Date(reputation.timestamp).toLocaleString('fr-FR')}
|
||||
Sources: {Object.keys(reputation.sources).join(', ')} • {formatDate(reputation.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -51,13 +52,13 @@ type ActiveTab = 'rotators' | 'persistent' | 'sophistication' | 'hunt';
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
return formatDateShort(iso);
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
interface SubnetIP {
|
||||
ip: string;
|
||||
@ -77,13 +78,7 @@ export function SubnetInvestigation() {
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return formatDateShort(dateString);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
||||
@ -52,7 +52,7 @@ type ActiveTab = 'detections' | 'matrix';
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n.toLocaleString('fr-FR');
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
|
||||
function confidenceBar(conf: number): JSX.Element {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDateShort } from '../utils/dateUtils';
|
||||
|
||||
interface Classification {
|
||||
ip?: string;
|
||||
@ -239,9 +240,7 @@ export function ThreatIntelView() {
|
||||
{filteredClassifications.slice(0, 50).map((classification, idx) => (
|
||||
<tr key={idx} className="hover:bg-background-card/50 transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">
|
||||
{new Date(classification.created_at).toLocaleDateString('fr-FR', {
|
||||
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||
})}
|
||||
{formatDateShort(classification.created_at)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-mono text-sm text-text-primary">
|
||||
|
||||
@ -4,7 +4,7 @@ import { InfoTip } from './Tooltip';
|
||||
|
||||
export interface Column<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
tooltip?: string;
|
||||
sortable?: boolean;
|
||||
align?: 'left' | 'right' | 'center';
|
||||
|
||||
@ -398,4 +398,18 @@ export const TIPS = {
|
||||
'Nœud de corrélation : entité reliée à l\'IP analysée.\n' +
|
||||
'Les connexions (arêtes) représentent des relations directes\n' +
|
||||
'(même subnet, même ASN, même JA4, même host cible).',
|
||||
|
||||
anubis_identification:
|
||||
'Identification des bots par les règles Anubis\n' +
|
||||
'(github.com/TecharoHQ/anubis) :\n\n' +
|
||||
'• User-Agent : correspondance par expression régulière\n' +
|
||||
' (ex. Googlebot, GPTBot, AhrefsBot…)\n' +
|
||||
'• IP / CIDR : plages d\'adresses connues des crawlers\n' +
|
||||
'• ASN : numéro de système autonome (ex. AS15169 = Google)\n' +
|
||||
'• Pays : code ISO du pays source\n\n' +
|
||||
'La règle la plus spécifique prend la priorité.\n\n' +
|
||||
'Actions :\n' +
|
||||
' ALLOW → bot légitime, exclu de l\'analyse ML\n' +
|
||||
' DENY → menace connue, flaggée directement\n' +
|
||||
' WEIGH → suspect, scoré par l\'IsolationForest',
|
||||
};
|
||||
|
||||
56
frontend/src/config.ts
Normal file
56
frontend/src/config.ts
Normal file
@ -0,0 +1,56 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Configuration centrale du dashboard JA4 SOC
|
||||
// Toutes les valeurs modifiables sont regroupées ici.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const CONFIG = {
|
||||
// ── API ──────────────────────────────────────────────────────────────────
|
||||
/** URL de base de l'API backend (relative, proxifiée par Vite en dev) */
|
||||
API_BASE_URL: '/api' as const,
|
||||
|
||||
// ── Thème ─────────────────────────────────────────────────────────────────
|
||||
/** Thème appliqué au premier chargement si aucune préférence n'est sauvegardée.
|
||||
* 'auto' = suit prefers-color-scheme du navigateur */
|
||||
DEFAULT_THEME: 'auto' as 'dark' | 'light' | 'auto',
|
||||
|
||||
/** Clé localStorage pour la préférence de thème */
|
||||
THEME_STORAGE_KEY: 'soc_theme',
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────
|
||||
/** Taille de page par défaut pour les listes */
|
||||
DEFAULT_PAGE_SIZE: 50,
|
||||
|
||||
/** Tailles de page disponibles */
|
||||
PAGE_SIZES: [25, 50, 100, 250] as const,
|
||||
|
||||
// ── Rafraîchissement ──────────────────────────────────────────────────────
|
||||
/** Intervalle de rafraîchissement automatique des métriques (ms). 0 = désactivé */
|
||||
METRICS_REFRESH_MS: 0,
|
||||
|
||||
// ── Détections ────────────────────────────────────────────────────────────
|
||||
/** Score d'anomalie en-dessous duquel une IP est considérée critique */
|
||||
CRITICAL_THRESHOLD: -0.5,
|
||||
|
||||
/** Score d'anomalie en-dessous duquel une IP est considérée HIGH */
|
||||
HIGH_THRESHOLD: -0.3,
|
||||
|
||||
// ── Anubis ────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Les bots sont identifiés par les règles Anubis (https://github.com/TecharoHQ/anubis).
|
||||
* Chaque règle peut correspondre sur :
|
||||
* - User-Agent (expression régulière)
|
||||
* - Adresse IP ou plage CIDR (IP_TRIE ClickHouse)
|
||||
* - Numéro ASN (Autonomous System Number)
|
||||
* - Code pays
|
||||
* La règle la plus spécifique (ID le plus bas dans le REGEXP_TREE) est appliquée en premier.
|
||||
* Actions possibles :
|
||||
* ALLOW → bot légitime identifié (Googlebot, Bingbot…) — exclu de l'analyse IF
|
||||
* DENY → menace connue — flaggée directement, bypass IsolationForest
|
||||
* WEIGH → trafic suspect — scoré par l'IsolationForest avec signal anubis_is_flagged=1
|
||||
*/
|
||||
ANUBIS_RULES_URL: 'https://github.com/TecharoHQ/anubis/tree/main/data',
|
||||
|
||||
// ── Application ───────────────────────────────────────────────────────────
|
||||
APP_NAME: 'JA4 SOC Dashboard',
|
||||
APP_VERSION: '12',
|
||||
} as const;
|
||||
86
frontend/src/utils/dateUtils.ts
Normal file
86
frontend/src/utils/dateUtils.ts
Normal file
@ -0,0 +1,86 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Utilitaires de formatage des dates et des nombres
|
||||
//
|
||||
// Les dates sont stockées en UTC dans ClickHouse (sans suffixe TZ).
|
||||
// Ces fonctions les convertissent dans le fuseau horaire local du navigateur
|
||||
// et utilisent la locale du navigateur pour l'affichage.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalise une chaîne datetime ClickHouse (sans TZ) en Date UTC.
|
||||
* ClickHouse retourne "2024-01-15 14:32:00" → on force Z pour UTC.
|
||||
*/
|
||||
function parseUTC(iso: string): Date {
|
||||
if (!iso) return new Date(NaN);
|
||||
// Déjà un ISO complet avec TZ → pas de modification
|
||||
if (iso.endsWith('Z') || iso.includes('+')) return new Date(iso);
|
||||
// "2024-01-15 14:32:00" ou "2024-01-15T14:32:00" → forcer UTC
|
||||
const normalized = iso.replace(' ', 'T');
|
||||
return new Date(normalized + 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date/heure complète dans la locale et le fuseau du navigateur.
|
||||
* Exemple : "15/01/2024, 15:32" (fr) ou "1/15/2024, 3:32 PM" (en-US)
|
||||
*/
|
||||
export function formatDate(iso: string): string {
|
||||
const d = parseUTC(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString(navigator.language || undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date courte (jour/mois heure:min) pour les tableaux.
|
||||
* Exemple : "15/01 15:32"
|
||||
*/
|
||||
export function formatDateShort(iso: string): string {
|
||||
const d = parseUTC(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString(navigator.language || undefined, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate uniquement la partie date.
|
||||
* Exemple : "15/01/2024"
|
||||
*/
|
||||
export function formatDateOnly(iso: string): string {
|
||||
const d = parseUTC(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString(navigator.language || undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate uniquement l'heure (heure:min).
|
||||
* Exemple : "15:32"
|
||||
*/
|
||||
export function formatTimeOnly(iso: string): string {
|
||||
const d = parseUTC(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleTimeString(navigator.language || undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un nombre entier avec séparateurs de milliers selon la locale du navigateur.
|
||||
* Exemple : 1234567 → "1 234 567" (fr) ou "1,234,567" (en-US)
|
||||
*/
|
||||
export function formatNumber(n: number): string {
|
||||
return n.toLocaleString(navigator.language || undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user