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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,4 @@ replace github.com/inspektor-gadget/inspektor-gadget => github.com/matthyx/inspe

replace github.com/cilium/ebpf => github.com/matthyx/ebpf v0.0.0-20260421101317-8a32d06def6c

replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05
replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260503184242-43795bb4f0b6
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -981,8 +981,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05 h1:RCEcduxCntYAuo8BleZu84Kk//X0gvsGrutQtdcLMn0=
github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/k8sstormcenter/storage v0.0.240-0.20260503184242-43795bb4f0b6 h1:pzIvtCkXBC6t4v7EIIekbltfBnWfvWKjB6ZsgdhkWr0=
github.com/k8sstormcenter/storage v0.0.240-0.20260503184242-43795bb4f0b6/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down
9 changes: 3 additions & 6 deletions pkg/rulemanager/cel/libraries/applicationprofile/exec.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package applicationprofile

import (
"slices"

"github.com/google/cel-go/common/types"

"github.com/google/cel-go/common/types/ref"
Expand All @@ -11,6 +9,7 @@ import (
"github.com/kubescape/node-agent/pkg/rulemanager/cel/libraries/cache"
"github.com/kubescape/node-agent/pkg/rulemanager/cel/libraries/celparse"
"github.com/kubescape/node-agent/pkg/rulemanager/profilehelper"
"github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector"
)

func (l *apLibrary) wasExecuted(containerID, path ref.Val) ref.Val {
Expand Down Expand Up @@ -85,10 +84,8 @@ func (l *apLibrary) wasExecutedWithArgs(containerID, path, args ref.Val) ref.Val
}

for _, exec := range cp.Spec.Execs {
if exec.Path == pathStr {
if slices.Compare(exec.Args, celArgs) == 0 {
return types.Bool(true)
}
if exec.Path == pathStr && dynamicpathdetector.CompareExecArgs(exec.Args, celArgs) {
return types.Bool(true)
}
}

Expand Down
130 changes: 130 additions & 0 deletions pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,136 @@ func TestExecWithArgsNoProfile(t *testing.T) {
assert.False(t, actualResult, "ap.was_executed_with_args should return false when no profile is available")
}

// TestExecWithArgsWildcardInProfile exercises wildcard tokens inside a
// user-defined ApplicationProfile's exec arg vector:
//
// "⋯" (DynamicIdentifier) — matches exactly one argument position.
// "*" (WildcardIdentifier) — matches zero or more consecutive args.
//
// The runtime exec arg vector is matched against the profile via
// dynamicpathdetector.CompareExecArgs.
func TestExecWithArgsWildcardInProfile(t *testing.T) {
objCache := objectcachev1.RuleObjectCacheMock{
ContainerIDToSharedData: maps.NewSafeMap[string, *objectcache.WatchedContainerData](),
}

objCache.SetSharedContainerData("test-container-id", &objectcache.WatchedContainerData{
ContainerType: objectcache.Container,
ContainerInfos: map[objectcache.ContainerType][]objectcache.ContainerInfo{
objectcache.Container: {
{
Name: "test-container",
},
},
},
})

profile := &v1beta1.ApplicationProfile{}
profile.Spec.Containers = append(profile.Spec.Containers, v1beta1.ApplicationProfileContainer{
Name: "test-container",
Execs: []v1beta1.ExecCalls{
// curl any URL: --user must be literal, value is one position.
{
Path: "/usr/bin/curl",
Args: []string{"--user", "⋯"},
},
// sh -c with any trailing payload (zero or more args).
{
Path: "/bin/sh",
Args: []string{"-c", "*"},
},
// ls -l in any directory — single trailing position.
{
Path: "/bin/ls",
Args: []string{"-l", "⋯"},
},
// echo with any number of greeting words after a literal anchor.
{
Path: "/bin/echo",
Args: []string{"hello", "*"},
},
},
Comment on lines +329 to +350
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 | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find profile arg patterns where "*" appears before a trailing literal anchor.
rg -nP 'Args:\s*\[]string\{[^}]*"\*"\s*,\s*"[^"]+"' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 51


🏁 Script executed:

# Check if the file exists and get basic info
wc -l pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 135


🏁 Script executed:

# Read the relevant test sections mentioned in the review (lines 329-350 and 364-390)
sed -n '320,420p' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 3418


🏁 Script executed:

# Find the implementation of the matcher to understand wildcard behavior
fd -i "exec" pkg/rulemanager/cel/libraries/applicationprofile/ -type f | grep -v test | head -5

Repository: k8sstormcenter/node-agent

Length of output: 240


🏁 Script executed:

# Find the implementation file that contains the matching logic
fd "exec" pkg/rulemanager/cel/libraries/applicationprofile/ -not -name "*test*" -type f

Repository: k8sstormcenter/node-agent

Length of output: 300


🏁 Script executed:

# Check the implementation to understand wildcard matching behavior
find pkg/rulemanager/cel/libraries/applicationprofile/ -name "*.go" -not -name "*test*" | head -5

Repository: k8sstormcenter/node-agent

Length of output: 358


🏁 Script executed:

# Read the exec.go implementation to understand wildcard matching semantics
cat -n pkg/rulemanager/cel/libraries/applicationprofile/exec.go

Repository: k8sstormcenter/node-agent

Length of output: 8561


🏁 Script executed:

# Find the dynamicpathdetector implementation
fd "dynamicpathdetector" --type f

Repository: k8sstormcenter/node-agent

Length of output: 51


🏁 Script executed:

# Search for CompareExecArgs function
rg "func.*CompareExecArgs" -A 30

Repository: k8sstormcenter/node-agent

Length of output: 51


🏁 Script executed:

# Check if there's any documentation or references to wildcard behavior
rg "wildcard|re-anchor|\*" pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 1114


🏁 Script executed:

# Read the full test structure more carefully around the test cases
sed -n '302,419p' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 4229


🏁 Script executed:

# Search for any documentation about wildcard re-anchoring behavior
rg "re-anchor|anchor" pkg/rulemanager/cel/libraries/applicationprofile/

Repository: k8sstormcenter/node-agent

Length of output: 361


🏁 Script executed:

# Look for test comments explaining the matching contract
rg -B5 -A5 "contract|behavior|semantics" pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 51


🏁 Script executed:

# Check if there are any other test files that might cover this pattern
find . -name "*test.go" -path "*/applicationprofile/*" | xargs grep -l "wildcard\|\*"

Repository: k8sstormcenter/node-agent

Length of output: 543


🏁 Script executed:

# Check the entire exec_test.go to understand all test cases and verify the structure
wc -l pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go && sed -n '385,410p' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 1012


🏁 Script executed:

# Verify if there's any evidence of CompareExecArgs implementation elsewhere
rg "CompareExecArgs" --type go -A 50 | head -100

Repository: k8sstormcenter/node-agent

Length of output: 8958


🏁 Script executed:

# Look for the dynamicpathdetector package location
find . -name "dynamicpathdetector*" -o -name "*pathdetector*" 2>/dev/null | head -20

Repository: k8sstormcenter/node-agent

Length of output: 51


🏁 Script executed:

# Check imports in exec.go to find the full import path for dynamicpathdetector
grep -n "import\|dynamicpathdetector" pkg/rulemanager/cel/libraries/applicationprofile/exec.go | head -20

Repository: k8sstormcenter/node-agent

Length of output: 243


🏁 Script executed:

# Search for any references to how wildcard matching should work
rg "WildcardIdentifier|DynamicIdentifier" -B2 -A2 pkg/rulemanager/cel/libraries/applicationprofile/

Repository: k8sstormcenter/node-agent

Length of output: 705


🏁 Script executed:

# Verify the exact line numbers mentioned in the review
sed -n '329,350p' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 623


🏁 Script executed:

# Check the "Also applies to" lines 364-390
sed -n '364,390p' pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go

Repository: k8sstormcenter/node-agent

Length of output: 1502


Add an explicit mid-profile * re-anchoring test case.

Current test coverage validates trailing * patterns well, but there's no explicit scenario where * must re-anchor to a later literal token. Adding one positive+negative pair will lock in the anchored behavior contract.

Proposed test additions
@@
 		Execs: []v1beta1.ExecCalls{
@@
 			// echo with any number of greeting words after a literal anchor.
 			{
 				Path: "/bin/echo",
 				Args: []string{"hello", "*"},
 			},
+			// wildcard in the middle must re-anchor to trailing literal.
+			{
+				Path: "/usr/bin/env",
+				Args: []string{"A", "*", "Z"},
+			},
 		},
 	})
@@
 		// echo hello *
 		{"echo hello world from test", "/bin/echo", []string{"hello", "world", "from", "test"}, true},
 		{"echo hello (no trailing args)", "/bin/echo", []string{"hello"}, true},
 		{"echo goodbye world — wrong literal anchor", "/bin/echo", []string{"goodbye", "world"}, false},
+
+		// mid-profile * re-anchoring
+		{"env A x y Z — wildcard re-anchors to trailing literal", "/usr/bin/env", []string{"A", "x", "y", "Z"}, true},
+		{"env A x y — missing trailing literal anchor", "/usr/bin/env", []string{"A", "x", "y"}, false},
 	}

Also applies to: 364-390

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go` around lines
329 - 350, Add a positive+negative test pair that exercises mid-profile "*"
re-anchoring in the Execs test table: within the Execs slice (type
v1beta1.ExecCalls) add an entry where Args contains a literal token, then "*"
and then another literal token (e.g., Args: []string{"cmd", "*", "end"}) and
assert a match for a command whose args have arbitrary middle tokens but end
with the trailing literal (positive), and a non-match for a command where the
later literal is missing or not anchored (negative). Update the corresponding
expectations in the test (same table near the existing curl/sh/ls/echo entries
in exec_test.go) so the test validates that "*" can re-anchor to a later literal
token between anchors.

})
objCache.SetApplicationProfile(profile)

env, err := cel.NewEnv(
cel.Variable("containerID", cel.StringType),
cel.Variable("path", cel.StringType),
cel.Variable("args", cel.ListType(cel.StringType)),
AP(&objCache, config.Config{}),
)
if err != nil {
t.Fatalf("failed to create env: %v", err)
}

testCases := []struct {
name string
path string
args []string
expectedResult bool
}{
// curl with --user, dynamic value
{"curl --user alice — ⋯ matches one arg", "/usr/bin/curl", []string{"--user", "alice"}, true},
{"curl --user alice bob — extra arg, ⋯ rejects", "/usr/bin/curl", []string{"--user", "alice", "bob"}, false},
{"curl --user — missing value, ⋯ requires one arg", "/usr/bin/curl", []string{"--user"}, false},
{"curl --pass alice — literal mismatch", "/usr/bin/curl", []string{"--pass", "alice"}, false},

// sh -c with arbitrary trailing payload
{"sh -c with single command", "/bin/sh", []string{"-c", "echo hi"}, true},
{"sh -c with multi-token command", "/bin/sh", []string{"-c", "while", "true;", "do", "sleep", "1;", "done"}, true},
{"sh -c with no trailing args (* matches zero)", "/bin/sh", []string{"-c"}, true},
{"sh -x — wrong flag", "/bin/sh", []string{"-x", "echo hi"}, false},

// ls -l in any directory
{"ls -l /var/log", "/bin/ls", []string{"-l", "/var/log"}, true},
{"ls -l with no directory (⋯ requires one)", "/bin/ls", []string{"-l"}, false},

// echo hello *
{"echo hello world from test", "/bin/echo", []string{"hello", "world", "from", "test"}, true},
{"echo hello (no trailing args)", "/bin/echo", []string{"hello"}, true},
{"echo goodbye world — wrong literal anchor", "/bin/echo", []string{"goodbye", "world"}, false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ast, issues := env.Compile(`ap.was_executed_with_args(containerID, path, args)`)
if issues != nil {
t.Fatalf("failed to compile expression: %v", issues.Err())
}

program, err := env.Program(ast)
if err != nil {
t.Fatalf("failed to create program: %v", err)
}

result, _, err := program.Eval(map[string]interface{}{
"containerID": "test-container-id",
"path": tc.path,
"args": tc.args,
})
if err != nil {
t.Fatalf("failed to eval program: %v", err)
}

actualResult := result.Value().(bool)
assert.Equal(t, tc.expectedResult, actualResult,
"profile %v vs runtime args %v: got %v, want %v",
profileArgsForPath(profile, tc.path), tc.args, actualResult, tc.expectedResult)
})
}
}

func profileArgsForPath(p *v1beta1.ApplicationProfile, path string) []string {
for _, c := range p.Spec.Containers {
for _, e := range c.Execs {
if e.Path == path {
return e.Args
}
}
}
return nil
}

func TestExecWithArgsCompilation(t *testing.T) {
objCache := objectcachev1.RuleObjectCacheMock{}

Expand Down
Loading