Metric · Security

What is vulnerability SLA compliance, and how do you measure it in Metabase?

Vulnerability SLA compliance is the share of findings remediated within their severity-based SLA window — criticals in days, lows in months. It's the metric that turns remediation from a best effort into a commitment, and the one auditors and customers ask for by name. Measure it in Metabase from findings synced from Tenable, Wiz, or Snyk joined to your SLA policy.

TL;DRclosed within SLA ÷ all findings due in period, by severity. The denominator is everything whose deadline fell in the window — including findings still open past due. Leave those out and the number lies.

What vulnerability SLA compliance measures

It measures whether the remediation process keeps its promises. Where mean time to remediate describes typical speed, SLA compliance is pass/fail against a stated policy — which is why it shows up in customer security reviews and audit evidence requests. It comes with a built-in work queue: every open finding past its due date is a named breach with an owner and an age.

What data does it need?

  • A vulnerability_findings table with first_seen_at, resolved_at, status, and normalized severity.
  • An SLA policy as data: a severity → sla_days mapping table the SQL joins against.
  • Asset metadata for grouping breaches by owning team (and for tighter SLAs on internet-facing assets, if your policy has them).
  • Stable finding identity across scans, so a finding's due date doesn't reset every time it's re-observed.

SQL patterns

SLA compliance by severity (deadlines in the last 90 days)PostgreSQL
WITH sla AS (
  SELECT * FROM (VALUES
    ('critical', 7),
    ('high', 30),
    ('medium', 90),
    ('low', 180)
  ) AS t (severity, sla_days)
)
SELECT
  f.severity,
  COUNT(*) AS findings_due,
  COUNT(*) FILTER (
    WHERE f.resolved_at IS NOT NULL
      AND f.resolved_at <= f.first_seen_at + s.sla_days * INTERVAL '1 day'
  ) AS closed_in_sla,
  ROUND(
    100.0 * COUNT(*) FILTER (
      WHERE f.resolved_at IS NOT NULL
        AND f.resolved_at <= f.first_seen_at + s.sla_days * INTERVAL '1 day'
    ) / NULLIF(COUNT(*), 0), 1
  ) AS sla_compliance_pct
FROM vulnerability_findings f
JOIN sla s ON s.severity = f.severity
-- everything whose SLA deadline fell in the last 90 days,
-- closed or not: open-and-overdue counts against you
WHERE f.first_seen_at + s.sla_days * INTERVAL '1 day'
      >= CURRENT_DATE - INTERVAL '90 days'
  AND f.first_seen_at + s.sla_days * INTERVAL '1 day' < CURRENT_DATE
GROUP BY f.severity
ORDER BY sla_compliance_pct;
Open findings past their SLA due date (the breach list)PostgreSQL
WITH sla AS (
  SELECT * FROM (VALUES
    ('critical', 7),
    ('high', 30),
    ('medium', 90),
    ('low', 180)
  ) AS t (severity, sla_days)
)
SELECT
  f.finding_id,
  f.severity,
  a.asset_group,
  f.first_seen_at::date AS first_seen,
  (f.first_seen_at + s.sla_days * INTERVAL '1 day')::date AS sla_due,
  CURRENT_DATE
    - (f.first_seen_at + s.sla_days * INTERVAL '1 day')::date AS days_overdue
FROM vulnerability_findings f
JOIN sla s ON s.severity = f.severity
JOIN assets a ON a.id = f.asset_id
WHERE f.status = 'open'
  AND f.first_seen_at + s.sla_days * INTERVAL '1 day' < now()
ORDER BY days_overdue DESC;

Pitfalls

Only counting closed findings.→ A denominator of closed findings excludes the worst breaches — the ones still open past due. Define the population by SLA deadline date, closed or not, or the metric improves as the backlog ages.
Starting the SLA clock from the wrong timestamp.→ Ticket creation rewards slow triage; pipeline import rewards slow syncs. The clock starts at first_seen_at — when a scanner first saw the finding — full stop.
One global SLA for every severity.→ A single 30-day window is simultaneously too slow for criticals and unrealistic for lows, so teams ignore it. Severity-based windows are the whole point — encode them as a mapping table, not as constants scattered through queries.
Applying SLA policy changes retroactively.→ Tightening critical SLAs from 14 days to 7 and recomputing history turns last quarter's compliant findings into breaches nobody actually committed to. Version the policy table with effective dates and judge each finding against the policy in force when it was found.

Where this metric applies

Metrics

Dashboards

FAQ

What SLA windows should we use?
There's no universal standard, but common starting points are 7 days for critical, 30 for high, 90 for medium, and 180 for low — tightened for internet-facing assets. What matters more than the exact numbers is that the windows are severity-based, written down, and encoded as data (a severity → sla_days mapping table) so the SQL and the policy document can never drift apart.
Do open findings count against compliance?
They have to. Define the population as every finding whose SLA deadline fell in the period — closed or not — and count only those closed in time as compliant. An open finding past its due date is the clearest breach there is; a closed-only denominator quietly excludes it and reports a number that improves as your backlog rots. The breach list of open, overdue findings is the operational half of the metric, and it belongs on the vulnerability management dashboard next to the percentage.
When does the SLA clock start?
At first_seen_at — when a scanner first observed the finding — not when a ticket was created or when your pipeline imported the row. Ticket-creation clocks reward slow triage; import clocks reward slow syncs. Tenable and Wiz both expose first-seen timestamps, and your mean time to remediate should run on the same clock so the two metrics reconcile.
How do you calculate vulnerability SLA compliance?
Divide findings closed within their severity's SLA window by all findings whose SLA deadline fell in the period: closed_in_sla / findings_due. Join findings to an SLA mapping table on severity, compute each finding's due date as first_seen_at + sla_days, and segment the result by severity — a 95% overall figure can hide a 60% critical figure.
How do you track vulnerability SLA compliance in Metabase?
Sync findings from Tenable, Wiz, or Snyk, keep the SLA policy as a small mapping table, and build two cards: compliance percentage by severity over the trailing 90 days, and the ranked list of open findings past due. The percentage is for reporting; the breach list is what teams actually work from, alongside open critical vulnerabilities on your security analytics stack.