post inicial

This commit is contained in:
lastdragon
2026-09-27 16:04:02 -06:00
parent d84587e1f3
commit 52864794f5
67 changed files with 15023 additions and 2 deletions
+306
View File
@@ -0,0 +1,306 @@
package policy
import (
"os"
"path/filepath"
"strings"
"testing"
)
func argvs(s ShellScript) string {
var parts []string
for _, c := range s.Commands {
parts = append(parts, strings.Join(c.Argv, " "))
}
return strings.Join(parts, " | ")
}
func TestParseShell(t *testing.T) {
cases := []struct{ in, want string }{
{`ls -la /etc`, `ls -la /etc`},
{`cat a | grep "x y" && echo 'z;w'`, `cat a | grep x y | echo z;w`},
{`FOO=1 BAR=2 env`, `env`},
{`echo $(rm -rf /tmp/x)`, `rm -rf /tmp/x | echo $(...)`},
{"echo `whoami`", `whoami | echo $(...)`},
{`diff <(ls a) <(ls b)`, `ls a | ls b | diff <(...) <(...)`},
{`if systemctl is-active nginx; then echo ok; fi`, `systemctl is-active nginx | echo ok`},
{`for f in a b c; do rm "$f"; done`, `rm $f`},
{"cat <<'EOF' > /etc/x\n$(evil)\nEOF\nls", `cat | ls`},
{`echo $((1+2))`, `echo $((...))`},
{`# comment only`, ``},
{"a \\\n b", `a b`},
{`[[ -f /x && -d /y ]] && echo yes`, `[[ | echo yes`},
}
for _, c := range cases {
got := argvs(ParseShell(c.in))
if got != c.want {
t.Errorf("ParseShell(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestParseShellRedirects(t *testing.T) {
s := ParseShell(`echo hi > /etc/motd 2>&1; sort < in.txt >> out.txt; cmd &> /dev/null`)
var got []string
for _, c := range s.Commands {
for _, r := range c.Redirects {
got = append(got, r.Op+r.Target)
}
}
want := ">/etc/motd >&1 <in.txt >>out.txt &>/dev/null"
if strings.Join(got, " ") != want {
t.Errorf("redirects = %q, want %q", strings.Join(got, " "), want)
}
}
func TestParseShellHeredocUnquotedExpands(t *testing.T) {
s := ParseShell("cat <<EOF\n$(reboot)\nEOF\n")
if !strings.Contains(argvs(s), "reboot") {
t.Errorf("unquoted heredoc substitution not detected: %q", argvs(s))
}
}
func TestParseShellOpaque(t *testing.T) {
for _, in := range []string{`echo "unterminated`, `case $x in a) rm -rf /;; esac`, "echo `x"} {
if len(ParseShell(in).Opaque) == 0 {
t.Errorf("%q should be opaque", in)
}
}
}
func TestClassify(t *testing.T) {
cases := map[string]Risk{
"ls -la": RiskReadOnly,
"systemctl status nginx": RiskReadOnly,
"systemctl restart nginx": RiskService,
"systemctl reboot": RiskReboot,
"sudo systemctl restart nginx": RiskService,
"sudo -u postgres psql": RiskUnknown,
"apt list --installed": RiskReadOnly,
"apt-get install -y nginx": RiskPackage,
"dpkg -l nginx": RiskReadOnly,
"dpkg -i x.deb": RiskPackage,
"ip addr": RiskReadOnly,
"ip -br a show": RiskReadOnly,
"ip route add default via 1.2.3.4": RiskAdmin,
"nft list ruleset": RiskReadOnly,
"nft add rule inet f c drop": RiskAdmin,
"iptables -L -n -v": RiskReadOnly,
"iptables -A INPUT -j DROP": RiskAdmin,
"rm -rf /tmp/x": RiskDelete,
"dd if=/dev/zero of=/dev/sda": RiskDestructive,
"mkfs.ext4 /dev/sdb1": RiskDestructive,
"reboot": RiskReboot,
"shutdown -h now": RiskReboot,
"curl -s https://x": RiskNetwork,
"sed -n 1,10p f": RiskReadOnly,
"sed -i s/a/b/ f": RiskWrite,
"timeout 5 ping -c1 host": RiskNetwork,
"env A=1 rm x": RiskDelete,
"python3 -c print(1)": RiskUnknown,
"nginx -t": RiskReadOnly,
"nginx -s reload": RiskService,
"tar -tzf x.tgz": RiskReadOnly,
"tar -xzf x.tgz": RiskWrite,
"sysctl net.ipv4.ip_forward": RiskReadOnly,
"sysctl -w net.ipv4.ip_forward=1": RiskAdmin,
"useradd bob": RiskAdmin,
"userdel bob": RiskDestructive,
"somebinary --version": RiskReadOnly,
"somebinary --run": RiskUnknown,
"$CMD x": RiskUnknown,
"git status": RiskReadOnly,
"git clone https://x": RiskNetwork,
"journalctl -u nginx -n 50": RiskReadOnly,
"journalctl --vacuum-time=2d": RiskDelete,
}
for cmd, want := range cases {
c := Classify(strings.Fields(cmd))
found := false
for _, r := range c.AllRisks() {
if r == want {
found = true
}
}
if !found {
t.Errorf("Classify(%q) risks = %v, want to include %s", cmd, c.AllRisks(), want)
}
}
}
func TestClassifyNested(t *testing.T) {
c := Classify([]string{"bash", "-lc", "ls && reboot"})
if c.ReadOnly() {
t.Error("bash -c with reboot must not be read-only")
}
c = Classify([]string{"find", "/tmp", "-name", "*.log", "-exec", "rm", "{}", ";"})
if c.ReadOnly() {
t.Error("find -exec rm must not be read-only")
}
c = Classify([]string{"find", "/var/log", "-name", "*.gz"})
if !c.ReadOnly() {
t.Errorf("plain find should be read-only: %v", c.AllRisks())
}
}
// guardFor crea un proyecto temporal con la política dada.
func guardFor(t *testing.T, directives string) (*Guard, string) {
t.Helper()
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "agent.md"), []byte("x"), 0o644)
p, err := Parse("```policy\n" + directives + "\n```\n")
if err != nil {
t.Fatal(err)
}
g, err := NewGuard(p, dir, "agent.md")
if err != nil {
t.Fatal(err)
}
return g, dir
}
func TestGuardPaths(t *testing.T) {
g, dir := guardFor(t, "EXECUTION_MODE=privileged\nALLOW_OUTSIDE_PROJECT_READ=true\nALLOW_DELETE=true")
allowed := []struct {
op Op
p string
}{
{OpRead, "notes.txt"},
{OpWrite, "sub/file.txt"},
{OpWrite, "memoria.md"},
{OpRead, "/etc/hostname"},
{OpRead, "agent.md"},
{OpDelete, "sub/old.txt"},
}
for _, c := range allowed {
if err := g.CheckPath(c.op, c.p, ""); err != nil {
t.Errorf("%s %s: unexpected %v", c.op, c.p, err)
}
}
denied := []struct {
op Op
p string
}{
{OpWrite, "agent.md"},
{OpWrite, "init.conf"},
{OpRead, "init.conf"},
{OpWrite, "02-new-task.md"},
{OpWrite, "agent.log"},
{OpWrite, "/etc/motd"}, // ALLOW_OUTSIDE_PROJECT_WRITE no declarado
{OpDelete, "/"},
{OpDelete, "/etc"},
{OpDelete, "/etc/*"},
{OpDelete, dir},
{OpDelete, "*.md"},
}
for _, c := range denied {
err := g.CheckPath(c.op, c.p, "")
if !IsDenied(err) {
t.Errorf("%s %s: expected denial, got %v", c.op, c.p, err)
}
}
}
func TestGuardSymlinkEscape(t *testing.T) {
g, dir := guardFor(t, "EXECUTION_MODE=privileged")
outside := t.TempDir()
if err := os.Symlink(outside, filepath.Join(dir, "link")); err != nil {
t.Skip(err)
}
if err := g.CheckPath(OpWrite, "link/x.conf", ""); !IsDenied(err) {
t.Errorf("write through symlink to outside dir must be denied, got %v", err)
}
if err := g.CheckPath(OpRead, "link/x.conf", ""); !IsDenied(err) {
t.Errorf("read through symlink needs ALLOW_OUTSIDE_PROJECT_READ, got %v", err)
}
}
func TestGuardToolsDirNeedsCapability(t *testing.T) {
g, _ := guardFor(t, "EXECUTION_MODE=privileged")
if err := g.CheckPath(OpWrite, "tools/check.sh", ""); !IsDenied(err) {
t.Errorf("tools/ requires ALLOW_SELF_TOOL_CREATION: %v", err)
}
g2, _ := guardFor(t, "ALLOW_SELF_TOOL_CREATION=true")
if err := g2.CheckPath(OpWrite, "tools/check.sh", ""); err != nil {
t.Errorf("unexpected %v", err)
}
}
func TestGuardCommandsGuarded(t *testing.T) {
g, dir := guardFor(t, "EXECUTION_MODE=guarded\nALLOW_NETWORK=true\nALLOW_OUTSIDE_PROJECT_READ=true")
ok := [][]string{
{"ls", "-la", "/etc"},
{"systemctl", "status", "nginx"},
{"ping", "-c1", "example.com"},
{"mkdir", "-p", "build"}, // escritura dentro del proyecto
{"cat", "/etc/os-release"},
}
for _, argv := range ok {
if _, err := g.CheckArgv(argv, dir); err != nil {
t.Errorf("%v: unexpected %v", argv, err)
}
}
bad := [][]string{
{"systemctl", "restart", "nginx"},
{"apt-get", "install", "x"},
{"rm", "x"},
{"cp", "a", "/etc/b"},
{"useradd", "bob"},
{"python3", "script.py"},
{"reboot"},
{"cat", "init.conf"},
{"sed", "-i", "s/a/b/", "agent.md"},
}
for _, argv := range bad {
if _, err := g.CheckArgv(argv, dir); !IsDenied(err) {
t.Errorf("%v: expected denial, got %v", argv, err)
}
}
}
func TestGuardScripts(t *testing.T) {
g, dir := guardFor(t, "EXECUTION_MODE=privileged\nALLOW_OUTSIDE_PROJECT_READ=true\nALLOW_DELETE=true\nALLOW_SERVICE_RESTART=true")
ok := []string{
"journalctl -u nginx | tail -n 50",
"echo hello > out.txt",
"systemctl restart nginx && systemctl is-active nginx",
"ls /etc 2>&1 | grep ssh",
"rm -f build/*.o",
}
for _, s := range ok {
if _, err := g.CheckScript(s, dir); err != nil {
t.Errorf("%q: unexpected %v", s, err)
}
}
bad := []string{
"echo x > /etc/motd", // escritura fuera sin ALLOW_OUTSIDE_PROJECT_WRITE
"cd /etc && rm motd", // cd seguido: rm afecta a /etc/motd
"ls; reboot", // ALLOW_REBOOT no declarado
"echo $(shutdown -h now)", // sustitución
"bash -c 'dd if=/dev/zero of=/dev/sda'", // destructivo anidado
"rm -rf /",
"echo evil >> agent.md",
"cat init.conf",
"apt-get install -y x",
}
for _, s := range bad {
if _, err := g.CheckScript(s, dir); !IsDenied(err) {
t.Errorf("%q: expected denial, got %v", s, err)
}
}
}
func TestGuardUnrestrictedStillRespectsReboot(t *testing.T) {
g, dir := guardFor(t, "EXECUTION_MODE=unrestricted\nALLOW_DELETE=true\nALLOW_OUTSIDE_PROJECT_WRITE=true")
if _, err := g.CheckArgv([]string{"mkfs.ext4", "/dev/sdz9"}, dir); err != nil {
t.Errorf("unrestricted should allow mkfs: %v", err)
}
if _, err := g.CheckArgv([]string{"systemctl", "reboot"}, dir); !IsDenied(err) {
t.Errorf("ALLOW_REBOOT=false must block reboot even in unrestricted: %v", err)
}
if _, err := g.CheckArgv([]string{"rm", "-rf", "/usr"}, dir); !IsDenied(err) {
t.Errorf("critical path deletion must always be denied: %v", err)
}
}