From fc9b108277f7d4b314bdc9054dd42a59df978498 Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Sat, 28 Mar 2026 19:02:18 +0000 Subject: [PATCH] Add args_regex support to custom binary detections Allows matching on process arguments via regex in addition to binary name, enabling detection of scripts run through interpreters. --- README.md | 8 +++-- cmd/iron-sensor/main.go | 5 +++- examples/config.example.yaml | 1 + internal/agent/signature.go | 33 ++++++++++++++++++--- internal/agent/signature_test.go | 50 ++++++++++++++++++++++++++++++-- internal/config/config.go | 5 ++-- 6 files changed, 89 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index de37833..e9de5f8 100644 --- a/README.md +++ b/README.md @@ -127,21 +127,23 @@ Detection works both at startup (scanning `/proc`) and live (via the exec tracep #### Custom binary detections -You can configure additional binary detections via the `detections` section in your config file. Each entry matches on the basename of `argv[0]`, the same way the built-in Claude Code and OpenClaw detections work. +You can configure additional binary detections via the `detections` section in your config file. Each entry matches on the basename of `argv[0]`, the same way the built-in Claude Code and OpenClaw detections work. Optionally, use `args_regex` to also require a regex match against the process arguments. ```yaml detections: binaries: - name: exfil_agent binary: exfil-tool - - name: custom_assistant - binary: my-ai-agent + - name: exfil_script + binary: bash + args_regex: "exfil\\.sh" ``` | Field | Description | |---|---| | `name` | Signature name that appears in emitted events (`signature_matched`) | | `binary` | Basename of `argv[0]` to match (e.g. `my-agent` matches `/usr/local/bin/my-agent`) | +| `args_regex` | Optional regex matched against `argv[1:]`. If set, at least one argument must match | Custom detections are appended to the built-in set — built-in agents are always detected regardless of config. diff --git a/cmd/iron-sensor/main.go b/cmd/iron-sensor/main.go index 1104fba..53eb2e9 100644 --- a/cmd/iron-sensor/main.go +++ b/cmd/iron-sensor/main.go @@ -55,7 +55,10 @@ func main() { defer emitter.Close() cls := classifier.New(cfg.Rules.Overrides) - sigs := agent.BuildSignatures(cfg.Detections.Binaries) + sigs, err := agent.BuildSignatures(cfg.Detections.Binaries) + if err != nil { + log.Fatalf("building detections: %v", err) + } tracker := agent.NewTracker(emitter, cls, sigs) log.Printf("iron-sensor %s starting (sink=%s)", events.SensorVersion, cfg.SinkType) diff --git a/examples/config.example.yaml b/examples/config.example.yaml index fe370c2..25dee70 100644 --- a/examples/config.example.yaml +++ b/examples/config.example.yaml @@ -16,6 +16,7 @@ detections: binaries: [] # - name: my_agent # signature name in events # binary: my-agent-bin # basename of argv[0] to match + # args_regex: "" # optional regex matched against argv[1:] rules: # Minimum severity to emit. 0 = alert, 1 = warn, 2 = info. diff --git a/internal/agent/signature.go b/internal/agent/signature.go index 1bbe412..179c591 100644 --- a/internal/agent/signature.go +++ b/internal/agent/signature.go @@ -1,8 +1,11 @@ package agent import ( - "iron-sensor/internal/config" + "fmt" + "regexp" "strings" + + "iron-sensor/internal/config" ) // Signature defines how to identify an AI coding agent process. @@ -25,12 +28,23 @@ func BuiltinSignatures() []Signature { } // BuildSignatures returns builtin signatures plus any configured binary detections. -func BuildSignatures(dets []config.BinaryDetection) []Signature { +func BuildSignatures(dets []config.BinaryDetection) ([]Signature, error) { sigs := make([]Signature, len(builtinSignatures)) copy(sigs, builtinSignatures) for _, d := range dets { bin := d.Binary name := d.Name + argsPattern := d.ArgsRegex + + var argsRe *regexp.Regexp + if argsPattern != "" { + var err error + argsRe, err = regexp.Compile(argsPattern) + if err != nil { + return nil, fmt.Errorf("detection %q: invalid args_regex: %w", name, err) + } + } + sigs = append(sigs, Signature{ Name: name, MatchExe: func(_ string) bool { @@ -40,11 +54,22 @@ func BuildSignatures(dets []config.BinaryDetection) []Signature { if len(argv) == 0 { return false } - return baseName(argv[0]) == bin + if baseName(argv[0]) != bin { + return false + } + if argsRe == nil { + return true + } + for _, a := range argv[1:] { + if argsRe.MatchString(a) { + return true + } + } + return false }, }) } - return sigs + return sigs, nil } var builtinSignatures = []Signature{ diff --git a/internal/agent/signature_test.go b/internal/agent/signature_test.go index ff2843c..0cee373 100644 --- a/internal/agent/signature_test.go +++ b/internal/agent/signature_test.go @@ -72,7 +72,8 @@ func TestBuildSignatures_CustomBinary(t *testing.T) { dets := []config.BinaryDetection{ {Name: "exfil_agent", Binary: "exfil-tool"}, } - sigs := BuildSignatures(dets) + sigs, err := BuildSignatures(dets) + require.NoError(t, err) // Should still match builtins. sig, ok := MatchWith(sigs, "/usr/local/bin/claude", []string{"claude"}) @@ -91,7 +92,8 @@ func TestBuildSignatures_CustomBinary(t *testing.T) { } func TestBuildSignatures_NoCustom(t *testing.T) { - sigs := BuildSignatures(nil) + sigs, err := BuildSignatures(nil) + require.NoError(t, err) require.Equal(t, len(BuiltinSignatures()), len(sigs)) } @@ -99,8 +101,50 @@ func TestBuildSignatures_CustomNoMatchOther(t *testing.T) { dets := []config.BinaryDetection{ {Name: "my_agent", Binary: "my-agent"}, } - sigs := BuildSignatures(dets) + sigs, err := BuildSignatures(dets) + require.NoError(t, err) _, ok := MatchWith(sigs, "/usr/bin/bash", []string{"bash"}) require.False(t, ok) } + +func TestBuildSignatures_ArgsRegex(t *testing.T) { + dets := []config.BinaryDetection{ + {Name: "exfil_script", Binary: "bash", ArgsRegex: `exfil\.sh`}, + } + sigs, err := BuildSignatures(dets) + require.NoError(t, err) + + // Should match bash running the exfil script. + sig, ok := MatchWith(sigs, "/usr/bin/bash", []string{"bash", "/tmp/exfil.sh"}) + require.True(t, ok) + require.Equal(t, "exfil_script", sig) + + // Should not match bash running something else. + _, ok = MatchWith(sigs, "/usr/bin/bash", []string{"bash", "other.sh"}) + require.False(t, ok) + + // Should not match bash with no args. + _, ok = MatchWith(sigs, "/usr/bin/bash", []string{"bash"}) + require.False(t, ok) +} + +func TestBuildSignatures_ArgsRegex_NoMatch_WrongBinary(t *testing.T) { + dets := []config.BinaryDetection{ + {Name: "exfil_script", Binary: "bash", ArgsRegex: `exfil\.sh`}, + } + sigs, err := BuildSignatures(dets) + require.NoError(t, err) + + _, ok := MatchWith(sigs, "/usr/bin/zsh", []string{"zsh", "exfil.sh"}) + require.False(t, ok) +} + +func TestBuildSignatures_InvalidRegex(t *testing.T) { + dets := []config.BinaryDetection{ + {Name: "bad", Binary: "bash", ArgsRegex: `[invalid`}, + } + _, err := BuildSignatures(dets) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid args_regex") +} diff --git a/internal/config/config.go b/internal/config/config.go index d7be3ff..2fcb1bc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,8 +19,9 @@ type DetectionsConfig struct { } type BinaryDetection struct { - Name string `yaml:"name"` - Binary string `yaml:"binary"` + Name string `yaml:"name"` + Binary string `yaml:"binary"` + ArgsRegex string `yaml:"args_regex"` } type FileSinkConfig struct {