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)
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
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.
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
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.
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).
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)
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]>
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]>
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
platform/vuln-scanner/src/inventory.pysrc/main.py,src/trivy.pysrc/state.py,src/report.pysrc/page.py, sidecar, HTTPRoute (login blocked, see operator action).gitea/workflows/cve-triage.yamlplatform/vuln-scanner/alerts.yaml,src/metrics.py, promtool tests.gitea/workflows/scripts/cve_triage.pyplatform/vuln-scanner/acceptances.yaml,src/acceptances.py, L0 checkrbac.yaml,networkpolicy.yaml, digest pins,renovate.json5Also 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.conformance(L2/L3) skipped: no cluster.approval-policyis 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
.gitea/**,conformance/**,clusters/**and RBAC, soapproval-policyis expected to be red in CI until an administrator reviews. That is the correct stopping point. No agent will post/approve-override.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 existingplatform/keycloak/keycloak-realm-clients.sops.yaml(needs the age private key), and the live realm needs a one-timekcadmclient creation. Exact ordered steps are inplatform/vuln-scanner/README.md, "Operator action required". Those steps are UNVERIFIED.Expected after deploy
workflow_dispatchrun.Spec escalations
credential-requiredis 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 inVULN_PRIVATE_REGISTRIES, short-circuiting before Trivy.Deviations from the recommended shape
script-srcin the CSP, one escaping path, and L1 tests exercise the real rendered page.report.json.UNVERIFIED until it runs in production
Nothing has been deployed. No runtime behaviour has been observed.
platform-adminsrestriction.10.43.210.10. The workflow has never run; Gitea behaviour is tested only against a fake.## Pending Approvalrow format, because issue #91 has no live approve row today.minimumReleaseAgebehaviour on a real update.🤖 Generated with Claude Code
New commits pushed, approval review dismissed automatically according to repository settings
New commits pushed, approval review dismissed automatically according to repository settings