Skip to content

[ LFX 2026 ] operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry#392

Merged
matthyx merged 3 commits into
kubescape:mainfrom
yugal07:feat/security-exception-watcher
Jul 12, 2026
Merged

[ LFX 2026 ] operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry#392
matthyx merged 3 commits into
kubescape:mainfrom
yugal07:feat/security-exception-watcher

Conversation

@yugal07

@yugal07 yugal07 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Related issue: kubescape/kubescape#1982 — Implement SecurityException CRD: kubescape posture path, operator watcher, and Headlamp plugin
Design: kubevuln/docs/security-exception-design.md — §Architecture → operator — Watch & Rescan, §Observability → Expiry
Completes on the scanner side:

Overview

This PR adds the operator half of the SecurityException design (issue kubescape/kubescape/1982
checklist item "Operator SecurityExceptionWatchHandler watching both kinds with
a debounced CooldownQueue, dispatching rescans on changes"
).

The posture path already reads SecurityException / ClusterSecurityException
CRDs at scan time (CRDExceptionsGetter). But nothing triggers a scan when an
exception changes: a team commits a new exception via GitOps, and the excepted
findings keep showing as failing until the next scheduled scan hours later — or
an exception passes its expiresAt and the now-unsuppressed findings stay hidden
until the next schedule. The GitOps loop the design promises ("exceptions live
alongside app code and take effect when applied") was open on the runtime side.

This adds SecurityExceptionWatchHandler: it watches both kinds, debounces bursts
of changes, and dispatches a cluster posture rescan so results reflect the current
set of exceptions promptly — plus a periodic expiry sweep so lapsed exceptions
resurface their findings without waiting for a schedule.

It is wired into the operator's existing watcher startup, gated on the posture
scanner being enabled:

// mainhandler/handlerequests.go — HandleWatchers
if mainHandler.config.Components().Kubescape.Enabled {
    seWatch := watcher.NewSecurityExceptionWatchHandler(
        mainHandler.config, mainHandler.k8sAPI.GetDynamicClient(), mainHandler.eventWorkerPool)
    go seWatch.SecurityExceptionWatch(ctx)
}

How it works

securityexceptions ─┐
                    ├─ watch (dynamic client) ─► CooldownQueue ─► rescanSignal ─► TypeRunKubescape
clustersecurity...──┘   + list-existing on start   (per-object   (coalesce+     (cluster WildWlid,
                                                     debounce)     throttle)      allcontrols/nsa/mitre)
expiry sweep (5m) ──────────────────────────────────────────────►┘
  • List-existing + watch, both kinds. On start the handler lists current
    objects (so exceptions present before the operator came up are honored) then
    follows changes on securityexceptions and clustersecurityexceptions at
    kubescape.io/v1beta1 via the dynamic client, re-establishing a dropped watch
    with a bounded retry.
  • Debounce (design's CooldownQueue). Every event goes through the operator's
    existing CooldownQueue, which collapses repeated events for the same object
    during a cooldown window.
  • Coalesce + throttle across objects. A GitOps apply touches many CRDs at once;
    each is a distinct queue key, so distinct events would otherwise each trigger a
    full rescan. A buffered-by-one rescanSignal collapses concurrent requests into a
    single pending rescan, and a post-dispatch throttle absorbs the rest of the burst
    — a set of exceptions applied together produces one rescan, not N.
  • Rescan = the operator's own startup scan. buildRescanCommand emits exactly
    the apis.TypeRunKubescape command the operator already fires on startup
    (cluster WildWlid, frameworks allcontrols/nsa/mitre), dispatched through
    the existing utils.AddCommandToChannel worker pool. Re-evaluating the whole
    cluster is idempotent and re-applies every exception; no new transport or command
    type is introduced.
  • Expiry sweep (design's expiry controller). A 5-minute ticker lists both kinds
    and requests a rescan when an exception has newly passed its spec.expiresAt
    (parsed as RFC3339). Consistent with the design, nothing writes status
    expiry is still evaluated by the scanners at scan time; the sweep only nudges a
    rescan so previously-suppressed findings resurface. Each expiry triggers exactly
    one rescan (tracked in expiredSeen), and the mark is cleared if expiresAt is
    later pushed back out, so a re-expiry is caught again.

What changed

  • watcher/securityexceptionwatcher.go (new)SecurityExceptionWatchHandler:
    the two watches (watchKind / consumeWatch / listExisting), the debounced
    event handler (handleEventsrequestRescan), the coalescing rescan dispatcher
    (rescanLoop), the expiry loop (expiryLoop / sweepExpired), and the pure
    helpers buildRescanCommand and parseExpiresAt. Tunables (cooldown, throttle,
    retry, sweep interval) are captured into the handler at construction so a running
    handler is unaffected by later changes and tests are race-free.
  • mainhandler/handlerequests.go — start the handler in HandleWatchers, gated
    on Components().Kubescape.Enabled (the posture scanner an exception affects). The
    dispatcher is wired to the existing eventWorkerPool; dispatchRescan is an
    injectable field so tests exercise the pipeline without a pool.
  • watcher/securityexceptionwatcher_test.go (new) — unit tests (below).

No new RBAC: the operator already has get/list/watch on the SecurityException
CRDs from helm-charts#875, and
dispatches through the same pool it already uses for scans.

Design decisions

  • Full cluster rescan, not per-workload targeting. The design mentions
    "determines affected namespaces/workloads, dispatches rescan commands." Resolving
    an exception's resources/objectSelector/namespaceSelector to a concrete
    workload set duplicates matching logic that already lives in the scanner and
    opa-utils. A cluster rescan is simpler, always correct, idempotent, and coalesced
    so it isn't wasteful. Targeted rescans are a clean follow-up (see below).
  • v1beta1. The handler lists/watches at kubescape.io/v1beta1, matching the
    shipped CRDs and CRDExceptionsGetter. A single named constant moves when the CRD
    graduates to v1.
  • Gated on Kubescape.Enabled. An exception only affects posture results, so
    the watcher runs exactly when the posture scanner does — no new config flag.

Testing

  • go build ./..., go vet ./..., and gofmt are clean.
  • go test -race -count=1 ./watcher/ passes for the new suite (9 tests). The one
    package failure, watcher/TestRegistryCommandWatch, is a pre-existing
    testcontainers integration test that times out booting its container in this
    environment (confirmed failing identically on a clean main); it is unrelated to
    this change.
  • Unit tests (securityexceptionwatcher_test.go) cover: buildRescanCommand
    shape; parseExpiresAt (present/absent/malformed/date-only); rescan coalescing
    (5 requests → 1 pending); the expiry sweep (one-shot per expiry, idempotent on
    re-sweep, and re-arm when expiresAt is pushed out); listExisting enqueue;
    event→rescan dispatch; and the full SecurityExceptionWatch path against a fake
    dynamic client. Two real concurrency issues surfaced by -race were fixed during
    development: a shared expiredSeen map (now mutex-guarded) and loops reading
    mutable package globals (now captured into the handler).

End-to-end, against a live kind cluster

Run with the released operator replaced by this build's handler (driven via a small
go run harness that constructs NewSecurityExceptionWatchHandler with the real
dynamic client and prints each dispatched command):

=== watching SecurityException/ClusterSecurityException for 45s ===
[info] security exception changed, requesting rescan. kind: ClusterSecurityException; name: staging-relax-c0034; type: ADDED
>>> RESCAN DISPATCHED  cmd=kubescapeScan wildWlid=wlid://cluster-kind-kubescape-test/namespace-
[info] security exception changed, requesting rescan. kind: SecurityException; name: watch-probe; namespace: production; type: ADDED
>>> RESCAN DISPATCHED  cmd=kubescapeScan wildWlid=wlid://cluster-kind-kubescape-test/namespace-
[info] security exception changed, requesting rescan. kind: SecurityException; name: watch-probe; namespace: production; type: DELETED
>>> RESCAN DISPATCHED  cmd=kubescapeScan wildWlid=wlid://cluster-kind-kubescape-test/namespace-
  • List-existing on start found the pre-existing ClusterSecurityException and
    dispatched a rescan.
  • Apply and delete of a SecurityException each produced a debounced rescan
    with the correct cluster WildWlid.
  • Paired with a kubescape scan against the same cluster, the dispatched rescan
    flips an excepted control (e.g. C-0034 on Deployment/nginx) from failed to
    passed (w/exceptions) and back on delete — closing the GitOps loop end to end.

Signed Commits

  • Yes, I signed my commits.

Summary by CodeRabbit

  • New Features

    • Added monitoring for SecurityException and ClusterSecurityException resources (runs when Kubescape and risk acceptance are enabled).
    • Security exception changes now trigger posture rescans.
    • Introduced automatic expiration handling so expired exceptions trigger rescans exactly once per expired state.
  • Bug Fixes

    • Added debounced/coalesced rescan requests to help prevent rescan storms and re-arm rescans after throttling.
  • Tests

    • Added a full test suite covering command construction, expiresAt parsing, event-driven rescan triggering, startup behavior, and idempotent expiration sweeps.

…escan on change or expiry

Signed-off-by: yugal07 <yashsadhwani544@gmail.com>
@yugal07 yugal07 changed the title operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry [ LFX 2026] operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry Jul 10, 2026
@yugal07 yugal07 changed the title [ LFX 2026] operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry [ LFX 2026 ] operator: watch SecurityException/ClusterSecurityException CRDs and rescan on change or expiry Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a capability-gated watcher for SecurityException and ClusterSecurityException resources. It processes live and existing objects, detects expirations, and dispatches throttled, coalesced cluster posture rescans with comprehensive tests.

Changes

Security exception monitoring

Layer / File(s) Summary
Risk-acceptance capability and watcher wiring
config/config.go, config/config_test.go, mainhandler/handlerequests.go
Adds the risk-acceptance capability and starts the watcher only when risk acceptance and Kubescape are enabled.
Watcher setup and Kubernetes event ingestion
watcher/securityexceptionwatcher.go, watcher/securityexceptionwatcher_test.go
The handler watches both CRD types, processes existing and live events, handles watch failures, and debounces changes.
Debounced event rescans
watcher/securityexceptionwatcher.go, watcher/securityexceptionwatcher_test.go
Exception changes are coalesced into throttled cluster posture rescan commands, with retry behavior for dispatch failures.
Expiry detection and re-arming
watcher/securityexceptionwatcher.go, watcher/securityexceptionwatcher_test.go
Periodic sweeps parse RFC3339 expirations, trigger once per newly expired object, clear stale tracking, and forget deleted objects.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KubernetesAPI
  participant SecurityExceptionWatchHandler
  participant RescanDispatcher
  KubernetesAPI->>SecurityExceptionWatchHandler: Emit exception resource event
  SecurityExceptionWatchHandler->>SecurityExceptionWatchHandler: Debounce and coalesce request
  SecurityExceptionWatchHandler->>RescanDispatcher: Dispatch cluster posture rescan
Loading

Possibly related issues

  • kubescape/kubescape 1982 — Directly covers the operator watcher and debounced rescan handling implemented here.
  • kubescape/headlamp-plugin 84 — Covers related support for the same security exception CRDs.

Suggested reviewers: matthyx

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: watching SecurityException CRDs and triggering rescans on change or expiry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@watcher/securityexceptionwatcher.go`:
- Around line 169-171: Separate the combined Bookmark/Error condition in the
watcher event loop: continue silently for watch.Bookmark, but for watch.Error
log the event’s diagnostic details before continuing. Update the logic around
the event-processing function to preserve existing skip behavior while making
watch errors observable.
- Around line 270-307: Update sweepExpired to avoid holding wh.mu during
Kubernetes List calls and expiry computation: collect and process all expired
keys outside the lock, then briefly lock only when checking and updating
wh.expiredSeen (and removing stale marks), preserving rescan behavior. Ensure
forgetExpired can acquire wh.mu while API calls are in progress.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 839cf4d0-59e3-41de-9637-634e8c297bfc

📥 Commits

Reviewing files that changed from the base of the PR and between 4266ad8 and 5c77ce4.

📒 Files selected for processing (3)
  • mainhandler/handlerequests.go
  • watcher/securityexceptionwatcher.go
  • watcher/securityexceptionwatcher_test.go

Comment thread watcher/securityexceptionwatcher.go Outdated
Comment thread watcher/securityexceptionwatcher.go
… calls

Signed-off-by: yugal07 <yashsadhwani544@gmail.com>

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for this — the design writeup, the coalescing/throttle model, the -race tests and the E2E against a live kind cluster are all excellent, and closing the GitOps loop on the runtime side is genuinely useful. I don't see a hard merge blocker; the change is safe by construction (idempotent full-cluster rescan, no status writes, no new RBAC/transport). A few things I'd like addressed or at least explicitly acknowledged before merge, one of them more important than the rest.

1. Watch resilience — list-once + bare re-watch can silently miss changes (most important)

watchKind calls listExisting exactly once, then loops calling dynamicClient.Resource(gvr)...Watch(ctx, metav1.ListOptions{}) with no ResourceVersion and no relist on reconnect (watcher/securityexceptionwatcher.go, watchKind/consumeWatch).

A watch channel closing mid-run is normal (apiserver rotates watches every few minutes). Because you neither carry a resourceVersion across the reconnect nor relist, any create/update/delete that lands in the gap between the old watch closing and the new Watch() being established is lost — nothing else recovers it (the expiry sweep only reacts to expiresAt, not to general changes). The failure mode is subtle: an exception applied at just the wrong moment never triggers a rescan, and the GitOps loop this PR is meant to close quietly doesn't fire.

Rather than hand-rolling list+watch, please use client-go machinery that handles this: a dynamic informer (dynamicinformer.NewDynamicSharedInformerFactory) or cache.NewListWatchFromClient + watchtools.NewRetryWatcher, which track resourceVersion and relist on "resource version too old". That also removes the manual watch.Bookmark/watch.Error/retry handling. This is the one item I'd consider close-to-blocking, because the whole point of the feature is not missing changes.

2. Rescan is dropped on dispatch error

In rescanLoop, if dispatchRescan returns an error (e.g. the shared eventWorkerPool is overloaded and Invoke returns ErrPoolOverload) the error is logged and the pending rescan is discarded — the change isn't reflected until the next unrelated event or the scheduled scan. Consider re-arming the signal on failure (wh.requestRescan() in the error branch) so a transient pool-overload retries after the throttle rather than being silently lost.

3. Please confirm the shipped RBAC before merge

The description says no new RBAC is needed because the operator already has get/list/watch on these CRDs from helm-charts#875. Worth double-checking the deployed ClusterRole actually covers both securityexceptions and clustersecurityexceptions at kubescape.io — if it doesn't, watchKind logs an error and retries every 5s forever with zero rescans and no other signal, i.e. a silent no-op. A cheap way to make that non-silent would be to surface a clearer/rate-limited error after N consecutive watch failures.

Minor / nits (non-blocking)

  • Shutdown leak: eventQueue (CooldownQueue) is never Stop()ed, and its eviction callback sends on an unbuffered channel while handleEvents has already returned on ctx.Done(), so the cache goroutine can block permanently. Harmless for a process-lifetime component, but a defer wh.eventQueue.Stop() in SecurityExceptionWatch would be tidy.
  • List consistency: listExisting pages via pager.New, but sweepExpired uses a plain unpaginated List. Fine at expected cardinalities; worth matching for consistency if the exception count can grow.
  • The v1beta1 constant and the full-cluster-vs-targeted-rescan tradeoff are already called out in the description and I agree with both choices as-is — good follow-up material, not merge concerns.

Nice work overall; addressing #1 (and ideally #2) would make this solidly mergeable.

@matthyx matthyx moved this to Waiting on Author in KS PRs tracking Jul 12, 2026
…scan on dispatch error, gate on riskAcceptance RBAC

Signed-off-by: yugal07 <yashsadhwani544@gmail.com>
@yugal07

yugal07 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, Matthias, all three addressed.

Replaced the hand-rolled list-once + bare re-watch with a dynamic informer per kind, so resourceVersion tracking and relist-on-reconnect are handled by client-go and no change is missed across watch rotations (this also removed the manual Bookmark/Error handling).
RescanLoop now re-arms the signal on a dispatch error so a transient pool overload retries after the throttle instead of being dropped.
Good catch on the RBAC - the operator ClusterRole only grants get/list/watch on these CRDs when capabilities.riskAcceptance=enable, but the watcher was gating on Kubescape.Enabled, so a default install (riskAcceptance disabled) started the watcher with no RBAC; I plumbed riskAcceptance into the operator config and now gate the watcher on RiskAcceptanceEnabled() && Kubescape.Enabled so it exactly matches the chart. Verified against a live kind cluster: create/update/delete each dispatch a rescan, an injected dispatch error still results in a retried rescan, and helm template confirms the SE rule is present only with riskAcceptance enabled, matching the new gate. As a non-silent safety net, a persistently failing watch now escalates to an RBAC hint after N consecutive failures.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
watcher/securityexceptionwatcher.go (1)

345-364: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider defer wh.mu.Unlock() for panic-safety.

The lock is released with an explicit wh.mu.Unlock() rather than defer. Currently the loop body only uses continue, so this is safe today, but wh.mu also gates resetWatchFailures, which runs on every single informer event. If a future change (or an unexpected panic from logger.L()/map access) panics inside this block, the lock would never release, permanently deadlocking all subsequent event processing.

🔒 Proposed fix
 	newlyExpired := false
 	wh.mu.Lock()
+	defer wh.mu.Unlock()
 	for _, e := range entries {
 		if !e.expired {
 			delete(wh.expiredSeen, e.key)
 			continue
 		}
 		if _, seen := wh.expiredSeen[e.key]; seen {
 			continue
 		}
 		wh.expiredSeen[e.key] = struct{}{}
 		newlyExpired = true
 		logger.L().Info("security exception expired, requesting rescan",
 			helpers.String("kind", e.kind),
 			helpers.String("name", e.name),
 			helpers.String("namespace", e.namespace))
 	}
-	wh.mu.Unlock()
 
 	if newlyExpired {
 		wh.requestRescan()
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@watcher/securityexceptionwatcher.go` around lines 345 - 364, Use defer
wh.mu.Unlock() immediately after acquiring wh.mu.Lock() in the
expiration-processing block, and remove the explicit unlock at the end. Preserve
the existing loop, expiredSeen updates, and logging behavior while ensuring the
lock is released during panics.
watcher/securityexceptionwatcher_test.go (1)

197-219: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed 50ms sleep before create is a timing-dependent synchronization point, not just a delay.

The informer factory is built with resync period 0 (no periodic relist), so if the Create races ahead of the reflector's Watch() call actually being established, the fake tracker's watch may never surface the event at all (there's no periodic resync to catch it later) — the test would then rely purely on the 3s timeout to fail, rather than flake intermittently under CI load where 50ms may not be enough for the goroutine scheduler to get the informer's watch running.

Since informer readiness isn't otherwise observable from the test today, consider exposing a sync signal (e.g., returning/using cache.WaitForCacheSync from startInformers) rather than a fixed sleep, to make this deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@watcher/securityexceptionwatcher_test.go` around lines 197 - 219, The
live-create test uses a fixed 50ms sleep instead of synchronizing with informer
readiness. Update the informer startup flow around SecurityExceptionWatch and
startInformers to expose or await cache synchronization via
cache.WaitForCacheSync, then create the exception only after synchronization
succeeds; remove the timing-based sleep while preserving the existing dispatch
assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@watcher/securityexceptionwatcher_test.go`:
- Around line 197-219: The live-create test uses a fixed 50ms sleep instead of
synchronizing with informer readiness. Update the informer startup flow around
SecurityExceptionWatch and startInformers to expose or await cache
synchronization via cache.WaitForCacheSync, then create the exception only after
synchronization succeeds; remove the timing-based sleep while preserving the
existing dispatch assertion.

In `@watcher/securityexceptionwatcher.go`:
- Around line 345-364: Use defer wh.mu.Unlock() immediately after acquiring
wh.mu.Lock() in the expiration-processing block, and remove the explicit unlock
at the end. Preserve the existing loop, expiredSeen updates, and logging
behavior while ensuring the lock is released during panics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d40d84ce-d993-4080-a34e-1483a5b6e0c8

📥 Commits

Reviewing files that changed from the base of the PR and between 5c77ce4 and fdca18d.

📒 Files selected for processing (5)
  • config/config.go
  • config/config_test.go
  • mainhandler/handlerequests.go
  • watcher/securityexceptionwatcher.go
  • watcher/securityexceptionwatcher_test.go

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed after fdca18d — all three points from my previous review are properly addressed, and cleanly. Thanks for the quick turnaround.

  1. Watch resilience ✅ — replaced the hand-rolled list-once + bare re-watch with a dynamicinformer.NewFilteredDynamicSharedInformerFactory per kind. The shared reflector tracks resourceVersion and relists on watch errors, so changes across watch rotations are no longer leaked, and the initial cache sync replays pre-existing exceptions as Adds. Nice touch handling DeletedFinalStateUnknown tombstones in enqueueInformerEvent.
  2. Dropped rescan on dispatch error ✅rescanLoop now re-arms via requestRescan() on dispatch failure, with the throttle sleep preventing a tight retry loop.
  3. RBAC ✅ — gated on RiskAcceptanceEnabled() (matching the Helm chart's conditional RBAC grant) and Kubescape.Enabled, plus a WatchErrorHandler that escalates to an explicit "verify the ClusterRole grants get/list/watch…" log after 3 consecutive failures — so a misconfigured Role is no longer a silent no-op. The config accessor has its own unit test too.

Both minor nits are handled as well: defer wh.eventQueue.Stop() in SecurityExceptionWatch, and sweepExpired now paginates via pager.New.

One note on the two open CodeRabbit comments: the "sweepExpired holds wh.mu across Kube API list calls" one is already resolved in this revision — the paginated List builds entries with no lock held, and wh.mu is only taken around the in-memory map update afterward. The watch.Error logging one is now moot since the informer's WatchErrorHandler covers that path.

CI is green (build, vet, cross-platform, basic tests, CodeQL all pass) and the new tests cover the live-create informer path, the re-arm, and the RBAC accessor. LGTM — no remaining blockers from my side.

@matthyx matthyx merged commit 7df14cf into kubescape:main Jul 12, 2026
11 checks passed
@matthyx matthyx moved this from Waiting on Author to To Archive in KS PRs tracking Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Archive

Development

Successfully merging this pull request may close these issues.

2 participants