Overview
The supplychain component reads Trivy Operator reports (and, later, its own Grype matches and registry-attached SBOMs) and posts one payload per image digest to the broker. The broker stores them keyed by digest (never per pod), joins them to the image inventory and serves them to the UI, the assistant and the CLI. kguardian reports what the sources found. It does not scan, and it never blocks a workload. All reads are paginated, capped and charged to the broker’s read-memory budget; they answer503 with Retry-After rather than a partial result.
No list response carries an SBOM document.
”In use” is unknown for now
Findings, CVE rows and exposure rows carryinUse: null and
inUseState: "unknown". They are filled when kguardian can tell which
packages a workload actually loads. Until then, treat every finding as
potentially reachable: unknown never means safe.
Sources
Each payload has asource, one of trivy-operator, grype or
registry; anything else is refused. Sources are stored side by side:
each payload replaces only its own (digest, source).
- Findings are deduplicated across sources on (vulnerability id,
package name, installed version). Two sources reporting the same thing
make one finding with
sources: ["grype", "trivy-operator"], never two. Severity, score and EPSS are the highest any source gives,kevis true if any source says so, and a fix from any source makes it fixable.fixedVersionslists every distinct fixed version the sources give, ordered by source; the broker does not pick one, because text order is not version order ("10.1"sorts before"9.2"). grypefindings addkev,kevDateAdded,epssandepssPercentilefrom the Grype DB, and its report names the SBOM(s) it matched (sbomSources). Trivy never sets these, sonullmeans unknown, not “not exploited”.- SBOMs are kept per source and never replace each other. A
registrySBOM is one found attached to the image in its registry, with theattestationit was found through. - Every report carries
sbomTrust, weakest first:attached-unbound(a bare document attached to the image),unverified(an in-toto statement naming the image, signature not checked),scanned(Trivy Operator’s in-cluster scan),verified. Agrypereport carries the weakest trust among the SBOMs it matched. Unknown values, and a registry SBOM that states none, are stored asattached-unbound. Onlyverifiedmay be shown as signed. - A BuildKit SBOM for one platform manifest names its index in
image.index_digest; it joins to an inventory digest equal to that index by theplatform_manifestrule.GET /images/{digest}/sbomlists every source’s SBOM and shows Trivy Operator’s unless you ask for another.
Trust
Whoever holds the supplychain token is trusted to write findings; the broker cannot tell a real scan from a made-up one. What it enforces limits the damage a bad or stolen token can do:- the closed set of sources above;
scanned_atat most 60 s in the future;- different content replaces stored content only from a strictly newer
scanned_at, so a payload cannot be pre-dated to win, and an empty payload cannot wipe findings unless it is a newer scan; - item caps enforced while parsing, and a global ceiling on half-sent SBOM page sets.
How a report is matched to what runs
Trivy Operator reports the digest from the pod spec or the registry, which for a multi-arch image is the index. The kubelet reports the platform manifest it pulled. The broker links each payload to inventory digests with the first rule that matches, and records which one:
An exact link retires any tag guess on the same image. Links are refreshed
at ingest and by the retention pass
(
broker.supplychain.retention.intervalSeconds, default 300 s), so a pod
that starts after its image was scanned is matched within one interval.
Ingest (supplychain token)
supplychain/README.md. {digest} must equal
image.digest.
- Scoped auth only. These routes need a token with the
supplychainscope (BROKER_TOKEN_SUPPLYCHAIN). With broker auth off, or with only the legacy shared token, they answer403. - Bodies.
Content-Encoding: gzip(or none), at most 1 MiB on the wire, received within 30 s (408otherwise). A gzip body is inflated into at mostbroker.supplychain.maxDecompressedBytes(default 8 MiB) and refused with413past it. Parsing stops each list at its cap: 20 000 findings or 10 000 components per request (413); nested lists keep their first entries. A body is read before it waits for anything, so a slow sender only holds its own connection. - One at a time. A single worker inflates, parses and writes, behind a
queue of 8 bodies; a full queue answers
503withRetry-After. The measured worst case is about 62 MiB of heap per ingest. - Paged SBOMs. Pages are staged by
(digest, source, set_id)and swapped in together once alltotalpages are present, in any order; duplicates are harmless. A newer set drops an older incomplete one. Sets whose pages stop arriving are discarded afterbroker.supplychain.retention.sbomPageTtlSeconds. At most 64 sets (256 MiB) are staged across the cluster; a new set beyond that answers429and counts inkguardian_supplychain_staged_sets_refused_total.
400; a contract violation (unknown
schema_version or source, digest mismatch, scanned_at too far ahead,
pages that disagree on total) 422.
The response is 200 (202 for a staged page) with a status:
GET /images/{digest}/vulnerabilities
Findings for one inventory digest, deduplicated across sources, most severe first.reports list means no vulnerability data for this image. That
is unknown, not clean. title and primaryUrl are third-party text:
render them, never fetch or execute them.
GET /images/{digest}/sbom
SBOM components, paged by id (limit default 100, max 500; after is a
component id). reports lists every source’s SBOM with its sbomTrust;
report is the one the items come from: ?source=, else Trivy
Operator’s, else another scanner’s, and a registry SBOM only when it is
the only one. report is null when there is no SBOM.
GET /images/{digest}/sbom/cyclonedx
The same selection as one CycloneDX 1.5 JSON document (download), with the source, join andsbomTrust in metadata.properties. File paths are
not included.
GET /vulnerabilities
Every CVE affecting a digest in the inventory, grouped by id, most severe first. It reads a summary the retention pass rebuilds each interval, so it costs the same however many findings there are;computedAt and
staleSeconds say how fresh it is (null until the first rebuild).
weakestJoin is workload_tag when any match behind the counts is
by tag only.
GET /vulnerabilities/{id}/exposure
Which images, workloads and namespaces a CVE affects, running or not, and what traffic reached them.window_hours (default 168, max 720) sets how
far back flows count. Up to 200 images and 200 workload containers are
listed; truncated says when there are more. 404 when no inventory
digest is affected.
How network exposure is computed
It is observed traffic, from the samepod_traffic rows network policies
are generated from, with the peer identity the broker stamps at ingest
(see Peer attribution):
- Take up to 50 of the workload’s pods the broker knows (live, or dead but not yet pruned), newest first.
- Take their rows inside the window, matched on pod name and
namespace.
flowsObservedcounts them (either direction) andingressFlowsObservedtheINGRESSones. - Of the
INGRESSrows, count distinct peers:ingressFromOtherNamespaces: a pod or service peer in another namespace;ingressFromUnattributedPeers: a peer IP the broker never matched to a pod, service or node (an external client, or an in-cluster pod it could not identify);ingressFromPublicIps: those of the unattributed peers that are internet-routable (not RFC 1918, CGNAT, link-local, ULA, loopback or documentation space);ingressFromNodes: node or host-network peers. A NodePort or LoadBalancer Service withexternalTrafficPolicy: Clusterrewrites outside clients to a node IP, so this counts as possible exposure. Kubelet probes arrive from node IPs too; the broker cannot tell them apart.
exposedistruewhen any of those is non-zero, andexposedVianames which (other_namespace,unattributed,public_ip,node). It isfalsewhenINGRESSflows were observed in the window and none came from outside. It isnull(unknown) when the broker knows no pods for the workload, or they had no ingress flows in the window, even if they had egress: the controller does not capture inbound UDP, so a UDP-only server shows outbound flows and no ingress. Nothing observed is not the same as nothing reachable.
false means no such ingress was seen in the window, not that none is
possible: a Service nobody called this week reads false. The direction
of a row is as the controller classified it.