Skip to content

tier-2 (execs): exec.go exact-path args constraint authoritative (no fall-through)#49

Open
entlein wants to merge 2 commits into
mainfrom
release/v0.3.26/tier-2-execs
Open

tier-2 (execs): exec.go exact-path args constraint authoritative (no fall-through)#49
entlein wants to merge 2 commits into
mainfrom
release/v0.3.26/tier-2-execs

Conversation

@entlein
Copy link
Copy Markdown

@entlein entlein commented May 20, 2026

Tier 2 of the v0.3.26 release chain

Execs layer. Branches off node-agent#48 (tier-1 path+opens) and pairs with storage#37 (release/v0.0.240/tier-2-execs).

Addresses the CodeRabbit PR #43 Major finding on pkg/rulemanager/cel/libraries/applicationprofile/exec.go:144, deferred from the NA #46 batch-1 PR body:

exec.go:144 — Exact path match should return false when args don't match, not fall through to pattern matching.

Problem

Pre-fix wasExecutedWithArgs flow on exact-path hit:

if pathStr in cp.Execs.Values {
    if pathStr in cp.ExecsByPath {
        if CompareExecArgs(...) { return true }
        // else: FALL THROUGH to pattern matching <-- bug
    } else {
        return true  // back-compat no-constraint
    }
}
for pattern in cp.Execs.Patterns { ... }

The fall-through on CompareExecArgs == false let a LOOSER pattern (e.g. /usr/bin/⋯ with profileArgs [*]) shadow the exact-path entry's strict args constraint. A rule-author who lists {Path: /usr/bin/curl, Args: [-X, GET]} expects ONLY GET requests to pass — but if their profile also contains a permissive /usr/bin/⋯ pattern, the fall-through silently allows DELETE/POST via the pattern.

Fix

Exact-path-with-constraint is producer-authored authoritative. Its match result IS the answer for this path — no fall-through:

if pathStr in cp.Execs.Values {
    if pathStr in cp.ExecsByPath {
        return CompareExecArgs(profileArgs, runtimeArgs)
    }
    return true  // back-compat no-constraint
}
// path NOT in Values — try Patterns
for pattern in cp.Execs.Patterns { ... }

Pattern-loop fall-through stays as-is — different patterns CAN encode different (path, args) shapes and a non-matching one shouldn't kill the loop. The asymmetry is intentional.

Local benchmarks (-count=3 -benchtime=200ms)

Case ns/op B/op allocs/op
exact_path_args_match ~890 2136 20
exact_path_args_mismatch ~860 2136 20
pattern_path_args_match ~450 360 13

exact_path_args_mismatch is no longer paying the pattern-loop cost it used to — AND no longer returns the wrong answer via Pattern shadow. The 2136 B / 20 allocs/op floor is pre-existing CEL value-wrapping; matcher logic itself didn't add allocations.

