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]>
76 lines
4.9 KiB
Markdown
76 lines
4.9 KiB
Markdown
# vuln-scanner — the CVE page and the five scan alerts
|
|
|
|
Alerts that link here: **`VulnScanStale`**, **`VulnDatabaseStale`**, **`VulnCoverageGap`**,
|
|
**`VulnCriticalFixAvailable`** and **`VulnTriageBotSilent`**.
|
|
|
|
Component: `platform/vuln-scanner/` (Flux Kustomization `vuln-scanner`, `wait: false`, nothing depends on
|
|
it). Design record: spec `docs/17-vulnerability-visibility.md`, ADR-0027. The page is
|
|
`https://cve.<domain>`, internal only, behind SSO, restricted to `platform-admins`.
|
|
|
|
Every mutating step below is an **operator** step (CLAUDE.md §5).
|
|
|
|
## What this means here
|
|
|
|
The scanner runs on a schedule and needs nobody to start, resume or complete a cycle. The bot is a
|
|
scheduled Gitea Actions workflow (`.gitea/workflows/cve-triage.yaml`). Each alert is one grouped email
|
|
for one condition and never names a CVE, image or workload; the page has the detail.
|
|
|
|
| Alert | Fires when | What it means |
|
|
|---|---|---|
|
|
| `VulnScanStale` | No complete run for 26 hours, or the metric is absent | Scanning has stopped, the scanner is down, or its scrape is broken. **It fires from first deployment until the first complete run** (a full run covers about 100 images, sequentially). |
|
|
| `VulnDatabaseStale` | Trivy DB older than 48 hours | `trivy --download-db-only` is failing; the run is degraded, not a success. |
|
|
| `VulnCoverageGap` | Any target `failed` for more than 24 hours | A registry unreachable or a scan keeps failing. `credential-required` targets do not count. |
|
|
| `VulnCriticalFixAvailable` | Unaccepted CRITICAL with a fix, first seen more than 72 hours ago | The backlog automation has not resolved. **Expected on first deployment.** |
|
|
| `VulnTriageBotSilent` | No heartbeat for 26 hours, or the metric is absent | The bot has not reported, so remediation states and Renovate approvals are stale. |
|
|
|
|
A failing scanner is contained by design: its Kustomization is `wait: false` and nothing depends on it, so
|
|
none of these alerts mean anything else on the platform is affected.
|
|
|
|
## First commands
|
|
|
|
All cluster access is read-only and goes through the pinned toolbox, always naming the context:
|
|
|
|
```bash
|
|
./infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod -n vuln-scanner get pods -o wide'
|
|
./infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod -n vuln-scanner logs deploy/vuln-scanner -c scanner --tail=100'
|
|
./infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod -n vuln-scanner exec deploy/vuln-scanner -c scanner -- python3 -c "import urllib.request as u; print(u.urlopen(\"http://127.0.0.1:9090/metrics\").read().decode())"'
|
|
```
|
|
|
|
The metrics are the quickest read: `vuln_scanner_targets{state}` gives the per-state counts, and
|
|
`vuln_scanner_last_complete_run_timestamp_seconds` the last complete run. Then open
|
|
`https://cve.<domain>`, Inventory view, and sort by state to see which targets, and why.
|
|
|
|
For the bot, read the most recent run of the `CVE triage` workflow in Gitea Actions and its job
|
|
summary.
|
|
|
|
## Likely causes, most likely first
|
|
|
|
**`VulnScanStale`** — (1) first deployment: wait for the first complete run; (2) pod not Running: see the
|
|
Pod's events and the `longhorn` PVC attachment; (3) metrics not scraped: `up{namespace="vuln-scanner"}`
|
|
and `platform/vuln-scanner/networkpolicy.yaml`'s Prometheus rule; (4) the scanner logged a run failure.
|
|
|
|
**`VulnDatabaseStale`** — egress to `ghcr.io:443` is blocked (the egress CIDR rule, or the registry is
|
|
down) or GitHub's rate limit; the log has the `trivy` error.
|
|
|
|
**`VulnCoverageGap`** — a registry is down or a tag was deleted. The page shows each failed target's reason.
|
|
A target whose registry refuses anonymous pulls is `credential-required` and does not fire this.
|
|
|
|
**`VulnCriticalFixAvailable`** — expected once on first deployment. Otherwise a Renovate update is not
|
|
landing: see the bot's dashboard issue and Renovate's dashboard. To accept a risk instead, add an entry to
|
|
`platform/vuln-scanner/acceptances.yaml` (reason plus an expiry at most 180 days out) through a normal PR.
|
|
|
|
**`VulnTriageBotSilent`** — (1) `RENOVATE_TOKEN` is missing or the workflow is disabled; (2) the bot
|
|
cannot reach the scanner's internal API (`gitea-actions` namespace -> `vuln-scanner:8081`,
|
|
`networkpolicy.yaml`; the bot falls back to the pinned ClusterIP when in-cluster DNS is unavailable to a
|
|
DinD job container); (3) the scanner is down too: `VulnScanStale` will also be firing.
|
|
|
|
## When to escalate or stop
|
|
|
|
- If the pod is crash-looping, read the scanner's logs before changing anything; a crash loop here is
|
|
contained and is not urgent for the rest of the platform.
|
|
- Do **not** weaken `networkpolicy.yaml` or `rbac.yaml` to make a scan pass. The scanner must never read
|
|
Secrets, hold a write verb, or reach the LAN. Those are the requirement (R-VULN-09), not an oversight.
|
|
- Do not edit `platform/vuln-scanner/alerts.yaml` thresholds to silence an alert; the spec fixes them.
|
|
- A persistent `VulnCriticalFixAvailable` for a component that cannot move is what the risk-acceptance file
|
|
is for, not silencing.
|