Author SHA1 Message Date
jakman85andClaude Opus 5.5 0fb44bb0a8 docs: specify automated vulnerability visibility for production (R-VULN-*, ADR-0027)
Adds R-VULN-01..09, docs/17 and ADR-0027: a scheduled digest scanner in the
cluster, a versioned report, an SSO page listing each CVE against its images,
containers and workloads, grouped email alerts, and a Renovate-shaped CI triage
bot. Renovate stays the only author of version changes, and "not scanned" is
always visible rather than reading as clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 09:21:52 -06:00
12 changed files with 413 additions and 1 deletions

No files matched your search

+5
View File
@@ -67,6 +67,11 @@ two things (e.g. "VM"), this file disambiguates.
- **DR drill** — a tested restore of resources + volumes proving R-STATE-03.
## Delivery & lifecycle
- **Coverage state** — whether a target was actually evaluated for vulnerabilities (`scanned`,
`pending`, `stale`, `failed`, `credential-required`, `by-tag`, `not-covered`). Zero findings means
something only in `scanned`. See [`docs/17-vulnerability-visibility.md`](docs/17-vulnerability-visibility.md).
- **CVE triage bot** — the scheduled CI workflow that summarises the vulnerability report and asks
Renovate to open pending updates; it never authors a version change.
- **GitOps** — desired state lives in git and is reconciled into the cluster automatically
(pull-based). Engine: **Flux CD**.
- **Drift correction** — Flux reverting live changes back to git-declared state (R-GITOPS-03).
+2 -1
View File
@@ -81,7 +81,8 @@ home-network-k3s/
│ ├── 13-service-catalog.md
│ ├── 14-testing-and-conformance.md
│ ├── 15-identity-and-access.md
│ └── 16-known-gaps.md
│ ├── 16-known-gaps.md
│ └── 17-vulnerability-visibility.md
├── decisions/ ← ADRs (one decision per file) + index/template
├── roadmap/ ← phases.md, workstreams.md
└── agents/ ← roles, coordination protocol, DoD, task board
+1
View File
@@ -75,3 +75,4 @@ pattern choices.
| [0023](adr-0023-automated-updates-renovate.md) | Automated updates: Renovate for images and charts, every change PR-gated | Accepted | R-UPD-01, R-UPD-02, R-UPD-03, R-TEST-03 |
| [0024](adr-0024-delivery-authority-ci-gate.md) | Delivery authority: the CI gate is the approver; sensitive-path review; attributed admin override | Accepted | R-TEST-03, R-TEST-06, R-GITOPS-01, R-SEC-02 |
| [0025](adr-0025-in-cluster-ai-program-manager.md) | In-cluster AI Program Manager (pm-agent) | Accepted | R-GITOPS-01, R-TEST-03, R-TEST-06, R-SEC-02 |
| [0027](adr-0027-vulnerability-visibility.md) | Vulnerability visibility: scheduled digest scanning with a Renovate-shaped bot | Accepted | R-VULN-01..09 |
@@ -0,0 +1,67 @@
# ADR-0027 — Vulnerability visibility: scheduled digest scanning with a Renovate-shaped bot
- Status: Accepted
- Date: 2026-11-09
- Serves: R-VULN-01..09, R-OBS-02, R-OBS-03, R-C2-02, R-UPD-02, R-SEC-02, R-IDP-06
- Supersedes: —
- Accepted by: operator merge of this change; effective on merge.
## Context
The operator has no view of the vulnerabilities in what production runs. Renovate moves versions
forward on a schedule, but it knows nothing about CVEs, and nothing in the platform assesses a running
image. Measured on 2026-11-09:
- 259 containers run from 98 unique digests, across eight or more registries, with no scanner present.
- Prometheus holds 427k head series, using about 2.75 GiB of its 3 GiB limit.
- etcd and Longhorn share a partition on every node (accepted risk, [docs/02](../docs/02-environments-and-provisioning.md)),
and etcd's own metrics are not scraped, so the headroom for added etcd writes cannot be measured.
- CI holds no cluster credential by decision, and has two runner slots that also serve the merge gate.
- The implementation repository is readable without authentication.
- Trivy's release pipeline was compromised in March 2026 (CVE-2026-33634).
## Decision
**A small scheduled scanner runs in the cluster, built on the Trivy CLI, and a Renovate-shaped CI
bot turns its results into action.** The full contract is [docs/17](../docs/17-vulnerability-visibility.md).
- **Scanner.** Takes its inventory from the Kubernetes API, scans each unique digest with Trivy, and
stores one versioned report on a small dedicated volume, outside etcd and Prometheus.
- **Page.** Served from that report behind SSO and restricted to `platform-admins`.
- **Metrics.** Aggregates only.
- **Bot.** A scheduled Gitea Actions workflow using the existing CI-scoped Renovate credential. It
maintains a counts-only dashboard issue and requests Renovate PRs through Renovate's own
dashboard approval mechanism.
- **Renovate stays the sole author of version changes.** No second bot edits a tag.
- **Delivery tooling, not a catalog service.** Its cluster-wide read role is something a
ServiceDefinition cannot express, so it is hand-authored under `platform/` with its own non-gating
Flux tier, as in ADR-0025.
## Alternatives considered
- **Trivy Operator.** The obvious choice, and rejected on measurement. It stores one report per
workload as a CRD, adding write load to an etcd we cannot currently measure and which shares its
disk with Longhorn; the largest images produce the largest reports, so size-limit failures would
hide exactly the worst offenders. It needs broad cluster permissions and job-creation rights. It
would also still need a bespoke aggregator, because the per-CVE page cannot be fed from Prometheus.
- **Grafana table over per-CVE Prometheus metrics.** Needs no new page, but adds tens of thousands of
long-labelled series to a Prometheus at about 92% of its memory limit. Raising the limit treats the
symptom; a CVE inventory is not time-series data.
- **Scanning from Gitea Actions.** Closest to Renovate literally, but CI has no view of what is
running, and a long scan would occupy one of the two runner slots that gate merges.
- **A third-party Trivy dashboard.** Assumes the operator's CRDs, and adds a further small-maintainer
supply-chain dependency to a sensitive surface.
- **An in-cluster bot with a Gitea token.** Puts a git-writing credential in the cluster, which
ADR-0023 rejects.
- **A CVE dashboard issue listing the findings.** The most Renovate-like, and rejected because the
repository is publicly readable and the list would be an attack map.
- **A bot that authors bump PRs itself.** A package's fixed version does not identify the image tag
containing it, and two version authors would contend. Rejected in favour of driving Renovate.
## Consequences
- Scanning and its output are hands-off, and "not scanned" is visible rather than silent.
- The bot reuses the Renovate token, so both act as `renovate-bot`. This avoids a new credential;
splitting them later changes one workflow secret.
- The platform carries a small custom component that it must maintain and test. The report schema
isolates the scanner, so Trivy can be replaced without touching the page or the bot.
- Findings for private-registry images, VMs and nodes remain unassessed, and are shown as such
([known gaps](../docs/16-known-gaps.md)).
- The critical-fix alert will fire on first deployment. That is the backlog becoming visible.
- A tick can only open a Renovate PR. Remediation of majors and blast-radius components still waits
for human review.
+22
View File
@@ -150,6 +150,28 @@ mandatory; `SHOULD` is strongly preferred and requires an ADR to deviate.
- **R-SEC-04** — The Kubernetes API MUST NOT be reachable from the public internet by any path (no
tunnel route, no edge forward, no public DNS record); it is LAN/VPN-only (ADR-0016).
### Vulnerability visibility — `R-VULN-*`
- **R-VULN-01** — The platform MUST maintain an inventory of every container image running in
production, by digest, mapped to the namespaces, workloads and containers using it, taken from the
running cluster and not from git.
- **R-VULN-02** — Every running image MUST be evaluated against a current vulnerability database at
least every 24 hours, and a newly running image within 2 hours.
- **R-VULN-03** — Any target not successfully evaluated MUST be visibly reported as such with its
reason. Absence of data MUST NOT be presented as absence of vulnerabilities.
- **R-VULN-04** — An authenticated page MUST list every known CVE and, for each, the images,
containers and workloads it affects, and MUST be searchable and filterable.
- **R-VULN-05** — Inventory, scanning, reporting and triage MUST run on a schedule with **no human
and no AI action**, and MUST fail loudly when they cannot run.
- **R-VULN-06** — Scan failure, staleness, and unaccepted CRITICAL vulnerabilities with an available
fix MUST be reported by **email**, as grouped alerts.
- **R-VULN-07** — A scheduled bot MUST maintain a single dashboard issue and request remediation
through Renovate. Renovate MUST remain the only author of version changes, and CVE detail MUST NOT
be published to any surface readable without authentication.
- **R-VULN-08** — Accepted risk MUST be declared in git, through the merge gate, with a reason and a
mandatory expiry, and MUST revert to open automatically when it expires.
- **R-VULN-09** — The scanner MUST be digest-pinned, MUST NOT auto-merge its own updates, MUST NOT
read Secrets or hold Kubernetes write access, and MUST hold no git credential.
### Identity & access (SSO) — `R-IDP-*`
- **R-IDP-01** — The platform MUST provide a central Identity Provider (SSO) as the single user
directory and login for platform services.
+3
View File
@@ -62,6 +62,9 @@ Grafana dashboards, surfaced in the C2 UI ([12](12-command-center-ui.md)):
- **Network**: Hubble flows, egress/VPN health, denied-flow trends.
- **Per-service**: one row/dashboard per catalog service with its declared SLIs.
The CVE listing is deliberately **not** a Grafana dashboard: per-CVE data does not belong in
Prometheus. See [17](17-vulnerability-visibility.md), which also defines the vulnerability alerts.
## Observability as onboarding output
The ServiceDefinition ([13](13-service-catalog.md)) declares observability expectations; the
+6
View File
@@ -78,6 +78,12 @@ each declared ceiling and carries any per-service cadence.
- **Pin escape hatch**: an operator can pin a service via git if an upstream release misbehaves —
the one sanctioned human touch, and it *stops* automation rather than performing an update.
## Vulnerability-driven requests
The CVE triage bot ([17](17-vulnerability-visibility.md)) can ask Renovate to open a pending update
for an image with an unaccepted CRITICAL that has a fix, using Renovate's own dashboard approval.
Renovate remains the only author of version changes, and every merge rule above still applies.
## Interaction with default-deny & egress
Updates change image versions, not network posture. A VPN-class service stays VPN-class across
updates; policies are reconciled from the (unchanged) ServiceDefinition.
+2
View File
@@ -37,6 +37,8 @@ root aggregates and links them. Status of this composition is tracked in
- **Platform health**: node status, Longhorn capacity/health, Flux reconciliation, cert expiry,
backup status, VPN tunnel state — sourced from the observability stack ([10](10-observability-and-alerting.md)).
- **Alerts**: current firing alerts (mirrors what email delivers).
- **Vulnerabilities**: a link to the CVE page, restricted to `platform-admins`
([17](17-vulnerability-visibility.md)).
## Actions (safe, audited)
+4
View File
@@ -102,6 +102,10 @@ Each maps to requirement IDs; phrased as Given/When/Then intent.
an email alert is delivered and the dashboard reflects it.
- **GitOps drift (R-GITOPS-03, L3):** *Given* a reconciled resource, *When* it is mutated live,
*Then* Flux reverts it to the git-declared state.
- **Vulnerability visibility (R-VULN-*, L0/L1/L2/L3):** *Given* the running inventory, *Then* every
image is either evaluated within its window or visibly reported as not evaluated, and each CVE is
listed against its images, containers and workloads. Full list in
[17](17-vulnerability-visibility.md).
- **Onboarding completeness (WS-APPS):** *Given* only a merged ServiceDefinition, *Then* the service
is fully wired (compute/storage/policy/egress/TLS/DNS/monitoring/update) with no manual step —
asserted by the union of the relevant tests above against that service.
+1
View File
@@ -84,6 +84,7 @@ These cannot be settled by reading either repository.
| R-C2-02 | Grafana dashboards are specified as embedded in the C2, but Homepage only links to Grafana. | `platform/homepage/configmap.yaml` |
| R-C2-02 | Flux status is specified via Headlamp's Flux view, the Flux UI or a Homepage widget, but Headlamp has no Flux plugin configured and Homepage has no widget. | `platform/headlamp/helmrelease.yaml`, `platform/homepage/configmap.yaml` |
| R-C2-02 | C2 actions are specified to include VM start/stop, Flux reconcile and alert acknowledgement, but only native Headlamp workload actions exist, with no plugins for the others. | `platform/headlamp/helmrelease.yaml` |
| R-VULN-01, R-VULN-02 | KubeVirt guests, node OS packages and the k3s binary are inventoried but not vulnerability-assessed; private-registry images need a read-only credential the operator has yet to mint. All report as not evaluated (docs/17). | not yet implemented |
## Service catalog
+289
View File
@@ -0,0 +1,289 @@
# 17 — Vulnerability Visibility
Serves: **R-VULN-01..09**, surfaces **R-OBS-02/03**, **R-C2-02**, **R-UPD-01/02**, **R-SEC-02**,
**R-IDP-06**. Decision record: [ADR-0027](../decisions/adr-0027-vulnerability-visibility.md).
## Purpose
The operator must be able to answer three questions at any time, with no manual step:
1. **What is running in production?** Every container image, by digest, and the workloads using it.
2. **What is it vulnerable to?** Every known CVE in those images, and where each one lands.
3. **Can I trust the answer?** How fresh it is, and exactly what was *not* examined.
The third question is as important as the first two. A vulnerability list that silently omits an
image reads the same as a clean bill of health. Everything below treats "not scanned" as a
first-class state that is always visible, and never as zero findings.
## Shape of the solution
The workflow is deliberately the same shape as Renovate ([11](11-automated-updates.md)): scheduled,
self-hosted, run to completion, machine-authored output, a single dashboard issue, and a loud failure
when it cannot run.
```
Kubernetes API (read-only)
│ running pods → digests → workloads
▼
┌──────────────────────────── namespace: vuln-scanner ─────────────────────────┐
│ Scanner (scheduled, one image at a time, Trivy) │
│ └─► Report store (small PVC) ─► report.json (schema below) │
│ ├─► CVE page cve.<domain>, SSO, platform-admins │
│ ├─► /metrics aggregates only → Prometheus → email │
│ └─► internal API ◄──────────────┐ │
└─────────────────────────────────────────────┼────────────────────────────────┘
│ read report; post heartbeat + remediation state
┌────────────────┴───────────────┐
│ Gitea Actions: CVE triage bot │ scheduled, before Renovate
└────────────────┬───────────────┘
│ scoped CI token
▼
CVE Dashboard issue (counts only) · approval ticks on Renovate's dashboard
│
▼
Renovate opens the PR → merge gate → Flux (the existing update path)
```
The split follows two standing boundaries: **CI holds no cluster credential**, and **the cluster
holds no git credential** ([ADR-0023](../decisions/adr-0023-automated-updates-renovate.md)). The
scanner sees the cluster and never writes to git. The bot writes to Gitea and never touches the
Kubernetes API.
## Inventory (R-VULN-01)
The inventory is taken from the **Kubernetes API**, never from git. Git states intent; the question
is what is actually running.
- It covers containers, init containers and ephemeral containers of every pod in every namespace.
- The identity of a running image is the **digest reported in the container status**. A mutable tag
is not an identity.
- Each pod resolves through its owner chain to the **top-level workload** the operator recognises
(Deployment, StatefulSet, DaemonSet, CronJob, or a bare Pod), not the intermediate ReplicaSet or Job.
- **CronJobs with no pod currently running are still inventoried**, from their pod template. Their
images are recorded by reference and marked `by-tag`, because no digest exists until they run.
- KubeVirt VMs and nodes are inventoried as **non-container targets** and reported as not covered
(see [scope](#scope)).
## Scanning (R-VULN-02, R-VULN-05)
| Property | Contract |
|---|---|
| Unit of work | One scan per **unique digest**, mapped back to all its workloads. Measured 2026-11-09: 259 containers resolve to 98 digests. |
| Full re-evaluation | Every running digest is evaluated against a current database **at least every 24 hours**, so a CVE published against an unchanged image is found without any deployment. |
| New digests | A digest not yet seen is scanned **within 2 hours** of first appearing. |
| Database freshness | A scan uses a vulnerability database no older than **48 hours**. Older than that, the run is reported as degraded, not as a success. |
| Severity scope | All severities are recorded, including those with no fix. Filtering is a page concern. |
| Failure isolation | One image failing (pull error, timeout, out of memory) records that image as `failed` with its reason and the run continues. |
| Load | Sequential, one image at a time, with CPU and memory limits. Scan caches MUST NOT live on a replicated Longhorn volume or in etcd, because etcd and Longhorn share a partition on every node ([02](02-environments-and-provisioning.md)). |
| Trigger | Schedule only. No human or AI action starts, resumes or completes a cycle. |
Findings persist a **first-seen** timestamp across runs, keyed by CVE and digest, so age is
measurable. A finding that disappears because an image was updated leaves the report at the next
inventory; one that disappears because a scan *failed* does not.
## Coverage (R-VULN-03)
Every inventoried target has exactly one state:
| State | Meaning |
|---|---|
| `scanned` | Evaluated within the 24-hour window. Zero findings is meaningful only in this state. |
| `pending` | Discovered and not yet scanned, within the 2-hour window. |
| `stale` | Last successful scan is older than the 24-hour window. Prior findings are retained and labelled stale. |
| `failed` | The most recent attempt failed. The reason is shown. Prior findings are retained. |
| `credential-required` | The registry refused anonymous access. Shown with the registry. |
| `by-tag` | Scanned by mutable reference, because no running digest exists. |
| `not-covered` | A target class this scanner cannot examine: VMs and nodes. |
The page and the report MUST NOT present a total or a "clean" indication that silently excludes any
target. The header of the page always states how many targets are in each state.
## The report contract
`report.json` is the one interface between the scanner, the page and the bot. It is versioned and
the implementation validates it against a schema.
```yaml
schemaVersion: 1
generatedAt: <RFC 3339>
database: { updatedAt: <RFC 3339>, version: <string> }
run: { startedAt: <RFC 3339>, finishedAt: <RFC 3339>, status: complete | degraded }
targets:
- id: <stable identifier>
type: image | vm | node
image: <registry/repository>
digest: <sha256:… or null>
state: <one of the coverage states>
reason: <string or null>
lastScannedAt: <RFC 3339 or null>
workloads:
- { namespace: <ns>, kind: <kind>, name: <name>, containers: [<name>] }
findings:
- id: <CVE or advisory id>
targetId: <targets[].id>
severity: CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN
cvss: <number or null>
package: <name>
installedVersion: <string>
fixedVersion: <string or null>
title: <string>
url: <advisory URL>
firstSeenAt: <RFC 3339>
status: open | fix-available | accepted
acceptance: { reason: <string>, expires: <date> } # present only when accepted
remediation: # written by the triage bot, may be empty
- { image: <registry/repository>, state: <state>, url: <PR URL or null>, updatedAt: <RFC 3339> }
```
Scanner-derived text (titles, package names, advisory URLs) originates upstream and is **untrusted**.
The page MUST escape it and serve a restrictive Content-Security-Policy, and only `http(s)` URLs may
become links.
## The CVE page (R-VULN-04)
A page at `cve.<domain>`, linked from the C2 landing page.
**Header, always visible:** report age, database age, per-state target counts, and open findings by
severity. If the report is stale or degraded, the page says so prominently and does not show a
normal state.
**CVEs view (default):** one row per CVE, with id (linking to the advisory), severity, CVSS,
package, fixed version, first seen, status, and the counts of affected images and workloads. Each
row expands to list **every image, container and workload** it touches, with namespace.
**Workloads view:** one row per workload, with its containers, images and per-severity counts,
expanding to its CVEs.
**Inventory view:** every target and its coverage state, *including those with zero findings*. This
is the answer to "what am I running".
**Interaction:** free-text search, and filters for severity, namespace, workload, status and "fix
available", with sortable columns. Filter state is in the URL so a view can be linked. The
default view excludes nothing.
**Remediation column:** for images with a fix available, one of `pr-open` (with link),
`approval-requested`, `no-update-identified` or `not-tracked`, taken from the bot.
**Access:** internal exposure only, behind SSO, restricted to `platform-admins` (R-IDP-06). A list of
what is exploitable where is sensitive, so this is not a "logged-in user" surface. The page is
read-only and offers no actions.
## Alerting (R-VULN-06)
Alerts are delivered through the existing Alertmanager email route. Each alert is **one grouped
message per condition**, never one per CVE.
| Alert | Fires when |
|---|---|
| Scan stale | No complete run within 26 hours, **or the metric is absent** |
| Database stale | Vulnerability database older than 48 hours |
| Coverage gap | Any target in `failed` for more than 24 hours |
| Critical fix available | Any unaccepted CRITICAL with a fix, first seen more than 72 hours ago |
| Triage bot silent | No bot heartbeat for 26 hours, **or the metric is absent** |
The 72-hour grace gives Renovate's daily cycle time to act, so email marks the cases automation has
not resolved. The critical-fix alert is expected to fire on first deployment.
Prometheus receives **aggregates only**: counts by severity and fixability, per-state target counts,
timestamps. It MUST NOT receive a per-CVE series. Measured 2026-11-09: Prometheus holds 427k head
series at about 2.75 GiB of a 3 GiB limit.
## The triage bot (R-VULN-05, R-VULN-07)
A scheduled Gitea Actions workflow that follows the conventions of the Renovate workflow: it fails
hard on a missing credential rather than skipping, has a `workflow_dispatch` dry run, never runs
concurrently with itself, and records what it did in the job summary. It runs **before** the daily
Renovate run so that approvals take effect the same morning.
Each run it:
1. Reads `report.json` over the internal API.
2. Creates or updates a single **CVE Dashboard** issue on the implementation repository.
3. Reads Renovate's Dependency Dashboard and open PRs, and derives each image's remediation state.
4. Ticks the **approval** checkbox on Renovate's dashboard for a pending update whose package matches
an image with an unaccepted CRITICAL that has a fix.
5. Posts its heartbeat and the remediation states back to the scanner.
**Renovate remains the only author of version changes** (R-UPD-02, ADR-0023). The bot never edits a
version, opens a bump PR or merges anything. A tick only asks Renovate to *open* a PR; majors and
blast-radius components still wait for human review, exactly as today.
**The dashboard issue carries aggregates only:** total findings by severity, per-state coverage,
timestamps and a link to the page. It MUST NOT name a CVE, package or image-to-workload relationship,
because the repository is readable without authentication (measured 2026-11-09). It MUST NOT carry any
label that the in-cluster Program Manager acts on ([ADR-0025](../decisions/adr-0025-in-cluster-ai-program-manager.md)).
The internal API accepts reads and a single bounded write (heartbeat plus remediation states, schema
validated). It is reachable only from the CI runner namespace, by network policy.
## Risk acceptance (R-VULN-08)
A CVE the operator has decided to live with is declared **in git**, through the merge gate, with
an expiry:
```yaml
- id: CVE-2026-00000
image: registry/repository # exact match; omit only for a platform-wide acceptance
reason: not reachable — feature disabled
expires: 2027-02-01 # mandatory, at most 180 days out
```
An accepted finding stays in the listing, marked `accepted` with its reason and expiry, and stops
counting toward the critical-fix alert. **When the expiry passes it reverts to open automatically.**
An entry with no reason or expiry is rejected by the gate.
## Security of the scanner itself (R-VULN-09)
A scanner reads every image and is a supply-chain target. Trivy's release and CI tooling were
compromised in March 2026 ([CVE-2026-33634](https://nvd.nist.gov/vuln/detail/CVE-2026-33634)), so:
- The scanner binary is pinned **by digest**. It is a Renovate blast-radius component and never
auto-merges, and an update is proposed only after a **minimum release age of 7 days**.
- Trivy 0.69.4, 0.69.5 and 0.69.6 MUST NOT be used.
- Identity: `get/list/watch` on pods and their owner kinds, CronJobs, nodes and VM instances, and
**nothing else**. It MUST NOT read Secrets or hold any write verb on the Kubernetes API.
- It holds no git credential and no registry credential.
- Network: ingress only from the Gateway for the page, from CI for the internal API and from
Prometheus for metrics. Egress only to the Kubernetes API, DNS, and TCP/443 on the public internet,
with cluster and LAN ranges excluded, through the direct path.
- It runs unprivileged, with a read-only root filesystem and no host namespaces or mounts.
- The scanner's own images appear in the report like any other.
## Scope
**In scope:** every container image running in a pod, on every node, in every namespace, including
the platform's own components.
**Not in scope for this version, and reported as `not-covered` rather than omitted:** guest OS
packages inside KubeVirt VMs, and node OS packages and the k3s binary. These are updated by the
mechanisms in [11](11-automated-updates.md) but are not vulnerability-assessed here.
Also out of scope: application dependency scanning of source, misconfiguration and secret scanning,
runtime exploit detection, and blocking deployment based on findings.
## Acceptance gates
Each is an automated, requirement-tagged test ([14](14-testing-and-conformance.md)).
- **L0:** digest-pinned scanner image; no banned version; Renovate rule keeps it out of auto-merge
with the minimum release age; RBAC contains no Secret access and no write verb; no per-CVE metric
label; alerts unit-tested, including the absent-metric cases; the bot workflow fails on a missing
credential; acceptance entries without reason or expiry rejected.
- **L1 (fixtures, no cluster):** inventory resolves ReplicaSet to Deployment and Job to CronJob;
two workloads sharing a digest produce one scan and both mappings; one failed image leaves the run
continuing with that target `failed` and its prior findings retained; an expired acceptance reverts
to open; the rendered page contains every fixture CVE, container and workload; a hostile title is
escaped; the page shows a warning for a stale report; a dashboard issue rendered from a fixture
contains no CVE id or package name; a tick is applied only to an `approve` row for a matching image
with a qualifying finding.
- **L2:** a newly started workload's digest appears in the report within the window; the page requires
a `platform-admins` login; the internal API is unreachable from an unrelated namespace.
- **L3:** breaking registry access produces `failed` targets and fires the coverage alert.
## Open questions
- **Private registry coverage.** The platform's own images need a registry credential, so they report
`credential-required`. Closing this requires a read-only registry token, an operator action.
- **Exploitability ranking.** Adding CISA KEV or EPSS data, and weighting by whether a workload is
publicly exposed, would rank the list better than CVSS alone.
- **VM and node assessment.** How to bring `not-covered` targets into scope.
+11
View File
@@ -105,6 +105,17 @@ alerts live.
---
## Phase 8 — Vulnerability Visibility
**Status:** open; may proceed alongside Phase 7.
**Entry:** ADR-0027 accepted.
**Do:** deliver the scanner, report, CVE page, alerts and triage bot in
[`../docs/17-vulnerability-visibility.md`](../docs/17-vulnerability-visibility.md).
**Exit:** every running image is evaluated within its window or visibly reported as not evaluated;
the acceptance gates in docs/17 pass.
**Serves:** R-VULN-01..09.
---
## Dependency flow
```