diff --git a/internal/tools/shell_deny_groups.go b/internal/tools/shell_deny_groups.go index 49dd9112..56fe4282 100644 --- a/internal/tools/shell_deny_groups.go +++ b/internal/tools/shell_deny_groups.go @@ -28,8 +28,10 @@ var DenyGroupRegistry = map[string]*DenyGroup{ regexp.MustCompile(`\b(mkfs|diskpart)\b|\bformat\s`), regexp.MustCompile(`\bdd\s+if=`), regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), - regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`), - regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), // fork bomb + regexp.MustCompile(`\b(shutdown|reboot|poweroff|halt)\b`), + regexp.MustCompile(`\b(init|telinit)\s+[06]\b`), // SysV shutdown/reboot + regexp.MustCompile(`\bsystemctl\s+(suspend|hibernate)\b`), // power management + regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), // fork bomb }, }, "data_exfiltration": { @@ -81,7 +83,10 @@ var DenyGroupRegistry = map[string]*DenyGroup{ Default: true, Patterns: []*regexp.Regexp{ regexp.MustCompile(`\bsudo\b`), - regexp.MustCompile(`\bsu\s+-`), + regexp.MustCompile(`\bsu\b`), + regexp.MustCompile(`\bdoas\b`), + regexp.MustCompile(`\bpkexec\b`), + regexp.MustCompile(`\brunuser\b`), regexp.MustCompile(`\bnsenter\b`), regexp.MustCompile(`\bunshare\b`), regexp.MustCompile(`\b(mount|umount)\b`), diff --git a/internal/tools/shell_deny_test.go b/internal/tools/shell_deny_test.go index 15d24815..c7c5de9f 100644 --- a/internal/tools/shell_deny_test.go +++ b/internal/tools/shell_deny_test.go @@ -1,6 +1,7 @@ package tools import ( + "regexp" "testing" ) @@ -49,6 +50,94 @@ func TestBase64DecodeShellDeny(t *testing.T) { } } +// mustDeny asserts all commands match at least one pattern. +func mustDeny(t *testing.T, patterns []*regexp.Regexp, commands ...string) { + t.Helper() + for _, cmd := range commands { + matched := false + for _, p := range patterns { + if p.MatchString(cmd) { + matched = true + break + } + } + if !matched { + t.Errorf("expected deny for %q", cmd) + } + } +} + +// mustAllow asserts no command matches any pattern. +func mustAllow(t *testing.T, patterns []*regexp.Regexp, commands ...string) { + t.Helper() + for _, cmd := range commands { + for _, p := range patterns { + if p.MatchString(cmd) { + t.Errorf("unexpected deny for %q (matched %s)", cmd, p.String()) + break + } + } + } +} + +func TestDestructiveOpsGaps(t *testing.T) { + patterns := DenyGroupRegistry["destructive_ops"].Patterns + + mustDeny(t, patterns, + // existing + "shutdown", "reboot", "poweroff", + "shutdown -h now", "reboot -f", + // new: halt + "halt", "halt -p", "systemctl halt", + // new: init/telinit + "init 0", "init 6", "telinit 0", "telinit 6", + // new: systemctl suspend/hibernate + "systemctl suspend", "systemctl hibernate", + ) + + mustAllow(t, patterns, + "halting the process", // "halt" inside word + "initialize", // "init" inside word + "initial setup", // "init" inside word + "init_db", // no space+digit after init + "init 1", // only 0 and 6 are blocked + "systemctl status", // not suspend/hibernate + "systemctl start nginx", + ) +} + +func TestPrivilegeEscalationGaps(t *testing.T) { + patterns := DenyGroupRegistry["privilege_escalation"].Patterns + + mustDeny(t, patterns, + // existing + "sudo ls", "sudo -i", + // su: all forms now blocked + "su", "su -", "su root", "su -l postgres", "su admin", + // new: doas + "doas reboot", "doas ls /root", "doas -u www sh", + // new: pkexec + "pkexec vim /etc/passwd", "pkexec /bin/bash", + // new: runuser + "runuser -l postgres", "runuser -u nobody -- /bin/sh", + // existing + "nsenter --target 1", "unshare -m", "mount /dev/sda1 /mnt", + ) + + mustAllow(t, patterns, + "summit", // not "su" + "sugar", // not "su" + "surplus", // not "su" + "issue", // not "su" + "result", // not "su" + "resume", // not "su" + "visual", // not "su" + "sushi", // not "su" + "doaspkg", // not "doas" (no word boundary) + "pkexecute", // not "pkexec" (no word boundary) + ) +} + func TestLimitedBuffer(t *testing.T) { t.Run("under limit", func(t *testing.T) { lb := &limitedBuffer{max: 100}