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
44 changes: 44 additions & 0 deletions docs/features/exec-file-cel-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Exec `file` CEL field

`event.file` is the file the kernel executed: `d_path(bprm->file)`, emitted by
`trace_exec` since Inspektor Gadget v0.57.0. It is symlink-resolved, like
`event.exepath`, but the two differ when the executed file is not the binary
that ends up running:

| Exec | `event.file` | `event.exepath` | `event.args[0]` |
|---|---|---|---|
| ELF binary `/bin/ls` | `/usr/bin/ls` | `/usr/bin/ls` | `ls` |
| Shebang script `/run/x.pid` (`#!/bin/sh`) | `/run/x.pid` | `/usr/bin/dash` | `/bin/sh` |
| binfmt_misc (e.g. a `.jar` handler) | the registered file | the interpreter | as passed |

Use `event.file` when a rule must match the executed file itself, for example a
script under a runtime-state directory. Keep `event.exepath` for profile
lookups: profiles store `exepath`, and `GetExecPathFromEvent` resolves the
alert path as file, then exepath, then comm.

## Presence and fallback

`has(event.file)` is true when the running gadget emits the field. It can still
be empty when the kernel path could not be resolved, so rules use a fallback:

```cel
(event.file != "" ? event.file : event.exepath)
```

`StructEvent` (ptrace and tests) reports `file` present when it is non-empty.

## Version gate

An agent built before this field existed does not register `event.file`, so a
rule that references it fails to compile there and is skipped. Gate such rules
with `agentVersionRequirement` set to the first release that carries this
change (kubescape/node-agent, and for private agents the first
private-node-agent release that pins it). The rules watcher skips a rule whose
requirement the agent does not meet before compiling it.

## Code

- `pkg/utils/cel.go`: the `file` entry in `CelFields`, `IsSet: presenceOf("file")`.
- `pkg/utils/struct_event.go`: `FieldPresent("file")`.
- `pkg/rulemanager/cel/cel_file_test.go`: shebang, missing-field and fallback cases
against both `StructEvent` and a synthetic exec datasource.
76 changes: 76 additions & 0 deletions pkg/rulemanager/cel/cel_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cel

import (
"testing"

"github.com/inspektor-gadget/inspektor-gadget/pkg/datasource"
"github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api"
"github.com/kubescape/node-agent/pkg/utils"
"github.com/stretchr/testify/require"
)

// newDatasourceFileExecEvent builds a synthetic "exec" datasource with comm and
// exepath and, when withFile is set, the "file" field trace_exec v0.57 emits.
func newDatasourceFileExecEvent(t *testing.T, exepath, file string, withFile bool) *utils.DatasourceEvent {
t.Helper()

ds, err := datasource.New(datasource.TypeSingle, "exec")
require.NoError(t, err)

commAcc, err := ds.AddField("comm", api.Kind_String)
require.NoError(t, err)
exeAcc, err := ds.AddField("exepath", api.Kind_String)
require.NoError(t, err)
var fileAcc datasource.FieldAccessor
if withFile {
fileAcc, err = ds.AddField("file", api.Kind_String)
require.NoError(t, err)
}

data, err := ds.NewPacketSingle()
require.NoError(t, err)
t.Cleanup(func() { ds.Release(data) })

require.NoError(t, commAcc.PutString(data, "sh"))
require.NoError(t, exeAcc.PutString(data, exepath))
if withFile {
require.NoError(t, fileAcc.PutString(data, file))
}

return &utils.DatasourceEvent{
Data: data,
Datasource: ds,
EventType: utils.ExecveEventType,
}
}

// TestCELExecFile covers event.file: for a shebang exec it is the script while
// event.exepath is the interpreter, so rules can match the executed file.
func TestCELExecFile(t *testing.T) {
shebang := &utils.StructEvent{
EventType: utils.ExecveEventType,
Comm: "x.pid",
Args: []string{"/bin/sh", "/run/x.pid"},
ExePath: "/usr/bin/dash",
File: "/run/x.pid",
}
require.True(t, evalTTY(t, shebang, `has(event.file) && event.file == "/run/x.pid"`))
require.True(t, evalTTY(t, shebang, `event.exepath == "/usr/bin/dash"`))

noFile := &utils.StructEvent{EventType: utils.ExecveEventType, Comm: "ls", ExePath: "/usr/bin/ls"}
require.False(t, evalTTY(t, noFile, `has(event.file)`))
require.True(t, evalTTY(t, noFile,
`(event.file != "" ? event.file : event.exepath) == "/usr/bin/ls"`),
"the documented fallback picks exepath when file is empty")

ds := newDatasourceFileExecEvent(t, "/usr/bin/dash", "/run/x.pid", true)
require.True(t, evalTTYEvent(t, ds, utils.ExecveEventType,
`has(event.file) && event.file == "/run/x.pid" && event.exepath == "/usr/bin/dash"`))

// A gadget that does not emit file (pre-v0.57): has() is false and the
// expression still compiles, so a guarded rule is not silently disabled.
old := newDatasourceFileExecEvent(t, "/usr/bin/dash", "", false)
require.False(t, evalTTYEvent(t, old, utils.ExecveEventType, `has(event.file)`))
require.True(t, evalTTYEvent(t, old, utils.ExecveEventType,
`(has(event.file) && event.file != "" ? event.file : event.exepath) == "/usr/bin/dash"`))
}
14 changes: 13 additions & 1 deletion pkg/utils/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func presenceOf(dsField string) ref.FieldTester {
//
// A field whose IsSet tester is presenceOf(...) can be absent at runtime when
// the running gadget does not emit it; rules should guard those with has().
// See docs/features/exec-tty-field.md for the tty fields specifically.
// See docs/features/exec-tty-field.md for the tty fields and
// docs/features/exec-file-cel-field.md for file.
var CelFields = map[string]*celtypes.FieldType{
"args": {
Type: celtypes.ListType,
Expand Down Expand Up @@ -309,6 +310,17 @@ var CelFields = map[string]*celtypes.FieldType{
return celtypes.String(x.Raw.GetExePath()), nil
}),
},
"file": {
Type: celtypes.StringType,
IsSet: presenceOf("file"),
GetFrom: ref.FieldGetter(func(target any) (any, error) {
x := target.(*xcel.Object[CelEvent])
if x.Raw == nil {
return nil, errCelObjectNil
}
return celtypes.String(x.Raw.GetFile()), nil
}),
},
"flags": {
Type: celtypes.ListType,
IsSet: isSet,
Expand Down
2 changes: 2 additions & 0 deletions pkg/utils/struct_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func (e *StructEvent) FieldPresent(name string) bool {
return e.TTYMajor != nil
case "tty_minor":
return e.TTYMinor != nil
case "file":
return e.File != ""
default:
return false
}
Expand Down
Loading