Files
ja4-platform/services/dashboard/backend/templates/features.html
toto c6ca352db9 feat(dashboard): add clickable drill-down to all data elements
Add navigation helpers (fmtASN, fmtCountry, fmtJA4, fmtBotName,
fmtThreatLink, fmtLabel) to base.html for SOC analyst drill-down.

Update all templates:
- overview.html: clickable table cells + ECharts click handlers for
  ASN, country, JA4, bot, and threat charts
- detections.html: URL param pre-filters, active filter bar with
  clear buttons, clickable ASN/country/JA4/threat in table
- scores.html: URL param pre-filters, clickable threat/JA4/country
- traffic.html: clickable JA4 and country columns
- ip_detail.html: clickable threat/JA4 in detections, clickable
  asn_org/country_code/asn_label in AI features grid
- network.html: click handlers on ASN treemap and country sunburst,
  fmtJA4Full/fmtLabel/fmtBotName/fmtASN in tables
- features.html: scatter plot click navigates to /ip/{ip}

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-08 14:58:48 +02:00

276 lines
12 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 content %}
<div class="space-y-6">
<h2 class="text-lg font-semibold text-white">Features ML — Exploration</h2>
<!-- Row 1: Radar + Feature Importance -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Profil Humain vs Bot (Radar)</h3>
<div id="chart-radar" style="height:360px"></div>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Importance des features (Variance)</h3>
<div id="chart-importance" style="height:360px"></div>
</div>
</div>
<!-- Row 2: Scatter 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">Scatter — Hit Velocity vs Fuzzing Index</h3>
<div id="chart-scatter" style="height:420px"></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 %}