use crate::lazy_pattern;
lazy_pattern!(
pub(crate) RE_OPAQUE_MCP_ENDPOINT,
r"(?i)(?:^|[:/@.\s])(ngrok|trycloudflare|workers\.dev|raw\.githubusercontent\.com|pastebin\.com)(?:$|[:/.])"
);
lazy_pattern!(
pub(crate) RE_MCP_NO_AUTH,
r#"(?is)("auth"\s*:\s*"none"|authentication\s*:\s*none|no auth|without auth|auth\s*:\s*none)"#
);
lazy_pattern!(
pub(crate) RE_MCP_INLINE_SECRET,
r#"(?is)(bearer\s+[A-Za-z0-9._-]{8,}|authorization\s*:\s*bearer\s+[A-Za-z0-9._-]{8,}|api[_-]?key["']?\s*[:=]\s*["']?[A-Za-z0-9._-]{8,}|_authtoken=|token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]{8,})"#
);
lazy_pattern!(
pub(crate) RE_MCP_PERMISSIVE_TOOLS,
r#"(?is)("tools"\s*:\s*\[[^\]]*"\*"|allow_all_tools|all_tools|tool_permissions\s*:\s*"all"|expose all tools)"#
);
lazy_pattern!(pub(crate) RE_QUOTED_TOOL_NAME, r#""([A-Za-z0-9._:-]{2,})""#);
lazy_pattern!(pub(crate) RE_MCP_TOOLS_ARRAY, r#"(?is)"tools"\s*:\s*\[([^\]]+)\]"#);
lazy_pattern!(pub(crate) RE_GENERIC_URL, r#"https?://[^\s"']+"#);
lazy_pattern!(pub(crate) RE_SHELL_SOURCE, r"(?m)^\s*\.\s+\S");
pub(crate) fn line_invokes_shell_or_interpreter(line: &str) -> bool {
line.split_whitespace().any(|token| {
let basename = token.rsplit(['/', '\\']).next().unwrap_or(token);
let mut basename_lower = basename.to_ascii_lowercase();
if basename_lower.ends_with(".exe") {
basename_lower.truncate(basename_lower.len() - 4);
}
matches!(
basename_lower.as_str(),
"bash"
| "sh"
| "dash"
| "zsh"
| "fish"
| "ksh"
| "csh"
| "tcsh"
| "pwsh"
| "powershell"
| "python"
| "node"
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_invokes_shell_or_interpreter_detects_bash_at_column_zero() {
assert!(line_invokes_shell_or_interpreter("bash install.sh"));
}
#[test]
fn line_invokes_shell_or_interpreter_detects_sh_at_column_zero() {
assert!(line_invokes_shell_or_interpreter("sh install.sh"));
}
#[test]
fn line_invokes_shell_or_interpreter_detects_absolute_interpreter_path() {
assert!(line_invokes_shell_or_interpreter("/bin/bash setup.sh"));
assert!(line_invokes_shell_or_interpreter("/usr/bin/python -c 'x'"));
}
#[test]
fn line_invokes_shell_or_interpreter_strips_exe_suffix() {
assert!(line_invokes_shell_or_interpreter(
"c:\\windows\\python.exe -c x"
));
}
#[test]
fn line_invokes_shell_or_interpreter_strips_exe_suffix_case_insensitive() {
assert!(line_invokes_shell_or_interpreter(
"C:\\Windows\\System32\\POWERSHELL.EXE -c x"
));
assert!(line_invokes_shell_or_interpreter("bash.EXE script.sh"));
assert!(line_invokes_shell_or_interpreter("python.Exe -c x"));
}
#[test]
fn line_invokes_shell_or_interpreter_handles_windows_backslash_path() {
assert!(line_invokes_shell_or_interpreter("c:\\python\\python -c x"));
}
#[test]
fn line_invokes_shell_or_interpreter_rejects_publish_word() {
assert!(!line_invokes_shell_or_interpreter("publish docs"));
}
#[test]
fn line_invokes_shell_or_interpreter_rejects_finish_word() {
assert!(!line_invokes_shell_or_interpreter("finish setup"));
assert!(!line_invokes_shell_or_interpreter("wash assets"));
assert!(!line_invokes_shell_or_interpreter("polish reports"));
}
#[test]
fn line_invokes_shell_or_interpreter_rejects_python3_basename() {
assert!(!line_invokes_shell_or_interpreter("python3 -c x"));
assert!(!line_invokes_shell_or_interpreter("node22 server.js"));
}
#[test]
fn line_invokes_shell_or_interpreter_handles_tab_indented_makefile_recipe() {
assert!(line_invokes_shell_or_interpreter("\tbash install.sh"));
assert!(line_invokes_shell_or_interpreter("\tsh install.sh"));
}
#[test]
fn line_invokes_shell_or_interpreter_rejects_empty_line() {
assert!(!line_invokes_shell_or_interpreter(""));
assert!(!line_invokes_shell_or_interpreter(" \t "));
}
#[test]
fn line_invokes_shell_or_interpreter_detects_extended_interpreter_set() {
assert!(line_invokes_shell_or_interpreter("zsh script.zsh"));
assert!(line_invokes_shell_or_interpreter("dash setup.sh"));
assert!(line_invokes_shell_or_interpreter("pwsh -c 'x'"));
assert!(line_invokes_shell_or_interpreter("powershell -c 'x'"));
}
#[test]
fn line_invokes_shell_or_interpreter_detects_alternative_shells() {
assert!(line_invokes_shell_or_interpreter("fish -c 'payload'"));
assert!(line_invokes_shell_or_interpreter("ksh script.ksh"));
assert!(line_invokes_shell_or_interpreter("csh -c 'cmd'"));
assert!(line_invokes_shell_or_interpreter("tcsh -c 'cmd'"));
assert!(line_invokes_shell_or_interpreter("/bin/fish script.fish"));
assert!(line_invokes_shell_or_interpreter("/usr/bin/ksh -c 'x'"));
}
#[test]
fn line_invokes_shell_or_interpreter_strips_mixed_case_exe() {
assert!(
line_invokes_shell_or_interpreter("POWERSHELL.eXe -c x"),
"mixed-case .eXe must be stripped and detected"
);
assert!(
line_invokes_shell_or_interpreter("bash.EXe script.sh"),
"mixed-case .EXe must be stripped and detected"
);
assert!(
line_invokes_shell_or_interpreter("/usr/bin/python.eXE -c x"),
"mixed-case .eXE must be stripped and detected"
);
}
#[test]
fn re_opaque_mcp_endpoint_matches_domain_boundaries() {
assert!(RE_OPAQUE_MCP_ENDPOINT.is_match("https://ngrok.io/tunnel"));
assert!(RE_OPAQUE_MCP_ENDPOINT.is_match("https://myapp.trycloudflare.com/"));
assert!(RE_OPAQUE_MCP_ENDPOINT
.is_match("https://raw.githubusercontent.com/owner/repo/main/file"));
assert!(RE_OPAQUE_MCP_ENDPOINT.is_match("https://pastebin.com/raw/abc123"));
assert!(
!RE_OPAQUE_MCP_ENDPOINT.is_match("https://my-ngrok-tunnel.example.com/"),
"ngrok as substring inside another hostname must not match"
);
assert!(
!RE_OPAQUE_MCP_ENDPOINT.is_match("https://not-really-pastebin.com.example.org/"),
"pastebin.com as substring must not match"
);
}
#[test]
fn re_mcp_inline_secret_matches_real_credentials() {
assert!(RE_MCP_INLINE_SECRET.is_match("authorization: bearer abc12345xyz"));
assert!(RE_MCP_INLINE_SECRET.is_match(r#""api_key": "sk-abc12345xyz""#));
assert!(RE_MCP_INLINE_SECRET.is_match("api-key=sk_live_abc12345"));
assert!(RE_MCP_INLINE_SECRET.is_match("token=abc12345xyz"));
assert!(RE_MCP_INLINE_SECRET.is_match("_authtoken="));
}
#[test]
fn re_mcp_inline_secret_rejects_keyword_only_mentions() {
assert!(
!RE_MCP_INLINE_SECRET.is_match("# Set up your api_key from the dashboard"),
"prose mentioning `api_key` without a value must not match",
);
assert!(
!RE_MCP_INLINE_SECRET.is_match("// authorization: bearer (described later)"),
"documentation mentioning `authorization: bearer` without a token must not match",
);
assert!(
!RE_MCP_INLINE_SECRET.is_match("Field name: api_key"),
"field-name documentation must not match",
);
}
}