feat(vuln): automated CVE visibility for production (R-VULN-*, ADR-0027) #165

Open
gitea-admin wants to merge 15 commits from feat/vuln-visibility into main
pull from: feat/vuln-visibility
Owner

Implements docs/17-vulnerability-visibility.md (ADR-0027) in full, R-VULN-01..09: a scheduled in-cluster Trivy scanner, an SSO-gated CVE page, aggregate-only metrics and alerts, and a Renovate-shaped triage bot.

Depends on spec PR Home-Network/home-network-k3s#4, merge that first; SPEC_PIN not bumped. The merge SHA does not exist yet, and open PR #160 owns the pin backlog.

Requirements and spec sections served

R-ID Spec section Where
R-VULN-01 Inventory platform/vuln-scanner/src/inventory.py
R-VULN-02 Scanning src/main.py, src/trivy.py
R-VULN-03 Coverage src/state.py, src/report.py
R-VULN-04 The CVE page src/page.py, sidecar, HTTPRoute (login blocked, see operator action)
R-VULN-05 Trigger, no human step scan loop, .gitea/workflows/cve-triage.yaml
R-VULN-06 Alerting platform/vuln-scanner/alerts.yaml, src/metrics.py, promtool tests
R-VULN-07 The triage bot .gitea/workflows/scripts/cve_triage.py
R-VULN-08 Risk acceptance platform/vuln-scanner/acceptances.yaml, src/acceptances.py, L0 check
R-VULN-09 Security of the scanner rbac.yaml, networkpolicy.yaml, digest pins, renovate.json5

Also surfaces R-OBS-02/03, R-C2-02, R-UPD-01/02, R-SEC-02, R-IDP-06.

Own Flux tier (clusters/base/vuln-scanner.yaml): wait: false, dependsOn: platform-config, nothing depends on it, so this tier cannot stall the platform.

Gate

Local toolbox gate on e1adc39: rc 3, UNDETERMINED, not green.

[skip] stage spec-immutability: SKIPPED (not run)
[skip] stage gitea-settings: SKIPPED (not run)
[skip] stage conformance: SKIPPED (not run)
[ ok ] approval-policy: green
[ ok ] decisions-log: green
[ ok ] lint: green
[ ok ] l0: green
[ ok ] render-golden: green
[ ok ] l1: green
[fail]   Stages that did not run: spec-immutability gitea-settings

conformance (L2/L3) skipped: no cluster. approval-policy is N/A offline and proves nothing locally. L2/L3 tests for this feature are scaffolds that do not run in CI.

Renovate validator (renovate:44.167.0, the tag the workflow runs): validated successfully.

