post inicial
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Risk es la categoría de riesgo de un comando.
|
||||
type Risk string
|
||||
|
||||
const (
|
||||
RiskReadOnly Risk = "read-only"
|
||||
RiskNetwork Risk = "network"
|
||||
RiskDownload Risk = "download"
|
||||
RiskWrite Risk = "write"
|
||||
RiskDelete Risk = "delete"
|
||||
RiskPackage Risk = "package"
|
||||
RiskService Risk = "service"
|
||||
RiskAdmin Risk = "admin"
|
||||
RiskDestructive Risk = "destructive"
|
||||
RiskReboot Risk = "reboot"
|
||||
RiskUnknown Risk = "unknown"
|
||||
)
|
||||
|
||||
// Classification describe un comando concreto (argv) tras desenvolver
|
||||
// envoltorios como sudo, env o timeout.
|
||||
type Classification struct {
|
||||
Program string // nombre base del programa efectivo
|
||||
Argv []string
|
||||
Risks []Risk
|
||||
// AllArgsArePaths indica que todos los argumentos no-opción deben
|
||||
// tratarse como rutas afectadas (rm, cp, chmod...).
|
||||
AllArgsArePaths bool
|
||||
// Nested contiene comandos anidados (bash -c, find -exec, xargs...).
|
||||
Nested []Classification
|
||||
// Script es el script de un "bash -c" anidado, si lo hay.
|
||||
Script *ShellScript
|
||||
}
|
||||
|
||||
// AllRisks devuelve los riesgos propios y de todos los comandos anidados.
|
||||
func (c Classification) AllRisks() []Risk {
|
||||
out := append([]Risk(nil), c.Risks...)
|
||||
for _, n := range c.Nested {
|
||||
out = append(out, n.AllRisks()...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ReadOnly indica si el comando y todos sus anidados son de solo lectura
|
||||
// (o solo red), es decir, seguros de ejecutar en --dry-run.
|
||||
func (c Classification) ReadOnly() bool {
|
||||
for _, r := range c.AllRisks() {
|
||||
if r != RiskReadOnly && r != RiskNetwork {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// readOnlyPrograms nunca modifican el sistema, sean cuales sean sus argumentos.
|
||||
var readOnlyPrograms = set(
|
||||
"ls", "dir", "cat", "tac", "head", "tail", "grep", "egrep", "fgrep", "rg", "zgrep", "zcat", "bzcat", "xzcat", "zstdcat",
|
||||
"stat", "file", "wc", "du", "df", "free", "uptime", "uname", "id", "whoami", "groups", "who", "w", "last", "lastlog",
|
||||
"ps", "pgrep", "pidof", "ss", "netstat", "lsof", "lsblk", "blkid", "lscpu", "lsmem", "lspci", "lsusb", "lsmod",
|
||||
"lsattr", "lsns", "lsipc", "lslocks", "lslogins", "findmnt", "cal", "printenv", "echo", "printf", "true", "false",
|
||||
"test", "[", "[[", "basename", "dirname", "realpath", "readlink", "pwd", "which", "whereis", "type", "sort", "uniq",
|
||||
"cut", "tr", "column", "nl", "fold", "fmt", "paste", "join", "comm", "diff", "cmp", "md5sum", "sha1sum", "sha224sum",
|
||||
"sha256sum", "sha384sum", "sha512sum", "b2sum", "cksum", "base64", "base32", "xxd", "hexdump", "od", "strings", "jq",
|
||||
"getent", "getconf", "nproc", "arch", "locale", "vmstat", "iostat", "mpstat", "pidstat", "sar", "systemd-analyze",
|
||||
"modinfo", "tree", "locate", "dmidecode", "sensors", "aa-status", "getenforce", "sestatus", "apt-cache", "dpkg-query",
|
||||
"named-checkconf", "named-checkzone", "cd", "export", "unset", "set", "read", "sleep", "wait", "exit", "return",
|
||||
"local", "shift", "declare", "typeset", "readonly", "let", "seq", "yes", "expr", "tput", "ldd", "nm",
|
||||
"objdump", "readelf", "lsinitramfs", "systemd-detect-virt", "virt-what", "numfmt", "rev", "shuf", "iconv", "less",
|
||||
"more", "man", "whatis", "apropos", "hash", "ulimit", "logname", "tty", "users",
|
||||
"free", "systemd-cgls", "systemd-cgtop", "diff3", "sdiff", "look", "zdiff", "zless", "zmore", "cmp")
|
||||
|
||||
// networkPrograms solo consultan la red.
|
||||
var networkPrograms = set(
|
||||
"ping", "ping6", "traceroute", "traceroute6", "tracepath", "mtr", "dig", "host", "nslookup", "whois", "nc", "ncat",
|
||||
"netcat", "telnet", "nmap", "arping", "ssh", "sftp", "ftp", "drill", "delv", "kdig", "socat",
|
||||
)
|
||||
|
||||
// destructivePrograms pueden destruir datos de forma irreversible.
|
||||
var destructivePrograms = set(
|
||||
"dd", "mkfs", "mke2fs", "mkswap", "wipefs", "fdisk", "sfdisk", "cfdisk", "gdisk", "sgdisk", "parted", "shred",
|
||||
"blkdiscard", "pvremove", "vgremove", "lvremove", "userdel", "deluser", "groupdel", "delgroup", "killall5",
|
||||
"mdadm", "zpool", "badblocks",
|
||||
)
|
||||
|
||||
// adminPrograms modifican la configuración del sistema.
|
||||
var adminPrograms = set(
|
||||
"chmod", "chown", "chgrp", "setfacl", "chattr", "useradd", "usermod", "groupadd", "groupmod", "gpasswd", "passwd",
|
||||
"chpasswd", "adduser", "addgroup", "chsh", "chfn", "kill", "pkill", "killall", "umount", "mount", "swapoff",
|
||||
"modprobe", "insmod", "rmmod", "depmod", "chroot", "pvcreate", "vgcreate", "lvcreate", "lvextend", "lvresize",
|
||||
"resize2fs", "xfs_growfs", "tune2fs", "e2fsck", "fsck", "update-grub", "grub-install", "update-initramfs",
|
||||
"update-alternatives", "ldconfig", "setcap", "newaliases", "postmap", "a2enmod", "a2dismod", "a2ensite",
|
||||
"a2dissite", "locale-gen", "update-ca-certificates", "certbot", "iptables-restore", "ip6tables-restore",
|
||||
"renice", "systemd-tmpfiles", "setenforce", "exportfs", "quotaon", "quotaoff",
|
||||
)
|
||||
|
||||
// writePrograms escriben archivos (sin llegar a administración del sistema).
|
||||
var writePrograms = set(
|
||||
"cp", "mv", "install", "ln", "touch", "mkdir", "tee", "truncate", "patch", "mkfifo", "mknod", "split", "csplit",
|
||||
"make", "cmake", "ninja", "meson", "gcc", "cc", "g++", "c++", "clang", "clang++", "ld", "as", "rustc", "cargo", "go",
|
||||
"javac", "ar", "strip", "zip", "7z", "convert", "sponge", "dos2unix", "unix2dos", "ssh-keygen",
|
||||
)
|
||||
|
||||
// packagePrograms: gestores de paquetes (subcomandos de lectura se tratan aparte).
|
||||
var packageReadVerbs = map[string]map[string]bool{
|
||||
"apt": set("list", "show", "search", "policy", "depends", "rdepends", "showsrc", "changelog", "madison", "help", "moo"),
|
||||
"apt-get": set("check", "changelog", "help"),
|
||||
"dnf": set("list", "info", "search", "repolist", "repoquery", "provides", "whatprovides", "history", "check-update", "help", "deplist", "updateinfo", "module-list"),
|
||||
"yum": set("list", "info", "search", "repolist", "provides", "whatprovides", "history", "check-update", "help", "deplist", "updateinfo"),
|
||||
"zypper": set("search", "se", "info", "if", "list-updates", "lu", "repos", "lr", "packages", "pa", "patches", "lp", "help"),
|
||||
"apk": set("info", "search", "list", "policy", "version", "dot", "stats", "help"),
|
||||
"snap": set("list", "info", "find", "search", "version", "help", "changes", "services", "connections"),
|
||||
"flatpak": set("list", "info", "search", "remote-ls", "remotes", "history", "help"),
|
||||
"pip": set("list", "show", "freeze", "check", "help", "search", "inspect", "debug", "config"),
|
||||
"pip3": set("list", "show", "freeze", "check", "help", "search", "inspect", "debug", "config"),
|
||||
}
|
||||
|
||||
func set(items ...string) map[string]bool {
|
||||
m := make(map[string]bool, len(items))
|
||||
for _, it := range items {
|
||||
m[it] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Classify clasifica un argv. Nunca devuelve una lista de riesgos vacía.
|
||||
func Classify(argv []string) Classification {
|
||||
return classify(argv, 0)
|
||||
}
|
||||
|
||||
func classify(argv []string, depth int) Classification {
|
||||
if len(argv) == 0 {
|
||||
return Classification{Risks: []Risk{RiskReadOnly}}
|
||||
}
|
||||
if depth > maxShellDepth {
|
||||
return Classification{Program: argv[0], Argv: argv, Risks: []Risk{RiskUnknown}}
|
||||
}
|
||||
prog := filepath.Base(argv[0])
|
||||
args := argv[1:]
|
||||
c := Classification{Program: prog, Argv: argv}
|
||||
|
||||
// Un nombre de programa dinámico ($CMD, $(...)) no se puede clasificar.
|
||||
if strings.ContainsAny(argv[0], "$`") {
|
||||
c.Risks = []Risk{RiskUnknown}
|
||||
return c
|
||||
}
|
||||
|
||||
// Envoltorios: el riesgo es el del comando interior.
|
||||
if inner, ok := unwrap(prog, args); ok {
|
||||
if inner == nil {
|
||||
c.Risks = []Risk{RiskUnknown}
|
||||
return c
|
||||
}
|
||||
n := classify(inner, depth+1)
|
||||
n.Nested = append([]Classification(nil), n.Nested...)
|
||||
return n
|
||||
}
|
||||
|
||||
// Intérpretes de shell con -c: se analiza el script.
|
||||
if isShell(prog) {
|
||||
if script, ok := shellDashC(args); ok {
|
||||
s := ParseShell(script)
|
||||
c.Script = &s
|
||||
c.Risks = []Risk{RiskReadOnly}
|
||||
if len(s.Opaque) > 0 {
|
||||
c.Risks = []Risk{RiskUnknown}
|
||||
}
|
||||
for _, sc := range s.Commands {
|
||||
c.Nested = append(c.Nested, classify(sc.Argv, depth+1))
|
||||
}
|
||||
return c
|
||||
}
|
||||
c.Risks = []Risk{RiskUnknown}
|
||||
return c
|
||||
}
|
||||
|
||||
// Consultas de versión/ayuda: seguras para cualquier programa.
|
||||
if len(args) == 1 && (args[0] == "--version" || args[0] == "--help" || args[0] == "-version") {
|
||||
c.Risks = []Risk{RiskReadOnly}
|
||||
return c
|
||||
}
|
||||
|
||||
c.Risks, c.AllArgsArePaths, c.Nested = programRisks(prog, args, depth)
|
||||
return c
|
||||
}
|
||||
|
||||
// programRisks contiene las reglas por programa y subcomando.
|
||||
func programRisks(prog string, args []string, depth int) (risks []Risk, allPaths bool, nested []Classification) {
|
||||
verb := firstNonFlag(args)
|
||||
has := func(flags ...string) bool { return hasAny(args, flags...) }
|
||||
one := func(r Risk) ([]Risk, bool, []Classification) { return []Risk{r}, false, nil }
|
||||
|
||||
switch prog {
|
||||
case "rm", "rmdir", "unlink":
|
||||
return []Risk{RiskDelete}, true, nil
|
||||
case "cp", "mv", "install", "ln", "touch", "mkdir", "truncate", "tee", "mkfifo":
|
||||
return []Risk{RiskWrite}, true, nil
|
||||
case "chmod", "chown", "chgrp", "setfacl", "chattr":
|
||||
return []Risk{RiskAdmin}, true, nil
|
||||
case "shred":
|
||||
return []Risk{RiskDestructive}, true, nil
|
||||
|
||||
case "find":
|
||||
return classifyFind(args, depth)
|
||||
case "xargs":
|
||||
inner := skipFlags(args, "-n", "-I", "-P", "-d", "-L", "-s", "-E", "-a", "--max-args", "--max-procs", "--delimiter", "--arg-file")
|
||||
if len(inner) == 0 {
|
||||
return one(RiskReadOnly) // xargs sin comando ejecuta echo
|
||||
}
|
||||
n := classify(inner, depth+1)
|
||||
return n.Risks, n.AllArgsArePaths, n.Nested
|
||||
|
||||
case "systemctl":
|
||||
switch verb {
|
||||
case "", "status", "is-active", "is-enabled", "is-failed", "is-system-running", "show", "cat", "list-units",
|
||||
"list-unit-files", "list-sockets", "list-timers", "list-jobs", "list-dependencies", "list-machines",
|
||||
"help", "get-default", "show-environment":
|
||||
return one(RiskReadOnly)
|
||||
case "start", "stop", "restart", "reload", "try-restart", "reload-or-restart", "try-reload-or-restart",
|
||||
"enable", "disable", "reenable", "mask", "unmask", "daemon-reload", "daemon-reexec", "kill", "reset-failed",
|
||||
"condrestart", "force-reload", "preset", "revert":
|
||||
return one(RiskService)
|
||||
case "reboot", "poweroff", "halt", "kexec", "suspend", "hibernate", "hybrid-sleep", "suspend-then-hibernate",
|
||||
"rescue", "emergency", "default", "isolate", "soft-reboot", "switch-root":
|
||||
return one(RiskReboot)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "service":
|
||||
if has("--status-all") || len(args) >= 2 && args[1] == "status" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskService)
|
||||
case "reboot", "shutdown", "poweroff", "halt":
|
||||
return one(RiskReboot)
|
||||
case "init", "telinit":
|
||||
if verb == "0" || verb == "6" {
|
||||
return one(RiskReboot)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
|
||||
case "apt", "apt-get", "aptitude", "dnf", "yum", "microdnf", "zypper", "apk", "snap", "flatpak", "pip", "pip3":
|
||||
if verb == "" || packageReadVerbs[prog][verb] {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return []Risk{RiskPackage, RiskNetwork}, false, nil
|
||||
case "apt-mark", "dpkg-reconfigure", "debconf-set-selections", "add-apt-repository":
|
||||
if prog == "apt-mark" && (verb == "showhold" || verb == "showauto" || verb == "showmanual") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskPackage)
|
||||
case "dpkg":
|
||||
if has("-i", "--install", "-r", "--remove", "-P", "--purge", "--configure", "--unpack", "-a", "--set-selections",
|
||||
"--clear-selections", "--add-architecture", "--remove-architecture", "-x", "--extract", "-b", "--build") {
|
||||
return one(RiskPackage)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "rpm":
|
||||
if has("-i", "--install", "-U", "--upgrade", "-e", "--erase", "-F", "--freshen", "--import", "--rebuilddb") {
|
||||
return one(RiskPackage)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "pacman":
|
||||
if verb == "" && len(args) > 0 {
|
||||
f := args[0]
|
||||
if strings.HasPrefix(f, "-Q") || strings.HasPrefix(f, "-F") || f == "-Ss" || f == "-Si" || f == "-Sl" || f == "-Sg" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
} else if len(args) > 0 && (strings.HasPrefix(args[0], "-Q") || args[0] == "-Ss" || args[0] == "-Si") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return []Risk{RiskPackage, RiskNetwork}, false, nil
|
||||
|
||||
case "ip":
|
||||
return classifyIP(args)
|
||||
case "nft":
|
||||
if verb == "list" || verb == "describe" || has("-c", "--check") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "iptables", "ip6tables", "iptables-legacy", "ip6tables-legacy", "iptables-nft", "ip6tables-nft", "ebtables", "arptables":
|
||||
if onlyFlags(args, "-L", "--list", "-S", "--list-rules", "-n", "--numeric", "-v", "--verbose", "-t", "--table", "-x", "--line-numbers", "-C", "--check") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "iptables-save", "ip6tables-save":
|
||||
return one(RiskReadOnly)
|
||||
case "ufw":
|
||||
if verb == "status" || verb == "show" || verb == "app" && len(args) > 1 && (args[1] == "list" || args[1] == "info") || verb == "version" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "firewall-cmd":
|
||||
for _, a := range args {
|
||||
if !(strings.HasPrefix(a, "--list") || strings.HasPrefix(a, "--get") || strings.HasPrefix(a, "--query") ||
|
||||
a == "--state" || strings.HasPrefix(a, "--zone") || a == "--permanent" || strings.HasPrefix(a, "--info")) {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "tc", "bridge":
|
||||
if verb == "" || containsWord(args, "show", "list", "ls") && !containsWord(args, "add", "del", "delete", "replace", "change", "set", "flush") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "nmcli":
|
||||
if containsWord(args, "modify", "up", "down", "add", "delete", "del", "reload", "import", "connect", "disconnect", "on", "off", "set", "edit", "clone") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "networkctl", "resolvectl", "loginctl", "hostnamectl", "timedatectl", "localectl", "busctl", "coredumpctl", "machinectl":
|
||||
if verb == "" || verb == "status" || verb == "show" || verb == "query" || verb == "list" || strings.HasPrefix(verb, "list") ||
|
||||
verb == "statistics" || verb == "dns" || verb == "domain" || verb == "info" || verb == "cat-config" || verb == "timesync-status" ||
|
||||
verb == "show-timesync" || verb == "tree" || verb == "introspect" || verb == "user-status" || verb == "session-status" || verb == "seat-status" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "ethtool":
|
||||
if has("-s", "--change", "-K", "--features", "--offload", "-G", "--set-ring", "-A", "--pause", "-C", "--coalesce",
|
||||
"-L", "--set-channels", "-E", "--change-eeprom", "-f", "--flash", "-r", "--negotiate", "-p", "--identify", "--reset", "-X", "-N", "-U") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "iw", "wg":
|
||||
if containsWord(args, "set", "add", "del", "connect", "disconnect", "setconf", "addconf", "syncconf") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
|
||||
case "mount":
|
||||
if len(args) == 0 || onlyFlags(args, "-l", "--show-labels", "-t", "--types", "-v") && !hasNonFlag(args, "-t", "--types") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "swapon":
|
||||
if len(args) == 0 || has("--show", "-s", "--summary") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "sysctl":
|
||||
for _, a := range args {
|
||||
if a == "-w" || a == "--write" || a == "-p" || a == "--load" || a == "--system" || strings.Contains(a, "=") && !strings.HasPrefix(a, "-") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "hostname":
|
||||
if verb == "" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "date":
|
||||
if has("-s", "--set") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "dmesg":
|
||||
if has("-c", "-C", "--clear", "--read-clear", "-D", "-E", "-n", "--console-off", "--console-on", "--console-level") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "journalctl":
|
||||
for _, a := range args {
|
||||
if strings.HasPrefix(a, "--vacuum") {
|
||||
return []Risk{RiskDelete}, false, nil
|
||||
}
|
||||
if a == "--rotate" || a == "--flush" || a == "--relinquish-var" || a == "--sync" || a == "--setup-keys" {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "crontab":
|
||||
if has("-l") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "top", "htop", "iotop", "atop":
|
||||
if has("-b", "--batch") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskUnknown) // interactivo: se colgaría hasta el timeout
|
||||
case "kill":
|
||||
if has("-l", "-L", "--list", "--table") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "smartctl":
|
||||
if has("-t", "--test", "-s", "--smart", "-o", "--offlineauto", "-S", "--saveauto", "-X", "--abort") {
|
||||
return one(RiskAdmin)
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "cryptsetup":
|
||||
switch verb {
|
||||
case "status", "luksDump", "isLuks", "luksUUID", "tcryptDump", "bitlkDump":
|
||||
return one(RiskReadOnly)
|
||||
case "luksFormat", "erase", "luksErase", "reencrypt", "luksKillSlot", "luksRemoveKey":
|
||||
return one(RiskDestructive)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "zfs":
|
||||
if verb == "destroy" {
|
||||
return one(RiskDestructive)
|
||||
}
|
||||
if verb == "list" || verb == "get" || verb == "status" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "lvm", "lvs", "vgs", "pvs", "lvdisplay", "vgdisplay", "pvdisplay", "pvscan", "vgscan", "lvscan":
|
||||
return one(RiskReadOnly)
|
||||
|
||||
case "sed":
|
||||
for _, a := range args {
|
||||
if a == "-i" || strings.HasPrefix(a, "-i") && !strings.HasPrefix(a, "--") || strings.HasPrefix(a, "--in-place") ||
|
||||
len(a) > 1 && a[0] == '-' && a[1] != '-' && strings.Contains(a, "i") {
|
||||
return []Risk{RiskWrite}, true, nil
|
||||
}
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "awk", "gawk", "mawk", "nawk", "perl", "python", "python3", "ruby", "node", "php", "lua", "tclsh", "Rscript":
|
||||
// Lenguajes con E/S y system(): no se pueden evaluar.
|
||||
return one(RiskUnknown)
|
||||
case "tar", "bsdtar":
|
||||
// Modo de listado: "tf", "-tzf", "--list" (sin crear ni extraer).
|
||||
mode := ""
|
||||
if len(args) > 0 && !strings.HasPrefix(args[0], "--") {
|
||||
mode = strings.TrimPrefix(args[0], "-")
|
||||
}
|
||||
if has("--list") || strings.Contains(mode, "t") && !strings.ContainsAny(mode, "cxruA") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
case "unzip":
|
||||
if has("-l", "-v", "-t", "-Z", "-p") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
case "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz", "zstd", "unzstd", "lz4", "lzma":
|
||||
if has("-l", "--list", "-t", "--test", "-c", "--stdout", "--to-stdout") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return []Risk{RiskWrite}, true, nil
|
||||
case "git":
|
||||
switch verb {
|
||||
case "status", "log", "diff", "show", "branch", "rev-parse", "ls-files", "blame", "describe", "shortlog",
|
||||
"grep", "cat-file", "ls-tree", "reflog", "config", "help", "version", "whatchanged", "tag", "remote":
|
||||
if verb == "config" && !has("--get", "--list", "-l", "--get-all", "--get-regexp") ||
|
||||
verb == "remote" && hasNonFlagAfterVerb(args, "remote") && !containsWord(args, "show", "-v", "get-url") ||
|
||||
verb == "tag" && hasNonFlagAfterVerb(args, "tag") && !has("-l", "--list") ||
|
||||
verb == "branch" && hasNonFlagAfterVerb(args, "branch") && !has("-l", "--list", "-a", "-r", "-v", "-vv") {
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "clone", "fetch", "pull", "push", "submodule":
|
||||
return []Risk{RiskNetwork, RiskWrite}, false, nil
|
||||
case "ls-remote":
|
||||
return one(RiskNetwork)
|
||||
}
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
case "curl":
|
||||
if has("-o", "--output", "-O", "--remote-name", "--remote-name-all", "-T", "--upload-file", "-c", "--cookie-jar", "-D", "--dump-header") {
|
||||
return []Risk{RiskNetwork, RiskDownload, RiskWrite}, false, nil
|
||||
}
|
||||
return one(RiskNetwork)
|
||||
case "wget", "aria2c":
|
||||
if has("-O-", "--output-document=-", "--spider", "-qO-") || has("-O") && containsWord(args, "-") {
|
||||
return one(RiskNetwork)
|
||||
}
|
||||
return []Risk{RiskNetwork, RiskDownload, RiskWrite}, false, nil
|
||||
case "scp", "rsync":
|
||||
r := []Risk{RiskWrite}
|
||||
for _, a := range args {
|
||||
if strings.Contains(a, ":") && !strings.HasPrefix(a, "-") {
|
||||
r = append(r, RiskNetwork)
|
||||
break
|
||||
}
|
||||
}
|
||||
return r, prog == "scp", nil
|
||||
case "openssl":
|
||||
switch verb {
|
||||
case "s_client", "s_time", "ocsp":
|
||||
return one(RiskNetwork)
|
||||
case "genrsa", "genpkey", "req", "ca", "rand", "enc", "pkcs12", "dhparam", "ecparam", "x509", "rsa", "ec", "pkey", "dgst", "smime", "cms":
|
||||
if has("-out", "-keyout", "-new", "-newkey") {
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
}
|
||||
}
|
||||
return one(RiskReadOnly)
|
||||
case "nginx", "apache2ctl", "apachectl", "httpd":
|
||||
if has("-t", "-T", "-v", "-V", "-S", "-M", "-l", "-L") || verb == "configtest" || verb == "-t" {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskService)
|
||||
case "sshd":
|
||||
if has("-t", "-T", "-G") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskService)
|
||||
case "visudo":
|
||||
if has("-c", "--check") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
return one(RiskAdmin)
|
||||
case "docker", "podman", "nerdctl":
|
||||
switch verb {
|
||||
case "ps", "images", "inspect", "logs", "info", "version", "stats", "top", "port", "diff", "history", "events":
|
||||
return one(RiskReadOnly)
|
||||
case "rm", "rmi", "prune", "system":
|
||||
return []Risk{RiskAdmin, RiskDelete}, false, nil
|
||||
}
|
||||
return []Risk{RiskAdmin, RiskNetwork}, false, nil
|
||||
case "command":
|
||||
if has("-v", "-V") {
|
||||
return one(RiskReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case readOnlyPrograms[prog]:
|
||||
return one(RiskReadOnly)
|
||||
case networkPrograms[prog]:
|
||||
return one(RiskNetwork)
|
||||
case destructivePrograms[prog] || strings.HasPrefix(prog, "mkfs."):
|
||||
return []Risk{RiskDestructive}, true, nil
|
||||
case adminPrograms[prog]:
|
||||
return one(RiskAdmin)
|
||||
case writePrograms[prog]:
|
||||
return []Risk{RiskWrite}, false, nil
|
||||
case prog == "eval" || prog == "source" || prog == "." || prog == "exec" || prog == "trap" || prog == "alias":
|
||||
return one(RiskUnknown)
|
||||
}
|
||||
return one(RiskUnknown)
|
||||
}
|
||||
|
||||
// unwrap desenvuelve comandos que ejecutan otro comando.
|
||||
// Devuelve (nil, true) si es un envoltorio pero no se pudo extraer el comando.
|
||||
func unwrap(prog string, args []string) ([]string, bool) {
|
||||
switch prog {
|
||||
case "sudo", "doas":
|
||||
if hasAny(args, "-s", "--shell", "-i", "--login", "-e", "--edit", "-l", "--list", "-v", "--validate", "-k", "-K") {
|
||||
if hasAny(args, "-l", "--list", "-v", "-k", "-K") && firstNonFlag(args) == "" {
|
||||
return []string{"true"}, true
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
return orNil(skipFlags(args, "-u", "--user", "-g", "--group", "-C", "--close-from", "-h", "--host", "-p", "--prompt", "-r", "--role", "-t", "--type", "-U", "--other-user", "-D", "--chdir", "-T", "--command-timeout")), true
|
||||
case "env":
|
||||
rest := skipFlags(args, "-u", "--unset", "-C", "--chdir", "-S", "--split-string")
|
||||
for len(rest) > 0 && assignmentRe.MatchString(rest[0]) {
|
||||
rest = rest[1:]
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
return []string{"printenv"}, true
|
||||
}
|
||||
return rest, true
|
||||
case "nice":
|
||||
return orNil(skipFlags(args, "-n", "--adjustment")), true
|
||||
case "ionice":
|
||||
return orNil(skipFlags(args, "-c", "--class", "-n", "--classdata", "-p", "--pid", "-P", "--pgid", "-u", "--uid")), true
|
||||
case "nohup", "setsid", "unbuffer", "builtin", "time", "chronic":
|
||||
return orNil(skipFlags(args)), true
|
||||
case "stdbuf":
|
||||
return orNil(skipFlags(args, "-i", "-o", "-e")), true
|
||||
case "timeout":
|
||||
rest := skipFlags(args, "-s", "--signal", "-k", "--kill-after")
|
||||
if len(rest) > 0 {
|
||||
rest = rest[1:] // duración
|
||||
}
|
||||
return orNil(rest), true
|
||||
case "watch":
|
||||
return orNil(skipFlags(args, "-n", "--interval", "-d")), true
|
||||
case "taskset":
|
||||
rest := skipFlags(args, "-c")
|
||||
if len(rest) > 0 && !hasAny(args, "-p", "--pid") {
|
||||
rest = rest[1:] // máscara
|
||||
}
|
||||
return orNil(rest), true
|
||||
case "chrt":
|
||||
rest := skipFlags(args)
|
||||
if len(rest) > 0 {
|
||||
rest = rest[1:] // prioridad
|
||||
}
|
||||
return orNil(rest), true
|
||||
case "busybox":
|
||||
return orNil(args), true
|
||||
case "su":
|
||||
for i, a := range args {
|
||||
if (a == "-c" || a == "--command") && i+1 < len(args) {
|
||||
return []string{"sh", "-c", args[i+1]}, true
|
||||
}
|
||||
}
|
||||
return nil, true
|
||||
case "command":
|
||||
if hasAny(args, "-v", "-V") {
|
||||
return nil, false // lo gestiona programRisks
|
||||
}
|
||||
return orNil(skipFlags(args, "-p")), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func orNil(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func isShell(prog string) bool {
|
||||
switch prog {
|
||||
case "sh", "bash", "dash", "zsh", "ksh", "ash", "mksh", "fish", "rbash":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shellDashC busca el script de "sh -c SCRIPT" (también -lc, -ec, ...).
|
||||
func shellDashC(args []string) (string, bool) {
|
||||
for i, a := range args {
|
||||
if len(a) > 1 && a[0] == '-' && a[1] != '-' && strings.Contains(a, "c") {
|
||||
if i+1 < len(args) {
|
||||
return args[i+1], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasPrefix(a, "-") {
|
||||
return "", false // sh script.sh: contenido no analizado
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func classifyFind(args []string, depth int) ([]Risk, bool, []Classification) {
|
||||
risks := []Risk{RiskReadOnly}
|
||||
var nested []Classification
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "-delete":
|
||||
risks = append(risks, RiskDelete)
|
||||
case "-fprint", "-fprint0", "-fprintf", "-fls":
|
||||
risks = append(risks, RiskWrite)
|
||||
case "-exec", "-execdir", "-ok", "-okdir":
|
||||
j := i + 1
|
||||
for j < len(args) && args[j] != ";" && args[j] != "+" {
|
||||
j++
|
||||
}
|
||||
nested = append(nested, classify(args[i+1:j], depth+1))
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return risks, false, nested
|
||||
}
|
||||
|
||||
func classifyIP(args []string) ([]Risk, bool, []Classification) {
|
||||
var words []string
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
if strings.HasPrefix(a, "-") {
|
||||
if a == "-n" || a == "-netns" || a == "-b" || a == "-batch" || a == "-rc" || a == "-rcvbuf" || a == "-f" || a == "-family" {
|
||||
if a == "-b" || a == "-batch" {
|
||||
return []Risk{RiskAdmin}, false, nil // ejecuta comandos de un archivo
|
||||
}
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
words = append(words, a)
|
||||
}
|
||||
if len(words) < 2 {
|
||||
return []Risk{RiskReadOnly}, false, nil
|
||||
}
|
||||
switch words[1] {
|
||||
case "show", "list", "ls", "lst", "get", "help", "monitor", "sh", "s", "l":
|
||||
return []Risk{RiskReadOnly}, false, nil
|
||||
}
|
||||
if words[0] == "netns" && (words[1] == "identify" || words[1] == "pids") {
|
||||
return []Risk{RiskReadOnly}, false, nil
|
||||
}
|
||||
return []Risk{RiskAdmin}, false, nil
|
||||
}
|
||||
|
||||
// firstNonFlag devuelve el primer argumento que no es una opción.
|
||||
func firstNonFlag(args []string) string {
|
||||
for _, a := range args {
|
||||
if !strings.HasPrefix(a, "-") {
|
||||
return a
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// skipFlags elimina opciones iniciales; las de valueFlags consumen un valor.
|
||||
func skipFlags(args []string, valueFlags ...string) []string {
|
||||
vf := set(valueFlags...)
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
a := args[i]
|
||||
if a == "--" {
|
||||
return args[i+1:]
|
||||
}
|
||||
if !strings.HasPrefix(a, "-") || a == "-" {
|
||||
break
|
||||
}
|
||||
if vf[a] {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i > len(args) {
|
||||
return nil
|
||||
}
|
||||
return args[i:]
|
||||
}
|
||||
|
||||
func hasAny(args []string, flags ...string) bool {
|
||||
for _, a := range args {
|
||||
for _, f := range flags {
|
||||
if a == f || strings.HasPrefix(f, "--") && strings.HasPrefix(a, f+"=") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsWord(args []string, words ...string) bool {
|
||||
return hasAny(args, words...)
|
||||
}
|
||||
|
||||
// onlyFlags informa si todos los argumentos que son opciones pertenecen a
|
||||
// allowed (los valores no-opción se permiten, p.ej. nombres de cadena).
|
||||
func onlyFlags(args []string, allowed ...string) bool {
|
||||
al := set(allowed...)
|
||||
for _, a := range args {
|
||||
if strings.HasPrefix(a, "-") {
|
||||
// Opciones combinadas tipo -nvL
|
||||
if len(a) > 2 && a[1] != '-' {
|
||||
for _, ch := range a[1:] {
|
||||
if !al["-"+string(ch)] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !al[a] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hasNonFlag indica si hay argumentos posicionales, ignorando los valores
|
||||
// de las opciones valueFlags.
|
||||
func hasNonFlag(args []string, valueFlags ...string) bool {
|
||||
vf := set(valueFlags...)
|
||||
for i := 0; i < len(args); i++ {
|
||||
if vf[args[i]] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(args[i], "-") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasNonFlagAfterVerb(args []string, verb string) bool {
|
||||
seen := false
|
||||
for _, a := range args {
|
||||
if !seen {
|
||||
seen = a == verb
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(a, "-") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user