fix(kubescape): stop reporting vulnerability_count 0 for every image - #76
Open
slashben wants to merge 1 commit into
Open
fix(kubescape): stop reporting vulnerability_count 0 for every image#76slashben wants to merge 1 commit into
slashben wants to merge 1 commit into
Conversation
list_vulnerability_manifests reported "vulnerability_count": 0 for every
manifest in every cluster, always. The count came from
len(manifest.Spec.Payload.Matches)
but the Kubescape aggregated API strips spec.payload.matches on LIST and
serves it only on GET -- sensibly, the payloads are megabytes. Matches is
therefore nil on every listed object, and len(nil) is 0.
This is a silent false negative on a security question. Measured on a live
cluster, docker.io/library/nginx:1.14.0 has 466 CVE matches (76 Critical,
133 High, 99 Medium, 56 Low, 102 Negligible, confirmed by GET on the same
manifest), and the tool returned:
{"image_tag": "docker.io/library/nginx:1.14.0",
"manifest_name": "docker.io-library-nginx-1.14.0-e34030",
"vulnerability_count": 0}
An agent asked for the cluster's most critical CVEs answered "No CVEs
detected across all container images ... your cluster's container images
currently have no known CVEs". It behaved correctly given its input: it
listed the manifests, saw every count at 0, and had no reason to drill in.
The data lied to it.
The count is not knowable from a LIST response, so it is removed rather
than corrected. An absent field cannot be mistaken for a measured zero.
Removing it is only safe if the agent is told where counts come from, so
the tool description now states that this is an index which does not report
vulnerability counts, that an entry appearing here says nothing about
whether the image is clean, and that kubescape_list_vulnerabilities returns
counts and severities for a given manifest. Without that, absence of data
reads as absence of risk -- the same false negative in a different costume.
Signed-off-by: Ben Hirschberg <ben@armosec.io>
Docs-exempt: bug fix; no existing doc describes the list response shape
Signed-off-by: Ben <ben@armosec.io>
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.
What's wrong
kubescape_list_vulnerability_manifestsreturns"vulnerability_count": 0for every image, in every cluster, always. An agent that lists images to find vulnerable ones is told everything is clean.On the test cluster,
nginx:1.14.0has 466 CVEs (76 Critical, 133 High). The tool returned:{"image_tag": "docker.io/library/nginx:1.14.0", "manifest_name": "docker.io-library-nginx-1.14.0-e34030", "vulnerability_count": 0}An agent asked for the cluster's critical CVEs answered, in 5.5s:
The agent did nothing wrong. It listed the images, saw every count at zero, and stopped. The data lied to it.
Why it happens
A little background, since it's specific to how Kubescape stores data.
Kubescape's in-cluster scanner writes results into Kubernetes-style objects. This provider reads them. The object for an image's CVEs (
VulnerabilityManifest) holds the full CVE list inspec.payload.matches— for one image that's a 2.7 MB object.Because those payloads are huge, Kubescape's API server omits
spec.payload.matcheswhen you LIST objects and only includes it when you GET a single one. That's sensible server behaviour. But the provider computed the count from the listed objects:On a LIST,
Matchesis alwaysnil.len(nil)is0. The field never measured anything.Confirmed on two separate clusters — every listed manifest reports
matches=nil, while a GET of the same object returns 466.The fix
Delete the field. The count isn't knowable from a LIST response, so it's removed rather than corrected: an absent field can't be mistaken for a measured zero.
The tool description now states that this tool is an index, that it does not report vulnerability counts, that an entry appearing in it says nothing about whether the image is clean, and that
kubescape_list_vulnerabilitiesis where counts come from. Without that, an agent reads absence of data as absence of risk — the same false negative in a different form. A test asserts the description names that tool, so this half can't silently regress.kubescape_list_vulnerabilitiesalready returns a correct severity breakdown, so the drill-down path exists today.Alternative considered
Kubescape also stores pre-aggregated summary objects that do carry counts. I didn't use them here: I couldn't confirm they're populated on every install type, and the mapping from summary back to image manifest turned out to be unreliable. Wiring a security-critical count to that without proof seemed worse than removing a field that is definitively always wrong. (Those objects are used, with verification, in #77.)
Risk / compatibility
vulnerability_countwill no longer find it — but it was always0, so nothing loses real information.Testing
go test ./pkg/kubescape/...— 51 pass, no cluster needed.go build,go vet,golangci-lint, andgo test -tags=test ./pkg/... ./internal/...(730 pass) all clean.Both tests were written first and observed failing.
One caveat worth stating plainly: the fake clientset can't reproduce this bug. Fakes return whatever you seed them with, so they always show a populated payload on LIST. That's part of why this survived — and why the unit tests here assert the field is absent rather than trying to recreate the server behaviour.
Ticket
None.