Complete rewrite of the SOC dashboard using FastAPI + Jinja2 + htmx + Chart.js + Tailwind CSS. Replaces the old React/Vite frontend with server-rendered templates. Dashboard pages: - Overview: KPIs, timeline chart, threat distribution, top IPs - Detections: paginated/filterable anomaly table - Scores: ml_all_scores with AE error & XGB prob columns - Traffic: HTTP logs with method/host filters - IP Investigation: full deep-dive (scores, features, HTTP logs, classify) - Classification: SOC feedback form + history - Features: AI + thesis feature stats - Models: scoring stats + model metadata API: 9 JSON endpoints with parameterized queries, sort whitelists SQL fixes: - 05_aggregation_tables: add deduplicate_merge_projection_mode - 11_views: fix nested aggregate (argMax inside sum) - 12_thesis_features: remove invalid 'let' bindings, fix groupArrayIf type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
66 lines
3.8 KiB
HTML
66 lines
3.8 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}JA4 SOC — Classifier{% endblock %}
|
|
{% block content %}
|
|
<div class="space-y-6 max-w-2xl">
|
|
<h2 class="text-lg font-semibold text-white">Classification SOC</h2>
|
|
<div class="bg-gray-900 rounded-xl p-6 border border-gray-800 space-y-4">
|
|
<div>
|
|
<label class="block text-sm text-gray-400 mb-1">Adresse IP</label>
|
|
<input type="text" id="cls-ip" placeholder="ex: 192.168.1.100" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-300 focus:border-brand-500 focus:outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm text-gray-400 mb-1">Classification</label>
|
|
<select id="cls-type" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-300">
|
|
<option value="bot">🤖 Bot malveillant</option>
|
|
<option value="legitimate">✅ Trafic légitime</option>
|
|
<option value="suspicious">⚠️ Suspect (à surveiller)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm text-gray-400 mb-1">Commentaire</label>
|
|
<textarea id="cls-comment" rows="3" placeholder="Raison de la classification..." class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-300 focus:border-brand-500 focus:outline-none resize-none"></textarea>
|
|
</div>
|
|
<button id="cls-submit" class="px-6 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 transition-colors">Envoyer la classification</button>
|
|
<div id="cls-result" class="text-sm"></div>
|
|
</div>
|
|
<!-- Recent classifications -->
|
|
<div class="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
|
<h3 class="text-sm font-medium text-gray-400 px-5 py-3 border-b border-gray-800">Classifications récentes</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="data-table"><thead><tr>
|
|
<th>Date</th><th>IP</th><th>Classification</th><th>Commentaire</th>
|
|
</tr></thead><tbody id="cls-history"></tbody></table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
document.getElementById('cls-submit').onclick = async () => {
|
|
const ip = document.getElementById('cls-ip').value.trim();
|
|
if (!ip) { alert('Veuillez saisir une IP'); return; }
|
|
try {
|
|
const r = await fetch('/api/classify', {method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({src_ip:ip, classification:document.getElementById('cls-type').value, comment:document.getElementById('cls-comment').value})});
|
|
const d = await r.json();
|
|
document.getElementById('cls-result').innerHTML = r.ok
|
|
? `<span class="text-green-400">✓ ${ip} classifié : ${d.classification}</span>`
|
|
: `<span class="text-red-400">✗ Erreur : ${d.detail||'unknown'}</span>`;
|
|
if (r.ok) loadHistory();
|
|
} catch(e) { document.getElementById('cls-result').innerHTML = `<span class="text-red-400">✗ ${e}</span>`; }
|
|
};
|
|
async function loadHistory() {
|
|
try {
|
|
const r = await fetch('/api/classifications'); const d = await r.json();
|
|
document.getElementById('cls-history').innerHTML = (d.data||[]).map(row => `<tr>
|
|
<td class="text-xs">${row.created_at||''}</td>
|
|
<td>${fmtIP(row.src_ip)}</td>
|
|
<td><span class="badge ${row.classification==='bot'?'badge-critical':row.classification==='legitimate'?'badge-low':'badge-medium'}">${row.classification}</span></td>
|
|
<td class="text-xs max-w-[300px] truncate">${row.comment||''}</td>
|
|
</tr>`).join('') || '<tr><td colspan="4" class="text-center text-gray-500 py-4">Aucune classification</td></tr>';
|
|
} catch(e) {}
|
|
}
|
|
loadHistory();
|
|
</script>
|
|
{% endblock %}
|