Operator action required

  1. Review the sensitive paths. This PR touches .gitea/**, conformance/**, clusters/** and RBAC, so approval-policy is expected to be red in CI until an administrator reviews. That is the correct stopping point. No agent will post /approve-override.
  2. Add the Keycloak client. R-VULN-04 is NOT delivered until this is done. Login fails closed (invalid_client) until then; the scanner, metrics and alerts are unaffected. An agent cannot do it: the client secret must be a new key in the existing platform/keycloak/keycloak-realm-clients.sops.yaml (needs the age private key), and the live realm needs a one-time kcadm client creation. Exact ordered steps are in platform/vuln-scanner/README.md, "Operator action required". Those steps are UNVERIFIED.

Expected after deploy

  • VulnCriticalFixAvailable will fire on day one. That is the backlog becoming visible, as ADR-0027 says.
  • VulnTriageBotSilent fires until the bot's first successful run at 07:15 UTC or a workflow_dispatch run.
  • VulnScanStale fires during the first full scan cycle, because the last-complete metric is absent until one completes.

Spec escalations

  1. credential-required is defined as "the registry refused anonymous access", but the egress contract excludes LAN ranges, so the scanner cannot reach the private registry to be refused. Implemented by naming private registries in VULN_PRIVATE_REGISTRIES, short-circuiting before Trivy.
  2. The egress contract lists only the Kubernetes API, DNS and public TCP/443, but the SSO sidecar must reach Keycloak. Egress additionally allows the Gateway on 443 and Keycloak.

Deviations from the recommended shape

  • Page is server-rendered with no JavaScript rather than inline JS: no script-src in the CSP, one escaping path, and L1 tests exercise the real rendered page.
  • Three pod ports (page, internal, metrics) so Prometheus cannot read report.json.
  • Python 3.14.9-alpine, the version the tests ran on; Trivy 0.76.1, as 0.76.2 was not old enough.

UNVERIFIED until it runs in production

Nothing has been deployed. No runtime behaviour has been observed.

  • A real Trivy run; every scanner test uses a recorded fixture and a fake runner.
  • The real Kubernetes API, browser rendering, OIDC login and the platform-admins restriction.
  • In-cluster DNS from a DinD job container, and reachability of the pinned ClusterIP fallback 10.43.210.10. The workflow has never run; Gitea behaviour is tested only against a fake.
  • Renovate's ## Pending Approval row format, because issue #91 has no live approve row today.
  • minimumReleaseAge behaviour on a real update.
  • Cgroup v2 memory behaviour under the scanner's limits (inferred).

🤖 Generated with Claude Code

Implements `docs/17-vulnerability-visibility.md` (ADR-0027) in full, R-VULN-01..09: a scheduled in-cluster Trivy scanner, an SSO-gated CVE page, aggregate-only metrics and alerts, and a Renovate-shaped triage bot. **Depends on spec PR Home-Network/home-network-k3s#4, merge that first; SPEC_PIN not bumped.** The merge SHA does not exist yet, and open PR #160 owns the pin backlog. ## Requirements and spec sections served | R-ID | Spec section | Where | |---|---|---| | R-VULN-01 | Inventory | `platform/vuln-scanner/src/inventory.py` | | R-VULN-02 | Scanning | `src/main.py`, `src/trivy.py` | | R-VULN-03 | Coverage | `src/state.py`, `src/report.py` | | R-VULN-04 | The CVE page | `src/page.py`, sidecar, HTTPRoute (**login blocked, see operator action**) | | R-VULN-05 | Trigger, no human step | scan loop, `.gitea/workflows/cve-triage.yaml` | | R-VULN-06 | Alerting | `platform/vuln-scanner/alerts.yaml`, `src/metrics.py`, promtool tests | | R-VULN-07 | The triage bot | `.gitea/workflows/scripts/cve_triage.py` | | R-VULN-08 | Risk acceptance | `platform/vuln-scanner/acceptances.yaml`, `src/acceptances.py`, L0 check | | R-VULN-09 | Security of the scanner | `rbac.yaml`, `networkpolicy.yaml`, digest pins, `renovate.json5` | Also surfaces R-OBS-02/03, R-C2-02, R-UPD-01/02, R-SEC-02, R-IDP-06. Own Flux tier (`clusters/base/vuln-scanner.yaml`): `wait: false`, `dependsOn: platform-config`, nothing depends on it, so this tier cannot stall the platform. ## Gate Local toolbox gate on `e1adc39`: **rc 3, UNDETERMINED, not green.** ``` [skip] stage spec-immutability: SKIPPED (not run) [skip] stage gitea-settings: SKIPPED (not run) [skip] stage conformance: SKIPPED (not run) [ ok ] approval-policy: green [ ok ] decisions-log: green [ ok ] lint: green [ ok ] l0: green [ ok ] render-golden: green [ ok ] l1: green [fail] Stages that did not run: spec-immutability gitea-settings ``` `conformance` (L2/L3) skipped: no cluster. `approval-policy` is N/A offline and proves nothing locally. L2/L3 tests for this feature are scaffolds that do not run in CI. Renovate validator (`renovate:44.167.0`, the tag the workflow runs): validated successfully. ## Operator action required 1. **Review the sensitive paths.** This PR touches `.gitea/**`, `conformance/**`, `clusters/**` and RBAC, so `approval-policy` is expected to be red in CI until an administrator reviews. That is the correct stopping point. No agent will post `/approve-override`. 2. **Add the Keycloak client. R-VULN-04 is NOT delivered until this is done.** Login fails closed (`invalid_client`) until then; the scanner, metrics and alerts are unaffected. An agent cannot do it: the client secret must be a new key in the existing `platform/keycloak/keycloak-realm-clients.sops.yaml` (needs the age private key), and the live realm needs a one-time `kcadm` client creation. Exact ordered steps are in `platform/vuln-scanner/README.md`, "Operator action required". Those steps are UNVERIFIED. ## Expected after deploy - **VulnCriticalFixAvailable will fire on day one.** That is the backlog becoming visible, as ADR-0027 says. - VulnTriageBotSilent fires until the bot's first successful run at 07:15 UTC or a `workflow_dispatch` run. - VulnScanStale fires during the first full scan cycle, because the last-complete metric is absent until one completes. ## Spec escalations 1. `credential-required` is defined as "the registry refused anonymous access", but the egress contract excludes LAN ranges, so the scanner cannot reach the private registry to be refused. Implemented by naming private registries in `VULN_PRIVATE_REGISTRIES`, short-circuiting before Trivy. 2. The egress contract lists only the Kubernetes API, DNS and public TCP/443, but the SSO sidecar must reach Keycloak. Egress additionally allows the Gateway on 443 and Keycloak. ## Deviations from the recommended shape - Page is server-rendered with no JavaScript rather than inline JS: no `script-src` in the CSP, one escaping path, and L1 tests exercise the real rendered page. - Three pod ports (page, internal, metrics) so Prometheus cannot read `report.json`. - Python 3.14.9-alpine, the version the tests ran on; Trivy 0.76.1, as 0.76.2 was not old enough. ## UNVERIFIED until it runs in production Nothing has been deployed. No runtime behaviour has been observed. - A real Trivy run; every scanner test uses a recorded fixture and a fake runner. - The real Kubernetes API, browser rendering, OIDC login and the `platform-admins` restriction. - In-cluster DNS from a DinD job container, and reachability of the pinned ClusterIP fallback `10.43.210.10`. The workflow has never run; Gitea behaviour is tested only against a fake. - Renovate's `## Pending Approval` row format, because issue #91 has no live approve row today. - `minimumReleaseAge` behaviour on a real update. - Cgroup v2 memory behaviour under the scanner's limits (inferred). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added 15 commits 2026-11-09 18:41:24 +00:00
Scheduled Gitea Actions bot in the Renovate shape: maintains a
counts-only CVE Dashboard issue, derives remediation states from
Renovate's dashboard and PRs, ticks approve rows for exact-matched
images with an unaccepted CRITICAL with a fix, and posts the heartbeat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stdlib-only scanner per docs/17: Kubernetes API inventory, Trivy runner,
persisted state, schema-validated report.json, strict acceptances parser,
server-rendered page, aggregate metrics, internal API, and a scan loop
that stays live while scanning.

L1 tests under conformance/tests/l1-rendering/vuln-scanner-* run the
actual modules against recorded fixtures with a fake Trivy runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Generated through pipes and encrypted to the repo's public age recipient; values never echoed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review findings on the integrated scanner:
- An OOM kill no longer wedges the loop: inFlight is persisted before each scan, and on restart
  that image is marked failed and sits out full runs for 20h.
- One bad upstream string can no longer freeze the report: safe_url requires an exact lowercase
  scheme, acceptance reasons are bounded, /triage urls use the same rule, and report.build
  repairs or drops any entry the schema would refuse. lastCompleteRunAt advances only after a
  report is actually published.
- The private registry reports credential-required, not failed: VULN_PRIVATE_REGISTRIES names
  it, and Trivy is not invoked.
- loadError clears after a full run against a good inventory.
- Every image under platform/vuln-scanner stays out of Renovate auto-merge.
- The page labels status and target state per line, and mixed acceptance reads as mixed.

The Keycloak client is still an operator action: its key must go in an existing encrypted file
that an agent must not modify. platform/vuln-scanner/README.md has the exact steps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(vuln): handoff for automated CVE visibility
gate / gate (pull_request) Failing after 54s
gate / conformance (pull_request) Skipped
e1adc3963c
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Some required checks failed
gate / gate (pull_request) Failing after 54s
Required
Details
gate / conformance (pull_request) Skipped
Some required checks were not successful.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/vuln-visibility:feat/vuln-visibility
git checkout feat/vuln-visibility
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#165