Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/component-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ jobs:
Test_22_AlertOnPartialNetworkProfileTest,
Test_23_RuleCooldownTest,
Test_24_ProcessTreeDepthTest,
Test_27_ApplicationProfileOpens
Test_27_ApplicationProfileOpens,
Test_32_UnexpectedProcessArguments
]
steps:
- name: Checkout code
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ require (
github.com/joncrlsn/dque v0.0.0-20241024143830-7723fd131a64
github.com/kubescape/backend v0.0.39
github.com/kubescape/go-logger v0.0.32
github.com/kubescape/k8s-interface v0.0.213
github.com/kubescape/storage v0.0.278
github.com/kubescape/k8s-interface v0.0.214
github.com/kubescape/storage v0.0.287
github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf
github.com/moby/sys/mountinfo v0.7.2
github.com/oleiade/lane/v2 v2.0.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -883,10 +883,10 @@ github.com/kubescape/backend v0.0.39 h1:B1QRfKCSFlzuE+jWOnk/l7EpH71/Q3n14KKq0QSn
github.com/kubescape/backend v0.0.39/go.mod h1:cMEGP8cXUZgY89YU4GRBGIla9HZW7grZsUtlCwvZgAE=
github.com/kubescape/go-logger v0.0.32 h1:4mI+XJOV8VFCMewrEE9VIFEIOhzXokYT3nFpNfXf4fM=
github.com/kubescape/go-logger v0.0.32/go.mod h1:Alj7JBQ8/WCxbXe8Ura6ZheSRK45E0p21M3xeqedX90=
github.com/kubescape/k8s-interface v0.0.213 h1:JaEVzgE5qwQ3rEjQ8tBMp48YX4yveitLfYNaCIk8j/A=
github.com/kubescape/k8s-interface v0.0.213/go.mod h1:WNYUG93aZ5kDmuaRKFLtVhp18Yc6EfaHdD1gLYtVTN4=
github.com/kubescape/storage v0.0.278 h1:/pOtKul443yb2Fzg/4MFq29oOaFoJg1okQaCGbcEVOk=
github.com/kubescape/storage v0.0.278/go.mod h1:FpV6tCrYXlp2kKWza4yr7zf2Y1q7IGgx871ndN7SMNo=
github.com/kubescape/k8s-interface v0.0.214 h1:j7KP0/5VvYOoQdBGV2+gRM3qnR8PWLAGF8RM/k/DmJ0=
github.com/kubescape/k8s-interface v0.0.214/go.mod h1:WNYUG93aZ5kDmuaRKFLtVhp18Yc6EfaHdD1gLYtVTN4=
github.com/kubescape/storage v0.0.287 h1:3POQZ4xiGTstVVGpHO94rMPQhK7v079vBBr4C/EuePM=
github.com/kubescape/storage v0.0.287/go.mod h1:ARiTDaeDWLqEcOIbH+zz4dwdMEVxubfu5X5ehdDOqPc=
github.com/kubescape/syft v1.32.0-ks.2 h1:xdUksUmKEyyVKsTfJDYW8Z5HawVJtelsUolPOsWtDx0=
github.com/kubescape/syft v1.32.0-ks.2/go.mod h1:E6Kd4iBM2ljUOUQvSt7hVK6vBwaHkMXwcvBZmGMSY5o=
github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf h1:hI0jVwrB6fT4GJWvuUjzObfci1CUknrZdRHfnRVtKM0=
Expand Down
14 changes: 9 additions & 5 deletions pkg/rulemanager/cel/libraries/applicationprofile/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/kubescape/node-agent/pkg/objectcache"
objectcachev1 "github.com/kubescape/node-agent/pkg/objectcache/v1"
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
"github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -309,8 +310,11 @@ func TestExecWithArgsNoProfile(t *testing.T) {
// 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.
// "⋯" (DynamicIdentifier) — matches exactly one argument position.
// "⋯⋯" (ExecArgsWildcard) — matches zero or more consecutive args.
//
// As of storage v0.0.287 the exec-args zero-or-more wildcard is "⋯⋯"
// (ExecArgsWildcard); "*" is a literal character in exec args.
//
// The runtime exec arg vector is matched against the profile via
// dynamicpathdetector.CompareExecArgs (added in
Expand Down Expand Up @@ -344,7 +348,7 @@ func TestExecWithArgsWildcardInProfile(t *testing.T) {
// sh -c with any trailing payload (zero or more args).
{
Path: "/bin/sh",
Args: []string{"-c", "*"},
Args: []string{"-c", dynamicpathdetector.ExecArgsWildcard},
},
// ls -l in any directory — single trailing position.
{
Expand All @@ -354,7 +358,7 @@ func TestExecWithArgsWildcardInProfile(t *testing.T) {
// echo with any number of greeting words after a literal anchor.
{
Path: "/bin/echo",
Args: []string{"hello", "*"},
Args: []string{"hello", dynamicpathdetector.ExecArgsWildcard},
},
},
})
Expand Down Expand Up @@ -385,7 +389,7 @@ func TestExecWithArgsWildcardInProfile(t *testing.T) {
// 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 -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
Expand Down
146 changes: 146 additions & 0 deletions pkg/rulemanager/cel/libraries/parse/default_rules_yaml_lint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package parse

import (
"bytes"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
)

func defaultRulesYAMLPath(t *testing.T) string {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..", ".."))
return filepath.Join(repoRoot, "tests", "chart", "templates", "node-agent", "default-rules.yaml")
}

// guardedExecPath matches the exepath-first ternary that wraps the 2-arg
// resolver — the backward-compatible form the bundled rules use:
//
// event.exepath != "" ? event.exepath : parse.get_exec_path(event.args, event.comm)
//
// Both YAML renderings are tolerated: literal "" (block scalars) and the
// escaped \"\" (double-quoted scalars).
var guardedExecPath = regexp.MustCompile(`event\.exepath\s*!=\s*\\?"\\?"\s*\?\s*event\.exepath\s*:\s*parse\.get_exec_path\(\s*event\.args\s*,\s*event\.comm\s*\)`)

// twoArgExecPath matches a 2-arg parse.get_exec_path(event.args, event.comm).
// It does NOT match the 3-arg form, because there event.comm is followed by
// ", event.exepath" rather than ")".
var twoArgExecPath = regexp.MustCompile(`parse\.get_exec_path\(\s*event\.args\s*,\s*event\.comm\s*\)`)

// TestDefaultRulesYAML_NoUnguardedTwoArgGetExecPath pins exec-path resolution
// in the bundled rules to an exepath-first form. A *bare* 2-arg
// parse.get_exec_path(args, comm) re-introduces the fork-shell mismatch: the
// rule side evaluates to a bare comm (e.g. "sh") while the recording side
// stores the kernel-resolved exepath (e.g. "/bin/sh"), silently breaking
// ap.was_executed lookups for execve patterns like `sh -c …`.
//
// Two forms are allowed and must be used instead:
// - the 3-arg parse.get_exec_path(event.args, event.comm, event.exepath)
// overload (node-agent v0.3.147+), used by R0040; and
// - the backward-compatible inline ternary
// event.exepath != "" ? event.exepath : parse.get_exec_path(event.args, event.comm),
// used by R0001/R0007/R1001/R1004 so they also run on agents predating the
// 3-arg overload.
//
// The check blanks out the guarded ternary occurrences (length-preserving so
// reported line numbers stay accurate), then flags any 2-arg call that remains.
func TestDefaultRulesYAML_NoUnguardedTwoArgGetExecPath(t *testing.T) {
data, err := os.ReadFile(defaultRulesYAMLPath(t))
if err != nil {
t.Fatalf("read default-rules.yaml: %v", err)
}

cleaned := guardedExecPath.ReplaceAllFunc(data, func(m []byte) []byte {
return bytes.Repeat([]byte(" "), len(m))
})

if locs := twoArgExecPath.FindAllIndex(cleaned, -1); len(locs) > 0 {
lines := lineNumbers(data, locs)
t.Errorf("found %d unguarded 2-arg parse.get_exec_path() call(s) at line(s) %v; "+
"use the 3-arg form parse.get_exec_path(event.args, event.comm, event.exepath) or the "+
"exepath-first ternary event.exepath != \"\" ? event.exepath : parse.get_exec_path(event.args, event.comm)",
len(locs), lines)
}
}

// TestDefaultRulesYAML_NoStringOnArgsList guards against CEL expressions that
// wrap the list-typed event.args field in string(). CEL's string() has no
// list overload, so e.g. string(event.args) compiles-then-fails at rule-eval
// time. When that expression is a rule's message or uniqueId, the rule manager
// drops the whole alert (rule_manager.go: getUniqueIdAndMessage error → the
// event is skipped) and spams error logs on every matching event — which broke
// R0040 delivery and tripped Test_02/Test_32. Render list fields with
// event.args.map(a, string(a)).join(" ") instead (precedent: R0006 uses
// event.flags.join(",")).
func TestDefaultRulesYAML_NoStringOnArgsList(t *testing.T) {
data, err := os.ReadFile(defaultRulesYAMLPath(t))
if err != nil {
t.Fatalf("read default-rules.yaml: %v", err)
}

stringOnArgs := regexp.MustCompile(`string\(\s*event\.args\s*\)`)
if locs := stringOnArgs.FindAllIndex(data, -1); len(locs) > 0 {
lines := lineNumbers(data, locs)
t.Errorf("found %d string(event.args) call(s) at line(s) %v; CEL string() has no list overload — "+
"render with event.args.map(a, string(a)).join(\" \")", len(locs), lines)
}
}

// TestDefaultRulesYAML_R1000DetectsDevShmViaArgv guards R1000 ("Process
// Executed from /dev/shm") against silently losing its /dev/shm detection.
// R1000 must inspect argv[0] (event.args[0]) and event.exepath / event.cwd
// directly — NOT route through parse.get_exec_path, because the 3-arg resolver
// prefers the kernel-resolved exepath (e.g. /bin/busybox for a busybox-symlinked
// applet) over the as-invoked argv[0] (/dev/shm/ls). Routing /dev/shm detection
// through get_exec_path therefore resolves the path away from /dev/shm and the
// rule never fires (regressed Test_02).
func TestDefaultRulesYAML_R1000DetectsDevShmViaArgv(t *testing.T) {
data, err := os.ReadFile(defaultRulesYAMLPath(t))
if err != nil {
t.Fatalf("read default-rules.yaml: %v", err)
}

// Isolate the R1000 rule block: from its id line to the next rule's id line.
text := string(data)
start := strings.Index(text, `id: "R1000"`)
if start < 0 {
t.Fatal(`R1000 rule not found in default-rules.yaml`)
}
rest := text[start+len(`id: "R1000"`):]
end := strings.Index(rest, `id: "R`)
if end < 0 {
end = len(rest)
}
block := rest[:end]

if strings.Contains(block, "get_exec_path") {
t.Errorf("R1000 routes /dev/shm detection through parse.get_exec_path; the resolver " +
"prefers exepath over argv[0], so /dev/shm/<applet> resolves away from /dev/shm and " +
"the rule never fires. Inspect event.args[0] / event.exepath / event.cwd directly.")
}
if !strings.Contains(block, "event.args[0]") {
t.Errorf("R1000 must inspect argv[0] (event.args[0]) so an exec invoked as /dev/shm/<applet> " +
"is detected even when exepath resolves to the symlink target (e.g. /bin/busybox)")
}
}

func lineNumbers(data []byte, locs [][]int) []int {
out := make([]int, 0, len(locs))
for _, loc := range locs {
line := 1
for i := 0; i < loc[0] && i < len(data); i++ {
if data[i] == '\n' {
line++
}
}
out = append(out, line)
}
return out
}
9 changes: 5 additions & 4 deletions tests/chart/templates/node-agent/default-rule-binding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ spec:
- "kubeconfig"
rules:
- ruleName: "Unexpected process launched"
- ruleName: "Unexpected process arguments"
- ruleName: "Files Access Anomalies in container"
- ruleName: "Syscalls Anomalies in container"
- ruleName: "Linux Capabilities Anomalies in container"
- ruleName: "DNS Anomalies in container"
- ruleName: "Unexpected service account token access"
- ruleName: "Workload uses Kubernetes API unexpectedly"
- ruleName: "Process executed from malicious source"
- ruleName: "Process Executed from /dev/shm"
- ruleName: "Process tries to load a kernel module"
- ruleName: "Drifted process executed"
- ruleName: "Disallowed ssh connection"
- ruleName: "SSH Connection to Unexpected Destination on Non-Standard Port"
- ruleName: "Fileless execution detected"
- ruleName: "Crypto miner launched"
- ruleName: "Process executed from mount"
Expand All @@ -36,7 +37,7 @@ spec:
- ruleName: "Unexpected Sensitive File Access"
- ruleName: "Hard link created over sensitive file"
- ruleName: "Exec to pod"
- ruleName: "Port forward"
- ruleName: "Port forward to pod"
- ruleName: "Unexpected Egress Network Traffic"
- ruleName: "Malicious Ptrace Usage"
- ruleName: "Unexpected Ptrace Syscall Usage"
- ruleName: "Unexpected io_uring Operation Detected"
Loading
Loading