mirror of
https://github.com/marcredhat/SIEM-toolkit-patched
synced 2026-06-11 05:41:19 +00:00
Split onboarding pipeline into detection-mapped vs parser-only groups
Sources without detection rules no longer show stages 5-6 as failures:
- Backend: has_detection_rules flag added per source; progress (pct) calculated
over 4 core stages for sources with no rules; detection stages marked na:true
- Frontend: pipeline splits into two sections —
'With Detection Coverage' (6-stage, full pipeline)
'Parser Only' (4-stage, stages 5-6 shown as — N/A)
Each section has its own Show/Hide completed toggle
- Collapsed by default; Show Pipeline toggle reveals both sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1339,22 +1339,41 @@ def get_onboarding_status(db: Session = Depends(get_db)):
|
|||||||
rules_for_src = rule_by_source.get(src.source_name, [])
|
rules_for_src = rule_by_source.get(src.source_name, [])
|
||||||
rules_firing = any(firing_cache.get(r, 0) > 0 for r in rules_for_src)
|
rules_firing = any(firing_cache.get(r, 0) > 0 for r in rules_for_src)
|
||||||
|
|
||||||
stages = [
|
has_detection_rules = len(rules_for_src) > 0
|
||||||
|
|
||||||
|
# Core stages (apply to every source)
|
||||||
|
core_stages = [
|
||||||
{"stage": "Data Received", "done": (src.event_count or 0) > 0},
|
{"stage": "Data Received", "done": (src.event_count or 0) > 0},
|
||||||
{"stage": "Parser File Exists", "done": parser_info is not None},
|
{"stage": "Parser File Exists", "done": parser_info is not None},
|
||||||
{"stage": "Parser Active", "done": parser_active},
|
{"stage": "Parser Active", "done": parser_active},
|
||||||
{"stage": "Source Labeled", "done": has_ds_name and parser_active},
|
{"stage": "Source Labeled", "done": has_ds_name and parser_active},
|
||||||
{"stage": "Detection Rules", "done": len(rules_for_src) > 0},
|
|
||||||
{"stage": "Rules Firing", "done": rules_firing},
|
|
||||||
]
|
]
|
||||||
completed = sum(1 for s in stages if s["done"])
|
# Detection stages (only meaningful when detection rules exist)
|
||||||
|
detection_stages = [
|
||||||
|
{"stage": "Detection Rules", "done": has_detection_rules, "na": False},
|
||||||
|
{"stage": "Rules Firing", "done": rules_firing, "na": False},
|
||||||
|
]
|
||||||
|
|
||||||
|
if has_detection_rules:
|
||||||
|
stages = core_stages + detection_stages
|
||||||
|
total = 6
|
||||||
|
else:
|
||||||
|
# Mark detection stages as N/A — don't count against progress
|
||||||
|
stages = core_stages + [
|
||||||
|
{"stage": "Detection Rules", "done": False, "na": True},
|
||||||
|
{"stage": "Rules Firing", "done": False, "na": True},
|
||||||
|
]
|
||||||
|
total = 4 # progress calculated over core stages only
|
||||||
|
|
||||||
|
completed = sum(1 for s in stages if s.get("done") and not s.get("na"))
|
||||||
out.append({
|
out.append({
|
||||||
"source": src.source_name,
|
"source": src.source_name,
|
||||||
"event_count": src.event_count,
|
"event_count": src.event_count,
|
||||||
"stages": stages,
|
"stages": stages,
|
||||||
"completed": completed,
|
"completed": completed,
|
||||||
"total": len(stages),
|
"total": total,
|
||||||
"pct": round(completed / len(stages) * 100),
|
"has_detection_rules": has_detection_rules,
|
||||||
|
"pct": round(completed / total * 100) if total else 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort: incomplete first, then by event volume
|
# Sort: incomplete first, then by event volume
|
||||||
|
|||||||
+40
-27
@@ -973,7 +973,6 @@ function obCopy() {
|
|||||||
|
|
||||||
// ── Onboarding Pipeline ───────────────────────────────────────────────────
|
// ── Onboarding Pipeline ───────────────────────────────────────────────────
|
||||||
|
|
||||||
let _obShowCompleted = false
|
|
||||||
|
|
||||||
let _obPipelineData = null
|
let _obPipelineData = null
|
||||||
|
|
||||||
@@ -992,13 +991,17 @@ async function loadOnboardingPipeline() {
|
|||||||
<span class="px-3 py-1.5 rounded-full bg-slate-800/60 ring-1 ring-white/5 text-xs text-slate-400">○ Not Started: ${data.not_started}</span>`
|
<span class="px-3 py-1.5 rounded-full bg-slate-800/60 ring-1 ring-white/5 text-xs text-slate-400">○ Not Started: ${data.not_started}</span>`
|
||||||
|
|
||||||
const STAGE_ICONS = ['📥','📄','⚙️','🏷️','🔍','🔔']
|
const STAGE_ICONS = ['📥','📄','⚙️','🏷️','🔍','🔔']
|
||||||
const incomplete = sources.filter(s => s.completed < s.total)
|
const STAGE_LABELS = ['Data Received','Parser File','Parser Active','Source Labeled','Detection Rules','Rules Firing']
|
||||||
const complete = sources.filter(s => s.completed === s.total)
|
|
||||||
|
// Split into two groups
|
||||||
|
const withRules = sources.filter(s => s.has_detection_rules)
|
||||||
|
const withoutRules = sources.filter(s => !s.has_detection_rules)
|
||||||
|
|
||||||
function renderRow(s, i) {
|
function renderRow(s, i) {
|
||||||
const stageDots = s.stages.map((st, si) =>
|
const stageDots = s.stages.map((st, si) => {
|
||||||
`<span class="cursor-default text-base ${st.done ? 'text-emerald-400' : 'text-slate-700'}" title="${esc(st.stage)}">${STAGE_ICONS[si] || '●'}</span>`
|
if (st.na) return `<span class="cursor-default text-base text-slate-800" title="${esc(st.stage)} — N/A (no detection rules mapped)">—</span>`
|
||||||
).join('')
|
return `<span class="cursor-default text-base ${st.done ? 'text-emerald-400' : 'text-slate-700'}" title="${esc(st.stage)}">${STAGE_ICONS[si] || '●'}</span>`
|
||||||
|
}).join('')
|
||||||
const pct = s.pct
|
const pct = s.pct
|
||||||
const barColor = pct === 100 ? 'bg-emerald-500' : pct >= 50 ? 'bg-amber-500' : 'bg-red-500'
|
const barColor = pct === 100 ? 'bg-emerald-500' : pct >= 50 ? 'bg-amber-500' : 'bg-red-500'
|
||||||
return `<tr class="${i % 2 === 1 ? 'bg-white/[0.015]' : ''} border-b border-white/5 hover:bg-white/[0.04] transition-colors">
|
return `<tr class="${i % 2 === 1 ? 'bg-white/[0.015]' : ''} border-b border-white/5 hover:bg-white/[0.04] transition-colors">
|
||||||
@@ -1014,23 +1017,24 @@ async function loadOnboardingPipeline() {
|
|||||||
</tr>`
|
</tr>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeCount = complete.length
|
function renderSection(title, desc, sectionSources, idPrefix) {
|
||||||
const toggleBtn = completeCount > 0
|
if (sectionSources.length === 0) return ''
|
||||||
? `<tr id="ob-complete-toggle-row" class="border-b border-white/5">
|
const incomplete = sectionSources.filter(s => s.completed < s.total)
|
||||||
|
const complete = sectionSources.filter(s => s.completed === s.total)
|
||||||
|
const toggleBtn = complete.length > 0
|
||||||
|
? `<tr id="${idPrefix}-toggle-row" class="border-b border-white/5">
|
||||||
<td colspan="4" class="py-2 px-4">
|
<td colspan="4" class="py-2 px-4">
|
||||||
<button onclick="obToggleCompleted()" class="text-xs text-slate-500 hover:text-gray-300 transition-colors">
|
<button onclick="obToggleSection('${idPrefix}')" class="text-xs text-slate-500 hover:text-gray-300 transition-colors">
|
||||||
<span id="ob-complete-toggle-label">${_obShowCompleted ? 'Hide' : 'Show'} completed (${completeCount})</span>
|
<span id="${idPrefix}-toggle-label">Show completed (${complete.length})</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`
|
</tr>` : ''
|
||||||
: ''
|
const completeSection = `<tbody id="${idPrefix}-complete-rows" class="hidden">${complete.map((s,i)=>renderRow(s,i)).join('')}</tbody>`
|
||||||
|
return `
|
||||||
const completeRows = complete.map((s, i) => renderRow(s, i))
|
<div class="mb-5">
|
||||||
const completeSection = `<tbody id="ob-complete-rows" class="${_obShowCompleted ? '' : 'hidden'}">${completeRows.join('')}</tbody>`
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="text-xs font-semibold text-slate-300 uppercase tracking-wide">${esc(title)}</span>
|
||||||
tableEl.innerHTML = `
|
<span class="text-xs text-slate-600">${esc(desc)}</span>
|
||||||
<div class="text-xs text-slate-600 mb-2 flex gap-4">
|
|
||||||
${STAGE_ICONS.map((icon, i) => `<span>${icon} Stage ${i+1}: ${['Data Received','Parser File','Parser Active','Source Labeled','Detection Rules','Rules Firing'][i]}</span>`).join('')}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-xl ring-1 ring-white/5">
|
<div class="overflow-x-auto rounded-xl ring-1 ring-white/5">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
@@ -1045,6 +1049,15 @@ async function loadOnboardingPipeline() {
|
|||||||
${completeSection}
|
${completeSection}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
tableEl.innerHTML = `
|
||||||
|
<div class="text-xs text-slate-600 mb-3 flex flex-wrap gap-3">
|
||||||
|
${STAGE_ICONS.map((icon,i) => `<span>${icon} ${STAGE_LABELS[i]}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
${renderSection('With Detection Coverage', `${withRules.length} sources — 6-stage pipeline`, withRules, 'ob-with')}
|
||||||
|
${renderSection('Parser Only', `${withoutRules.length} sources — no detection rules mapped (stages 5–6 N/A)`, withoutRules, 'ob-without')}
|
||||||
${sources.length === 0 ? '<p class="text-slate-600 text-sm text-center py-4">No active sources found — sync sources on the Coverage Map first.</p>' : ''}`
|
${sources.length === 0 ? '<p class="text-slate-600 text-sm text-center py-4">No active sources found — sync sources on the Coverage Map first.</p>' : ''}`
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (tableEl) tableEl.innerHTML = `<p class="text-red-400 text-sm">${esc(e.message)}</p>`
|
if (tableEl) tableEl.innerHTML = `<p class="text-red-400 text-sm">${esc(e.message)}</p>`
|
||||||
@@ -1061,14 +1074,14 @@ function obTogglePipeline() {
|
|||||||
if (btn) btn.textContent = _obPipelineVisible ? 'Hide Pipeline' : 'Show Pipeline'
|
if (btn) btn.textContent = _obPipelineVisible ? 'Hide Pipeline' : 'Show Pipeline'
|
||||||
}
|
}
|
||||||
|
|
||||||
function obToggleCompleted() {
|
function obToggleSection(idPrefix) {
|
||||||
_obShowCompleted = !_obShowCompleted
|
const rows = document.getElementById(`${idPrefix}-complete-rows`)
|
||||||
const rows = document.getElementById('ob-complete-rows')
|
const label = document.getElementById(`${idPrefix}-toggle-label`)
|
||||||
const label = document.getElementById('ob-complete-toggle-label')
|
if (!rows) return
|
||||||
if (rows) rows.classList.toggle('hidden', !_obShowCompleted)
|
const hidden = rows.classList.toggle('hidden')
|
||||||
if (label) {
|
if (label) {
|
||||||
const count = rows?.querySelectorAll('tr').length || 0
|
const count = rows.querySelectorAll('tr').length
|
||||||
label.textContent = (_obShowCompleted ? 'Hide' : 'Show') + ` completed (${count})`
|
label.textContent = (hidden ? 'Show' : 'Hide') + ` completed (${count})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user