pub(crate) const READ_ONLY_BINARIES: &[&str] = &[
"ls",
"cat",
"bat",
"head",
"tail",
"wc",
"stat",
"file",
"pwd",
"echo",
"printf",
"grep",
"egrep",
"fgrep",
"rg",
"ag",
"ack",
"fd",
"tree",
"du",
"df",
"basename",
"dirname",
"realpath",
"readlink",
"whoami",
"id",
"date",
"env",
"printenv",
"which",
"type",
"uname",
"hostname",
"cksum",
"md5sum",
"sha1sum",
"sha256sum",
"diff",
"cmp",
"sort",
"uniq",
"cut",
"tr",
"column",
"less",
"more",
"jq",
"yq",
"true",
"false",
"test",
"[",
"nl",
"tac",
"rev",
"comm",
"join",
"paste",
"fold",
"fmt",
"expand",
"unexpand",
"xxd",
"od",
"hexdump",
"strings",
"nm",
"objdump",
"readelf",
"size",
"sha224sum",
"sha384sum",
"sha512sum",
"b2sum",
"ps",
"groups",
"logname",
"arch",
"nproc",
"uptime",
"free",
"vmstat",
"lscpu",
"lsblk",
"lsusb",
"lspci",
"tty",
"cd",
"pushd",
"popd",
"dirs",
"base64",
"seq",
];
pub(crate) const PS_READ_ONLY_CMDLETS: &[&str] = &[
"get-content",
"get-childitem",
"get-item",
"get-itemproperty",
"get-location",
"get-date",
"get-command",
"get-alias",
"get-variable",
"get-process",
"get-service",
"get-member",
"get-history",
"get-psdrive",
"get-filehash",
"get-host",
"get-error",
"select-string",
"test-path",
"resolve-path",
"split-path",
"join-path",
"compare-object",
"out-string",
"write-output",
"write-host",
"dir",
"gc",
"gci",
"gi",
"gl",
"gal",
"gv",
"gps",
"gsv",
"gm",
"gcm",
"sls",
];
pub(crate) const GIT_READ_ONLY: &[&str] = &[
"status",
"log",
"diff",
"show",
"remote",
"describe",
"rev-parse",
"blame",
"ls-files",
"ls-tree",
"cat-file",
"shortlog",
"reflog",
"whatchanged",
"grep",
"rev-list",
"merge-base",
"show-ref",
"for-each-ref",
"name-rev",
"show-branch",
"count-objects",
"version",
];
pub(crate) const NETWORK_BINARIES: &[&str] = &[
"curl", "wget", "nc", "ncat", "netcat", "socat", "ssh", "scp", "sftp", "rsync", "ftp", "telnet",
];
pub(crate) const PROCESS_BINARIES: &[&str] = &[
"python",
"python2",
"python3",
"node",
"deno",
"bun",
"ruby",
"perl",
"php",
"bash",
"sh",
"zsh",
"fish",
"pwsh",
"powershell",
"cargo",
"npm",
"pnpm",
"yarn",
"make",
"docker",
"kubectl",
"go",
"java",
];
pub(crate) const WRAPPERS: &[&str] = &[
"sudo", "doas", "env", "nohup", "time", "nice", "setsid", "stdbuf", "command", "xargs", "then",
"else", "do",
];
pub(crate) fn redirect_target_after(tok: &str) -> Option<&str> {
let rest = tok.trim_start_matches(|c: char| c.is_ascii_digit());
if let Some(r) = rest.strip_prefix("&>") {
return Some(r.trim_start_matches('>'));
}
let after = rest.strip_prefix('>')?;
if after.starts_with('&') {
return None;
}
Some(after.trim_start_matches('>'))
}
pub(crate) fn redirect_write_target(tokens: &[String], i: usize) -> Option<&str> {
let after = redirect_target_after(&tokens[i])?;
let raw = if after.is_empty() {
tokens.get(i + 1).map(String::as_str)?
} else {
after
};
Some(
raw.trim_end_matches([';', '&', '|'])
.trim_matches(['"', '\'']),
)
}
pub(crate) fn is_safe_device_write(path: &str) -> bool {
const SAFE_DEVICES: &[&str] = &[
"/dev/null",
"/dev/zero",
"/dev/full",
"/dev/tty",
"/dev/stdin",
"/dev/stdout",
"/dev/stderr",
"/dev/random",
"/dev/urandom",
];
SAFE_DEVICES.contains(&path) || path.starts_with("/dev/fd/")
}