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 `
| Source | -Events | -Est. GB | +Source | +Events | +Est. 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 `| Rule Name | -Alerts (${s.period_days}d) | -Status | -
|---|
Last synced: ${new Date(s.checked_at).toLocaleString()}
` : '') + tableEl.innerHTML = ` +| Rule Name | +Alerts (${s.period_days}d) | +Status | +
|---|
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 `| Rule | -Required Sources | -Status | -Alerts | -
|---|
Source badges: green = healthy · red = inactive · amber = no parser
` + tableEl.innerHTML = ` +| Rule Name | +Required Sources | +Status | +Alerts | +
|---|
Source badges: green = healthy · red = inactive · amber = no parser
` } // ── Router ────────────────────────────────────────────────────────────────