mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 02:55:51 +00:00
6725d6a784
Refs #1034
193 lines
6.6 KiB
Go
193 lines
6.6 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCompilePatternsIgnoresInvalidRegex(t *testing.T) {
|
|
res := compilePatterns([]string{"^df(\\s|$)", "["})
|
|
if len(res) != 1 {
|
|
t.Fatalf("expected 1 compiled regex, got %d", len(res))
|
|
}
|
|
}
|
|
|
|
func TestDefaultPolicyEvaluate(t *testing.T) {
|
|
p := DefaultPolicy()
|
|
|
|
cases := []struct {
|
|
name string
|
|
command string
|
|
want PolicyDecision
|
|
}{
|
|
{"blocked", "rm -rf /", PolicyBlock},
|
|
{"blocked sudo", "sudo rm -rf /", PolicyBlock},
|
|
{"blocked sudo with flags", "sudo -u root rm -rf /", PolicyBlock},
|
|
{"blocked proc environ", "cat /proc/self/environ", PolicyBlock},
|
|
{"blocked proc kcore", "cat /proc/kcore", PolicyBlock},
|
|
{"auto approve", "df -h", PolicyAllow},
|
|
{"auto approve safe proc meminfo", "cat /proc/meminfo", PolicyAllow},
|
|
{"auto approve ipv4 ping probe", "ping -c 1 -W 1 10.0.0.1", PolicyAllow},
|
|
{"auto approve ipv6 ping probe", "ping -c 1 -W 1 2001:db8::1", PolicyAllow},
|
|
{"auto approve podman inspect", "podman inspect api", PolicyAllow},
|
|
{"auto approve podman system df", "podman system df", PolicyAllow},
|
|
{"require approval", "systemctl restart nginx", PolicyRequireApproval},
|
|
{"podman restart requires approval", "podman restart api", PolicyRequireApproval},
|
|
{"podman stop requires approval", "podman stop api", PolicyRequireApproval},
|
|
{"generic proc read requires approval", "cat /proc/1/status", PolicyRequireApproval},
|
|
{"hostname ping requires approval", "ping -c 1 -W 1 example.com", PolicyRequireApproval},
|
|
{"unknown defaults to approval", "echo hello", PolicyRequireApproval},
|
|
{"sudo with flags remains conservative", "sudo -u root df -h", PolicyRequireApproval},
|
|
{"compound command requires approval", "df -h && echo ok", PolicyRequireApproval},
|
|
{"compound ping requires approval", "ping -c 1 -W 1 10.0.0.1; echo ok", PolicyRequireApproval},
|
|
{"find delete requires approval", "find /var -type f -delete", PolicyRequireApproval},
|
|
|
|
// Proxmox VM control - should require approval, not be blocked
|
|
{"qm reboot requires approval", "qm reboot 201", PolicyRequireApproval},
|
|
{"qm shutdown requires approval", "qm shutdown 201", PolicyRequireApproval},
|
|
{"pct reboot requires approval", "pct reboot 100", PolicyRequireApproval},
|
|
{"pct shutdown requires approval", "pct shutdown 100", PolicyRequireApproval},
|
|
|
|
// Host-level system commands should be blocked
|
|
{"bare reboot blocked", "reboot", PolicyBlock},
|
|
{"bare shutdown blocked", "shutdown", PolicyBlock},
|
|
{"shutdown now blocked", "shutdown now", PolicyBlock},
|
|
{"reboot now blocked", "reboot -f", PolicyBlock},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := p.Evaluate(tc.command); got != tc.want {
|
|
t.Fatalf("Evaluate(%q) = %q, want %q", tc.command, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPolicyHelpers(t *testing.T) {
|
|
p := DefaultPolicy()
|
|
if !p.IsBlocked("rm -rf /") {
|
|
t.Fatalf("expected rm -rf / to be blocked")
|
|
}
|
|
if !p.IsBlocked("sudo -u root rm -rf /") {
|
|
t.Fatalf("expected sudo -u root rm -rf / to be blocked")
|
|
}
|
|
if !p.IsBlocked("cat /proc/self/environ") {
|
|
t.Fatalf("expected cat /proc/self/environ to be blocked")
|
|
}
|
|
if !p.NeedsApproval("echo hello") {
|
|
t.Fatalf("expected echo hello to require approval by default")
|
|
}
|
|
if !p.NeedsApproval("cat /proc/1/status") {
|
|
t.Fatalf("expected cat /proc/1/status to require approval")
|
|
}
|
|
if !p.IsAutoApproved("df -h") {
|
|
t.Fatalf("expected df -h to be auto approved")
|
|
}
|
|
if !p.IsAutoApproved("cat /proc/meminfo") {
|
|
t.Fatalf("expected cat /proc/meminfo to be auto approved")
|
|
}
|
|
if !p.IsAutoApproved("ping -c 1 -W 1 10.0.0.1") {
|
|
t.Fatalf("expected single-target ping probe to be auto approved")
|
|
}
|
|
}
|
|
|
|
func TestDefaultPolicyVerifyWindow(t *testing.T) {
|
|
p := DefaultPolicy()
|
|
if p.VerifyWindow != DefaultVerifyWindow {
|
|
t.Fatalf("default verify window = %v, want %v", p.VerifyWindow, DefaultVerifyWindow)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeVerifyWindowBounds(t *testing.T) {
|
|
cases := []struct {
|
|
in time.Duration
|
|
want time.Duration
|
|
}{
|
|
{0, DefaultVerifyWindow},
|
|
{-1 * time.Second, DefaultVerifyWindow},
|
|
{30 * time.Second, 30 * time.Second},
|
|
{DefaultVerifyWindow, DefaultVerifyWindow},
|
|
{MaxVerifyWindow, MaxVerifyWindow},
|
|
{MaxVerifyWindow + time.Second, MaxVerifyWindow},
|
|
{1 * time.Hour, MaxVerifyWindow},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := NormalizeVerifyWindow(tc.in); got != tc.want {
|
|
t.Fatalf("NormalizeVerifyWindow(%v) = %v, want %v", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommandPolicyNormalizeBoundsInPlace(t *testing.T) {
|
|
p := &CommandPolicy{VerifyWindow: time.Hour}
|
|
p.Normalize()
|
|
if p.VerifyWindow != MaxVerifyWindow {
|
|
t.Fatalf("Normalize did not clamp verify_window: got %v, want %v", p.VerifyWindow, MaxVerifyWindow)
|
|
}
|
|
|
|
q := &CommandPolicy{VerifyWindow: 0}
|
|
q.Normalize()
|
|
if q.VerifyWindow != DefaultVerifyWindow {
|
|
t.Fatalf("Normalize did not default verify_window: got %v, want %v", q.VerifyWindow, DefaultVerifyWindow)
|
|
}
|
|
}
|
|
|
|
func TestCommandPolicyJSONRoundtripVerifyWindow(t *testing.T) {
|
|
original := &CommandPolicy{
|
|
AutoApprove: []string{"^df$"},
|
|
RequireApproval: []string{"^systemctl\\s+restart"},
|
|
Blocked: []string{"^reboot$"},
|
|
VerifyWindow: 90 * time.Second,
|
|
}
|
|
|
|
raw, err := json.Marshal(original)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"verify_window":"1m30s"`) {
|
|
t.Fatalf("verify_window should serialize as a duration string; got %s", raw)
|
|
}
|
|
|
|
var decoded CommandPolicy
|
|
if err := json.Unmarshal(raw, &decoded); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if decoded.VerifyWindow != 90*time.Second {
|
|
t.Fatalf("roundtripped verify_window = %v, want %v", decoded.VerifyWindow, 90*time.Second)
|
|
}
|
|
}
|
|
|
|
func TestCommandPolicyJSONUnmarshalAppliesBounds(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want time.Duration
|
|
}{
|
|
{"empty defaults to default window", `{}`, DefaultVerifyWindow},
|
|
{"zero string defaults to default", `{"verify_window":"0s"}`, DefaultVerifyWindow},
|
|
{"over-cap clamps to max", `{"verify_window":"1h"}`, MaxVerifyWindow},
|
|
{"reasonable value passes through", `{"verify_window":"5m"}`, 5 * time.Minute},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var p CommandPolicy
|
|
if err := json.Unmarshal([]byte(tc.raw), &p); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if p.VerifyWindow != tc.want {
|
|
t.Fatalf("verify_window = %v, want %v", p.VerifyWindow, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandPolicyJSONUnmarshalRejectsInvalidDuration(t *testing.T) {
|
|
var p CommandPolicy
|
|
if err := json.Unmarshal([]byte(`{"verify_window":"not-a-duration"}`), &p); err == nil {
|
|
t.Fatalf("expected error for invalid verify_window")
|
|
}
|
|
}
|