Skip to content

Addon to PR 323: Moving Exec Args from NA to Storage - #328

Merged
matthyx merged 4 commits into
kubescape:mainfrom
k8sstormcenter:upstream-pr/sbob-execs
Jun 25, 2026
Merged

matthyx merged 4 commits into
kubescape:mainfrom
k8sstormcenter:upstream-pr/sbob-execs

Conversation

@entlein

@entlein entlein commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

currently kubescape/node-agent#805 is built against my mirror of this branch, and the test results and benchmarks are at the bottom.

There are edits to outdated comments, the only relevant part of this PR is moving this function from NA to storage to avoid duplication of logic.

func argMatches(profileArg, runtimeArg string) bool {
	if profileArg == DynamicIdentifier {
		return true
	}
	if strings.Contains(profileArg, DynamicIdentifier) || strings.Contains(profileArg, WildcardIdentifier) {
		return CompareDynamic(profileArg, runtimeArg)
	}
	return profileArg == runtimeArg
}

Summary by CodeRabbit

  • Refactor

    • Improved exec-argument matching to more clearly separate constraint handling from wildcard/token matching, with stricter anchored behavior when required.
  • Tests

    • Added coverage for embedded dynamic/wildcard matching, literal * staying literal, and boundary/adversarial cases to prevent unintended matches.
    • Added property-style and regression-style test suites plus a new benchmark for exec-arg matcher performance.
  • Documentation

    • Removed upstream attribution from inline notes and clarified wildcard semantics and matching contracts.

…ow various wildcards

Signed-off-by: entlein <einentlein@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors exec-argument token matching in the dynamic path detector by centralizing matching logic into new helper functions, adds comprehensive test coverage for embedded wildcard/dynamic tokens and differential verification, and removes upstream PR attribution from documentation throughout the package.

Changes

Dynamic Path Detector - Exec Args Token Matching and Package Documentation

Layer / File(s) Summary
Exec args token matching refactor
pkg/registry/file/dynamicpathdetector/compare_exec_args.go
New argMatches helper centralizes token-to-arg matching: bare DynamicIdentifier matches any single arg, tokens containing DynamicIdentifier delegate to matchExecArgSegments for segment-wise comparison (splitting on / with equal counts and DynamicIdentifier matching exactly one segment), others use literal equality. Strict matcher base-case memoization clarifies that profile-fully-consumed success requires runtime consumption. Non-wildcard head failure is explicit when runtime args exhausted. Documentation updates clarify args-required vs empty-profile constraint handling.
Exec args comprehensive test coverage
pkg/registry/file/dynamicpathdetector/tests/compare_exec_args_test.go, execargs_wildcard_ap_test.go (new), glob_semantics_demo_test.go (new), matchexecargs_property_test.go (new), compare_dynamic_adversarial_test.go (new), benchmark_test.go
Five new test files provide differential verification against naive oracle implementation, contract invariants validation, adversarial boundary case coverage, integration tests with real ApplicationProfile objects, and glob semantics demonstrations. Existing compare_exec_args_test.go is expanded with embedded ⋯/* path-token validation (postgres versioned paths, edge positioning, literal enforcement), literal * non-broadening coverage, adjacent dynamic identifier matching, four-state (ArgsRequired, profileArgs) contract with named struct fields and explicit empty-vs-nil behavior, and reworded wildcard semantics using ExecArgsWildcard constant. Benchmark additions include BenchmarkMatchExecArgs with hot-path and stress scenarios.
Core module and test suite documentation cleanup
pkg/registry/file/dynamicpathdetector/analyze_endpoints.go, analyze_opens.go, analyzer.go, types.go, tests/analyze_endpoints_internal_test.go, tests/analyze_endpoints_test.go, tests/analyze_opens_test.go, tests/benchmark_test.go, tests/compare_dynamic_memoise_test.go, tests/consolidate_opens_test.go, tests/coverage_test.go, tests/profile_test.go
Removes specific upstream PR/author attributions from comments, skip messages, and assertion failure text while preserving all logic and assertions. Rephrases documentation to clarify endpoint key format derivation via buildEndpointKey, prefix-trie lookup intent, prefix boundary guard "falls through" semantics, * dispatch hot-path performance/allocation characteristics, countStarSegments wildcard vs embedded distinction, DynamicIdentifier single-segment matching, DefaultCollapseConfig fallback role and mutation safety, memoization contracts, CI-flakiness mitigation, and allocation expectations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • kubescape/storage#322: Directly extends the same exec-arg matching logic modified in this PR to support wildcards in exec args through the refactored token-matching semantics and new ExecArgsWildcard constant definitions.

Suggested labels

release

Suggested reviewers

  • matthyx

Poem

🐰 A rabbit hops through exec args deep,
Wildcards embedded now match just right,
Tokens centralized, cleaner to keep,
Documentation scrubbed—no PR in sight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title references a prior PR (323) but does not clearly specify what the primary change is. The phrase 'Moving Exec Args from NA to Storage' is somewhat vague regarding the specific functional change. Consider clarifying the title to better describe the main change, such as 'Add exec arguments matching logic to dynamicpathdetector' or 'Consolidate exec args matching from node-agent'. This would make the primary objective clearer for future reference.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the relocation of the exec-arg matcher into storage. The refactor reads clean, build is green, and go test ./pkg/registry/file/dynamicpathdetector/... passes. The doc-comment cleanup is fine. One blocker to resolve before merge, plus a nit.

Blocker — argMatches reinterprets literal * / ⋯ in argv as wildcards

compare_exec_args.go:

if strings.Contains(profileArg, DynamicIdentifier) || strings.Contains(profileArg, WildcardIdentifier) {
    return CompareDynamic(profileArg, runtimeArg)
}

The strings.Contains gate can't tell an analyzer-emitted wildcard token from a * that a process was genuinely invoked with. Any profile arg in which * (or ⋯) lands on a /-delimited segment boundary now matches via CompareDynamic instead of literal equality, so the anchored match silently broadens. Confirmed against this branch:

  • CompareExecArgs(["/bin/*"], ["/bin/ls"]) → true (was literal-only: a profile that recorded the literal arg /bin/* now matches /bin/<anything>)
  • CompareExecArgs(["cmd","/var/log/*/a"], ["cmd","/var/log/X/a"]) → true

