Hand-authored platform/vuln-scanner (non-gating Flux tier, digest-pinned Trivy 0.76.1 and python 3.14.9-alpine, read-only RBAC, per-pod oauth2-proxy, internal-only cve.<domain>), five alerts with promtool tests, R-VULN-01..09 registry entries, and vuln-* L0 checks with mutation self-tests. src/ and the bot workflow are integrated separately. Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
4.9 KiB
YAML
97 lines
4.9 KiB
YAML
---
|
|
# Alert rules for the vulnerability scanner (R-VULN-06, docs/17 "Alerting").
|
|
#
|
|
# ONE ALERT, ONE GROUPED EMAIL PER CONDITION, never one per CVE. The scanner's metrics are aggregates
|
|
# with no per-CVE, per-image or per-workload label; `max()` strips scrape labels, so each rule yields a
|
|
# single alert instance, and Alertmanager's `group_by: [alertname, namespace]` route delivers one email.
|
|
#
|
|
# THE TWO RULES WHOSE ABSENT-METRIC CASE IS LOAD-BEARING: VulnScanStale and VulnTriageBotSilent. Their
|
|
# metrics are absent until the first complete run / first bot heartbeat, and absent when the scanner
|
|
# or the scrape is dead; both are `or absent(...)` so a scanner that has never run, or an unscraped
|
|
# one, fires instead of reading as healthy. Pinned by alerts.promtool-test.yaml in both directions.
|
|
# VulnCriticalFixAvailable is expected to fire on first deployment (the backlog made visible).
|
|
#
|
|
# Severity follows the platform-health convention: warning (a condition to act on, not a page).
|
|
apiVersion: monitoring.coreos.com/v1
|
|
kind: PrometheusRule
|
|
metadata:
|
|
name: vuln-scanner
|
|
namespace: vuln-scanner
|
|
labels:
|
|
app.kubernetes.io/name: vuln-scanner
|
|
spec:
|
|
groups:
|
|
- name: platform-health.vulnerability-scanner
|
|
rules:
|
|
- alert: VulnScanStale
|
|
# No complete run within 26 hours, or the metric is absent. 15m rides out a pod restart.
|
|
expr: >-
|
|
(time() - max(vuln_scanner_last_complete_run_timestamp_seconds) > 93600)
|
|
or absent(vuln_scanner_last_complete_run_timestamp_seconds)
|
|
for: 15m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: No complete vulnerability scan in over 26 hours
|
|
description: >-
|
|
The scanner has not completed a run within 26 hours, or no scan metric exists at all
|
|
(the scanner or its scrape is down). Findings on the CVE page are ageing.
|
|
runbook_url: "https://${BASE_DOMAIN}/runbooks/vuln-scanner"
|
|
|
|
- alert: VulnDatabaseStale
|
|
# Vulnerability database older than 48 hours (docs/17): the run is degraded, not a success.
|
|
expr: time() - max(vuln_scanner_database_updated_timestamp_seconds) > 172800
|
|
for: 15m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: Vulnerability database is older than 48 hours
|
|
description: >-
|
|
The Trivy vulnerability database has not been refreshed in over 48 hours, so scans
|
|
evaluate images against stale advisories and the run reports as degraded.
|
|
runbook_url: "https://${BASE_DOMAIN}/runbooks/vuln-scanner"
|
|
|
|
- alert: VulnCoverageGap
|
|
# A target has been `failed` for more than 24 hours. credential-required targets are not
|
|
# `failed` and do not count, so a private registry cannot hold this alert firing.
|
|
expr: max(vuln_scanner_failed_targets_over_24h) > 0
|
|
for: 15m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "{{ $value }} image(s) have failed scanning for over 24 hours"
|
|
description: >-
|
|
One or more running images have failed scanning for more than 24 hours, so what is
|
|
reported for them is out of date. The CVE page lists each target and the reason.
|
|
runbook_url: "https://${BASE_DOMAIN}/runbooks/vuln-scanner"
|
|
|
|
- alert: VulnCriticalFixAvailable
|
|
# An unaccepted CRITICAL with a fix, first seen over 72 hours ago. The grace period gives
|
|
# Renovate's daily cycle time to act, so this marks what automation has not resolved.
|
|
expr: max(vuln_scanner_critical_fix_overdue) > 0
|
|
for: 15m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "{{ $value }} CRITICAL finding(s) with a fix available for over 72 hours"
|
|
description: >-
|
|
One or more unaccepted CRITICAL vulnerabilities have had a fix available for more than
|
|
72 hours. The CVE page has the details; this message deliberately names none of them.
|
|
runbook_url: "https://${BASE_DOMAIN}/runbooks/vuln-scanner"
|
|
|
|
- alert: VulnTriageBotSilent
|
|
# No bot heartbeat for 26 hours, or the metric is absent (the bot has never run, or the
|
|
# scanner or its scrape is down).
|
|
expr: >-
|
|
(time() - max(vuln_scanner_triage_heartbeat_timestamp_seconds) > 93600)
|
|
or absent(vuln_scanner_triage_heartbeat_timestamp_seconds)
|
|
for: 15m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: CVE triage bot has not reported in over 26 hours
|
|
description: >-
|
|
The scheduled CVE triage bot has not posted a heartbeat in over 26 hours (or never
|
|
has), so remediation states are stale and no approvals are being requested.
|
|
runbook_url: "https://${BASE_DOMAIN}/runbooks/vuln-scanner"
|