Skip to content

feat(policy): show which rules a workload breaks, on the workload's page - #1425

Merged
hisco merged 3 commits into
mainfrom
feat/policy-violations-on-resource
Aug 12, 2026
Merged

hisco merged 3 commits into
mainfrom
feat/policy-violations-on-resource

Conversation

@hisco

@hisco hisco commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Radar reads the cluster's policy results and passes them to /api/ai/* and MCP — and to nothing a person can see. Ask the AI assistant and it tells you checkout-api fails two rules. Open that same Deployment in Radar and there is no mention that policies exist at all.

This adds GET /api/policy/resource/{kind}/{namespace}/{name} and a Policy section on the Pod and workload drawers, between Status and Strategy — the same tier as Permissions, which is the existing "what is this allowed to do, what is wrong with it" material.

Verified against a live Kyverno 1.18.2 cluster, 40 indexed report subjects. Real output on a real Deployment:

▾ Policy
   [Error]  add-default-labels · evaluation
            failed to evaluate policy: type mismatch: unexpected type name
            "Object.metadata", expected "Object.spec.template.metadata"
   [Fail]   audit-require-labels
            Deployments should carry a 'team' label
   [Fail]   policy-demo/ns-require-resources
            Containers must declare resource limits
   2 other checks passing

Worst result first, then policy name, so ordering is stable between polls and an engine error does not sink below rule failures. Passing checks are counted but not listed — a heavily-policed workload would otherwise bury two failures in a wall of green — and the count is what proves a check actually ran.

The unhappy paths are the bulk of this

An empty finding list means several unrelated things. Rendering them identically would tell an operator they are compliant in the cases where nothing was checked — the exact failure this section exists to prevent. Each state says which one it is:

State What the section says
Violations lists them, auto-expanded
All passing "All 5 checks passing."
Evaluated, nothing matched "No policy applies to this resource."
Index not ready "…has not been checked", with the reason code
Still warming up "Policy results are still loading from the cluster."
Caller denied "You don't have permission to view policy results in this namespace."
No engine installed section absent — a disclaimer on every cluster without a policy engine is noise

"Everything passed" and "nothing examined it" are deliberately different sentences. So are "denied" and "failed": a denial is an expected state on a namespace-scoped account and gets calm copy, while a genuine fault stays red. Same division the RBAC sections already draw, reusing their precedent rather than inventing one.

A result can be real and incomplete at once. When some report families were unreadable, or the index is frozen, the section says so beneath the findings — because "All checks passing" alone would claim coverage we do not have. PolicyReportStatus already carried these fields; its own doc comment anticipates this consumer.

Authorization

An RBAC check, deliberately not parseNamespacesForUser. That helper folds in the header's namespace view filter, which is a display preference — and testing against a live cluster showed a 403 on every namespace outside the current filter, which would have broken this section for any resource reached by deep link, search or topology navigation. Caught before merge precisely because it was exercised on a real cluster rather than a fixture.

A denial returns 403 with a message, never an empty list: an empty list reads as "nothing is violated", which is a claim we have not earned.

Testing

go test ./... clean in both modules · make tsc clean · k8s-ui 2187 passing.

Ten component tests covering every state above, including the assertions that one state must not look like another (No policy applies must not contain the word "passing"). Three backend tests for result normalization, worst-first ordering, and the counts-include-passes contract.

Live verification of both principal paths on the same cluster: a Deployment showing three real violations, and a purpose-built compliant Pod showing "All 5 checks passing".

Scope

Resource → policies. The reverse direction — from a policy, which resources are failing it right now — needs an inverted index and is separate. The report list gained a Subject column recently, which covers browsing; this covers the question people actually open a workload to ask.


Note

Medium Risk
Adds a new RBAC-gated read API over policy reports and wires it into resource drawers. Risk is moderate because authorization and empty-result semantics matter, but the change is read-only and does not alter core auth infrastructure.

Overview
Surfaces cluster policy results on Pod and workload drawers via a new GET /api/policy/resource/{kind}/{namespace}/{name} endpoint and a Policy section, so operators can see which rules a resource breaks without asking the AI.

The response carries coverage state (evaluated, status, deniedGroups, liveUpdates) alongside findings and counts, so empty results are never mistaken for compliance. Passing checks are counted but not listed; non-pass findings sort worst-first. Auth checks list access on either wgpolicyk8s.io/policyreports or openreports.io/reports (not the namespace view filter) and returns 403 on deny rather than an empty list.

The UI distinguishes violations, all-passing, no policy matched, warmup/unavailable, RBAC denial, and no engine installed (section omitted), and notes incomplete coverage when some report families were unreadable or the index is frozen.

Reviewed by Cursor Bugbot for commit a3f43aa. Bugbot is set up for automated code reviews on this repo. Configure here.

Radar reads the cluster's policy results and passed them only to /api/ai/* and
MCP. An AI assistant could tell you your checkout service fails two rules; the
person looking at that same service in Radar saw no mention that policies exist.

Adds GET /api/policy/resource/{kind}/{namespace}/{name} and a Policy section on
the Pod and workload drawers, between Status and Strategy — the same tier as
Permissions, which is the existing "what is this allowed to do, what is wrong
with it" material.

The unhappy paths are the bulk of the work. An empty finding list means several
unrelated things — nothing violated, no engine installed, the caller may not
read the reports, the index is still warming — and rendering them all as blank
space tells an operator they are compliant in the cases where nothing was
checked. Each state says which one it is, and a result that is real but
incomplete says so rather than claiming coverage it does not have.

Authorization is an RBAC check, deliberately not the namespace view filter:
that filter is a display preference, and denying on it broke the section for
every resource reached by deep link, search or topology navigation from outside
the current filter. A denial is a 403, never an empty list.
@hisco
hisco requested a review from nadaverell as a code owner August 12, 2026 09:13

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6d25444. Configure here.

Comment thread internal/server/policy_handlers.go Outdated
hisco added 2 commits August 12, 2026 12:17
CodeQL flagged two high alerts on the regex HTML-stripper the tests used to
work around React splitting adjacent interpolations with comment markers. The
alerts were about the sanitisation pattern, not a real vulnerability — the
helper only read markup the test had just produced, and its output went into an
assertion rather than a DOM.

Removing the helper is still the right fix rather than suppressing it. The
splitting was worth avoiding on its own: a sentence broken into three text
nodes is worse for anything reading the rendered output, screen readers
included. Each string is now one expression, the tests assert on the markup
directly, and the flagged pattern is gone rather than silenced.
…name

The two families do not share a resource name: wgpolicyk8s.io serves
`policyreports`, openreports.io serves `reports`. Authorizing against the first
alone asks about a resource that is not served on an openreports-only cluster —
which is where Kyverno 1.15+ writes — so the answer said nothing about whether
the caller could see their own policy results.

Permission on either family is now enough, since the findings are merged from
whichever families are served and reading the one that actually carries data is
what matters.

The comment asserting the two shared a name was wrong, and wrong against
evidence already visible in this repo: the family table in policy_reports.go
names them separately.
@hisco
hisco merged commit f6b9372 into main Aug 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants