Files
ja4-platform/services/dashboard/backend/templates/features.html
toto 63ba6d203c feat(dashboard): complete SOC dashboard with full monitoring and workflows
- models.html: Full rewrite — 6 KPIs, scoring volume timeline, anomaly rate
  chart, threat breakdown per model, enhanced model cards with validation gate
- classify.html: SOC workflow — suggested unclassified IPs, quick-classify
  buttons, classification stats pie, pre-fill from URL params
- traffic.html: Clickable rows → ip_detail, column sorting, status column,
  search filter, doc tooltips on all chart sections
- scores.html: Search input, clickable rows → ip_detail, LEGITIMATE_BROWSER
  filter button, doc tooltips on distribution + scatter charts
- ip_detail.html: Resource cascade section (headless browser detection),
  status column in HTTP logs table
- detections.html: Doc tooltips on threat/reason/ASN chart sections
- features.html: Doc tooltips on radar/importance/scatter sections
- api.py: 4 new endpoints — /api/models/timeline, /api/models/threats,
  /api/classify/stats, /api/classify/suggested. Traffic API: status + search.

46 routes total. All tests pass (dashboard + bot-detector 36/36).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 01:25:01 +02:00

305 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}JA4 SOC — Features ML{% endblock %}
{% block page_title %}
Features ML
<span class="relative inline-block ml-1"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
<h4>Exploration des features</h4>
<p>Visualisez les 72 features ML extraites : comportementales (velocity, fuzzing), réseau (port_density, JA4), et thesis §5 (entropie, cadence, drift).</p>
<p><strong>Radar :</strong> Compare les profils ISP (humain) vs datacenter (bot). <strong>Scatter :</strong> Identifiez visuellement les clusters anormaux.</p>
<p class="doc-source">Source : view_ai_features_1h, view_thesis_features_1h</p>
</div></span>
{% endblock %}
{% block content %}
<div class="space-y-6">
<!-- Row 1: Radar + Feature Importance -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="section-card">
<div class="section-header"><span class="section-title">Profil Humain vs Bot (Radar)
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
<h4>Comparaison ISP vs Datacenter</h4>
<p>Profil moyen des sessions ISP (humaines) vs sessions datacenter (bots potentiels). Les axes sont les features ML normalisées.</p>
<p><strong>Interprétation :</strong> Plus la zone rouge dépasse la verte, plus la feature est discriminante. hit_velocity, fuzzing_index et post_ratio sont typiquement les plus discriminants.</p>
<p class="doc-source">Source : view_ai_features_1h GROUP BY asn_label</p>
</div></span>
</span></div>
<div class="section-body"><div id="chart-radar" style="height:360px"></div></div>
</div>
<div class="section-card">
<div class="section-header"><span class="section-title">Importance des features (Variance)
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
<h4>Feature importance</h4>
<p>Variance inter-classe (ISP vs datacenter) de chaque feature. Les features à haute variance discriminent le mieux bots et humains.</p>
<p><strong>Usage :</strong> Les features en tête sont les plus utiles pour le modèle EIF. Celles à variance nulle sont élaguées automatiquement.</p>
<p class="doc-source">Source : view_ai_features_1h</p>
</div></span>
</span></div>
<div class="section-body"><div id="chart-importance" style="height:360px"></div></div>
</div>
</div>
<!-- Row 2: Scatter full-width -->
<div class="section-card">
<div class="section-header"><span class="section-title">Scatter — Hit Velocity vs Fuzzing Index
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
<h4>Scatter bidimensionnel</h4>
<p>Chaque point = une session IP. X = cadence de requêtes, Y = diversité des paths. Les clusters séparés du groupe principal sont des anomalies.</p>
<p><strong>Action :</strong> Cliquez sur un point pour ouvrir la page IP détail.</p>
<p class="doc-source">Source : view_ai_features_1h</p>
</div></span>
</span></div>
<div class="section-body"><div id="chart-scatter" style="height:420px"></div></div>
</div>
<!-- Row 3: Distribution histograms (3-col grid) -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">hit_velocity</h3>
<div id="dist-hit_velocity" style="height:200px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">fuzzing_index</h3>
<div id="dist-fuzzing_index" style="height:200px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">post_ratio</h3>
<div id="dist-post_ratio" style="height:200px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">asset_ratio</h3>
<div id="dist-asset_ratio" style="height:200px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">temporal_entropy</h3>
<div id="dist-temporal_entropy" style="height:200px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-xs font-medium text-gray-400 mb-2">path_diversity_ratio</h3>
<div id="dist-path_diversity_ratio" style="height:200px"></div>
</div>
</div>
<!-- Row 4: Temporal heatmap full-width -->
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Heatmap temporelle (jour × heure)</h3>
<div id="chart-heatmap" style="height:320px"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const LABEL_COLORS = {human:'#22c55e', datacenter:'#ef4444', hosting:'#f97316', unknown:'#6b7280'};
const DAYS = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'];
const HOURS = Array.from({length:24}, (_,i) => String(i).padStart(2,'0')+'h');
let charts = {};
function initChart(id) {
const el = document.getElementById(id);
if (!el) return null;
if (charts[id]) charts[id].dispose();
charts[id] = echarts.init(el);
return charts[id];
}
async function loadAll() {
try {
const [feat, behav, heat] = await Promise.all([
fetch('/api/features').then(r => r.json()),
fetch('/api/behavior').then(r => r.json()),
fetch('/api/heatmap').then(r => r.json()),
]);
// ── Radar: Human vs Bot profiles ──
const radarKeys = [
{key:'avg_velocity', label:'Velocity'},
{key:'avg_fuzz', label:'Fuzzing'},
{key:'avg_post', label:'POST ratio'},
{key:'avg_asset', label:'Asset ratio'},
{key:'avg_direct', label:'Direct access'},
{key:'avg_entropy', label:'Entropy'},
{key:'avg_path_div', label:'Path diversity'},
{key:'avg_browser', label:'Browser score'},
];
const hp = feat.human_profile || {};
const bp = feat.bot_profile || {};
const maxVals = radarKeys.map(f => Math.max(hp[f.key] || 0, bp[f.key] || 0) || 1);
const radarChart = initChart('chart-radar');
if (radarChart) {
radarChart.setOption(ecBase({
tooltip: ecTooltip(),
legend: {data:['Humain','Bot'], top:10, textStyle:{color:EC_TEXT}},
radar: {
indicator: radarKeys.map((f,i) => ({name:f.label, max:1})),
shape:'polygon',
splitArea:{areaStyle:{color:['rgba(99,102,241,0.05)','rgba(99,102,241,0.1)']}},
splitLine:{lineStyle:{color:EC_GRID}},
axisLine:{lineStyle:{color:EC_GRID}},
axisName:{color:EC_TEXT, fontSize:11},
},
series:[{
type:'radar',
data:[
{
name:'Humain',
value: radarKeys.map((f,i) => (hp[f.key]||0) / maxVals[i]),
areaStyle:{color:'rgba(34,197,94,0.2)'},
lineStyle:{color:'#22c55e', width:2},
itemStyle:{color:'#22c55e'},
},
{
name:'Bot',
value: radarKeys.map((f,i) => (bp[f.key]||0) / maxVals[i]),
areaStyle:{color:'rgba(239,68,68,0.2)'},
lineStyle:{color:'#ef4444', width:2},
itemStyle:{color:'#ef4444'},
},
]
}]
}));
}
// ── Feature Importance (horizontal bar) ──
const fi = (feat.feature_importance || []).sort((a,b) => a.variance - b.variance);
const impChart = initChart('chart-importance');
if (impChart && fi.length) {
impChart.setOption(ecBase({
tooltip: ecTooltip({trigger:'axis', axisPointer:{type:'shadow'}}),
grid: {left:150, right:30, top:10, bottom:30},
yAxis: {
type:'category',
data: fi.map(f => f.name),
axisLine:{lineStyle:{color:EC_GRID}},
axisLabel:{color:EC_TEXT, fontSize:11, width:140, overflow:'truncate'},
},
xAxis: {
type:'value',
splitLine:{lineStyle:{color:EC_GRID, type:'dashed'}},
axisLabel:{color:EC_TEXT},
name:'Variance', nameTextStyle:{color:EC_TEXT},
},
series:[{
type:'bar', data: fi.map(f => f.variance), barWidth:'60%',
itemStyle:{color: new echarts.graphic.LinearGradient(0,0,1,0,[
{offset:0, color:'#6366f1'}, {offset:1, color:'#8b5cf6'}
])},
label:{show:true, position:'right', color:EC_TEXT, fontSize:10, formatter:p => p.value.toFixed(4)},
}]
}));
}
// ── Scatter: hit_velocity vs fuzzing_index ──
const scatter = behav.scatter || [];
const scatterChart = initChart('chart-scatter');
if (scatterChart && scatter.length) {
const groups = {};
scatter.forEach(p => {
const lbl = p.asn_label || 'unknown';
if (!groups[lbl]) groups[lbl] = [];
groups[lbl].push(p);
});
const series = Object.entries(groups).map(([label, pts]) => ({
name: label,
type: 'scatter',
data: pts.map(p => [
p.hit_velocity || 0,
p.fuzzing_index || 0,
Math.max(4, Math.min(30, Math.sqrt(p.hits || 1) * 2)),
p.ip, p.bot_name,
]),
symbolSize: (val) => val[2],
itemStyle: {color: LABEL_COLORS[label] || '#6b7280', opacity:0.75},
}));
scatterChart.setOption(ecBase({
tooltip: ecTooltip({
trigger:'item',
formatter: p => {
const d = p.data;
return `<b>${d[3]||''}</b><br>Label: ${p.seriesName}<br>` +
`Velocity: ${d[0].toFixed(3)}<br>Fuzzing: ${d[1].toFixed(3)}` +
(d[4] ? `<br>Bot: ${d[4]}` : '');
}
}),
legend: {data: Object.keys(groups), top:5, textStyle:{color:EC_TEXT}},
grid: {left:60, right:30, top:40, bottom:40},
xAxis: {
type:'value', name:'Hit Velocity', nameTextStyle:{color:EC_TEXT},
splitLine:{lineStyle:{color:EC_GRID, type:'dashed'}}, axisLabel:{color:EC_TEXT},
},
yAxis: {
type:'value', name:'Fuzzing Index', nameTextStyle:{color:EC_TEXT},
splitLine:{lineStyle:{color:EC_GRID, type:'dashed'}}, axisLabel:{color:EC_TEXT},
},
series,
}));
scatterChart.on('click', params => {
const ip = params.data?.[3];
if (ip) window.location.href = '/ip/' + encodeURIComponent(ip);
});
}
// ── Distribution histograms ──
const distKeys = ['hit_velocity','fuzzing_index','post_ratio','asset_ratio','temporal_entropy','path_diversity_ratio'];
const dists = behav.distributions || {};
distKeys.forEach(key => {
const data = dists[key] || [];
const ch = initChart('dist-' + key);
if (!ch || !data.length) return;
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'axis', axisPointer:{type:'shadow'}}),
grid: {left:45, right:10, top:8, bottom:25},
xAxis: {
type:'category', data: data.map(d => d.bucket),
axisLabel:{color:EC_TEXT, fontSize:9, rotate:30}, axisLine:{lineStyle:{color:EC_GRID}},
},
yAxis: {
type:'value', splitLine:{lineStyle:{color:EC_GRID, type:'dashed'}},
axisLabel:{color:EC_TEXT, fontSize:9},
},
series:[{
type:'bar', data: data.map(d => d.cnt), barWidth:'70%',
itemStyle:{color:'#6366f1', borderRadius:[2,2,0,0]},
}]
}));
});
// ── Temporal heatmap ──
const cells = heat.cells || [];
const heatChart = initChart('chart-heatmap');
if (heatChart && cells.length) {
const maxCnt = Math.max(...cells.map(c => c.cnt), 1);
heatChart.setOption(ecBase({
tooltip: ecTooltip({
formatter: p => `${DAYS[p.data[1]]} ${HOURS[p.data[0]]}<br>Requêtes: <b>${p.data[2]}</b>`
}),
grid: {left:60, right:40, top:10, bottom:30},
xAxis: {
type:'category', data:HOURS, splitArea:{show:true},
axisLabel:{color:EC_TEXT, fontSize:10}, axisLine:{lineStyle:{color:EC_GRID}},
},
yAxis: {
type:'category', data:DAYS, splitArea:{show:true},
axisLabel:{color:EC_TEXT, fontSize:11}, axisLine:{lineStyle:{color:EC_GRID}},
},
visualMap: {
min:0, max:maxCnt, calculable:true, orient:'vertical', right:0, top:'center',
inRange:{color:['#1e1b4b','#4338ca','#6366f1','#a78bfa','#f97316','#ef4444']},
textStyle:{color:EC_TEXT}, borderColor:'transparent',
},
series:[{
type:'heatmap',
data: cells.map(c => [c.hour, c.dow, c.cnt]),
label:{show:false},
emphasis:{itemStyle:{shadowBlur:6, shadowColor:'rgba(0,0,0,0.4)'}},
}]
}));
}
} catch(e) { console.error('Features load error:', e); }
}
loadAll();
setInterval(loadAll, 60000);
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
</script>
{% endblock %}