feat(collapse): rule-aware protection of sensitive open prefixes#335
feat(collapse): rule-aware protection of sensitive open prefixes#335matthyx wants to merge 3 commits into
Conversation
📝 WalkthroughWalkthroughAdds an OpenProtection model and reloadable store, exposes config for protected open matchers, updates the dynamic PathAnalyzer to pin protected prefixes (never-collapse/pinned budget behavior), wires the store into container profile deflation and server startup, and adds tests for protection and reloader behavior. ChangesProtected Path Prefix Detection
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@go.mod`:
- Line 10: The go.mod currently pins github.com/armosec/armoapi-go to a
pseudo-version (5cfa1e630e5d); replace this temporary pseudo-version with the
appropriate tagged release (e.g., vX.Y.Z) for github.com/armosec/armoapi-go to
ensure reproducible builds, then update the module by running the Go tooling
(e.g., use `go get github.com/armosec/armoapi-go@<tag>` and `go mod tidy`) and
verify go.sum and the build succeed; ensure any references to the pseudo-version
in go.mod are removed and the dependency now references the official tag.
🪄 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: 4af06910-f4ba-4c7b-bcfb-623933563a9e
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (7)
go.modpkg/config/config.gopkg/registry/file/containerprofile_processor.gopkg/registry/file/dynamicpathdetector/analyzer.gopkg/registry/file/dynamicpathdetector/protection.gopkg/registry/file/dynamicpathdetector/protection_test.gopkg/registry/file/dynamicpathdetector/types.go
|
Summary:
|
Profile deflation generalises high-cardinality open paths to keep stored
profiles bounded (e.g. /etc with many files -> /etc/⋯, or the whole tree ->
/⋯/⋯). That generalisation silently defeats anomaly rules such as R0010
(unexpected /etc/shadow access): once /etc collapses to a wildcard,
ap.was_path_opened("/etc/shadow.evil") matches it and the rule can never fire.
Pin rule-declared sensitive prefixes (and their ancestors) to literal during
collapse so a never-seen sensitive path stays distinguishable from a generalised
one. The analyzer skips collapsing any node on the ancestor chain of — or within
the subtree of — a protected prefix.
- dynamicpathdetector: NewPathAnalyzerWithConfigsAndProtection + OpenProtection
(exact/prefix/suffix/contains; suffix/contains resolved against the observed
open paths since they have no fixed location). protectedNode is O(1) for the
common node via a precomputed ancestor set + top-dir fast-reject; the cost
never scales with the number of protected prefixes. Zero overhead when no
protection is configured.
- DeflateContainerProfileSpec takes the OpenProtection; OpenProtectionFromMatchers
converts armotypes.OpenMatchers. config.Config.ProtectedOpenMatchers feeds the
in-cluster processor (populated by the operator from the rule library); the
backend feeds it from MongoDB rules.
Requires armoapi-go v0.0.719 (OpenMatchers / UnionOpenProtection).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
33957d1 to
cb033d5
Compare
|
Summary:
|
matthyx
left a comment
There was a problem hiding this comment.
Reviewed the rule-aware collapse protection. The design is sound, it builds clean, and go test ./pkg/registry/file/dynamicpathdetector/... passes. The protectedNode O(1) fast-path and the zero-overhead-when-unconfigured guarantee both hold up.
The armoapi-go re-pin called out in the PR body is already done — go.mod is on the tagged v0.0.719 (not the pseudo-version), the tag exists upstream, and armosec/armoapi-go#656 is merged. So that pre-merge item is cleared.
Two things I'd want resolved (or at least explicitly documented as accepted limitations) before merge — left inline. Neither is a compile/correctness bug; both are gaps between what the PR claims to guarantee and what it actually guarantees.
| if len(p.Suffix) > 0 || len(p.Contains) > 0 { | ||
| for _, op := range openPaths { | ||
| if p.matchesUnanchored(op) { | ||
| out = append(out, op) // pin this open's ancestor chain |
There was a problem hiding this comment.
Blocker (partial coverage): Suffix/Contains protection is learning-dependent and does NOT protect never-seen paths — which is the whole point of the PR.
Exact/Prefix pin statically (their ancestor chain is kept literal even if the sensitive file was never opened during learning). But Suffix/Contains are only pinned for paths that were actually observed in openPaths. If no sibling matching the matcher was opened during learning, and that directory is high-cardinality, the directory collapses to /dir/⋯ and a first-ever access to a matching file is covered by the wildcard — i.e. the R0010-class false negative the PR sets out to fix still happens for these two matcher kinds.
Concrete: matcher Suffix: ["_key"], and during learning /etc/ssh had 80 non-key files opened but no *_key file. /etc/ssh collapses to /etc/ssh/⋯, and a novel /etc/ssh/ssh_host_ed25519_key is then covered → rule can't fire. The test passes only because ssh_host_rsa_key was in the observed opens, which pins /etc/ssh; remove that one observed key and the guarantee disappears.
This asymmetry (static for exact/prefix, best-effort for suffix/contains) isn't surfaced anywhere a rule author would see it. Options: (a) document it loudly as a known limitation of suffix/contains matchers, (b) accept that suffix/contains can only ever be best-effort here and steer rule authors toward exact/prefix for must-detect paths. At minimum the PR description's "keeps a never-seen sensitive path distinguishable" should be scoped to exact/prefix.
| } | ||
| protectedPrefixes = openProtection.ProtectedPrefixes(openPaths) | ||
| } | ||
| openAnalyzer := dynamicpathdetector.NewPathAnalyzerWithConfigsAndProtection(OpenDynamicThreshold, nil, protectedPrefixes) |
There was a problem hiding this comment.
Blocker to confirm (profile-size blowback): pinning a sensitive prefix makes its entire ancestor directory uncollapsible, which can flip the profile to TooLarge.
Protecting e.g. /etc/shadow pins /etc to literal — so every distinct file ever opened under /etc now survives as its own entry instead of folding to /etc/⋯. On a chatty workload (or with several rule-protected top-level dirs), that growth feeds straight into the size accumulation in PreSave, and size > MaxContainerProfileSize sets helpers.TooLarge.
If a TooLarge profile is dropped from / not used by anomaly detection downstream, this is a self-defeating outcome: turning on protection for R0010 could disable detection for the whole container — strictly worse than the wildcard it was meant to fix. The Performance section measures time (+0.4 ms) but not the size delta, which is the dimension deflation exists to bound.
Please confirm what TooLarge does downstream, and add a worst-case size figure (e.g. /etc with N literals) next to the timing table. If TooLarge degrades detection, this needs a guard (cap pinned literals per protected dir, or fall back to collapse when a pinned subtree itself exceeds a budget).
|
Re-reviewed
One non-blocking note: when the budget trips, protection silently flips off for that directory (the test confirms LGTM to merge. |
|
Summary:
|
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
0841458 to
030c0af
Compare
|
Summary:
|
…gMap The rule-aware collapse protection added in this branch fed the protected open matchers from static config (config.protectedOpenMatchers), set once at startup. That cannot track RuntimeRuleAlertBinding changes without a restart. Move the processor to a shared, concurrency-safe OpenProtectionStore read on every PreSave (the open-event hot path) and add an OpenProtectionReloader that periodically reads a ConfigMap (DefaultNamespace / openProtectionConfigMapName) and refreshes the store. This is the in-cluster reader side of the "operator writes one object, storage refreshes periodically" wiring: the operator watches RuntimeRuleAlertBinding, resolves selectors against the rule library to the union of profileDataRequired.opens, and writes the ConfigMap (producer side implemented separately). Behaviour: - The store is seeded from config (ProtectedOpenMatchers) so environments without the operator/ConfigMap keep working unchanged (zero value = legacy collapse). - A missing ConfigMap keeps the current protection rather than wiping it, avoiding a transient unprotection window before the operator creates it. - The reloader only starts when openProtectionConfigMapName is set. The kube-client GET + ticker loop need a live cluster and are flagged for CI; the parse (ParseOpenProtectionConfigMap), store Get/Set, and reload-once logic (present/NotFound/default-interval) are unit-tested with a fake clientset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/registry/file/containerprofile_processor.go (1)
92-98:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCopy matcher slices when converting into
OpenProtection.This conversion currently aliases the caller’s
armotypes.OpenMatchersslices into the shared store. That undermines the store’s concurrency contract: a caller that retains and mutatesm.Prefix/m.ExactafterSetcan change the live protection set without taking the store lock, andGet()readers will observe that mutation concurrently.Possible fix
func OpenProtectionFromMatchers(m armotypes.OpenMatchers) dynamicpathdetector.OpenProtection { return dynamicpathdetector.OpenProtection{ - Exact: m.Exact, - Prefix: m.Prefix, - Suffix: m.Suffix, - Contains: m.Contains, + Exact: append([]string(nil), m.Exact...), + Prefix: append([]string(nil), m.Prefix...), + Suffix: append([]string(nil), m.Suffix...), + Contains: append([]string(nil), m.Contains...), } }🤖 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 `@pkg/registry/file/containerprofile_processor.go` around lines 92 - 98, OpenProtectionFromMatchers currently aliases the input slice fields (m.Exact, m.Prefix, m.Suffix, m.Contains) into the returned dynamicpathdetector.OpenProtection which allows callers to mutate those slices after Set and break the store's concurrency guarantees; fix it in OpenProtectionFromMatchers by allocating new slices for each field (preserving length/capacity as appropriate), copying the elements from m.Exact/m.Prefix/m.Suffix/m.Contains into those new slices, and returning those copies (handle nil inputs by leaving nil or returning empty slices consistently) so the returned OpenProtection owns its own slice memory.
🧹 Nitpick comments (1)
pkg/registry/file/openprotection_test.go (1)
78-115: ⚡ Quick winAdd a malformed-ConfigMap regression case here.
The load-bearing behavior in
reloadOnceis “parse failure logs and preserves the previously seeded protection.” This suite coversNotFound, but not the invalid-JSON path, so a future refactor could accidentally start clearing the store on bad operator data without a test catching it.Example test case
func TestOpenProtectionReloaderReloadOnce(t *testing.T) { const ns, name = "kubescape", "storage-open-protection" @@ t.Run("missing configmap keeps current protection", func(t *testing.T) { client := fake.NewSimpleClientset() // no configmap store := NewOpenProtectionStore(armotypes.OpenMatchers{Prefix: []string{"/etc/shadow"}}) r := NewOpenProtectionReloader(client, ns, name, time.Minute, store) if err := r.reloadOnce(context.Background()); err != nil { t.Fatalf("reloadOnce should tolerate NotFound: %v", err) } if got := store.Get(); len(got.Prefix) != 1 || got.Prefix[0] != "/etc/shadow" { t.Fatalf("expected seeded protection preserved on NotFound, got %+v", got) } }) + + t.Run("invalid configmap keeps current protection", func(t *testing.T) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, + Data: map[string]string{OpenProtectionConfigMapKey: "{not json"}, + } + client := fake.NewSimpleClientset(cm) + store := NewOpenProtectionStore(armotypes.OpenMatchers{Prefix: []string{"/etc/shadow"}}) + r := NewOpenProtectionReloader(client, ns, name, time.Minute, store) + if err := r.reloadOnce(context.Background()); err == nil { + t.Fatal("expected parse error") + } + if got := store.Get(); len(got.Prefix) != 1 || got.Prefix[0] != "/etc/shadow" { + t.Fatalf("expected seeded protection preserved on parse error, got %+v", got) + } + })🤖 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 `@pkg/registry/file/openprotection_test.go` around lines 78 - 115, Add a test that ensures reloadOnce preserves the existing OpenProtectionStore when the ConfigMap exists but contains malformed JSON: create a fake client with a ConfigMap named as in NewOpenProtectionReloader(ns,name) whose Data[OpenProtectionConfigMapKey] is invalid JSON, seed the store via NewOpenProtectionStore(armotypes.OpenMatchers{Prefix: []string{"/etc/shadow"}}), call r.reloadOnce(context.Background()), assert no error that would clear the store and that store.Get() still contains the original prefix; reference reloadOnce, NewOpenProtectionReloader, NewOpenProtectionStore, and OpenProtectionConfigMapKey to locate the code under test.
🤖 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 `@pkg/registry/file/dynamicpathdetector/analyzer.go`:
- Around line 474-479: The current logic in updateNodeStats doesn't enforce
PinnedSubtreeBudget for the immediate overflowing insert path because
handleNewSegment increments parent.Count after updateNodeStats runs; modify the
flow so that after a new child is inserted (in handleNewSegment or
processSegment) you immediately re-check the parent's count and call
updateNodeStats (or perform the collapse) when threshold ==
NeverCollapseThreshold && parent.Count > PinnedSubtreeBudget; alternatively
implement the collapse directly in processSegment under the same condition to
guarantee collapse instead of leaving the subtree literal above budget, and add
a regression test that creates exactly PinnedSubtreeBudget+1 unique children to
validate the boundary behavior.
In `@pkg/registry/file/openprotection.go`:
- Around line 107-117: NewOpenProtectionReloader currently accepts nil
dependencies and causes a later panic in reloadOnce; update
NewOpenProtectionReloader to validate that client and store are non-nil up front
(and do the same validation for any other NewOpenProtectionReloader-like
constructors), and fail fast with a clear error (e.g., panic or log.Fatalf) if
either client == nil or store == nil so the misconfiguration is rejected eagerly
instead of causing a runtime panic in reloadOnce or other methods on
OpenProtectionReloader.
---
Outside diff comments:
In `@pkg/registry/file/containerprofile_processor.go`:
- Around line 92-98: OpenProtectionFromMatchers currently aliases the input
slice fields (m.Exact, m.Prefix, m.Suffix, m.Contains) into the returned
dynamicpathdetector.OpenProtection which allows callers to mutate those slices
after Set and break the store's concurrency guarantees; fix it in
OpenProtectionFromMatchers by allocating new slices for each field (preserving
length/capacity as appropriate), copying the elements from
m.Exact/m.Prefix/m.Suffix/m.Contains into those new slices, and returning those
copies (handle nil inputs by leaving nil or returning empty slices consistently)
so the returned OpenProtection owns its own slice memory.
---
Nitpick comments:
In `@pkg/registry/file/openprotection_test.go`:
- Around line 78-115: Add a test that ensures reloadOnce preserves the existing
OpenProtectionStore when the ConfigMap exists but contains malformed JSON:
create a fake client with a ConfigMap named as in
NewOpenProtectionReloader(ns,name) whose Data[OpenProtectionConfigMapKey] is
invalid JSON, seed the store via
NewOpenProtectionStore(armotypes.OpenMatchers{Prefix: []string{"/etc/shadow"}}),
call r.reloadOnce(context.Background()), assert no error that would clear the
store and that store.Get() still contains the original prefix; reference
reloadOnce, NewOpenProtectionReloader, NewOpenProtectionStore, and
OpenProtectionConfigMapKey to locate the code under test.
🪄 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: a23f9d7d-a1f4-46a0-ae2d-4200e1bd8ab3
📒 Files selected for processing (11)
main.gopkg/apiserver/apiserver.gopkg/cmd/server/start.gopkg/config/config.gopkg/registry/file/containerprofile_processor.gopkg/registry/file/dynamicpathdetector/analyzer.gopkg/registry/file/dynamicpathdetector/protection.gopkg/registry/file/dynamicpathdetector/protection_test.gopkg/registry/file/dynamicpathdetector/types.gopkg/registry/file/openprotection.gopkg/registry/file/openprotection_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- pkg/config/config.go
- pkg/registry/file/dynamicpathdetector/types.go
- pkg/registry/file/dynamicpathdetector/protection.go
| func (ua *PathAnalyzer) updateNodeStats(node *SegmentNode, threshold int) { | ||
| if node.Count > threshold && !node.IsNextDynamic() { | ||
| effectiveThreshold := threshold | ||
| if threshold == NeverCollapseThreshold && node.Count > PinnedSubtreeBudget { | ||
| effectiveThreshold = PinnedSubtreeBudget | ||
| } | ||
| if node.Count > effectiveThreshold && !node.IsNextDynamic() { |
There was a problem hiding this comment.
Enforce the pinned-subtree budget on the overflowing insert.
This fallback only runs when the node is revisited. The path that first takes a protected directory from PinnedSubtreeBudget to PinnedSubtreeBudget+1 increments the parent's Count inside handleNewSegment, after updateNodeStats has already checked that parent on the current walk. If learning stops there, the subtree stays literal above budget, which breaks the stated "collapse instead of TooLarge" tradeoff. Please re-check the parent immediately after inserting a new child, or collapse directly from processSegment when threshold == NeverCollapseThreshold && node.Count > PinnedSubtreeBudget, and add a boundary regression with exactly PinnedSubtreeBudget+1 unique children.
Possible fix sketch
func (ua *PathAnalyzer) processSegment(node *SegmentNode, segment string, threshold int) *SegmentNode {
...
- return ua.handleNewSegment(node, segment)
+ child := ua.handleNewSegment(node, segment)
+ if threshold == NeverCollapseThreshold && node.Count > PinnedSubtreeBudget {
+ return ua.createDynamicNode(node)
+ }
+ return child
}🤖 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 `@pkg/registry/file/dynamicpathdetector/analyzer.go` around lines 474 - 479,
The current logic in updateNodeStats doesn't enforce PinnedSubtreeBudget for the
immediate overflowing insert path because handleNewSegment increments
parent.Count after updateNodeStats runs; modify the flow so that after a new
child is inserted (in handleNewSegment or processSegment) you immediately
re-check the parent's count and call updateNodeStats (or perform the collapse)
when threshold == NeverCollapseThreshold && parent.Count > PinnedSubtreeBudget;
alternatively implement the collapse directly in processSegment under the same
condition to guarantee collapse instead of leaving the subtree literal above
budget, and add a regression test that creates exactly PinnedSubtreeBudget+1
unique children to validate the boundary behavior.
| func NewOpenProtectionReloader(client kubernetes.Interface, namespace, name string, interval time.Duration, store *OpenProtectionStore) *OpenProtectionReloader { | ||
| if interval <= 0 { | ||
| interval = DefaultOpenProtectionRefreshInterval | ||
| } | ||
| return &OpenProtectionReloader{ | ||
| client: client, | ||
| namespace: namespace, | ||
| name: name, | ||
| interval: interval, | ||
| store: store, | ||
| } |
There was a problem hiding this comment.
Validate client and store up front.
reloadOnce dereferences both unconditionally, so NewOpenProtectionReloader(nil, ..., nil) builds an object that only fails later with a panic. Since this constructor is exported, it should reject invalid dependencies eagerly instead of turning miswiring into a runtime crash.
Possible fix
-func NewOpenProtectionReloader(client kubernetes.Interface, namespace, name string, interval time.Duration, store *OpenProtectionStore) *OpenProtectionReloader {
+func NewOpenProtectionReloader(client kubernetes.Interface, namespace, name string, interval time.Duration, store *OpenProtectionStore) (*OpenProtectionReloader, error) {
+ if client == nil {
+ return nil, fmt.Errorf("open-protection reloader client is nil")
+ }
+ if store == nil {
+ return nil, fmt.Errorf("open-protection reloader store is nil")
+ }
if interval <= 0 {
interval = DefaultOpenProtectionRefreshInterval
}
- return &OpenProtectionReloader{
+ return &OpenProtectionReloader{
client: client,
namespace: namespace,
name: name,
interval: interval,
store: store,
- }
+ }, nil
}Also applies to: 123-139
🤖 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 `@pkg/registry/file/openprotection.go` around lines 107 - 117,
NewOpenProtectionReloader currently accepts nil dependencies and causes a later
panic in reloadOnce; update NewOpenProtectionReloader to validate that client
and store are non-nil up front (and do the same validation for any other
NewOpenProtectionReloader-like constructors), and fail fast with a clear error
(e.g., panic or log.Fatalf) if either client == nil or store == nil so the
misconfiguration is rejected eagerly instead of causing a runtime panic in
reloadOnce or other methods on OpenProtectionReloader.
Problem
Profile deflation generalises high-cardinality open paths to keep stored profiles bounded (e.g.
/etcwith many files →/etc/⋯, or the whole tree →/⋯/⋯). That generalisation silently defeats anomaly rules such as R0010 (unexpected/etc/shadowaccess): once/etccollapses to a wildcard,ap.was_path_opened("/etc/shadow.evil")matches it and the rule can never fire.Fix
Pin rule-declared sensitive prefixes (and their ancestors) to literal during collapse, so a never-seen sensitive path stays distinguishable from a generalised one. The analyzer skips collapsing any node on the ancestor chain of — or within the subtree of — a protected prefix.
dynamicpathdetector:NewPathAnalyzerWithConfigsAndProtection+OpenProtection(exact/prefix/suffix/contains;suffix/containsare resolved against the observed open paths since they have no fixed location).protectedNodeis O(1) for the common node via a precomputed ancestor set + top-dir fast-reject; cost never scales with the number of protected prefixes, and there is zero overhead when no protection is configured.DeflateContainerProfileSpectakes anOpenProtection;OpenProtectionFromMatchersconvertsarmotypes.OpenMatchers.config.Config.ProtectedOpenMatchersfeeds the in-cluster processor (populated by the operator from the rule library viaarmotypes.UnionOpenProtection); the backend feeds it from MongoDB rules.Tested
TestProtectedPrefixKeepsSensitiveDetectableandTestOpenProtectionExactSuffixContains: with/etcfar over the collapse threshold, never-seen sensitive paths (/etc/shadow.evil,/etc/sudoers.bak,/etc/sudoers.d/99-evil,/etc/ssh/ssh_host_ed25519_key,/home/alice/.ssh/evil) stay uncovered, while/procstill collapses (bloat control preserved). Existingdynamicpathdetectorsuite unaffected (zero-valueOpenProtection= legacy behaviour).Performance
Deflation is per profile PreSave (consolidation), not per open event — off the per-event hot path. Benchmarked on a ~1,340-open profile:
(The precomputed
protectedNodekeeps this at ~1.4× rather than the ~6.4× a naive per-prefix scan would cost.)Deps
armoapi-go→ pseudo-versionv0.0.719-0.20260610161827-5cfa1e630e5dcarryingOpenMatchers/UnionOpenProtection(feat(armotypes): add UnionOpenProtection helper for rule-aware profile collapse armosec/armoapi-go#656). Re-pin to the tagged release before merge.Related
UnionOpenProtectionhelper)🤖 Generated with Claude Code
Summary by CodeRabbit