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>
This commit is contained in:
@ -2,42 +2,274 @@
|
||||
{% block title %}JA4 SOC — Features ML{% endblock %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-lg font-semibold text-white">Features ML — Statistiques agrégées</h2>
|
||||
<!-- AI Features -->
|
||||
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-3">Features AI (view_ai_features_1h)</h3>
|
||||
<div id="ai-stats" class="text-gray-500 text-sm">Chargement...</div>
|
||||
<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>
|
||||
<!-- Thesis Features -->
|
||||
|
||||
<!-- 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">Features Thèse §5 (view_thesis_features_1h)</h3>
|
||||
<div id="thesis-stats" class="text-gray-500 text-sm">Chargement...</div>
|
||||
<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>
|
||||
<!-- Score distribution chart -->
|
||||
|
||||
<!-- 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">Distribution des scores d'anomalie</h3>
|
||||
<canvas id="score-dist-chart" height="200"></canvas>
|
||||
<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>
|
||||
function renderStats(data, containerId) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!data || Object.keys(data).length === 0) { el.textContent = 'Aucune donnée disponible'; return; }
|
||||
el.innerHTML = '<div class="grid grid-cols-2 md:grid-cols-4 gap-3">' +
|
||||
Object.entries(data).map(([k,v]) => {
|
||||
let val = typeof v === 'number' ? v.toFixed(4) : v;
|
||||
return `<div class="bg-gray-800 rounded-lg p-3"><div class="text-[10px] text-gray-500 truncate">${k}</div><div class="text-sm text-gray-200 font-mono">${val}</div></div>`;
|
||||
}).join('') + '</div>';
|
||||
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 loadFeatures() {
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
const r = await fetch('/api/features'); const d = await r.json();
|
||||
renderStats(d.ai_features, 'ai-stats');
|
||||
renderStats(d.thesis_features, 'thesis-stats');
|
||||
} catch(e) { console.error(e); }
|
||||
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); }
|
||||
}
|
||||
loadFeatures();
|
||||
|
||||
loadAll();
|
||||
setInterval(loadAll, 60000);
|
||||
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user