use super::super::shell_lex::DASH_C_SHELLS;
use super::credential_print_taint::is_identifier;
const SECURITY_OPTS_WITH_ARG: &[char] = &[
'a', 'c', 'C', 'd', 'D', 'G', 'j', 'l', 'p', 'P', 'r', 's', 't',
];
const GREP_OPTS_WITH_ARG: &[char] = &['e', 'f', 'm', 'A', 'B', 'C', 'd', 'D'];
const RG_OPTS_WITH_ARG: &[char] = &[
'e', 'f', 'g', 't', 'T', 'm', 'A', 'B', 'C', 'E', 'j', 'M', 'r',
];
const GREP_LONG_WITH_ARG: &[&str] = &[
"--regexp",
"--file",
"--max-count",
"--after-context",
"--before-context",
"--context",
"--include",
"--exclude",
"--label",
"--glob",
"--type",
"--replace",
];
const EVALUATORS: &[&str] = &[
"eval",
"source",
".",
"osascript",
"ssh",
"python",
"python3",
"perl",
"ruby",
"node",
"deno",
"php",
"fish",
"bun",
"lua",
"luajit",
"pwsh",
"powershell",
];
pub(crate) fn basename(word: &str) -> String {
let word = word.strip_prefix('\\').unwrap_or(word);
word.rsplit('/').next().unwrap_or(word).to_ascii_lowercase()
}
fn is(a: &str, want: &str) -> bool {
a.eq_ignore_ascii_case(want)
}
pub(super) fn credential_fds(program: &str, args: &[String]) -> (bool, bool) {
match program {
"security" => security_fds(args),
"gcloud" => {
let token = args.iter().position(|a| is(a, "auth")).is_some_and(|at| {
args[at + 1..]
.iter()
.any(|a| is(a, "print-access-token") || is(a, "print-identity-token"))
});
let helper = args.iter().position(|a| is(a, "config")).is_some_and(|at| {
args[at + 1..].iter().any(|a| is(a, "config-helper"))
&& config_helper_prints(&args[at + 1..])
});
(token || helper, false)
}
_ => (false, false),
}
}
fn security_fds(args: &[String]) -> (bool, bool) {
let Some(sub) = args.iter().position(|a| {
is(a, "find-generic-password") || is(a, "find-internet-password") || is(a, "dump-keychain")
}) else {
return (false, false);
};
let dump = is(&args[sub], "dump-keychain");
let (mut w, mut g) = (false, false);
let mut rest = args[sub + 1..].iter();
while let Some(tok) = rest.next() {
let Some(cluster) = tok.strip_prefix('-').filter(|c| !c.starts_with('-')) else {
continue;
};
for (i, ch) in cluster.char_indices() {
match ch {
'd' if dump => w = true,
_ if dump => {}
'w' => w = true,
'g' => g = true,
c if SECURITY_OPTS_WITH_ARG.contains(&c) => {
if i + c.len_utf8() == cluster.len() {
rest.next();
}
break;
}
_ => {}
}
}
}
(w, g)
}
fn config_helper_prints(args: &[String]) -> bool {
let mut format = None;
for (n, a) in args.iter().enumerate() {
if let Some(v) = a.strip_prefix("--format=") {
format = Some(v.to_string());
} else if a == "--format" {
format = args.get(n + 1).cloned();
}
}
match format {
Some(f) if f.contains('(') => {
let f = f.to_ascii_lowercase();
f.contains("credential") || f.contains("token")
}
_ => true,
}
}
pub(super) fn first_credential_program(argv: &[String]) -> Option<usize> {
argv.iter()
.position(|w| matches!(basename(w).as_str(), "security" | "gcloud"))
}
pub(super) fn consumes_stdin(program: &str, args: &[String]) -> bool {
let has = |flag: &str| args.iter().any(|a| a == flag);
let has_word = |w: &str| args.iter().any(|a| is(a, w));
match program {
"pbcopy" => true,
"wc" => args.iter().all(|a| wc_flag_ok(a)),
"grep" | "egrep" | "fgrep" => grep_quiet(args, GREP_OPTS_WITH_ARG),
"rg" => grep_quiet(args, RG_OPTS_WITH_ARG),
"docker" | "podman" | "helm" => has_word("login") && has("--password-stdin"),
"gh" => has_word("auth") && has_word("login") && has("--with-token"),
_ => false,
}
}
fn wc_flag_ok(a: &str) -> bool {
if let Some(long) = a.strip_prefix("--") {
return matches!(long, "bytes" | "lines" | "words" | "chars" | "");
}
match a.strip_prefix('-') {
Some(cluster) if !cluster.is_empty() => cluster.chars().all(|c| "clwm".contains(c)),
_ => true,
}
}
fn grep_quiet(args: &[String], with_arg: &[char]) -> bool {
let mut i = 0;
while let Some(a) = args.get(i) {
i += 1;
if a == "--" {
return false;
}
if a == "--quiet" || a == "--silent" {
return true;
}
if a.starts_with("--") {
if GREP_LONG_WITH_ARG.contains(&a.as_str()) {
i += 1;
}
continue;
}
let Some(cluster) = a.strip_prefix('-') else {
continue;
};
for (at, ch) in cluster.char_indices() {
if ch == 'q' {
return true;
}
if with_arg.contains(&ch) {
if at + ch.len_utf8() == cluster.len() {
i += 1;
}
break;
}
}
}
false
}
pub(super) fn enables_xtrace(program: &str, argv: &[String], args: &[String]) -> bool {
if argv
.iter()
.any(|w| w.starts_with("SHELLOPTS=") && w.to_ascii_lowercase().contains("xtrace"))
{
return true;
}
match program {
"set" => options_enable_xtrace(args),
"setopt" => args.iter().any(|a| is_xtrace_name(a)),
p if DASH_C_SHELLS.contains(&p) || p == "fish" => options_enable_xtrace(args),
_ => false,
}
}
fn is_xtrace_name(name: &str) -> bool {
name.to_ascii_lowercase().replace('_', "") == "xtrace"
}
fn options_enable_xtrace(args: &[String]) -> bool {
let mut i = 0;
while let Some(a) = args.get(i) {
i += 1;
let Some(cluster) = a.strip_prefix('-').filter(|c| !c.starts_with('-')) else {
return false;
};
if cluster.contains('x') {
return true;
}
if cluster.ends_with('o') {
if args.get(i).is_some_and(|v| is_xtrace_name(v)) {
return true;
}
i += 1;
continue;
}
if cluster.ends_with('c') {
return false;
}
}
false
}
pub(crate) fn code_operands<'a>(program: &str, args: &'a [String]) -> Vec<&'a str> {
let all = || args.iter().map(String::as_str).collect();
let (short, long): (&[char], &[&str]) = match program {
"eval" | "source" | "." | "ssh" | "pwsh" | "powershell" => return all(),
"deno" if args.first().is_some_and(|a| a == "eval") => return all(),
"deno" => return Vec::new(),
"perl" => (&['e', 'E'], &[]),
"ruby" | "osascript" => (&['e'], &[]),
"node" | "bun" => (&['e', 'p'], &["--eval", "--print"]),
"lua" | "luajit" => (&['e'], &[]),
"php" => (&['r'], &[]),
_ => (&['c'], &["--command"]),
};
let mut code = Vec::new();
let mut i = 0;
while let Some(a) = args.get(i) {
i += 1;
if let Some((_, value)) = a.split_once('=').filter(|(n, _)| long.contains(n)) {
code.push(value);
continue;
}
let cluster = a.strip_prefix('-').filter(|c| !c.starts_with('-'));
let at = cluster.and_then(|c| c.find(|ch| short.contains(&ch)).map(|at| &c[at + 1..]));
match at {
Some("") => {}
Some(attached) => {
code.push(attached);
continue;
}
None if long.contains(&a.as_str()) => {}
None => continue,
}
if let Some(value) = args.get(i) {
code.push(value);
i += 1;
}
}
code
}
pub(crate) fn is_evaluator(program: &str) -> bool {
evaluator_name(program).is_some()
}
pub(crate) fn evaluator_name(program: &str) -> Option<&str> {
let known = |p: &str| EVALUATORS.contains(&p) || DASH_C_SHELLS.contains(&p);
if known(program) {
return Some(program);
}
let bare = program.trim_end_matches(|c: char| c.is_ascii_digit() || matches!(c, '.' | '-'));
(bare.len() < program.len() && !bare.is_empty() && known(bare)).then_some(bare)
}
pub(crate) const KEYWORDS: &[&str] = &[
"!", "{", "}", "if", "then", "else", "elif", "do", "while", "until", "time", "coproc",
];
pub(crate) const COMPOUND_OPENERS: &[&str] =
&["{", "while", "until", "if", "for", "select", "case", "[["];
pub(super) fn keyword_words(argv: &[String], from: usize) -> (usize, bool) {
let mut at = from;
let mut coproc = false;
while let Some(word) = argv.get(at).map(String::as_str)
&& KEYWORDS.contains(&word)
{
at += 1;
at += usize::from(word == "time" && argv.get(at).is_some_and(|w| w == "-p"));
if word == "coproc" {
coproc = true;
let named = argv.get(at).is_some_and(|n| is_identifier(n))
&& argv
.get(at + 1)
.is_some_and(|o| COMPOUND_OPENERS.contains(&o.as_str()));
at += usize::from(named);
}
}
(at - from, coproc)
}