Security KPIs (key performance indicators) turn your scanners' reports into numerical answers: how long it takes to detect a flaw (MTTD), to fix it (MTTR), what the overall risk is, what share of the estate is really scanned. This lesson is for the DevOps engineer or CISO who already has scans in their CI/CD pipeline and wants to turn them into a dashboard that triggers decisions.
The formulas are given in PromQL and checked with promtool, the testing tool of Prometheus 3.15.0: on a set of test series, the example Risk Score is indeed 2.45, and the naive version of the same query disappears as soon as no critical vulnerability is open, that is precisely when everything is fine.
What you will learn
- Define the five categories of security KPIs: detection, remediation, posture, coverage, efficiency
- Calculate MTTD, MTTR and the Risk Score with formulas and worked examples
- Set realistic fix SLAs per severity level
- Build Grafana dashboards suited to three audiences
- Test Prometheus alerting rules before deploying them
- Avoid the common traps: vanity metrics, the Cobra effect, indicator overload
Prerequisites
Before starting, check that the following building blocks are already in place. This lesson covers neither installing a vulnerability scanner nor setting up a CI/CD pipeline: it assumes that detection exists and focuses on its measurement.
- Scanning tools: Trivy, Grype or equivalent, plugged into the pipelines
- CI/CD pipeline: GitLab CI, GitHub Actions or Jenkins, with security stages
- Observability: Prometheus and Grafana, or equivalent, to collect and display
- Triage process: the Vulnerability management lesson sets the deadlines used here
Why measure security?
A team that "does security" without measuring it can neither prove its effectiveness nor spot where it is losing ground. Imagine a CTO asking: "how long does it take us to fix a critical vulnerability?". Without an indicator, the only honest answer is a vague estimate; with an MTTR tracked per criticality, it is a figure, and a trend.
Security KPIs answer four questions. Do we detect fast enough? A flaw published on Monday and discovered on Friday leaves a four-day attack window. Do we fix fast enough? The time between detection and fix determines the real exposure. Is the posture improving? Without a historical trend, there is no way to know. Where should we invest? Metrics per team and per application point to the risk areas.
The 5 categories of security KPIs
Indicators fall into five families, each answering a different question. Keeping them apart avoids comparing figures that do not mean the same thing, a common trap at the start. The diagram sums up each family and its flagship indicator; the table gives the question and examples.
| Category | Key question | Example indicators |
|---|---|---|
| Detection | How fast do we find problems? | MTTD, new CVEs per day |
| Remediation | How fast do we fix? | MTTR per severity, SLA compliance, vulnerability debt |
| Posture | What is our overall risk level? | Risk Score, vulnerability density, monthly trend |
| Coverage | What share of the estate is analysed? | % of assets scanned, adoption in pipelines |
| Efficiency | Do our tools work well? | False positive rate, cost per vulnerability handled |
Detection metrics
Detection measures the ability to identify vulnerabilities before they are exploited. The faster it is, the shorter the exposure window: it is the first figure an attacker takes advantage of.
MTTD, Mean Time To Detect
MTTD (Mean Time To Detect) measures the time between the appearance of a vulnerability and its discovery by your tools. For a known flaw, the starting point is the publication of the CVE; for a code flaw, the date of the commit that introduces it.
MTTD = date de détection - date d'apparition de la vulnérabilitéThe following targets are common benchmarks, not a standard. They assume a frequent scheduled scan and checks before the commit.
| Context | Target MTTD | Rationale |
|---|---|---|
| Critical vulnerabilities (CVE) | < 1 hour | Hourly scheduled scans on the registry |
| Code vulnerabilities (SAST) | < 24 hours | At the latest at the next build |
| Exposed secrets | < 15 minutes | Pre-commit hooks and continuous monitoring |
Prometheus implementation for MTTD
To collect MTTD automatically, a small exporter (a program that exposes
metrics over HTTP) translates your scanners' results into the Prometheus
exposition format. Naming follows the project's conventions: the _total
suffix is reserved for counters, and the unit appears in the name.
# Prometheus exposition format (not YAML)# TYPE security_vulnerability_mttd_hours gaugesecurity_vulnerability_mttd_hours{severity="critical", source="trivy", team="platform"} 0.5security_vulnerability_mttd_hours{severity="high", source="trivy", team="platform"} 4.2security_vulnerability_mttd_hours{severity="critical", source="grype", team="frontend"} 0.8The PromQL query (Prometheus's query language) that gives the average MTTD per severity over seven days is the following:
avg by (severity) (avg_over_time(security_vulnerability_mttd_hours[7d]))Remediation metrics
Remediation measures the ability to fix once the flaw is detected. This is often where teams have the most room for improvement, and where SLAs make sense.
MTTR, Mean Time To Remediate
MTTR (Mean Time To Remediate) measures the time between detection and verified fix, that is the scan that confirms the flaw has gone. The diagram follows a vulnerability from detection to closure, with the target time for each criticality.
If the exporter publishes the duration of each fix as a Prometheus summary
(_sum and _count), MTTR in hours is calculated per criticality:
sum by (severity) (rate(security_remediation_duration_seconds_sum[7d]))/sum by (severity) (rate(security_remediation_duration_seconds_count[7d]))/ 3600SLA per severity level
MTTR only makes sense relative to criticality: a critical flaw does not wait as long as a minor flaw. The SLAs (Service Level Agreement, a maximum time commitment) below are the same as in the Vulnerability management lesson, where priority comes from triage by exposure, KEV, EPSS and CVSS.
| Severity | MTTR SLA | Rationale | Action if exceeded |
|---|---|---|---|
| Critical | < 24 hours | Probable or confirmed exploitation | Immediate escalation, production fix |
| High | < 7 days | High risk of exploitation | Priority in the current sprint |
| Medium | < 30 days | Moderate risk | Normal backlog planning |
| Low | < 90 days | Limited impact | Best effort, often accepted as debt |
Vulnerability debt
Vulnerability debt counts the flaws still open beyond their SLA. Like
technical debt, it accrues "interest": the older it gets, the more it costs to
pay off. With a gauge exposing the age of each open vulnerability and
another exposing the SLA per severity, the query compares the two series on
the label severity:
count by (severity) ( security_vulnerability_open_age_seconds > on (severity) group_left () security_sla_seconds)Tested with promtool on three vulnerabilities, two critical ones open for
48 hours and 1 hour, a high one for 24 hours: the query returns 1 for
critical and nothing for high, whose SLA is seven days.
| Indicator | Formula | Target |
|---|---|---|
| Debt volume | Open vulnerabilities beyond SLA | < 5% of the total |
| Average debt age | Average days overdue | < 10 days |
| Trend | Week-on-week change | Decreasing |
Security posture metrics
These indicators give a view of risk beyond individual flaws. The OWASP Top 10:2025, published in its final version in January 2026, has two new categories: A03 Software Supply Chain Failures and A10 Mishandling of Exceptional Conditions. An indicator that ignores the supply chain (dependencies, base images, pipelines) therefore underestimates part of the real risk; the details of the ranking are in the OWASP Top 10 lesson.
Risk Score, overall risk score
The Risk Score aggregates open vulnerabilities, weighted by criticality, into a single figure relative to the size of the estate. It is used to communicate with management and to follow a trend: a falling score signals an improvement, a rising score a deterioration to explain.
Risk Score = (C × 10) + (H × 5) + (M × 2) + (L × 0,5) ────────────────────────────────────────── nombre d'actifs
C, H, M, L = vulnérabilités ouvertes Critical, High, Medium, LowThe weighting makes a critical flaw count twenty times more than a low one. With 100 assets carrying 2 Critical, 15 High, 50 Medium and 100 Low:
Risk Score = (2×10 + 15×5 + 50×2 + 100×0,5) / 100 = (20 + 75 + 100 + 50) / 100 = 2,45The score falls in the Good zone, just below the attention threshold of 2.5: the effort goes to the 2 Critical and the 15 High. The scale below is an internal convention to agree as a team, not a standard.
| Score | Level | Interpretation |
|---|---|---|
| < 1.0 | Excellent | Mature security posture |
| 1.0 - 2.5 | Good | A few points to improve |
| 2.5 - 5.0 | Attention | Corrective actions needed |
| > 5.0 | Critical | Urgent remediation plan |
Calculating the Risk Score in PromQL, without gaps
The direct translation into PromQL hides a measured trap. A sum over a
severity that has no series (zero open critical flaws, so no exposed line)
does not return 0: it returns nothing, and the whole addition disappears.
Tested with promtool, the naive version returns nil on an estate with no
critical vulnerability, where the corrected version returns 1.25. The
dashboard goes blank exactly when the posture is good.
groups: - name: security-kpis-records rules: - record: security:risk_score expr: | ( (sum(security_vulnerabilities_open{severity="critical"}) or vector(0)) * 10 + (sum(security_vulnerabilities_open{severity="high"}) or vector(0)) * 5 + (sum(security_vulnerabilities_open{severity="medium"}) or vector(0)) * 2 + (sum(security_vulnerabilities_open{severity="low"}) or vector(0)) * 0.5 ) / sum(security_assets_total)The or vector(0) pattern replaces a missing series with a zero. On the
test set that reproduces the example (2, 15, 50 and 100 vulnerabilities,
100 assets), promtool test rules confirms the value 2.45.
Vulnerability Density
Vulnerability density relates the number of flaws to the size of the code or to the number of assets, to compare fairly teams of different sizes. Five severe flaws in 10,000 lines are more worrying than fifty in 500,000.
Densité = vulnérabilités High et Critical / 1 000 lignes de code ou = vulnérabilités High et Critical / nombre de conteneurssum by (team) (security_vulnerabilities_open{severity=~"critical|high"})/count by (team) (container_info)Coverage metrics
Coverage measures the share of the IT estate actually analysed by your tools. A vulnerability in an unscanned asset is a blind spot: the danger exists, but no indicator shows it.
Scan coverage
Each asset type has its ratio. The target is 100%, because a single forgotten asset may be the one the attacker uses; shadow IT (systems nobody has listed) is the main enemy of this indicator.
| Coverage type | Formula | Target |
|---|---|---|
| Scanned repositories | Repositories with a scan / total repositories | 100% |
| Scanned images | Scanned images / total registry images | 100% |
| Pipelines with security | Pipelines with a security stage / total | 100% |
| Analysed dependencies | Dependencies covered by SCA / total | 100% |
sum(security_assets_scanned) / sum(security_assets_total) * 100Adoption of security practices
Beyond tools, measure the adoption of good practices by teams: share of repositories with secret-detection pre-commit hooks, share of trained developers. These rates are calculated like coverage, from metrics exposed by your internal tooling.
sum(team_precommit_hooks_enabled) / count(team_info) * 100sum(developer_security_training_completed) / count(developer_info) * 100Tool efficiency metrics
These indicators assess what your security investments bring in: a tool that drowns the team in alerts costs more than it protects.
False positive rate
A false positive is an alert about a problem that is not one. A high rate erodes the teams' confidence, and they end up ignoring real alerts: this is the "crying wolf" effect.
Taux FP = alertes fermées comme faux positif / total des alertes × 100The following thresholds are starting benchmarks: they depend on the technology analysed and the maturity of the rules.
| Tool | Acceptable FP rate | Action if exceeded |
|---|---|---|
| SAST (code analysis) | < 20% | Refine the rules, exclude known patterns |
| SCA (dependencies) | < 5% | Check the detected versions, write VEX documents |
| DAST (dynamic tests) | < 15% | Calibrate the scans, add context |
| Secret detection | < 2% | Refine the rules, list the test tokens |
Return on investment of tools
The ROI (return on investment) of a tool is calculated from the cost of avoided incidents, a value that is always estimated. Any formula therefore rests on an assumption, to be written down explicitly: a detected vulnerability is not an avoided incident.
ROI (%) = (incidents évités estimés × coût moyen d'un incident - coût total de l'outil) / coût total de l'outil × 100The total cost of an open source tool such as Trivy is not zero: it is the time spent integrating, maintaining and triaging its alerts. For the cost of an incident, the IBM Cost of a Data Breach 2026 report puts the global average cost of a data breach at 4.99 million dollars, up 12% in a year; your own estimate (response time, lost revenue, GDPR penalties) remains more telling for management.
Dashboard architecture
A KPI system is only worth anything if it is consulted. Dashboards are split by audience, each with its own rhythm: data rolls up from the team to management, decisions and alerts go back down. The principle follows that of observability, applied to security.
Executive level (CISO)
Management needs a summary view, decision-oriented, readable without technical expertise, and consulted every month as an automated report. It shows the Risk Score and its six-month trend, SLA compliance, the gap between objectives and results, and the three main risks of the moment.
Operations level (SecOps)
The operational security team needs actionable details, every day: MTTD and MTTR per team and severity, volume of open vulnerabilities with their age, real-time alerts on new critical ones, and the list of the ten vulnerabilities to handle first.
Team level (developers)
Development teams must see their own metrics, reviewed every sprint: vulnerabilities in their repositories, the state of the security checks in their pipelines, the debt of their scope and progress against their objectives.
Grafana implementation
Here is an example Grafana configuration for the operational dashboard, in JSON import format, with the PromQL queries used. The metric names are those of the previous sections; adapt them to what your exporter exposes.
{ "dashboard": { "title": "Security KPIs - Operations", "tags": ["security", "kpis", "operations"], "timezone": "browser", "panels": [ { "title": "Risk Score Trend", "type": "timeseries", "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "targets": [ { "expr": "security:risk_score", "legendFormat": "Risk Score" } ], "fieldConfig": { "defaults": { "thresholds": { "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 2.5 }, { "color": "red", "value": 5 } ] } } } }, { "title": "MTTR by Severity (hours)", "type": "bargauge", "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "targets": [ { "expr": "security:mttr_hours:7d", "legendFormat": "{{severity}}" } ], "options": { "orientation": "horizontal", "displayMode": "gradient" } }, { "title": "Open vulnerabilities over SLA", "type": "stat", "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 }, "targets": [ { "expr": "sum(security:vulnerabilities_over_sla:count) or vector(0)", "legendFormat": "over SLA" } ] }, { "title": "Open vulnerabilities by Severity", "type": "piechart", "gridPos": { "h": 8, "w": 6, "x": 6, "y": 8 }, "targets": [ { "expr": "sum by (severity) (security_vulnerabilities_open)", "legendFormat": "{{severity}}" } ] }, { "title": "Top 10 Vulnerable Repositories", "type": "table", "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "targets": [ { "expr": "topk(10, sum by (repository) (security_vulnerabilities_open{severity=~\"critical|high\"}))", "format": "table" } ] } ], "refresh": "5m" }}# Overall Risk Score (recording rule from the posture section)security:risk_score
# Average MTTR per severity, in hours, over 7 dayssum by (severity) (rate(security_remediation_duration_seconds_sum[7d]))/sum by (severity) (rate(security_remediation_duration_seconds_count[7d]))/ 3600
# SLA compliance: share of fixes done on time, over 30 dayssum(increase(security_remediations_total{within_sla="true"}[30d]))/sum(increase(security_remediations_total[30d])) * 100
# New vulnerabilities detected over a rolling weeksum(increase(security_vulnerabilities_detected_total[7d]))
# Scan coveragesum(security_assets_scanned) / sum(security_assets_total) * 100Monthly CISO report
For management, a structured monthly report is worth more than a real-time dashboard to interpret alone. The template below takes up the indicators of the executive audience; the trend and objective columns matter more than the raw value.
# Rapport sécurité - [MOIS ANNÉE]
## Résumé exécutif
| Indicateur | Valeur | Tendance | Objectif | Statut ||------------|--------|----------|----------|--------|| Risk Score | X,XX | en baisse de 15 % | < 2,0 | OK / attention / hors cible || Conformité SLA Critical | XX % | en hausse de 5 % | > 95 % | OK / attention / hors cible || Couverture de scan | XX % | stable | 100 % | OK / attention / hors cible || MTTR Critical (heures) | XX | en baisse de 20 % | < 24 | OK / attention / hors cible |
## Faits marquants
1. [Événement positif : ex. « Correction de la CVE-XXXX en 4 heures »]2. [Point d'attention : ex. « Hausse des vulnérabilités Medium »]3. [Initiative en cours : ex. « SAST déployé sur 15 nouveaux dépôts »]
## Risques majeurs
| Risque | Impact | Probabilité | Mitigation ||--------|--------|-------------|------------|| [Description] | Élevé/Moyen/Faible | Élevée/Moyenne/Faible | [Action] |
## Actions du mois suivant
- [ ] [Action prioritaire 1]- [ ] [Action prioritaire 2]- [ ] [Action prioritaire 3]Automated alerting
Dashboards are consulted now and then; critical situations call for alerts
that go off on their own. These rules are loaded by Prometheus itself,
through rule_files in prometheus.yml: Prometheus evaluates the thresholds,
Alertmanager only routes and notifies afterwards (chat, e-mail, on-call).
- name: security-kpis-alerts rules: - alert: NewCriticalVulnerability expr: increase(security_vulnerabilities_detected_total{severity="critical"}[1h]) > 0 for: 5m labels: severity: critical team: security annotations: summary: "Nouvelle vulnérabilité critique détectée" description: "{{ $value }} nouvelle(s) vulnérabilité(s) critique(s) en une heure" - alert: CriticalSLABreach expr: security_vulnerability_open_age_seconds{severity="critical"} > 86400 labels: severity: critical team: security annotations: summary: "SLA Critical dépassé" description: "Vulnérabilité critique ouverte depuis plus de 24 h : {{ $labels.cve_id }}" - alert: RiskScoreHigh expr: security:risk_score > 5 for: 1h labels: severity: warning team: security annotations: summary: "Risk Score élevé" description: "Le Risk Score global est de {{ $value }}, au-dessus du seuil de 5" - alert: ScanCoverageLow expr: security:scan_coverage:percent < 95 for: 24h labels: severity: warning team: security annotations: summary: "Couverture de scan insuffisante" description: "Seulement {{ $value | printf \"%.1f\" }} % des actifs sont scannés"An alerting rule is tested before it is deployed. promtool check rules
checks the syntax, and promtool test rules replays test series to
check that an alert fires, or does not. Measured with Prometheus 3.15.0:
the full file (four recording rules, four alerts) passes both, and
CriticalSLABreach fires for the only critical vulnerability open for 48 hours.
promtool check rules security-kpis.rules.yml# SUCCESS: 8 rules foundpromtool test rules security-kpis.test.yml# SUCCESSImplementation plan
A KPI system is set up gradually. Trying to instrument everything in the first week is the best way to give up after a month; the steps below add one level of measurement at a time.
-
Weeks 1-2: basic instrumentation
Export to Prometheus the number of open vulnerabilities per severity, from the existing scanners. It is the raw material for everything else.
-
Weeks 3-4: operational dashboard
A first Grafana dashboard: volume per severity, distribution, most affected repositories.
-
Month 2: time-based metrics
Add MTTD and MTTR, which means timestamping detection and fix, often through tickets or GitLab and GitHub labels.
-
Month 3: SLAs and alerting
Set the SLAs per severity, write the alerts and their tests, and communicate the commitments to the teams.
-
Month 4 onwards: extension
Dashboards per audience, coverage, automated monthly report.
Traps to avoid
A KPI system can go off the rails without anyone noticing for months. The following four traps are the most frequent, and each has its antidote.
1. The race for numbers
Measuring for the sake of measuring, with no action. A KPI that never triggers a decision is useless. Each indicator must answer the question: "what will we do if this figure exceeds such a threshold?". With no answer, remove it from the dashboard.
2. Vanity metrics
Showing "0 critical vulnerabilities" is rewarding, but misleading if you only scan 10% of the estate. A vanity metric flatters without informing: always put it in context with coverage, and be wary of an indicator that never goes down.
3. The Cobra effect
The Cobra effect takes its name from a colonial anecdote: a bounty on dead cobras is said to have pushed residents to breed them. It occurs when teams optimise the metric rather than security, for example by closing vulnerabilities as "accepted risk" to improve MTTR without fixing anything. Track acceptances separately.
4. Indicator overload
Too many KPIs drown the information. Stick to 10 to 15 indicators, with a clear hierarchy between those of the daily review and those of the monthly review. To place this work in a wider trajectory, the Assessing your maturity with OWASP DSOMM lesson offers a complete grid.
Further resources
These resources are authoritative on the subject and help justify your choice of thresholds to an auditor or a risk committee. They were checked on 1 October 2026.
| Resource | Description | Link |
|---|---|---|
| OWASP Top 10:2025 | Ranking of application risks, including category A03 on the supply chain | owasp.org |
| NIST Cybersecurity Framework 2.0 | Framework with six functions, including the Govern function | nist.gov |
| IBM Cost of a Data Breach 2026 | Annual study on the average cost of a data breach | ibm.com |
| Prometheus, metric naming | Naming conventions, including the _total suffix for counters | prometheus.io |
| Prometheus, rule unit testing | Format of promtool test rules test files | prometheus.io |
Key points
- MTTD and MTTR measure responsiveness: under one hour to detect a critical flaw, under 24 hours to fix it.
- SLAs (24 h, 7 d, 30 d, 90 d) are maximums: track the compliance rate, not just the average.
- The Risk Score weights open vulnerabilities by criticality; the lesson's example is 2.45, Good zone.
- In PromQL, a sum over a missing series is not 0: without
or vector(0), the Risk Score disappears when there is no critical flaw. - Scan coverage targets 100%: an unscanned asset is an invisible flaw.
- Three levels of dashboards (executive, operations, team) serve three decision rhythms.
- An alerting rule is tested with
promtool test rulesbefore it is deployed. - Start with 3 to 4 KPIs that trigger decisions, then widen.
Knowledge check
Check that you have grasped the essentials of this lesson. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Assessing your DevSecOps maturity with OWASP DSOMM
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
The questions below cover the most common doubts about security KPIs: the difference between MTTD and MTTR, choosing thresholds, calculating the Risk Score and tooling.
promtool test rules. In PromQL, add or vector(0) to each sum: without it, the score disappears as soon as no critical vulnerability is open.