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 }