diff --git a/pkg/rulemanager/cel/libraries/applicationprofile/ap.go b/pkg/rulemanager/cel/libraries/applicationprofile/ap.go index ce86d7ab8..99ab11831 100644 --- a/pkg/rulemanager/cel/libraries/applicationprofile/ap.go +++ b/pkg/rulemanager/cel/libraries/applicationprofile/ap.go @@ -130,6 +130,28 @@ func (l *apLibrary) Declarations() map[string][]cel.FunctionOpt { }), ), }, + // ap.was_path_opened_with_suffix and ap.was_path_opened_with_prefix + // — rule-author contract (CodeRabbit upstream PR #807 finding #7): + // + // These helpers answer "did any RECORDED concrete path open match + // this suffix/prefix?". When the profile-projection cache is in + // pass-through mode (no rule declared an Opens-projection slice, + // so cp.Opens.All == true), wildcard patterns in cp.Opens.Patterns + // are NOT scanned via string-level HasSuffix/HasPrefix because the + // pattern text contains '*' / '⋯' tokens whose string shape doesn't + // safely answer suffix/prefix questions (see open.go comment). + // Concrete-only Values are scanned. + // + // False-negative gap: if a profile entry is `/var/log/pods/*/foo.log`, + // the runtime path `/var/log/pods/web-7d6f/volumes/foo.log` actually + // matches this pattern, but `was_path_opened_with_suffix("/foo.log")` + // returns FALSE because the pattern text doesn't end in `/foo.log` + // literally. Rule authors who need wildcard-aware coverage should + // either: (a) declare an Opens projection slice in the rule's + // ProfileDataRequired (then SuffixHits/PrefixHits become authoritative + // and the projector pre-computes the hit map for wildcard entries), + // or (b) use ap.was_path_opened(path) which DOES run dynamic-segment + // matching over Patterns via CompareDynamic. "ap.was_path_opened_with_suffix": { cel.Overload( "ap_was_path_opened_with_suffix", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType, diff --git a/pkg/rulemanager/cel/libraries/applicationprofile/open.go b/pkg/rulemanager/cel/libraries/applicationprofile/open.go index ec0a8310c..62a4abedf 100644 --- a/pkg/rulemanager/cel/libraries/applicationprofile/open.go +++ b/pkg/rulemanager/cel/libraries/applicationprofile/open.go @@ -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,17 +112,21 @@ 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 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). for openPath := range cp.Opens.Values { if strings.HasSuffix(openPath, suffixStr) { return types.Bool(true) } } - for _, openPath := range cp.Opens.Patterns { - if strings.HasSuffix(openPath, suffixStr) { - return types.Bool(true) - } - } return types.Bool(false) } // Projection applied — SuffixHits is authoritative; absent key = undeclared. @@ -149,17 +160,18 @@ 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 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). 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) { - return types.Bool(true) - } - } return types.Bool(false) } // Projection applied — PrefixHits is authoritative; absent key = undeclared.