Kubescape stores its findings as objects in the cluster, one set per surface. A workload with no
result object reads exactly like a workload with nothing wrong, so "complete coverage" needs a
fixed answer to complete coverage of what? This page is that answer (#4262, part of #3156), and
scripts/check-kubescape-result-coverage.sh is
the read-only check that compares the live results against it.
A namespace is in scope unless it is listed in
scripts/kubescape-unscanned-namespaces.tsv. That
is the same reviewed list scripts/guard-kubescape-scan-scope.sh ties to the operator's
excludeNamespaces, so the expected set and the scanner's own scope can only move together, in a
reviewed change. The check does not keep a second exclusion list.
| Surface | Expected object | Result object | Missing | Stale |
|---|---|---|---|---|
| Posture | every Deployment, StatefulSet, DaemonSet and CronJob | a workloadconfigurationscan for that workload, carrying control results |
no result, or a result with no controls | not observable (see below) |
| Vulnerability | every image a long-lived regular container, init sidecar or ephemeral container is running, keyed by image digest | a vulnerabilitymanifestsummary for that digest |
no result for the digest | scanned more than 7 days ago |
| Runtime | every running ReplicaSet, StatefulSet revision and DaemonSet revision | an applicationprofile and a networkneighborhood, both completed/complete |
either half absent | still learning after its learning period |
Why each set is drawn where it is:
- Posture uses the workload kinds, not a control count. Earlier surveys counted controls and disagreed with each other, because the number of controls reported depends on the object kind and on which frameworks the operator loaded. The unit that can be missing is the per-workload result, so that is what is counted. Each result must also carry control results, since an object with no controls is the "scanned but evaluated nothing" case. Jobs are not expected: the operator scans CronJobs, not the Jobs they create.
- Vulnerability is keyed by image digest. Ten replicas of one image need one scan, and two
containers pinned to the same digest share it. Counting containers inflates the denominator
and makes coverage depend on replica counts. A long-lived container is a regular container, a
running init sidecar, or a running ephemeral container whose pod is not owned by a Job; that
includes operator-managed pods such as CloudNativePG instances, which run for as long as a
Deployment's pods do. Completed one-shot init containers are outside the live expected set. If
any included container has no SHA-256 image identity, the check returns
UNKNOWN; it never drops an unidentified image from the expected set. - Runtime expects the pair. The node agent needs both profiles to judge a container's
behaviour, so one half on its own is a gap. A
partialcompletion means the agent started watching after the container did, so the profile does not describe the whole container; the check reports it as a failure. A profile still learning inside its learning period (24 hours by default, read from the profile's own label) isPENDING, not a failure. - A vulnerability summary must agree with itself. The storage server keeps each result twice: a
metadata row, which every list reads, and the stored object, which a read by name returns. When
the two carry different
resourceVersions the summary isSPLIT: every later update is refused as a version conflict, so the scanner keeps re-scanning but the result can never be refreshed and turns stale. A split is reported even while the scan time still looks current, because it is already guaranteed to go stale. The two reads are seconds apart, so a summary the scanner rewrites in that window can show as split once; a real split is still there on the next run.
The posture objects carry no per-object timestamp: creationTimestamp is the first scan, and the
object is updated in place afterwards. So a posture result that exists but was not refreshed by
the latest scan cannot be told apart from a current one here. Posture freshness is watched at the
scanner instead, by the data age of the whole surface.
The CI gate (ksail workload scan --framework nsa,mitre --compliance-threshold 98 in
.github/workflows/ci.yaml) and this check answer different questions, and neither replaces the
other:
The combined NSA/MITRE floor is based on measured Linux CI results. The main baseline publisher uses the same 98% threshold. Neither a passing threshold nor complete result coverage means that every control passes or that suppressed findings have been remediated. Rebaseline upward from actual CI evidence as gaps close; preserve failed findings and treat incomplete observations as unknown.
- The floor scores the rendered manifests with the platform's exceptions applied. It says whether the posture that is evaluated is good enough.
- The coverage check says whether every workload the floor scores also has a live result. It computes no score, so it cannot raise or lower the floor's number.
The posture expected set is the workload part of what the CI scan scores. RBAC objects, Services
and other non-workload kinds are scored by the floor but are not in this set. A live posture figure
is only comparable to the floor when this check passes. With a gap, the live figure describes a
smaller population than the one the floor scored. The live objects are also pre-exception (their
appliedIgnoreRules is empty), so a live score read off them is never the floor's number, even
at full coverage.
It needs read access to the cluster and issues only kubectl get:
scripts/check-kubescape-result-coverage.sh --context <kube-context>It prints one MISSING, STALE, SPLIT, PARTIAL or EMPTY line per failure, PENDING and
ORPHAN lines for information, and one COVERAGE line per surface. It exits 0 when every
expected object has a current result, 1 when any is missing, stale or split, and 2 when it
cannot check. An empty read of any input, or posture objects or vulnerability summaries that read
back short, is 2, never a pass. The same fail-closed result applies when a running pod lacks a
configured container status or controller revision, a posture result's namespace label disagrees
with its Kubernetes namespace, a scan time is invalid or in the future, a vulnerability summary has
no resourceVersion, or a present runtime learning period is malformed. Workloads and pods
are classified directly against the reviewed exclusion list, so an object observed after the
earlier Namespace read remains in scope.
The aggregated storage API returns .spec as null on a LIST, which reads exactly like "no
controls". So the check lists the posture objects, then reads the workload-kind ones back by name
in batches. Only the four workload kinds are read back. Vulnerability summaries are read back the
same way, to compare each stored version with the listed one. A run takes about 40 seconds.
A lasting split blocks the scanner's updates. Preserve the result while investigating it: repeat successful LIST and full-object GET reads, bind them to the same name, namespace and UID, and compare both versions again. A changed object or failed, partial or malformed read is an unknown observation, not permission to repair it. A single mismatch during a concurrent scan is not enough.
Do not use deletion or a forced update as the routine repair. The storage implementation must actually enforce the intended identity and version preconditions; supplying them in a command does not prove that it does. Deletion also removes the available finding until a successful rescan, whose completion is not guaranteed by the delete operation.
A production repair belongs in a reviewed Platform change under #4263. Prefer reconciling a lagging metadata row to an intact, proven newer stored object while preserving its UID, payload and scan timestamp. Before an operator runs any repair, its evidence must establish:
- The exact summary identity and both observed versions, with complete reads and a repeatable split.
- Write semantics that enforce those observations atomically and refuse concurrent scanner changes, replacement objects, missing or corrupt payloads, and an unproven direction of version drift.
- A recovery path for the affected storage and a bounded scope that leaves unrelated results intact.
After the repair, confirm fresh LIST and full-object GET agreement for the same identity. Then verify a successful scanner update and a current scan result, and re-run the full coverage check. Metadata agreement alone establishes storage coherence, not scan freshness, a paired vulnerability result, or complete coverage. Keep the remaining missing and stale results tracked in #4263. The detector stays read-only; it performs none of these repairs.
The revision 9 compatibility image adds native flattened-metadata decoding to the bounded, non-destructive repair introduced in revision 8; the versioned metadata wrapper remains supported. Before the API server and background writers start, it reads only vulnerability-summary metadata and the corresponding payloads. It accepts the same UID, name, namespace and creation time, an intact regular payload file, and a payload exactly one version ahead with a non-decreasing scan timestamp. A conditional SQLite update replaces only the exact observed metadata row. Missing or corrupt payloads, links, replacement objects, invalid versions and concurrent metadata changes are refused and reported. Payload bytes and scan times are retained. The same check runs under the summary's write lock before later scanner updates, so a failed metadata write can be recovered on retry without accepting a stale client's update.
The revision 10 publisher compares retained creation times at the second-level precision used by Kubernetes metadata JSON. Gob retains fractional seconds, so comparing its timestamp directly with a JSON metadata row can falsely report an identity change. The comparison keeps the payload intact and retains the name, namespace, UID, version-direction and scan-time guards.
Publication does not deploy this image. Before a separate immutable image-pin change, verify
its signed digest, a recovery path for the retained store and the repeated split observations.
Preserve the chart's single-writer Recreate rollout. After deployment, retain the reconciliation
receipt, compare both ordinary API reads, observe a successful scanner update and run the complete
coverage check. A refusal or a remaining missing, stale or unpaired result keeps #4263 open.
CI has no cluster, so it runs only the fixture test
(scripts/tests/test-check-kubescape-result-coverage.sh). The test includes the negative control:
one expected result removed from a passing set must fail.
| Surface | Expected | Current | Failing |
|---|---|---|---|
| Posture | 96 | 96 | 0 |
| Vulnerability | 77 | 55 | 8 missing, 14 stale |
| Runtime | 77 | 70 | 2 (one missing pair and one partial pair), plus 10 profiles still learning |
The remaining vulnerability and runtime gaps are the work of #4263 and #4264.
The coverage check asks whether every expected object has a result. The opposite question, whether every result still has an object, needs its own answer, because a stored posture result is not proof that its object exists (#3697).
Two upstream components remove a result once its object is deleted, and for most kinds neither does so on this cluster:
- The operator deletes a result when it sees the object's deletion, but only for the kinds its
continuous-scanning watch names. That watch is empty here on purpose (see
continuousScanningin the kubescape HelmRelease), and a deletion it never saw is not revisited. - The storage service compares the stored results with the live cluster on every cleanup interval, but only for Pods, CronJobs, DaemonSets, Deployments, Jobs, ReplicaSets and StatefulSets. That list is hard-coded, in the pinned v0.0.297 and in v0.0.348 alike; a result of any other kind is skipped.
So the result of a deleted Deployment disappears within hours, while the result of a deleted Role, RoleBinding, ServiceAccount, Secret, ConfigMap, Service, policy or host-data object stays indefinitely. A reader that treats the result set as an inventory then answers "does this still exist?" with yes for an object that is gone, and counts findings nobody can remediate.
scripts/report-kubescape-scan-orphans.sh compares
every result with the live list of its kind. It is read-only (kubectl api-resources and
kubectl get only) and puts each result in one of three classes:
| Class | Meaning |
|---|---|
live |
the list of its kind was read and holds the object |
orphaned |
the list of its kind was read and does not hold the object |
unknown |
nothing was observed either way: the list failed, the cluster does not serve the kind under one name, or the result carries no usable object reference |
scripts/report-kubescape-scan-orphans.sh --context <kube-context>
scripts/report-kubescape-scan-orphans.sh --context <kube-context> --resource summariesThe first form checks the workloadconfigurationscans, the second the
workloadconfigurationscansummaries a posture count is built from. It prints one line per kind,
the totals and a verdict, and exits 0 when every result is live, 1 when any is orphaned,
and 2 when no orphan was found but something is unknown. A failed read never makes a result
orphaned, and a run that read nothing, or stopped before its verdict, is 2.
Three properties decide whether its answer can be trusted:
- The object is the one the
kubescape.io/wlidannotation names. Thekubescape.io/workload-namelabel cannot hold every object name (a colon is replaced, a long name is cut), so matching on it reports live objects as gone. A result that describes an RBAC subject names the binding that grants it, and is checked against that binding. - Live objects are read as tables. kubectl then asks the API server for names and columns only, so no object body, and in particular no Secret data, is requested.
unknowndepends on the credential. A credential that may not list a kind leaves every result of that kindunknown. A read credential without permission to list Secrets, for example, never classifies the Secret results.
The default output holds kinds and counts only. --list live, --list orphaned or
--list unknown writes that class's results to stdout, one per line, and moves the report to
stderr; those rows name objects, so keep them out of issues, pull requests and workflow logs. To
compute a posture figure over objects that exist, hydrate only the --list live rows of the
summaries (see the exception oracle).
CI has no cluster, so it runs only the fixture test
(scripts/tests/test-report-kubescape-scan-orphans.sh). Its negative controls: a result whose
object exists is never orphaned, and a refused or malformed list ends as unknown.
Nothing deletes an orphaned result yet. The report only makes them visible; removing them is the remaining work of #3697.
| Result set | Results | Live | Orphaned | Unknown |
|---|---|---|---|---|
workloadconfigurationscans |
2725 | 1625 | 388 | 712 |
workloadconfigurationscansummaries |
2918 | 1625 | 456 | 837 |
The 156 detailed results of the five workload kinds present (Deployment, StatefulSet, DaemonSet, Job, CronJob) hold no orphan, which is the storage cleanup at work; every orphan is of a kind that cleanup skips. All but one of the unknown detailed results are Secret results, which that credential may not list. A sample of 39 orphaned and 35 live results was read back one object at a time, and every one agreed with its class.