From a7ebcac9a6572d5cfb256258d7db88b378ee20c9 Mon Sep 17 00:00:00 2001 From: Mick <119439091+mickbrowns1@users.noreply.github.com> Date: Fri, 22 May 2026 12:08:56 -0400 Subject: [PATCH] Revert "Add product grouping to rule displays across coverage and threat pages" This reverts commit 7620d1fcc8c3c5290a58b1224a764f1737540bf0. --- backend/routers/coverage.py | 29 +---- frontend/index.html | 218 +++++++++--------------------------- 2 files changed, 55 insertions(+), 192 deletions(-) diff --git a/backend/routers/coverage.py b/backend/routers/coverage.py index 96479ef..29766bd 100644 --- a/backend/routers/coverage.py +++ b/backend/routers/coverage.py @@ -81,17 +81,6 @@ def _extract_mitre(rule: dict) -> tuple[list[str], list[dict]]: return list(dict.fromkeys(tactics)), unique_techniques -def _product_from_data_sources(data_sources: list) -> str: - """Derive a product label from a rule's data_sources list. - Prefers the first non-SentinelOne entry (e.g. 'AWS CloudTrail', 'Okta'), - falls back to 'SentinelOne' for generic endpoint rules. - """ - if not data_sources: - return "SentinelOne" - non_s1 = [d for d in data_sources if d.lower() not in ("sentinelone", "s1")] - return non_s1[0] if non_s1 else data_sources[0] - - def _star_query_texts(rule: dict) -> list[str]: """ Extract all PowerQuery/filter strings from a STAR rule. @@ -743,10 +732,8 @@ def get_coverage_map(db: Session = Depends(get_db)): query_texts = _star_query_texts(raw_data) data_sources = rule_parser.extract_data_sources(query_texts) - product = _product_from_data_sources(data_sources) - for ds in data_sources: - rule_by_source.setdefault(ds, []).append({"rule": rule.name, "type": rule.rule_type, "product": product}) + rule_by_source.setdefault(ds, []).append({"rule": rule.name, "type": rule.rule_type}) # Fields to ignore when computing "missing" — these are metadata/schema fields # always present in events regardless of the parser @@ -802,8 +789,6 @@ def get_coverage_map(db: Session = Depends(get_db)): for r in rule_by_source.get(src.source_name, []) if r["type"] == "library" ] - # Sort rules so grouped-by-product rendering is stable - rules_for_src.sort(key=lambda r: (r.get("product", ""), r["rule"])) # Close-match suggestions — shown when there are no library rules for this source. close_matches: list = [] @@ -1077,16 +1062,6 @@ def get_rule_firing_cache(db: Session = Depends(get_db)): never_fired_count = total_rules - len(fired) period_days = rows[0].period_days if rows else 30 checked_at = rows[0].checked_at.isoformat() if rows and rows[0].checked_at else None - - # Build rule_name → product lookup from ParsedRule raw JSON - rule_product: dict[str, str] = {} - for rule in db.query(ParsedRule).filter_by(rule_type="library").all(): - try: - raw_data = json.loads(rule.raw) if rule.raw else {} - except Exception: - raw_data = {} - rule_product[rule.name] = _product_from_data_sources(raw_data.get("data_sources", [])) - return { "rules": [ { @@ -1094,7 +1069,6 @@ def get_rule_firing_cache(db: Session = Depends(get_db)): "alert_count": r.alert_count, "period_days": r.period_days, "checked_at": r.checked_at.isoformat() if r.checked_at else None, - "product": rule_product.get(r.rule_name, "SentinelOne"), } for r in rows ], @@ -1305,7 +1279,6 @@ def get_dependency_map(db: Session = Depends(get_db)): "generated_alerts": generated_alerts, "at_risk": at_risk, "no_sources": len(data_sources) == 0, - "product": _product_from_data_sources(data_sources), }) # Sort: at-risk first, then by source count desc, then alphabetical diff --git a/frontend/index.html b/frontend/index.html index 5443e06..15f65f1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -104,17 +104,14 @@ function barChart(rows, labelKey, valueKey) { const bh = Math.max(2, Math.floor((val / max) * chartH)) const x = padL + i * (chartW / rows.length) + (chartW / rows.length - bw) / 2 const y = padT + chartH - bh - // If label looks like a date (YYYY-MM-DD), show MM/DD; otherwise truncate to 16 chars + // If label looks like a date (YYYY-MM-DD), show MM/DD; otherwise truncate to 10 chars const rawLbl = String(r[labelKey] || '') - const isDate = /^\d{4}-\d{2}-\d{2}$/.test(rawLbl) - const lbl = esc(isDate ? rawLbl.slice(5, 10) : (rawLbl.length > 16 ? rawLbl.slice(0, 15) + '…' : rawLbl)) + const lbl = esc(/^\d{4}-\d{2}-\d{2}$/.test(rawLbl) ? rawLbl.slice(5, 10) : rawLbl.slice(0, 10)) // value label on top of bar const valLbl = val >= 1000 ? (val/1000).toFixed(1)+'k' : val - return `${esc(rawLbl)}: ${val.toLocaleString()} - + return ` ${valLbl} - ${lbl} - ` + ${lbl}` }).join('') return `${defs}${ticks}${bars}` @@ -657,62 +654,15 @@ function cvSetFilter(f) { const firingPopulated = cvData?.summary?.firing_cache_populated === true if (s.rule_count) { if (firingPopulated) { - const rules = s.rules || [] - // Group rules by product label - const groups = {} - for (const r of rules) { - const prod = r.product || 'SentinelOne' - if (!groups[prod]) groups[prod] = [] - groups[prod].push(r) - } - const prodNames = Object.keys(groups).sort((a, b) => { - // Put SentinelOne last (it's the generic catch-all) - if (a === 'SentinelOne') return 1 - if (b === 'SentinelOne') return -1 - return a.localeCompare(b) + const ruleItems = (s.rules || []).map(r => { + const alerts = r.alert_count || 0 + if (alerts > 0) { + return `${esc(r.rule.length > 40 ? r.rule.slice(0, 40) + '…' : r.rule)} (${alerts})` + } else { + return `⚠ ${esc(r.rule.length > 40 ? r.rule.slice(0, 40) + '…' : r.rule)}` + } }) - const groupHtml = prodNames.map(prod => { - const prodRules = groups[prod] - const fired = prodRules.filter(r => (r.alert_count || 0) > 0).length - const headerColor = fired === prodRules.length ? 'text-emerald-400' : fired > 0 ? 'text-amber-400' : 'text-slate-400' - const uid = 'dg_' + Math.random().toString(36).slice(2) - const ruleItems = prodRules.map(r => { - const alerts = r.alert_count || 0 - const label = r.rule.length > 50 ? r.rule.slice(0, 50) + '…' : r.rule - if (alerts > 0) { - return `
${esc(label)} (${alerts})
` - } else { - return `
⚠ ${esc(label)}
` - } - }).join('') - return `
- - -
` - }).join('') - return `
${groupHtml}
` - } - // Firing cache not populated — show grouped product summary (no expand) - const rules = s.rules || [] - if (rules.length > 0) { - const groups = {} - for (const r of rules) { - const prod = r.product || 'SentinelOne' - groups[prod] = (groups[prod] || 0) + 1 - } - const prodNames = Object.keys(groups).sort((a, b) => { - if (a === 'SentinelOne') return 1 - if (b === 'SentinelOne') return -1 - return a.localeCompare(b) - }) - if (prodNames.length === 1) { - return `${s.rule_count} rules · ${esc(prodNames[0])}` - } - const breakdown = prodNames.map(p => `${esc(p)}: ${groups[p]}`).join('
') - return `${s.rule_count} rules
${breakdown}` + return `
${ruleItems.join('')}
` } return `${s.rule_count} rule${s.rule_count !== 1 ? 's' : ''}` } @@ -903,17 +853,17 @@ async function igLoad() { const name = r['dataSource.name'] || r.name || 'unknown' const evts = r.events || 0 return ` - ${esc(name)} - ${evts.toLocaleString()} - ${(evts/1e6*0.5).toFixed(3)} + ${esc(name)} + ${evts.toLocaleString()} + ${(evts/1e6*0.5).toFixed(3)} ` }) document.getElementById('ig-sources').innerHTML = rows.length ? ` - - - + + + ${rows.join('')}
SourceEventsEst. GBSourceEventsEst. GB
` : `

No data in this period.

` @@ -2106,57 +2056,30 @@ async function loadFiringStatus() { ${statCard('Never Fired', s.never_fired, s.never_fired > 0 ? 'text-amber-400' : 'text-gray-500')} ` - // Group rules by product - const firingGroups = {} - for (const r of data.rules) { - const prod = r.product || 'SentinelOne' - if (!firingGroups[prod]) firingGroups[prod] = [] - firingGroups[prod].push(r) - } - const firingProdNames = Object.keys(firingGroups).sort((a, b) => { - if (a === 'SentinelOne') return 1 - if (b === 'SentinelOne') return -1 - return a.localeCompare(b) - }) - - const groupSections = firingProdNames.map(prod => { - const rules = firingGroups[prod] - const firedCount = rules.filter(r => r.alert_count > 0).length - const headerColor = firedCount === rules.length ? 'text-emerald-400' : firedCount > 0 ? 'text-amber-400' : 'text-slate-400' - const uid = 'fg_' + prod.replace(/[^a-z0-9]/gi, '_') - const rows = rules.map((r, i) => { - const badge = r.alert_count > 0 - ? `Active` - : `Silent` - return ` - ${esc(r.rule_name)} - ${r.alert_count.toLocaleString()} - ${badge} - ` - }).join('') - return `
- - -
` + const top20 = data.rules.slice(0, 20) + const rows = top20.map((r, i) => { + const badge = r.alert_count > 0 + ? `Active` + : `Silent` + return ` + ${esc(r.rule_name)} + ${r.alert_count.toLocaleString()} + ${badge} + ` }).join('') - tableEl.innerHTML = groupSections + - (s.checked_at ? `

Last synced: ${new Date(s.checked_at).toLocaleString()}

` : '') + tableEl.innerHTML = ` +
+ + + + + + + ${rows} +
Rule NameAlerts (${s.period_days}d)Status
+
+ ${s.checked_at ? `

Last synced: ${new Date(s.checked_at).toLocaleString()}

` : ''}` } catch(e) { if (tableEl) tableEl.innerHTML = `

${esc(e.message)}

` } @@ -2206,24 +2129,7 @@ function depMapRender() { no_parser:'bg-amber-900/50 text-amber-300 border-amber-700', } - // Group by product - const depGroups = {} - for (const r of display) { - const prod = r.product || 'SentinelOne' - if (!depGroups[prod]) depGroups[prod] = [] - depGroups[prod].push(r) - } - const depProdNames = Object.keys(depGroups).sort((a, b) => { - // At-risk products first, then alphabetical, SentinelOne last - const aRisk = depGroups[a].some(r => r.at_risk) - const bRisk = depGroups[b].some(r => r.at_risk) - if (aRisk !== bRisk) return aRisk ? -1 : 1 - if (a === 'SentinelOne') return 1 - if (b === 'SentinelOne') return -1 - return a.localeCompare(b) - }) - - const makeDepRows = (groupRules) => groupRules.map((r, i) => { + const rows = display.map((r, i) => { const statusBadge = r.at_risk ? `⚠ At Risk` : `✓ Covered` @@ -2246,35 +2152,19 @@ function depMapRender() { ` }).join('') - const groupedHtml = depProdNames.map(prod => { - const groupRules = depGroups[prod] - const atRiskCount = groupRules.filter(r => r.at_risk).length - const headerColor = atRiskCount > 0 ? 'text-red-400' : 'text-emerald-400' - const uid = 'dep_' + prod.replace(/[^a-z0-9]/gi, '_') - return `
- - -
` - }).join('') - - tableEl.innerHTML = groupedHtml + - `

Source badges: green = healthy · red = inactive · amber = no parser

` + tableEl.innerHTML = ` +
+ + + + + + + + ${rows} +
Rule NameRequired SourcesStatusAlerts
+
+

Source badges: green = healthy · red = inactive · amber = no parser

` } // ── Router ────────────────────────────────────────────────────────────────