-
Notifications
You must be signed in to change notification settings - Fork 16
feat(cel): was_path_opened* CEL helpers in applicationprofile library #811
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
base: main
Are you sure you want to change the base?
Changes from all commits
d78728d
8e505e6
e30da5c
654dedd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,13 @@ func (l *apLibrary) wasPathOpened(containerID, path ref.Val) ref.Val { | |
| return types.Bool(false) | ||
| } | ||
|
|
||
| // wasPathOpenedWithFlags answers whether the projected ApplicationProfile | ||
| // contains an open-entry whose path matches the given path. The flags | ||
| // argument is parsed and validated for shape but is not used for matching | ||
| // in v1 — the OpenFlagsByPath projection slice is out of scope for v1 | ||
| // (composite-key projection would balloon the cache footprint). When the | ||
| // flags-projection slice is added in a future spec revision, this helper | ||
| // becomes the path-AND-flag matcher and v1 callers continue to work. | ||
| func (l *apLibrary) wasPathOpenedWithFlags(containerID, path, flags ref.Val) ref.Val { | ||
| if l.objectCache == nil { | ||
| return types.NewErr("objectCache is nil") | ||
|
|
@@ -105,14 +112,32 @@ func (l *apLibrary) wasPathOpenedWithSuffix(containerID, suffix ref.Val) ref.Val | |
| } | ||
|
|
||
| if cp.Opens.All { | ||
| // All entries retained — scan to check for the suffix. | ||
| // All entries retained (no rule declared SuffixHits-style | ||
| // projection). Scan concrete entries in Values first — exact | ||
| // strings.HasSuffix is correct for those. | ||
| for openPath := range cp.Opens.Values { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: |
||
| if strings.HasSuffix(openPath, suffixStr) { | ||
| return types.Bool(true) | ||
| } | ||
| } | ||
| for _, openPath := range cp.Opens.Patterns { | ||
| if strings.HasSuffix(openPath, suffixStr) { | ||
| // Patterns hold dynamic entries (containing `*` / `⋯`). We | ||
| // can't run strings.HasSuffix on the raw pattern text — a | ||
| // pattern like "/var/log/pods/*/volumes/..." has wildcard | ||
| // tokens that don't textually end with "foo.log" even though | ||
| // its concrete realisations might. Matthias upstream PR #811 | ||
| // review: a NARROWER fallback is the answer here — split off | ||
| // the pattern's concrete tail (the literal text after the | ||
| // last wildcard segment) and only check HasSuffix against | ||
| // that. If the pattern ends in a wildcard segment, the tail | ||
| // is empty and concrete realisations could match ANY suffix — | ||
| // be permissive (return true) to avoid the false-negative on | ||
| // rules that omit profileDataRequired.opens. | ||
| for _, openPattern := range cp.Opens.Patterns { | ||
| tail := patternConcreteSuffix(openPattern) | ||
| if tail == "" { | ||
| return types.Bool(true) | ||
| } | ||
| if strings.HasSuffix(tail, suffixStr) { | ||
| return types.Bool(true) | ||
| } | ||
| } | ||
|
|
@@ -149,14 +174,24 @@ func (l *apLibrary) wasPathOpenedWithPrefix(containerID, prefix ref.Val) ref.Val | |
| } | ||
|
|
||
| if cp.Opens.All { | ||
| // All entries retained — scan to check for the prefix. | ||
| // All entries retained. Scan concrete entries in Values first — | ||
| // exact strings.HasPrefix is correct for those. | ||
| for openPath := range cp.Opens.Values { | ||
| if strings.HasPrefix(openPath, prefixStr) { | ||
| return types.Bool(true) | ||
| } | ||
| } | ||
| for _, openPath := range cp.Opens.Patterns { | ||
| if strings.HasPrefix(openPath, prefixStr) { | ||
| // Patterns: same narrower-fallback strategy as the suffix path. | ||
| // Split off the pattern's concrete head (the literal text | ||
| // BEFORE the first wildcard segment). If the pattern starts | ||
| // with a wildcard, concrete realisations could match ANY | ||
| // prefix — be permissive. Matthias upstream PR #811 review. | ||
| for _, openPattern := range cp.Opens.Patterns { | ||
| head := patternConcretePrefix(openPattern) | ||
| if head == "" { | ||
| return types.Bool(true) | ||
| } | ||
| if strings.HasPrefix(head, prefixStr) { | ||
| return types.Bool(true) | ||
| } | ||
| } | ||
|
|
@@ -173,3 +208,77 @@ func (l *apLibrary) wasPathOpenedWithPrefix(containerID, prefix ref.Val) ref.Val | |
| return types.Bool(hit) | ||
| } | ||
|
|
||
| // patternConcreteSuffix returns the literal text at the tail of a | ||
| // wildcard-bearing path pattern, dropped to start after the LAST | ||
| // wildcard segment's trailing `/`. Returns the input unchanged when | ||
| // no wildcard segments are present, or "" when the pattern ends in | ||
| // a wildcard segment (concrete realisations could match any suffix). | ||
| // | ||
| // Examples: | ||
| // | ||
| // "/var/log/⋯/foo.log" → "foo.log" (last wildcard `⋯`, concrete tail follows) | ||
| // "/var/log/pods/*" → "" (trailing wildcard, permissive caller) | ||
| // "/var/log/foo.log" → "/var/log/foo.log" (no wildcards, whole pattern) | ||
| // "*" → "" (lone wildcard) | ||
| // | ||
| // Matthias upstream PR #811 review. | ||
| func patternConcreteSuffix(p string) string { | ||
| lastWildEnd := -1 | ||
| i := 0 | ||
| for i < len(p) { | ||
| segStart := i | ||
| for i < len(p) && p[i] != '/' { | ||
| i++ | ||
| } | ||
| seg := p[segStart:i] | ||
| if seg == "*" || seg == dynamicpathdetector.DynamicIdentifier { | ||
| lastWildEnd = i | ||
| } | ||
| if i < len(p) { | ||
| i++ // skip `/` | ||
| } | ||
| } | ||
| if lastWildEnd < 0 { | ||
| return p | ||
| } | ||
| if lastWildEnd >= len(p) { | ||
| return "" | ||
| } | ||
| // lastWildEnd points at the `/` after the wildcard segment. Keep | ||
| // the slash so callers querying with leading-slash suffixes match | ||
| // correctly (every concrete realisation has that slash too). | ||
| return p[lastWildEnd:] | ||
| } | ||
|
|
||
| // patternConcretePrefix is the mirror of patternConcreteSuffix — | ||
| // returns the literal text at the HEAD of the pattern up to (but not | ||
| // including) the first wildcard segment. Returns the input unchanged | ||
| // when no wildcard segments are present, or "" when the pattern starts | ||
| // with a wildcard segment. | ||
| // | ||
| // Matthias upstream PR #811 review. | ||
| func patternConcretePrefix(p string) string { | ||
| i := 0 | ||
| for i < len(p) { | ||
| segStart := i | ||
| for i < len(p) && p[i] != '/' { | ||
| i++ | ||
| } | ||
| seg := p[segStart:i] | ||
| if seg == "*" || seg == dynamicpathdetector.DynamicIdentifier { | ||
| if segStart == 0 { | ||
| return "" | ||
| } | ||
| // segStart is at the wildcard segment; the byte BEFORE it | ||
| // is the `/` separator. Keep the slash in the returned | ||
| // prefix so callers querying with trailing-slash prefixes | ||
| // match (every concrete realisation has that slash too). | ||
| return p[:segStart] | ||
| } | ||
| if i < len(p) { | ||
| i++ // skip `/` | ||
| } | ||
| } | ||
| return p | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| package applicationprofile | ||
|
|
||
| import ( | ||
| "strconv" | ||
| "testing" | ||
|
|
||
| "github.com/google/cel-go/common/types" | ||
| "github.com/kubescape/node-agent/pkg/objectcache" | ||
| ) | ||
|
|
||
| // BenchmarkWasPathOpenedWithSuffix_AllMode exercises the pass-through | ||
| // (Opens.All == true) suffix path under three representative profile | ||
| // shapes: | ||
| // | ||
| // - values_only: 50 concrete entries, no Patterns | ||
| // - patterns_concrete: 50 concrete entries + 10 Patterns whose tail | ||
| // is literal (the typical /var/log/⋯/foo.log shape) | ||
| // - patterns_wildcard: 50 concrete entries + 10 Patterns ending in a | ||
| // wildcard segment (the permissive-arm shape) | ||
| // | ||
| // Captures Matthias's upstream PR #811 contract numbers for the PR | ||
| // description. | ||
| func BenchmarkWasPathOpenedWithSuffix_AllMode(b *testing.B) { | ||
| shapes := []struct { | ||
| name string | ||
| values int | ||
| patterns []string | ||
| }{ | ||
| {"values_only", 50, nil}, | ||
| {"patterns_concrete", 50, []string{ | ||
| "/var/log/⋯/access.log", "/var/log/⋯/error.log", "/opt/⋯/server.log", | ||
| "/etc/⋯/audit.log", "/var/run/⋯/state.log", "/srv/⋯/app.log", | ||
| "/var/cache/⋯/tmp.log", "/usr/share/⋯/data.log", "/home/⋯/user.log", | ||
| "/proc/⋯/status.log", | ||
| }}, | ||
| {"patterns_wildcard", 50, []string{ | ||
| "/var/log/pods/*", "/var/log/containers/*", "/etc/cron.d/*", | ||
| "/opt/⋯", "/srv/*", "/var/run/*", | ||
| "/usr/local/⋯", "/home/⋯", "/tmp/⋯", "/run/⋯", | ||
| }}, | ||
| } | ||
| for _, sh := range shapes { | ||
| b.Run(sh.name, func(b *testing.B) { | ||
| values := make(map[string]struct{}, sh.values) | ||
| for i := 0; i < sh.values; i++ { | ||
| values["/usr/lib/x86_64-linux-gnu/libcrypto.so."+strconv.Itoa(i)] = struct{}{} | ||
| } | ||
| pcp := &objectcache.ProjectedContainerProfile{ | ||
| Opens: objectcache.ProjectedField{ | ||
| All: true, | ||
| Values: values, | ||
| Patterns: sh.patterns, | ||
| }, | ||
| } | ||
| lib := &apLibrary{objectCache: &mockObjectCacheForPattern{pcp: pcp}} | ||
| suffix := types.String(".log") | ||
| cid := types.String("bench-cid") | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = lib.wasPathOpenedWithSuffix(cid, suffix) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // BenchmarkWasPathOpenedWithPrefix_AllMode mirrors the suffix bench | ||
| // for the prefix path. | ||
| func BenchmarkWasPathOpenedWithPrefix_AllMode(b *testing.B) { | ||
| shapes := []struct { | ||
| name string | ||
| values int | ||
| patterns []string | ||
| }{ | ||
| {"values_only", 50, nil}, | ||
| {"patterns_concrete", 50, []string{ | ||
| "/var/log/⋯/access.log", "/var/log/⋯/error.log", "/opt/⋯/server.log", | ||
| "/etc/⋯/audit.log", "/var/run/⋯/state.log", | ||
| }}, | ||
| {"patterns_wildcard", 50, []string{ | ||
| "*/run", "*/log", "*/cache", | ||
| "⋯", "*", | ||
| }}, | ||
| } | ||
| for _, sh := range shapes { | ||
| b.Run(sh.name, func(b *testing.B) { | ||
| values := make(map[string]struct{}, sh.values) | ||
| for i := 0; i < sh.values; i++ { | ||
| values["/usr/lib/x86_64-linux-gnu/libcrypto.so."+strconv.Itoa(i)] = struct{}{} | ||
| } | ||
| pcp := &objectcache.ProjectedContainerProfile{ | ||
| Opens: objectcache.ProjectedField{ | ||
| All: true, | ||
| Values: values, | ||
| Patterns: sh.patterns, | ||
| }, | ||
| } | ||
| lib := &apLibrary{objectCache: &mockObjectCacheForPattern{pcp: pcp}} | ||
| prefix := types.String("/var/") | ||
| cid := types.String("bench-cid") | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = lib.wasPathOpenedWithPrefix(cid, prefix) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // BenchmarkPatternConcreteSuffix isolates the helper to confirm zero | ||
| // allocation regardless of pattern shape. | ||
| func BenchmarkPatternConcreteSuffix(b *testing.B) { | ||
| cases := []string{ | ||
| "/var/log/⋯/foo.log", | ||
| "/var/log/pods/*", | ||
| "/var/log/foo.log", | ||
| "*", | ||
| "/var/⋯/log/⋯/foo.log", | ||
| } | ||
| for _, c := range cases { | ||
| b.Run(c, func(b *testing.B) { | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = patternConcreteSuffix(c) | ||
| } | ||
| }) | ||
| } | ||
| } |
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.
Helper contract comment is out of sync with runtime behavior.
Lines 139-154 still describe
cp.Opens.Patternsas effectively not participating in suffix/prefix answers, butopen.gonow scans patterns viapatternConcreteSuffix/patternConcretePrefixin pass-through mode. This doc mismatch can mislead rule authors.🤖 Prompt for AI Agents