Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions pkg/rulemanager/cel/libraries/applicationprofile/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,28 @@ func (l *apLibrary) wasExecutedWithArgs(containerID, path, args ref.Val) ref.Val
// wildcard token in Args.
if _, ok := cp.Execs.Values[pathStr]; ok {
if profileArgs, ok := cp.ExecsByPath[pathStr]; ok {
if dynamicpathdetector.CompareExecArgs(profileArgs, runtimeArgs) {
return types.Bool(true)
}
} else {
// State 2: ExecsByPath absent → back-compat "no argv constraint".
return types.Bool(true)
// Exact path IS in the profile WITH an authored argv
// constraint. The constraint is authoritative — its match
// result IS the answer for this path. Do NOT fall through
// to pattern matching: a looser pattern (e.g. `/usr/bin/*`
// with profileArgs `[*]`) would silently override the
// exact-path's stricter args constraint, defeating
// rule-author intent.
//
// CodeRabbit upstream PR #43 exec.go:144 (Major). Before
// this fix the function fell through on CompareExecArgs
// false, letting a permissive Pattern shadow the strict
// exact-path constraint.
return types.Bool(dynamicpathdetector.CompareExecArgs(profileArgs, runtimeArgs))
}
// State 2: ExecsByPath absent → back-compat "no argv constraint".
return types.Bool(true)
}
// Pattern path match: dynamic-segment paths in cp.Execs.Patterns.
// Args matching mirrors the exact-path case.
// Path NOT in Values — try Pattern path match: dynamic-segment paths
// in cp.Execs.Patterns. Args matching mirrors the exact-path case
// EXCEPT here the pattern is the only constraint, so a mismatched
// args fall-through to the NEXT pattern is acceptable (different
// patterns can encode different (path, args) shapes).
for _, execPath := range cp.Execs.Patterns {
if dynamicpathdetector.CompareDynamic(execPath, pathStr) {
if profileArgs, ok := cp.ExecsByPath[execPath]; ok {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package applicationprofile

import (
"testing"

"github.com/google/cel-go/common/types"
"github.com/kubescape/node-agent/pkg/objectcache"
)


// BenchmarkWasExecutedWithArgs covers the R0040 hot path in the four
// representative shapes:
//
// - exact_path_args_match: literal path in Values + matching argv
// - exact_path_args_mismatch: literal path in Values + mismatching argv
// (post-tier-2 fix: returns false without
// falling through to pattern matching)
// - pattern_path_args_match: path absent from Values, matches a Pattern,
// ExecsByPath has the matching argv
// - no_match: path absent from Values AND no Pattern
// matches (terminal false)
//
// Reports allocs/op for the steady-state matcher. The CEL value-wrapping
// floor is unchanged by this PR — only the exec.go control flow changed
// (no fall-through on exact-path strict-args mismatch).
func BenchmarkWasExecutedWithArgs(b *testing.B) {
values := map[string]struct{}{
"/usr/bin/curl": {},
"/usr/sbin/sshd": {},
"/bin/ls": {},
"/usr/bin/python": {},
}
patterns := []string{
"/usr/local/bin/⋯",
"/opt/⋯/bin/⋯",
}
execsByPath := map[string][]string{
"/usr/bin/curl": {"-X", "GET", "*"},
"/usr/sbin/sshd": {"-D"},
"/bin/ls": {"-la", "⋯"},
"/usr/bin/python": {"⋯", "*"},
"/usr/local/bin/⋯": {"*"},
"/opt/⋯/bin/⋯": {"*"},
}
pcp := &objectcache.ProjectedContainerProfile{
Execs: objectcache.ProjectedField{
All: true,
Values: values,
Patterns: patterns,
},
ExecsByPath: execsByPath,
}
lib := &apLibrary{objectCache: &mockObjectCacheForPattern{pcp: pcp}}

cases := []struct {
name, path string
args []string
}{
{"exact_path_args_match", "/usr/bin/curl", []string{"-X", "GET", "https://api.example.com"}},
{"exact_path_args_mismatch", "/usr/bin/curl", []string{"-X", "DELETE", "https://api.example.com"}},
{"pattern_path_args_match", "/usr/local/bin/myhelper", []string{"--config", "/etc/myhelper.yaml"}},
// `no_match` would fall through to isExecInPodSpec which needs
// K8sObjectCache wiring — out of scope for a matcher bench.
}
cid := types.String("bench-cid")
for _, c := range cases {
path := types.String(c.path)
// Encode argv as a CEL list — let the matcher parse it like
// the real CEL call site does.
argsRefVal := types.NewStringList(types.DefaultTypeAdapter, c.args)
b.Run(c.name, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = lib.wasExecutedWithArgs(cid, path, argsRefVal)
}
})
}
}
129 changes: 113 additions & 16 deletions pkg/rulemanager/cel/libraries/applicationprofile/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,34 @@ func (l *apLibrary) wasPathOpenedWithSuffix(containerID, suffix ref.Val) ref.Val

if cp.Opens.All {
// All entries retained (no rule declared SuffixHits-style
// projection). Scan ONLY concrete entries in Values — Patterns
// contain wildcard tokens ('*' / '⋯') whose text doesn't safely
// answer suffix questions. CodeRabbit PR #43 open.go:79: a
// retained Pattern like "/var/log/pods/*/volumes/..." doesn't
// end with the concrete suffix "foo.log", but the concrete open
// it stands in for might — strings.HasSuffix on the pattern
// text returns false and produces a false negative. Patterns
// are inherently wildcard-shaped; concrete-path semantics live
// in Values (and in SuffixHits when projection is active).
// projection). Scan concrete entries in Values first — exact
// strings.HasSuffix is correct for those.
for openPath := range cp.Opens.Values {
if strings.HasSuffix(openPath, suffixStr) {
return types.Bool(true)
}
}
// 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)
}
}
return types.Bool(false)
}
// Projection applied — SuffixHits is authoritative; absent key = undeclared.
Expand Down Expand Up @@ -160,18 +174,27 @@ func (l *apLibrary) wasPathOpenedWithPrefix(containerID, prefix ref.Val) ref.Val
}

if cp.Opens.All {
// All entries retained — scan ONLY Values (concrete paths).
// Patterns contain wildcard tokens whose text doesn't safely
// answer prefix questions; a pattern starting with "/var/⋯/log"
// matches concrete paths starting with "/var/anything/log" but
// strings.HasPrefix against the pattern text returns false for
// "/var/foo/log...". Same fix as wasPathOpenedWithSuffix above.
// CodeRabbit PR #43 open.go:79 (Also applies to 111-123).
// 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)
}
}
// 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)
}
}
return types.Bool(false)
}
// Projection applied — PrefixHits is authoritative; absent key = undeclared.
Expand All @@ -185,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 == dynamicpathdetector.WildcardIdentifier || 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 == dynamicpathdetector.WildcardIdentifier || 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
}

Loading
Loading