-
Notifications
You must be signed in to change notification settings - Fork 28
fix(cel): scan Opens.Patterns in was_path_opened_with_suffix/prefix #935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
entlein
wants to merge
1
commit into
kubescape:main
Choose a base branch
from
k8sstormcenter:fix/suffix-prefix-scan-patterns
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
148 changes: 148 additions & 0 deletions
148
pkg/rulemanager/cel/libraries/containerprofile/open_atomicwriter_test.go
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package containerprofile | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/google/cel-go/common/types" | ||
| "github.com/kubescape/node-agent/pkg/objectcache" | ||
| ) | ||
|
|
||
| // The kubelet writes projected volumes through an "atomic writer": the real files | ||
| // live under a timestamped directory that is replaced wholesale on rotation, and a | ||
| // `..data` symlink points at the current one. A ServiceAccount token is therefore | ||
| // read at a path like | ||
| // | ||
| // /run/secrets/kubernetes.io/serviceaccount/..2026_08_27_14_27_52.163845901/token | ||
| // | ||
| // The timestamped segment is volatile, so dynamicpathdetector collapses it and the | ||
| // learned ContainerProfile records | ||
| // | ||
| // /run/secrets/kubernetes.io/serviceaccount/⋯/token | ||
| // | ||
| // which — because it contains a collapse token — is stored in Opens.Patterns, not | ||
| // Opens.Values. | ||
| // | ||
| // R0006 gates on `!cp.was_path_opened_with_suffix(containerId, '/token')`. With the | ||
| // Opens.All branch scanning Values only, a correctly-learned profile answers "no" | ||
| // and R0006 fires on every SA-token read for the life of the workload. R0008 has the | ||
| // same shape via /proc/⋯/environ. | ||
| // | ||
| // These tests pin the behaviour the rules actually need. They fail before the | ||
| // Patterns scan is added to wasPathOpenedWithSuffix / wasPathOpenedWithPrefix. | ||
| // See k8sstormcenter/node-agent#98. | ||
|
|
||
| func newPatternProfile(patterns []string, values ...string) *containerProfileLibrary { | ||
| vals := map[string]struct{}{} | ||
| for _, v := range values { | ||
| vals[v] = struct{}{} | ||
| } | ||
| if len(vals) == 0 { | ||
| vals = nil | ||
| } | ||
| pcp := &objectcache.ProjectedContainerProfile{ | ||
| Opens: objectcache.ProjectedField{ | ||
| All: true, | ||
| Values: vals, | ||
| Patterns: patterns, | ||
| }, | ||
| } | ||
| return &containerProfileLibrary{objectCache: &mockObjectCacheForPattern{pcp: pcp}} | ||
| } | ||
|
|
||
| func boolOf(t *testing.T, v interface{ Value() any }) bool { | ||
| t.Helper() | ||
| b, ok := v.Value().(bool) | ||
| if !ok { | ||
| t.Fatalf("expected a bool result, got %T (%v)", v.Value(), v) | ||
| } | ||
| return b | ||
| } | ||
|
|
||
| // R0006: the profile records the token open as a pattern with a concrete leaf. | ||
| func TestSuffix_AtomicWriterServiceAccountToken(t *testing.T) { | ||
| lib := newPatternProfile([]string{ | ||
| "/run/secrets/kubernetes.io/serviceaccount/⋯/token", | ||
| }) | ||
| if !boolOf(t, lib.wasPathOpenedWithSuffix(types.String("cid"), types.String("/token"))) { | ||
| t.Error("suffix '/token' against recorded pattern " + | ||
| "'/run/secrets/kubernetes.io/serviceaccount/⋯/token': expected true. " + | ||
| "Returning false makes R0006 fire on every SA-token read of a correctly " + | ||
| "learned profile (issue #98)") | ||
| } | ||
| } | ||
|
|
||
| // R0008: same mechanism, /proc/<pid>/environ. | ||
| func TestSuffix_ProcfsEnviron(t *testing.T) { | ||
| lib := newPatternProfile([]string{"/proc/⋯/environ"}) | ||
| if !boolOf(t, lib.wasPathOpenedWithSuffix(types.String("cid"), types.String("/environ"))) { | ||
| t.Error("suffix '/environ' against recorded pattern '/proc/⋯/environ': " + | ||
| "expected true (R0008 false-positive otherwise)") | ||
| } | ||
| } | ||
|
|
||
| // A pattern whose LEAF is itself a wildcard cannot answer a concrete suffix | ||
| // question. HasSuffix returns false, which is the same answer as skipping the | ||
| // pattern entirely — so scanning Patterns is never worse than not scanning them. | ||
| func TestSuffix_WildcardLeafStillUnmatched(t *testing.T) { | ||
| lib := newPatternProfile([]string{"/var/log/pods/⋯"}) | ||
| if boolOf(t, lib.wasPathOpenedWithSuffix(types.String("cid"), types.String("/foo.log"))) { | ||
| t.Error("suffix '/foo.log' against wildcard-leaf pattern '/var/log/pods/⋯': " + | ||
| "expected false; the pattern text cannot answer this") | ||
| } | ||
| } | ||
|
|
||
| // Prefix side: the segments before the first collapse token are concrete, so every | ||
| // concrete path the pattern stands for really does start with them. | ||
| func TestPrefix_ConcreteHeadOfPattern(t *testing.T) { | ||
| lib := newPatternProfile([]string{"/run/secrets/kubernetes.io/serviceaccount/⋯/token"}) | ||
| if !boolOf(t, lib.wasPathOpenedWithPrefix(types.String("cid"), | ||
| types.String("/run/secrets/"))) { | ||
| t.Error("prefix '/run/secrets/' against pattern " + | ||
| "'/run/secrets/kubernetes.io/serviceaccount/⋯/token': expected true; " + | ||
| "the pattern head is concrete") | ||
| } | ||
| } | ||
|
|
||
| func TestPrefix_UnrelatedHeadStillUnmatched(t *testing.T) { | ||
| lib := newPatternProfile([]string{"/run/secrets/kubernetes.io/serviceaccount/⋯/token"}) | ||
| if boolOf(t, lib.wasPathOpenedWithPrefix(types.String("cid"), types.String("/etc/"))) { | ||
| t.Error("prefix '/etc/' against a /run/... pattern: expected false") | ||
| } | ||
| } | ||
|
|
||
| // Values must keep working, and must still win without consulting Patterns. | ||
| func TestSuffix_ConcreteValueStillMatches(t *testing.T) { | ||
| lib := newPatternProfile(nil, "/var/log/concrete.log") | ||
| if !boolOf(t, lib.wasPathOpenedWithSuffix(types.String("cid"), types.String(".log"))) { | ||
| t.Error("suffix '.log' against concrete value '/var/log/concrete.log': expected true") | ||
| } | ||
| } | ||
|
|
||
| // The two code paths for the same helper must agree. projection_apply.go builds | ||
| // SuffixHits with strings.HasSuffix over EVERY raw entry including dynamic ones, so | ||
| // a projected profile already answers true for ⋯/token. The Opens.All branch | ||
| // answering false for the same profile is the inconsistency issue #98 reports. | ||
| func TestSuffix_AllBranchAgreesWithProjectedBranch(t *testing.T) { | ||
| const entry = "/run/secrets/kubernetes.io/serviceaccount/⋯/token" | ||
| const suffix = "/token" | ||
|
|
||
| all := newPatternProfile([]string{entry}) | ||
| allAnswer := boolOf(t, all.wasPathOpenedWithSuffix(types.String("cid"), types.String(suffix))) | ||
|
|
||
| // what projection_apply.go would compute for the same raw entry | ||
| projected := &containerProfileLibrary{objectCache: &mockObjectCacheForPattern{ | ||
| pcp: &objectcache.ProjectedContainerProfile{ | ||
| Opens: objectcache.ProjectedField{ | ||
| All: false, | ||
| SuffixHits: map[string]bool{suffix: true}, | ||
| }, | ||
| }, | ||
| }} | ||
| projectedAnswer := boolOf(t, | ||
| projected.wasPathOpenedWithSuffix(types.String("cid"), types.String(suffix))) | ||
|
|
||
| if allAnswer != projectedAnswer { | ||
| t.Errorf("same profile, two code paths, different answers: "+ | ||
| "Opens.All branch=%v, projected branch=%v", allAnswer, projectedAnswer) | ||
| } | ||
| } |
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not match a query that includes the collapse token.
⋯is wildcard metadata, not a concrete path segment. For example,strings.HasSuffix("/var/log/⋯/foo.log", "⋯/foo.log")andstrings.HasPrefix("/var/⋯/log/foo", "/var/⋯")return true, but the retained pattern does not prove either literal path relation. A CEL rule with either query can incorrectly suppress a rule result.pkg/rulemanager/cel/libraries/containerprofile/open.go#L144-L146: only compare a pattern when the queried suffix is wholly concrete relative to the collapse token.pkg/rulemanager/cel/libraries/containerprofile/open.go#L196-L198: only compare a pattern when the queried prefix is wholly concrete relative to the collapse token.Add regression cases where the query contains
⋯.📍 Affects 1 file
pkg/rulemanager/cel/libraries/containerprofile/open.go#L144-L146(this comment)pkg/rulemanager/cel/libraries/containerprofile/open.go#L196-L198🤖 Prompt for AI Agents