(An embedded * that is not a whole segment, e.g. echo *, still compares literally, so the divergence is specifically the segment-boundary * / trailing /* case.)

For a detection matcher this is a false-negative direction: a profiled exec entry matches more runtime argv vectors than before, so a malicious exec that happens to fall under a literal-glob arg won't trip R0040. The argMatches doc asserts "any * seen is embedded in a path token (the form the analyzer emits)" — that assumption holds for analyzer output but not for user-authored profiles or recorded literal-glob args, and there's no escaping (\* is noted as a v0.0.2 future).

Can you confirm whether this matches node-agent's current behavior (i.e. this is intended parity, not a new broadening), and whether literal-* exec args are considered out of scope? If it's intended, a one-line note on argMatches stating literal */⋯ in argv are deliberately treated as wildcards would close it. If not, the gate needs to distinguish a real token from a literal character.

Nit

analyzer.go hasPrefixAtBoundary: the reflowed empty-prefix comment now breaks mid-sentence ("falls through to" / "pathPrefix[0] == '/'") and reads awkwardly — worth re-wrapping.

@matthyx matthyx moved this to Needs Reviewer in KS PRs tracking Jun 5, 2026
@entlein

entlein commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

New plan, we use a different non-overloaded character. I ll next update NA 805 and then ... we retest it all :)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
pkg/registry/file/dynamicpathdetector/tests/matchexecargs_property_test.go (1)

97-119: ⚡ Quick win

Bound default runtime of the exhaustive differential test.

This loop currently checks 2801 × 2801 = 7,845,601 pairs, each with a naive recursive oracle call. Great for deep validation, but expensive for routine CI. Consider a smaller default bound and an opt-in exhaustive mode.

