CTEM Heat Map
The Heat Map is your continuous-exposure dashboard. It aggregates every finding from every scan in your tenant — manual, scheduled, adversary-driven — into one grid: assets down, controls across. One glance shows which assets are getting worse.
Where to find it: https://app.cybrium.ai/findings/heat-map (also in the sidebar under Heat Map between ASPM and Compliance).
What you see
Top bar
- Window picker — 7d / 30d / 90d. Resets the aggregation window.
- 4 KPIs — total findings, critical open, high open, regressed in last 7 days. Regression count is the headline number to wake up to.
Grid
Rows are assets (whatever Finding.host was set to — domain, IP, hostname, repo URL). Columns are control families (resolved from ScanJob.scan_type — Email Security, SSL/TLS, DAST, M365, Secret Scan, etc.).
Each cell shows:
- Number — total findings in that (asset × control) over the window
- Background colour — opacity scaled by count, hue keyed to the cell's max severity (red = critical, orange = high, amber = medium, blue = low, slate = info)
+Nchip (when present) — rules that regressed in the last 7 days- Sparkline — 7-bucket trend showing when findings landed within the window
Click any cell to open a side panel with:
- Per-cell tile of counts (findings, regressed-7d, scan count)
- Sparkline
- "Open underlying findings" button (filters /findings by host + scan_type)
- "Edit schedule for this control" button (jumps to Settings → Scheduled Scans)
Recent regressions
Below the grid, a table of the 10 most recent regressions across the tenant. A "regression" means a rule that first appeared in the last 7 days against an asset Cybrium was already scanning before that window — distinguished from "we just started monitoring this asset". Each row links straight into the relevant finding.
How the numbers are computed
count = COUNT(Finding) where scan.completed_at >= now - window
severity_max = max(severity) per (host, scan_type) cell — critical > high > medium > low > info
open = same as count (status taxonomy is on the roadmap; today all findings in window are treated as open)
regressed_7d = number of distinct rule_ids in the cell whose earliest first-seen is in the last 7 days
AND the cell's earliest scan completion across history is BEFORE the 7-day window
trend_30d = 7-bucket histogram of new findings, equally spaced across the window
Source of truth: the Finding table joined to ScanJob for completed_at + scan_type. No new model writes — every read is recomputed.
Demo path
- Settings → Scheduled Scans → create 3-4 schedules (Email Security daily, SSL weekly, M365 daily, Secret Scan weekly)
- Wait one day for them to fire (or click Run now on each to seed)
- Open Heat Map → grid populates with cells per (target × scan_type)
- The 7-day window shows what's new vs. just-discovered
- Click any cell with
+Nto see the regression detail
API
curl -fsSL https://app.cybrium.ai/api/scans/findings/heat-map/?window_days=30 \
-b "$AUTH_COOKIE"
Returns a JSON payload shaped like:
{
"generated_at": "2026-06-02T...",
"window_days": 30,
"axes": {
"assets": [ { "key": "example.com", "label": "example.com", "kind": "domain", "criticality": "high" } ],
"controls": [ { "key": "email_security", "label": "Email Security ..." } ]
},
"cells": [
{ "asset_key": "example.com", "control_key": "email_security",
"severity_max": "high", "count": 3, "open": 3, "regressed_7d": 1,
"trend_30d": [0,0,1,1,2,2,3], "scan_count": 4 }
],
"regressions_recent": [
{ "asset_key": "example.com", "control_key": "email_security",
"rule_id": "cymail.dnsbl.listed", "first_seen": "...", "severity": "high" }
],
"summary": {
"total_findings": 27, "open": 27, "critical_open": 2, "high_open": 9,
"regressed_7d": 4, "asset_count": 6, "control_count": 5, "scan_count": 18
}
}
Limits
- Computed on read; large tenants (>10k findings in window) will see slower loads. A snapshot table is on the roadmap.
- "Open" is a count proxy today — the Finding status taxonomy (resolved / false_positive / accepted-risk) lands in a follow-up sprint; today the heat map treats every finding in the window as open.
- Asset criticality is inferred from
inventory.Asset.business_unit+ a hostname heuristic ("prod", "payments", "api.", etc.). For accuracy, set business_unit explicitly on your Asset rows. - No cross-tenant view — this is a per-tenant page. Platform-admin cross-tenant heat map is a future sprint.