Companion storage tier-2 (#37) ships zero-alloc CompareExecArgs dispatch — applies to the same R0040 hot path this matcher consumes.

Reproduce:

git worktree add /tmp/wt-na-tier2 release/v0.3.26/tier-2-execs
cd /tmp/wt-na-tier2
go test -run=^$ -bench='BenchmarkWasExecutedWithArgs' -benchmem -count=3 -benchtime=200ms ./pkg/rulemanager/cel/libraries/applicationprofile/...

Component test evidence

Run 26152411366 (Component Tests on release/v0.3.26/tier-2-execs with STORAGE_TAG=tier2 + STORAGE_REF=2db8af1e):

  • 27/29 component matrix legs green — includes:
    • Test_32_UnexpectedProcessArguments ✅ — directly exercises wasExecutedWithArgs (the matcher this PR fixes)
    • Test_27_ApplicationProfileOpens
    • Test_33_AnalyzeOpensWildcardAnchoring
    • Test_29_SignedApplicationProfile + Test_30_TamperedSignedProfiles + Test_31_TamperDetectionAlert
  • 2 failures: Test_09_FalsePositiveTest + Test_10_MalwareDetectionTestpre-existing on main (same legs were red on tier-1 CT 26121369338 and on post-PR#43 main CT 25969722538), not introduced by this PR
  • 1 workflow-plumbing trigger-integration-tests failure — same auto-trigger branch-name issue addressed in storage#36 (not a code issue)

Tier-1 dependencies still in effect

This branch builds on top of tier-1 path-opens (#48). The narrower-fallback Patterns scan for was_path_opened_with_suffix/_prefix is included; Test_27 + Test_33 green confirm no regression.

Test plan

  • go build ./... clean
  • go test ./pkg/rulemanager/cel/libraries/applicationprofile/... -count=1 green
  • BenchmarkWasExecutedWithArgs runs clean (table above)
  • Component tests 27/29 green on paired storage tier-2 (run 26152411366)
  • Test_32_UnexpectedProcessArguments green — the most relevant component-test leg for this fix

Entlein added 2 commits May 19, 2026 21:31
…true)

Addresses Matthias's upstream PR kubescape#811 blocking review (2026-05-19T10:35Z):

  Blocking: projectField() still keeps dynamic open paths only in
  cp.Opens.Patterns when cp.Opens.All == true. Removing the Patterns
  scan here makes ap.was_path_opened_with_suffix() / _with_prefix()
  return false for dynamic entries in pass-through mode, so rules
  that omit profileDataRequired.opens regress. If the old
  strings.HasPrefix/HasSuffix behavior on pattern text is too
  permissive, we still need a narrower fallback here instead of
  ignoring Patterns entirely.

Context
-------

Pre-PR-kubescape#811, CodeRabbit (PR #43 review on open.go:79) flagged that
running strings.HasSuffix("/var/log/pods/*/volumes/...", "foo.log")
returns false even though the pattern's concrete realisations might
end in "foo.log" — a false negative. We addressed it by SKIPPING
the Patterns scan in the All=true branch. Matthias's review reverses
that: skipping is too aggressive and silently regresses rules that
omit profileDataRequired.opens (which rely on the All=true pass-through
behavior).

Fix
---

Narrower fallback — when Opens.All == true and the Values scan didn't
match, scan Patterns with split semantics:

  patternConcreteSuffix(p)  → literal text after the LAST wildcard
                              segment in p; "" if p ends in a wildcard
                              segment.
  patternConcretePrefix(p)  → literal text before the FIRST wildcard
                              segment in p; "" if p starts with a
                              wildcard segment.

Suffix/prefix matcher then:
  for each pattern:
    tail/head := patternConcrete{Suffix,Prefix}(pattern)
    if tail/head == "":
        return true   // wildcard tail/head → permissive (concrete
                      // realisations could match ANY suffix/prefix)
    if strings.HasSuffix/HasPrefix(tail/head, query):
        return true

Boundary slashes are KEPT in the returned concrete suffix/prefix so
queries with leading/trailing slashes match correctly (every concrete
realisation has the boundary slash too).

Tests
-----

Updated previous "PatternsNotScanned" tests (which pinned the
pre-kubescape#811 CR fix) to reflect Matthias's new contract:

  TestWasPathOpenedWithSuffix_PatternsScannedWithConcreteTail
    pattern "/var/log/⋯/foo.log", query ".log"   → true (tail "/foo.log" has suffix ".log")
    pattern "/var/log/⋯/foo.log", query ".txt"   → false

  TestWasPathOpenedWithSuffix_PatternWildcardTail_Permissive
    pattern "/var/log/pods/*", any suffix         → true

  TestWasPathOpenedWithPrefix_PatternsScannedWithConcreteHead
    pattern "/var/⋯/log/foo", query "/var/"      → true (head "/var/" has prefix "/var/")
    pattern "/var/⋯/log/foo", query "/etc/"      → false

  TestWasPathOpenedWithPrefix_PatternWildcardHead_Permissive
    pattern "*/run", any prefix                   → true

  TestPatternConcreteSuffix_AndPrefix
    pins the splitter helpers against 7 representative shapes
    (no_wildcards, trailing_star, leading_star, mid_ellipsis,
    both_mid, lone_star, lone_ellipsis).

Benchmarks (new file: open_bench_test.go)
-----------------------------------------

Helper itself — zero allocation across all shapes:

  BenchmarkPatternConcreteSuffix//var/log/⋯/foo.log   11.5 ns/op  0 B/op  0 allocs/op
  BenchmarkPatternConcreteSuffix//var/log/pods/*      10.0 ns/op  0 B/op  0 allocs/op
  BenchmarkPatternConcreteSuffix//var/log/foo.log      9.4 ns/op  0 B/op  0 allocs/op
  BenchmarkPatternConcreteSuffix/*                     1.6 ns/op  0 B/op  0 allocs/op
  BenchmarkPatternConcreteSuffix//var/⋯/log/⋯/foo.log 14.8 ns/op  0 B/op  0 allocs/op

Full matcher (50 Values + 10 Patterns) — the Patterns scan adds
~20 ns on top of the Values-only path with no extra allocations
(the steady 56 B/op / 3 allocs/op is pre-existing CEL value-wrapping
cost, unrelated to this change):

  BenchmarkWasPathOpenedWithSuffix_AllMode/values_only         372 ns/op  56 B/op  3 allocs/op
  BenchmarkWasPathOpenedWithSuffix_AllMode/patterns_concrete   391 ns/op  56 B/op  3 allocs/op
  BenchmarkWasPathOpenedWithSuffix_AllMode/patterns_wildcard   393 ns/op  56 B/op  3 allocs/op
  BenchmarkWasPathOpenedWithPrefix_AllMode/values_only         351 ns/op  56 B/op  3 allocs/op
  BenchmarkWasPathOpenedWithPrefix_AllMode/patterns_concrete   395 ns/op  56 B/op  3 allocs/op
  BenchmarkWasPathOpenedWithPrefix_AllMode/patterns_wildcard   428 ns/op  56 B/op  3 allocs/op
Addresses the CodeRabbit PR #43 Major finding on
`pkg/rulemanager/cel/libraries/applicationprofile/exec.go:144`,
deferred from the NA #46 batch-1 PR body. Tracked there as:

  exec.go:144 — Exact path match should return false when args don't
  match, not fall through to pattern matching.

Problem
-------

Pre-fix `wasExecutedWithArgs` flow on exact-path hit:

  if pathStr in cp.Execs.Values:
      if pathStr in cp.ExecsByPath:
          if CompareExecArgs(...) → return true
          // else: FALL THROUGH to pattern matching
      else:
          return true (back-compat no-constraint)
  for pattern in cp.Execs.Patterns:
      ...

The fall-through on `CompareExecArgs == false` would let a LOOSER
pattern (e.g. `/usr/bin/*` with profileArgs `[*]`) shadow the
exact-path entry's strict args constraint. A rule-author who lists
`{Path: /usr/bin/curl, Args: [-X, GET]}` expects that ONLY GET
requests pass — but if their profile also contains a permissive
`/usr/bin/⋯` pattern, the fall-through silently allows DELETE/POST/…
via the pattern.

Fix
---

When the exact path IS in Values AND ExecsByPath has an entry, the
constraint result IS the answer for this path. Return it directly —
no fall-through:

  if pathStr in cp.Execs.Values:
      if pathStr in cp.ExecsByPath:
          return CompareExecArgs(profileArgs, runtimeArgs)
      // else: back-compat no-constraint
      return true
  // path NOT in Values — try Patterns
  for pattern in cp.Execs.Patterns: ...

Pattern-loop fall-through stays as-is — different patterns CAN encode
different `(path, args)` shapes and a non-matching one shouldn't kill
the loop. The asymmetry is intentional: exact-path-with-constraint is
producer-authored authoritative, pattern-with-constraint is one
candidate among many.

Bench (`-count=3 -benchtime=200ms`)
-----------------------------------

  exact_path_args_match           ~890 ns/op  2136 B/op  20 allocs/op
  exact_path_args_mismatch        ~860 ns/op  2136 B/op  20 allocs/op
  pattern_path_args_match         ~450 ns/op   360 B/op  13 allocs/op

The exact-path mismatch case is no longer paying the pattern-loop
cost it used to (and used to silently return true via the pattern
shadow). The remaining alloc/op floor is pre-existing CEL value
wrapping (matcher unchanged).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@entlein has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 53 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cfd26a3c-acb8-415c-bade-05e3b5bbd0a1

📥 Commits

Reviewing files that changed from the base of the PR and between 58247d7 and 9e79d1b.

📒 Files selected for processing (5)
  • pkg/rulemanager/cel/libraries/applicationprofile/exec.go
  • pkg/rulemanager/cel/libraries/applicationprofile/exec_bench_test.go
  • pkg/rulemanager/cel/libraries/applicationprofile/open.go
  • pkg/rulemanager/cel/libraries/applicationprofile/open_bench_test.go
  • pkg/rulemanager/cel/libraries/applicationprofile/open_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v0.3.26/tier-2-execs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Performance Benchmark Results

Node-Agent Resource Usage
Metric BEFORE AFTER Delta
Avg CPU (cores) 0.224 0.209 -6.7%
Peak CPU (cores) 0.232 0.219 -5.7%
Avg Memory (MiB) 312.083 265.238 -15.0%
Peak Memory (MiB) 314.207 269.211 -14.3%
Dedup Effectiveness (AFTER only)
Event Type Passed Deduped Ratio
capabilities 1 0 0.0%
hardlink 6000 0 0.0%
http 1765 119395 98.5%
network 901 77925 98.9%
open 28986 622752 95.6%
symlink 6000 0 0.0%
syscall 978 1872 65.7%
Event Counters
Metric BEFORE AFTER
capability_counter 10 8
dns_counter 1441 1419
exec_counter 7232 0
network_counter 95062 93728
open_counter 785678 777108
syscall_counter 3652 3430

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant