Bot-detector:
- B1.1: campaign_id and raw_anomaly_score now inserted into ml_detected_anomalies
- B1.4/B1.5: log_decision argument order fixed (cycle_id, name)
- B1.7: AE broadcast error — model now returns features list, scoring
uses model's features instead of current cycle's (prevents dim mismatch)
- B1.8: Anubis ALLOW bots now get bot_name from anubis_bot_name
Dashboard:
- C1.1: XSS in ip_detail.html — {{ ip | tojson }} instead of raw string
- C1.2: Stored XSS via innerHTML — added escapeHtml() helper, all user-facing
formatters (fmtIP, fmtASN, fmtCountry, fmtJA4, fmtBotName, fmtLabel) sanitized
- C2.1: status filter now correctly filters http_version column
- C2.2: heatmap toDayOfWeek() - 1 for 0-indexed JS days
SQL:
- B1.3: view_ip_recurrence worst_score uses max() not min() (0=normal, 1=anomal)
- B1.6: view_resource_cascade_1h joined into view_thesis_features_1h (§5.4)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
194 lines
9.9 KiB
HTML
194 lines
9.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}JA4 SOC Dashboard{% endblock %}</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
brand: { 50:'#eef2ff',100:'#e0e7ff',500:'#6366f1',600:'#4f46e5',700:'#4338ca',900:'#312e81' },
|
|
},
|
|
fontFamily: {
|
|
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
|
.threat-critical { color: #ef4444; font-weight: 700; }
|
|
.threat-high { color: #f97316; font-weight: 600; }
|
|
.threat-medium { color: #eab308; }
|
|
.threat-low { color: #22c55e; }
|
|
.threat-normal { color: #6b7280; }
|
|
.kpi-card { @apply bg-gray-800 rounded-xl p-5 border border-gray-700; }
|
|
.data-table { @apply w-full text-sm text-left; }
|
|
.data-table th { @apply px-4 py-3 bg-gray-800 text-gray-300 font-medium border-b border-gray-700 sticky top-0; }
|
|
.data-table td { @apply px-4 py-2.5 border-b border-gray-800 text-gray-300; }
|
|
.data-table tbody tr:hover { @apply bg-gray-800/50; }
|
|
.nav-link { @apply px-4 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors text-sm font-medium; }
|
|
.nav-link.active { @apply bg-brand-600 text-white; }
|
|
.badge { @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium; }
|
|
.badge-critical { @apply bg-red-500/20 text-red-400; }
|
|
.badge-high { @apply bg-orange-500/20 text-orange-400; }
|
|
.badge-medium { @apply bg-yellow-500/20 text-yellow-400; }
|
|
.badge-low { @apply bg-green-500/20 text-green-400; }
|
|
.badge-normal { @apply bg-gray-500/20 text-gray-400; }
|
|
.badge-known { @apply bg-blue-500/20 text-blue-400; }
|
|
.filter-btn { @apply px-3 py-1.5 text-xs rounded-lg border border-gray-700 text-gray-400 hover:border-brand-500 hover:text-brand-500 transition-colors cursor-pointer; }
|
|
.filter-btn.active { @apply border-brand-500 bg-brand-500/20 text-brand-500; }
|
|
|
|
/* Micro-animations */
|
|
@keyframes fadeUp {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-in { animation: fadeUp 0.4s ease-out both; }
|
|
.kpi-card { animation: fadeUp 0.4s ease-out both; }
|
|
.kpi-card:nth-child(1) { animation-delay: 0ms; }
|
|
.kpi-card:nth-child(2) { animation-delay: 50ms; }
|
|
.kpi-card:nth-child(3) { animation-delay: 100ms; }
|
|
.kpi-card:nth-child(4) { animation-delay: 150ms; }
|
|
.kpi-card:nth-child(5) { animation-delay: 200ms; }
|
|
.kpi-card:nth-child(6) { animation-delay: 250ms; }
|
|
|
|
/* Live status pulse */
|
|
@keyframes pulse-dot {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
.live-dot {
|
|
width: 6px; height: 6px;
|
|
background: #22c55e;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
animation: pulse-dot 1.5s ease-in-out infinite;
|
|
}
|
|
</style>
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body class="bg-gray-950 text-gray-200 min-h-screen">
|
|
<!-- Top Nav -->
|
|
<nav class="sticky top-0 z-50 border-b border-gray-800" style="background: linear-gradient(135deg, #0f172a 0%, #111827 50%, #0f172a 100%);">
|
|
<div class="max-w-[1600px] mx-auto px-4 flex items-center h-14 gap-2">
|
|
<a href="/" class="flex items-center gap-2 mr-6">
|
|
<div class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-sm">J4</div>
|
|
<span class="text-white font-semibold hidden sm:inline">JA4 SOC</span>
|
|
</a>
|
|
<a href="/" class="nav-link {% if active_page == 'overview' %}active{% endif %}">Overview</a>
|
|
<a href="/detections" class="nav-link {% if active_page == 'detections' %}active{% endif %}">Détections</a>
|
|
<a href="/scores" class="nav-link {% if active_page == 'scores' %}active{% endif %}">Scores</a>
|
|
<a href="/traffic" class="nav-link {% if active_page == 'traffic' %}active{% endif %}">Trafic</a>
|
|
<a href="/features" class="nav-link {% if active_page == 'features' %}active{% endif %}">Features</a>
|
|
<a href="/network" class="nav-link {% if active_page == 'network' %}active{% endif %}">Réseau</a>
|
|
<a href="/models" class="nav-link {% if active_page == 'models' %}active{% endif %}">Modèles</a>
|
|
<a href="/classify" class="nav-link {% if active_page == 'classify' %}active{% endif %}">Classifier</a>
|
|
<div class="flex-1"></div>
|
|
<span class="flex items-center gap-2 text-xs text-gray-500">
|
|
<span class="live-dot"></span>
|
|
<span id="clock"></span>
|
|
</span>
|
|
</div>
|
|
</nav>
|
|
<!-- Content -->
|
|
<main class="max-w-[1600px] mx-auto px-4 py-6">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
<script>
|
|
// ── Clock ──
|
|
function updateClock() {
|
|
document.getElementById('clock').textContent = new Date().toLocaleString('fr-FR');
|
|
}
|
|
updateClock(); setInterval(updateClock, 1000);
|
|
|
|
// ── Existing helpers ──
|
|
function threatBadge(level) {
|
|
const map = {
|
|
'CRITICAL':'badge-critical','HIGH':'badge-high','MEDIUM':'badge-medium',
|
|
'LOW':'badge-low','NORMAL':'badge-normal','KNOWN_BOT':'badge-known','ANUBIS_DENY':'badge-critical'
|
|
};
|
|
return `<span class="badge ${map[level]||'badge-normal'}">${level}</span>`;
|
|
}
|
|
function escapeHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
function fmtIP(ip) {
|
|
if (!ip) return '';
|
|
let s = String(ip).replace('::ffff:','');
|
|
return `<a href="/ip/${encodeURIComponent(s)}" class="text-brand-500 hover:underline">${escapeHtml(s)}</a>`;
|
|
}
|
|
function fmtScore(v) {
|
|
let n = parseFloat(v);
|
|
if (isNaN(n)) return '-';
|
|
let color = n > 0.7 ? 'text-red-400' : n > 0.4 ? 'text-orange-400' : n > 0.1 ? 'text-yellow-400' : 'text-green-400';
|
|
return `<span class="${color}">${n.toFixed(4)}</span>`;
|
|
}
|
|
|
|
// ── Navigation helpers ──
|
|
function fmtASN(org) {
|
|
if (!org) return '';
|
|
return `<a href="/detections?asn_org=${encodeURIComponent(org)}" class="text-blue-400 hover:underline cursor-pointer">${escapeHtml(org)}</a>`;
|
|
}
|
|
function fmtCountry(cc) {
|
|
if (!cc) return '';
|
|
const flags = {'FR':'🇫🇷','DE':'🇩🇪','NL':'🇳🇱','GB':'🇬🇧','ES':'🇪🇸','US':'🇺🇸','RU':'🇷🇺','IT':'🇮🇹','JP':'🇯🇵','CN':'🇨🇳','KR':'🇰🇷','BR':'🇧🇷','AU':'🇦🇺','CA':'🇨🇦','IN':'🇮🇳'};
|
|
return `<a href="/detections?country_code=${encodeURIComponent(cc)}" class="hover:underline cursor-pointer">${flags[cc]||'🏳️'} ${escapeHtml(cc)}</a>`;
|
|
}
|
|
function fmtJA4(ja4) {
|
|
if (!ja4) return '';
|
|
return `<a href="/detections?ja4=${encodeURIComponent(ja4)}" class="text-purple-400 hover:underline cursor-pointer font-mono text-xs" title="${escapeHtml(ja4)}">${escapeHtml(ja4.substring(0,20))}…</a>`;
|
|
}
|
|
function fmtJA4Full(ja4) {
|
|
if (!ja4) return '';
|
|
return `<a href="/detections?ja4=${encodeURIComponent(ja4)}" class="text-purple-400 hover:underline cursor-pointer font-mono text-xs">${escapeHtml(ja4)}</a>`;
|
|
}
|
|
function fmtBotName(name) {
|
|
if (!name) return '';
|
|
return `<a href="/detections?bot_name=${encodeURIComponent(name)}" class="text-cyan-400 hover:underline cursor-pointer">${escapeHtml(name)}</a>`;
|
|
}
|
|
function fmtThreatLink(level) {
|
|
if (!level) return '';
|
|
return `<a href="/detections?threat_level=${encodeURIComponent(level)}" class="cursor-pointer">${threatBadge(level)}</a>`;
|
|
}
|
|
function fmtLabel(label) {
|
|
if (!label) return '';
|
|
const colors = {human:'text-green-400 bg-green-500/10',datacenter:'text-red-400 bg-red-500/10',hosting:'text-orange-400 bg-orange-500/10'};
|
|
return `<span class="px-1.5 py-0.5 rounded text-xs ${colors[label]||'text-gray-400 bg-gray-500/10'}">${escapeHtml(label)}</span>`;
|
|
}
|
|
|
|
// ── ECharts helpers ──
|
|
const EC_COLORS = ['#6366f1','#22c55e','#f97316','#ef4444','#3b82f6','#eab308','#ec4899','#14b8a6','#8b5cf6','#f43f5e'];
|
|
const EC_TEXT = '#9ca3af';
|
|
const EC_GRID = '#374151';
|
|
|
|
function ecBase(overrides) {
|
|
return Object.assign({
|
|
backgroundColor: 'transparent',
|
|
textStyle: { color: EC_TEXT, fontFamily: 'Inter, system-ui, sans-serif' },
|
|
animation: true, animationDuration: 600,
|
|
}, overrides);
|
|
}
|
|
|
|
function ecTooltip(extra) {
|
|
return Object.assign({
|
|
backgroundColor: '#1f2937', borderColor: '#374151',
|
|
textStyle: { color: '#e5e7eb', fontSize: 12 },
|
|
}, extra);
|
|
}
|
|
</script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|