fix(kubescape): report health correctly and make the level filter work - #75
Open
slashben wants to merge 2 commits into
Open
fix(kubescape): report health correctly and make the level filter work#75slashben wants to merge 2 commits into
slashben wants to merge 2 commits into
Conversation
check_health reported `healthy: false` on a fully working Kubescape install, and the `level` filter on list_vulnerability_manifests silently did nothing. Both were verified on a live cluster running the kubescape-operator chart 1.40.4. Three fixes: 1. The spdx.softwarecomposition.kubescape.io resources are served by an aggregated API server (the storage service, wired via an APIService), not by CRDs. The four CustomResourceDefinitions().Get() lookups therefore returned IsNotFound on every install, forever, so check_health always reported vulnerability_crd and configuration_crd as errors and failed the whole check. Availability is now probed by listing each resource through the storage client the data tools already use, which also proves the path those tools depend on rather than a proxy for it. A single list per resource answers both "is the API reachable?" and "is there data?", so this makes four fewer API calls than before. The apiextensions client is no longer needed and is removed. Check keys are unchanged (*_crd) so existing consumers of the output are unaffected; only the lookup and the message wording change. 2. The pod label selectors matched the wrong pods. app.kubernetes.io/name=kubescape-operator is the chart-wide label carried by every pod the chart creates, so "operator_pods: 7/7" was really an all-kubescape-pods count, and app.kubernetes.io/name=storage matched nothing at all — storage_pods reported "No storage pods found" while the storage pod was Running. Per-component identity lives in the plain `app` label, so the selectors are now app=operator and app=storage. A missing storage pod is now an error that fails the health check rather than a warning: every data tool in this provider reads through the storage service. 3. list_vulnerability_manifests advertised a `level` filter but passed it as a labelSelector, and the storage API server ignores labelSelector on list (kubescape/storage#363) — all three values returned byte-identical output. Filtering now happens client-side on the same image-level/workload-level predicate the response already reports, so the filter and the output cannot disagree. An unrecognised level is now a validation error instead of silently meaning "both". Tests are fake-client based and need no cluster. The level-filter test installs a reactor that reproduces the storage server's actual behaviour of ignoring labelSelector, so a server-side filter cannot pass it. Signed-off-by: Ben Hirschberg <ben@armosec.io> Docs-exempt: bug fix; no existing doc describes check_health or the level filter Signed-off-by: Ben <ben@armosec.io>
…ore v0.0.305 kubescape/storage#362 added label selector support to list operations and shipped in v0.0.305 on 2026-08-20, so the behaviour the level filter works around is not unconditional. The kubescape-operator chart still pins storage v0.0.298, and a client cannot know which server version it is talking to, so the client-side filter stays -- it returns the same answer against either. Comments only; no behaviour change. Signed-off-by: Ben Hirschberg <ben@armosec.io> Docs-exempt: comment-only change, no behavioral change Signed-off-by: Ben <ben@armosec.io>
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
kubescape_check_healthreportshealthy: falseon a fully working Kubescape install, and the advertisedlevelfilter onkubescape_list_vulnerability_manifestssilently does nothing. Both were verified on a live cluster running thekubescape-operatorchart 1.40.4 (scanner v4.0.12), with all 7 podsRunningand every data tool returning correct results.Three fixes, all in
pkg/kubescape. No new dependencies; one client removed.1.
check_healthprobed for CRDs that never existThe
spdx.softwarecomposition.kubescape.ioresources are served by an aggregated API server — thestoragepod, wired viaAPIService v1beta1.spdx.softwarecomposition.kubescape.io. They are not CRDs, and no CRD by those names exists in any configuration.handleCheckHealthdid fourCustomResourceDefinitions().Get(...)lookups, which therefore returnIsNotFoundon every install, forever:…emitted while
kubectl get vulnerabilitymanifests -Areturned data. Confirmed independently:check_healthis the tool an agent calls first, and answering "is this working?" is its whole job — so an agent would tell the user Kubescape is broken and recommend reinstalling something already installed correctly.Fix. Availability is now probed by listing each resource through the
spdxClientthe data tools already use. That proves the exact path those tools depend on rather than a proxy for it — discovery could report a resource as registered while the storage pod is dead and every tool still fails. One list per resource answers both "is the API reachable?" and "is there data?", so this makes four fewer API calls than before. Theapiextensionsclient is no longer needed and is removed.Check keys are unchanged (
vulnerability_crd,configuration_crd, …) so existing consumers of the output are unaffected; only the lookup and the message wording change."CRD not installed"becomes"VulnerabilityManifests API not available - the Kubescape storage service may be unavailable, or vulnerability scanning may not be enabled".This also fixes a latent gating bug:
application_profiles_dataandnetwork_neighborhoods_datawere nested inside the always-failing CRD lookup, so those two checks never ran at all.2. Health pod selectors matched the wrong pods
app.kubernetes.io/name=kubescape-operatoris the chart-wide label carried by every pod the chart creates. Per-component identity is the plainapplabel:So
"operator_pods": "7/7 pods running"was really an all-kubescape-pods count — ifnode-agentdied you got6/7with no way to tell which component failed — and"storage_pods": "No storage pods found"was emitted whilestorage-5ff6f76c7f-ngb2dwas1/1 Running.Fix. Selectors are now
app=operatorandapp=storage.A missing storage pod is now an error that fails the health check, not a warning: every data tool in this provider reads through the storage service, so the one check that would catch the failure mode breaking all nine of them previously matched nothing and would not have failed the check if it had.
3. The
levelfilter silently did nothinglist_vulnerability_manifestsadvertiseslevel:image,workload, orboth. The provider's logic was correct and the labels really exist on the objects, but all three calls returned byte-identical output:level6f1e27dfb8fblevel=image6f1e27dfb8fblevel=workload6f1e27dfb8fbThe root cause is the storage server version the chart deploys. Storage servers before v0.0.305 ignore
labelSelectoron list — a nonsense selector returns every row — across all six resources:kubescape/storage#362 added selector support and shipped in v0.0.305 (2026-08-20), so this is already fixed upstream. But
kubescape-operatorstill pinsstorage: v0.0.298(2026-07-25, seven releases earlier), which is what the measurements above were taken against, and a client of this API cannot know which server version it is talking to. kubescape/storage#363 tracks the remaining generated/aggregated resources.Fix. Filtering now happens client-side on
isImageLevel— the same predicate already computed to populate theimage_level/workload_levelfields in the response — so the filter and the output cannot disagree, and it returns the same answer against a pre- or post-v0.0.305 storage server. An unrecognisedlevelis now a validation error instead of silently meaning "both".Once the chart ships a storage ≥ v0.0.305, pushing this back to a server-side selector would save transferring the unfiltered list. That would need a version floor or a capability probe to stay correct on older installs, so I have left it client-side here; happy to revisit if you would rather gate on a minimum storage version.
Testing
go test ./pkg/kubescape/...— 60 pass, no cluster required. Fullmake fmt vet lint testclean (./pkg/... ./internal/..., 739 pass). The pre-existingtest/e2esuite needs a live kind cluster and was not run.New tests were written first and observed failing against the current code. Two points worth noting:
app=<component>— i.e. the real shape. That is what makes the old selector fail and the new one pass.labelSelector, returning every object regardless. Without it the fake clientset honours selectors, and a server-side filter passes the test while returning unfiltered results against the storage version the chart actually deploys — which is exactly how this bug survived.Two existing tests whose premise was "no CRDs installed" are rewritten to exercise real API-unavailability via a
ServiceUnavailablereactor, including a partial-degradation case (one resource down, another still reportingok).Out of scope
Deliberately not in this PR, to keep it reviewable: unbounded tool output (
list_vulnerabilitiesreturned 200 KB / ~50k tokens for a single image on a 4-workload cluster), the permanentinitErrorwith no re-init path, and theinternal/errorsfallback that marks unrecoverable errorsRetryable: Yes. Happy to open issues for these if useful.Ticket
None.