Suggested change
 import (
+	"os"
 	"strings"
 	"testing"
@@
 func TestMatchExecArgs_DifferentialAgainstNaiveOracle(t *testing.T) {
 	profAlphabet := []string{"a", "b", "*", dp.ExecArgsWildcard, dp.DynamicIdentifier, "/x/" + dp.DynamicIdentifier, "/x/*"}
 	runAlphabet := []string{"a", "b", "c", "*", "/x/y", "/x/y/z", "/x/*"}
 
-	profiles := enumerate(profAlphabet, 4)
-	runtimes := enumerate(runAlphabet, 4)
+	maxLen := 3
+	if os.Getenv("DP_EXHAUSTIVE_EXECARGS") == "1" {
+		maxLen = 4
+	}
+	profiles := enumerate(profAlphabet, maxLen)
+	runtimes := enumerate(runAlphabet, maxLen)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/registry/file/dynamicpathdetector/tests/matchexecargs_property_test.go`
around lines 97 - 119, The TestMatchExecArgs_DifferentialAgainstNaiveOracle
function currently enumerates profiles and runtimes with depth 4, creating over
7.8 million test pairs which is expensive for routine CI. Reduce the default
enumeration depth to a smaller value (such as 2 or 3) to speed up the test, and
add a conditional check using testing.Short() to enable the exhaustive depth-4
enumeration only when opt-in exhaustive testing is desired (i.e., when not
running in short mode). This way the function will run quickly by default but
allow for deep validation when explicitly requested.
pkg/registry/file/dynamicpathdetector/tests/compare_exec_args_test.go (1)

464-470: ⚡ Quick win

Reduce wall-clock flake risk in the ReDoS regression assertion.

Line 468’s fixed 100ms budget is likely to flap on contended runners even when behavior is correct. Consider a looser budget (or benchmark-only enforcement) so this remains a semantic regression test, not a scheduler-speed test.

Suggested change
-	if elapsed > 100*time.Millisecond {
+	const redosBudget = 500 * time.Millisecond
+	if elapsed > redosBudget {
 		t.Errorf("matcher took %v on adversarial input — memoisation regression?", elapsed)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/registry/file/dynamicpathdetector/tests/compare_exec_args_test.go` around
lines 464 - 470, The fixed 100ms timeout in the elapsed time check is causing
test flakiness on contended runners due to scheduler variability. To fix this,
increase the time budget significantly (e.g., to 500ms or 1s) to account for
system contention while still catching actual performance regressions in the
memoised matcher logic, or alternatively move this timing assertion to a
benchmark test instead where it can be properly controlled. Update the condition
comparing elapsed time and the corresponding error message to reflect the looser
budget or remove the wall-clock timing check entirely from the test if moving to
a benchmark-based approach.
pkg/registry/file/dynamicpathdetector/tests/glob_semantics_demo_test.go (1)

17-20: ⚡ Quick win

Align opens * comments with actual trailing-star semantics.

The comments currently describe * as uniformly “zero-or-more,” but in pkg/registry/file/dynamicpathdetector/analyzer.go (compareSegmentsIndex), trailing * matches one-or-more remaining segments. This demo text should reflect that distinction to avoid misleading future test authors.

Suggested comment fix
-//     "*"  matches ZERO-OR-MORE path segments
+//     "*"  matches ZERO-OR-MORE path segments in the middle, and
+//          ONE-OR-MORE segments when trailing
@@
-		"/var/log/*",            // zero-or-more trailing segments
+		"/var/log/*",            // one-or-more trailing segments

Also applies to: 39-40

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/registry/file/dynamicpathdetector/tests/glob_semantics_demo_test.go`
around lines 17 - 20, The comments describing the `*` wildcard semantics in the
test file incorrectly state that `*` matches "ZERO-OR-MORE" path segments, but
the actual implementation in the `compareSegmentsIndex` function in the analyzer
module shows that a trailing `*` matches one-or-more remaining segments. Update
the comment text to accurately reflect that trailing `*` matches one-or-more
segments rather than zero-or-more, ensuring the documentation aligns with the
actual implementation behavior and prevents misleading future readers of these
test semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/registry/file/dynamicpathdetector/tests/compare_exec_args_test.go`:
- Around line 464-470: The fixed 100ms timeout in the elapsed time check is
causing test flakiness on contended runners due to scheduler variability. To fix
this, increase the time budget significantly (e.g., to 500ms or 1s) to account
for system contention while still catching actual performance regressions in the
memoised matcher logic, or alternatively move this timing assertion to a
benchmark test instead where it can be properly controlled. Update the condition
comparing elapsed time and the corresponding error message to reflect the looser
budget or remove the wall-clock timing check entirely from the test if moving to
a benchmark-based approach.

In `@pkg/registry/file/dynamicpathdetector/tests/glob_semantics_demo_test.go`:
- Around line 17-20: The comments describing the `*` wildcard semantics in the
test file incorrectly state that `*` matches "ZERO-OR-MORE" path segments, but
the actual implementation in the `compareSegmentsIndex` function in the analyzer
module shows that a trailing `*` matches one-or-more remaining segments. Update
the comment text to accurately reflect that trailing `*` matches one-or-more
segments rather than zero-or-more, ensuring the documentation aligns with the
actual implementation behavior and prevents misleading future readers of these
test semantics.

In `@pkg/registry/file/dynamicpathdetector/tests/matchexecargs_property_test.go`:
- Around line 97-119: The TestMatchExecArgs_DifferentialAgainstNaiveOracle
function currently enumerates profiles and runtimes with depth 4, creating over
7.8 million test pairs which is expensive for routine CI. Reduce the default
enumeration depth to a smaller value (such as 2 or 3) to speed up the test, and
add a conditional check using testing.Short() to enable the exhaustive depth-4
enumeration only when opt-in exhaustive testing is desired (i.e., when not
running in short mode). This way the function will run quickly by default but
allow for deep validation when explicitly requested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d52bd55-70b7-4a51-9e37-612e841b5d01

📥 Commits

Reviewing files that changed from the base of the PR and between 787cc43 and 35603ac.

📒 Files selected for processing (9)
  • pkg/registry/file/dynamicpathdetector/analyzer.go
  • pkg/registry/file/dynamicpathdetector/compare_exec_args.go
  • pkg/registry/file/dynamicpathdetector/tests/benchmark_test.go
  • pkg/registry/file/dynamicpathdetector/tests/compare_dynamic_adversarial_test.go
  • pkg/registry/file/dynamicpathdetector/tests/compare_exec_args_test.go
  • pkg/registry/file/dynamicpathdetector/tests/execargs_wildcard_ap_test.go
  • pkg/registry/file/dynamicpathdetector/tests/glob_semantics_demo_test.go
  • pkg/registry/file/dynamicpathdetector/tests/matchexecargs_property_test.go
  • pkg/registry/file/dynamicpathdetector/types.go
✅ Files skipped from review due to trivial changes (3)
  • pkg/registry/file/dynamicpathdetector/tests/compare_dynamic_adversarial_test.go
  • pkg/registry/file/dynamicpathdetector/analyzer.go
  • pkg/registry/file/dynamicpathdetector/types.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/registry/file/dynamicpathdetector/tests/benchmark_test.go
  • pkg/registry/file/dynamicpathdetector/compare_exec_args.go

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 35603ac. The blocker is resolved — thanks for the thorough rework.

The exec matcher no longer overloads *. ⋯ (one arg / one segment) and the new ExecArgsWildcard ⋯⋯ (zero-or-more whole args) are dedicated sentinels, and argMatches now treats * as literal data. matchExecArgSegments does its own equal-count, literal-segment compare instead of delegating to CompareDynamic, so the file-path * semantics no longer leak into argv matching. Verified the previously over-broad cases are now literal:

  • CompareExecArgs(["/bin/*"], ["/bin/ls"]) → false (matches only literal /bin/*)
  • CompareExecArgs(["/plugins/*"], ["/plugins/evil.so"]) → false
  • CompareExecArgs([..,"/var/log/*/a"], [..,"/var/log/X/a"]) → false

…while the intended ⋯ path-token matching still works (/usr/lib/postgresql/⋯/bin/postgres matches the versioned runtime path and rejects an extra segment). Build is green and the full package test suite passes, including the new property / AP / glob-semantics tests. The comment-attribution nit is cleaned up too.

One coordination note (not blocking this PR): since * is now data and ⋯⋯ is the positional zero-or-more sentinel, node-agent (#805) must emit ⋯⋯ — not * — when it generates the "zero-or-more args" form, or learned profiles won't match. Worth a line in the NA PR description so the two land together.

LGTM on the storage side once #805 is aligned.

@entlein

entlein commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor Author

One coordination note (not blocking this PR): since * is now data and ⋯⋯ is the positional zero-or-more sentinel, node-agent (#805) must emit ⋯⋯ — not * — when it generates the "zero-or-more args" form, or learned profiles won't match. Worth a line in the NA PR description so the two land together.

I dont think so , node-agent was never supposed to pre-aggregate the APs into ⋯⋯ , or did I misunderstand? The strange new character is only supposed to be externally supplied and now correctly used in comparison-statements, not created by the learnt profiles.
I ll recheck, but that was kinda the whole reason not to use the * character

I m assuming the ⋯⋯ is from an out-of-band analysis, where a user explicitely states that some executable is intended to do have any argument.

@matthyx
matthyx merged commit 80a921e into kubescape:main Jun 25, 2026
7 checks passed
@matthyx matthyx moved this from Needs Reviewer to To Archive in KS PRs tracking Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants