use regex::Regex;
use std::sync::LazyLock;
#[derive(Debug, Clone)]
pub enum Severity {
High,
Medium,
Low,
}
#[derive(Debug, Clone)]
pub enum Category {
DockerUnpinned,
JavaScriptFetch,
PythonFetch,
ShellFetch,
}
pub struct Pattern {
pub regex: &'static LazyLock<Regex>,
pub severity: Severity,
pub category: Category,
pub description: &'static str,
}
macro_rules! re {
($name:ident, $pattern:expr) => {
pub static $name: LazyLock<Regex> = LazyLock::new(|| Regex::new($pattern).unwrap());
};
}
re!(SH_CURL_LATEST, r#"curl\b.*[/=]latest[/\s"]"#);
re!(SH_WGET_LATEST, r#"wget\b.*[/=]latest[/\s"]"#);
re!(SH_GH_RELEASE_LATEST, r"gh\s+release\s+download\s");
re!(SH_CURL_UNVERSIONED, r#"curl\b.*https?://[^\s"']+"#);
re!(SH_WGET_UNVERSIONED, r#"wget\b.*https?://[^\s"']+"#);
re!(
SH_PIP_UNVERSIONED,
r"pip3?\s+install\s+[a-zA-Z][a-zA-Z0-9_-]*(\s|$)"
);
re!(
SH_NPM_UNVERSIONED,
r"npm\s+install\s+(@[a-zA-Z][a-zA-Z0-9_-]*/)?[a-zA-Z][a-zA-Z0-9_-]*(\s|$)"
);
re!(SH_GO_INSTALL_LATEST, r"go\s+install\s+\S+@latest");
re!(
SH_IWR_LATEST,
r#"(?i)(Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\b.*[/=]latest[/\s"]"#
);
re!(
SH_IWR_UNVERSIONED,
r#"(?i)(Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\b.*https?://[^\s"']+"#
);
re!(
SH_PIPE_SHELL,
r"(?i)\b(curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(bash|sh|zsh|dash|ash|ksh|fish|python3?)\b"
);
re!(
SH_PROC_SUB_FETCH,
r"(?i)\b(bash|sh|zsh|dash|ash|ksh|fish|python3?)\s+<\(\s*(curl|wget)\b"
);
re!(
SH_CMD_SUB_FETCH,
r#"(?i)\b(bash|sh|zsh|eval)\b[^"']*["']?\$\(\s*(curl|wget)\b"#
);
re!(
SH_IEX_FETCH,
r"(?i)\b(iex|Invoke-Expression)\b.*\b(iwr|Invoke-WebRequest|Invoke-RestMethod|irm|DownloadString)\b"
);
re!(SH_GIT_CLONE, r"git\s+clone\s");
re!(GIT_CHECKOUT_SHA, r"git\s+checkout\s+[0-9a-f]{40}\b");
re!(
SH_CARGO_INSTALL_UNVERSIONED,
r"cargo\s+install\s+[a-zA-Z][a-zA-Z0-9_-]*(\s|$)"
);
re!(
SH_GEM_INSTALL_UNVERSIONED,
r"gem\s+install\s+[a-zA-Z][a-zA-Z0-9_-]*(\s|$)"
);
pub static SHELL_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &SH_CURL_LATEST,
severity: Severity::High,
category: Category::ShellFetch,
description: "curl fetching from a 'latest' URL — can change without notice",
},
Pattern {
regex: &SH_WGET_LATEST,
severity: Severity::High,
category: Category::ShellFetch,
description: "wget fetching from a 'latest' URL — can change without notice",
},
Pattern {
regex: &SH_GO_INSTALL_LATEST,
severity: Severity::Medium,
category: Category::ShellFetch,
description: "go install @latest — not version-pinned",
},
Pattern {
regex: &SH_IWR_LATEST,
severity: Severity::High,
category: Category::ShellFetch,
description: "PowerShell fetching from a 'latest' URL — can change without notice",
},
]
});
pub static SHELL_URL_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &SH_CURL_UNVERSIONED,
severity: Severity::Medium,
category: Category::ShellFetch,
description: "curl fetching URL without version pinning",
},
Pattern {
regex: &SH_WGET_UNVERSIONED,
severity: Severity::Medium,
category: Category::ShellFetch,
description: "wget fetching URL without version pinning",
},
Pattern {
regex: &SH_IWR_UNVERSIONED,
severity: Severity::Medium,
category: Category::ShellFetch,
description: "PowerShell fetching URL without version pinning",
},
]
});
pub static SHELL_PIPE_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &SH_PIPE_SHELL,
severity: Severity::High,
category: Category::ShellFetch,
description: "fetch piped to shell — payload not written to disk, cannot be checksummed",
},
Pattern {
regex: &SH_PROC_SUB_FETCH,
severity: Severity::High,
category: Category::ShellFetch,
description: "shell reading fetched content via process substitution — bypasses pinning",
},
Pattern {
regex: &SH_CMD_SUB_FETCH,
severity: Severity::High,
category: Category::ShellFetch,
description: "shell executing fetched content via command substitution — bypasses pinning",
},
Pattern {
regex: &SH_IEX_FETCH,
severity: Severity::High,
category: Category::ShellFetch,
description: "PowerShell Invoke-Expression on fetched content — bypasses pinning",
},
]
});
re!(JS_FETCH_LATEST, r#"fetch\s*\(.*[/=]latest[/\s"']"#);
re!(JS_AXIOS_LATEST, r#"axios\.\w+\s*\(.*[/=]latest[/\s"']"#);
re!(JS_GOT_LATEST, r#"got\s*\(.*[/=]latest[/\s"']"#);
re!(JS_HTTP_LATEST, r#"https?\.get\s*\(.*[/=]latest[/\s"']"#);
re!(JS_EXEC_CURL, r"exec\w*\s*\(.*\bcurl\b");
re!(JS_CHILD_PROC_CURL, r"child_process.*\bcurl\b");
re!(JS_FETCH_URL, r#"fetch\s*\(\s*["'`]https?://"#);
re!(JS_AXIOS_URL, r#"axios\.\w+\s*\(\s*["'`]https?://"#);
pub static JS_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &JS_FETCH_LATEST,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "fetch() with 'latest' URL — runtime supply chain risk",
},
Pattern {
regex: &JS_AXIOS_LATEST,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "axios request to 'latest' URL",
},
Pattern {
regex: &JS_GOT_LATEST,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "got() request to 'latest' URL",
},
Pattern {
regex: &JS_HTTP_LATEST,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "http.get() to 'latest' URL",
},
Pattern {
regex: &JS_EXEC_CURL,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "exec() shelling out to curl — runtime fetch bypasses pinning",
},
Pattern {
regex: &JS_CHILD_PROC_CURL,
severity: Severity::High,
category: Category::JavaScriptFetch,
description: "child_process curl — runtime fetch bypasses pinning",
},
]
});
pub static JS_URL_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &JS_FETCH_URL,
severity: Severity::Medium,
category: Category::JavaScriptFetch,
description: "fetch() to external URL without version pinning",
},
Pattern {
regex: &JS_AXIOS_URL,
severity: Severity::Medium,
category: Category::JavaScriptFetch,
description: "axios request to external URL without version pinning",
},
]
});
re!(DOCKER_FROM_LATEST, r"(?i)^FROM\s+\S+:latest\b");
re!(
DOCKER_FROM_UNTAGGED,
r"(?i)^FROM\s+[a-z][a-z0-9._/-]*(\s|$)"
);
re!(DOCKER_FROM_DIGEST, r"(?i)^FROM\s+\S+@sha256:");
re!(DOCKER_RUN_CURL, r"(?i)^RUN\b.*\bcurl\b");
re!(DOCKER_RUN_WGET, r"(?i)^RUN\b.*\bwget\b");
re!(DOCKER_ADD_URL, r"(?i)^ADD\b[^#]*\bhttps?://\S+");
pub static DOCKER_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &DOCKER_FROM_LATEST,
severity: Severity::High,
category: Category::DockerUnpinned,
description: "FROM :latest — image not pinned to specific version",
},
Pattern {
regex: &DOCKER_FROM_UNTAGGED,
severity: Severity::High,
category: Category::DockerUnpinned,
description: "FROM without tag — implicitly pulls :latest",
},
Pattern {
regex: &DOCKER_RUN_CURL,
severity: Severity::Medium,
category: Category::DockerUnpinned,
description: "curl in Dockerfile RUN — check URL is versioned",
},
Pattern {
regex: &DOCKER_RUN_WGET,
severity: Severity::Medium,
category: Category::DockerUnpinned,
description: "wget in Dockerfile RUN — check URL is versioned",
},
]
});
pub static DOCKER_URL_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![Pattern {
regex: &DOCKER_ADD_URL,
severity: Severity::Medium,
category: Category::DockerUnpinned,
description: "Dockerfile ADD with URL source — build-time fetch bypasses pinning",
}]
});
re!(
PY_URLLIB_LATEST,
r#"urllib\.request\.urlopen\s*\(.*[/=]latest[/\s"']"#
);
re!(
PY_REQUESTS_LATEST,
r#"requests\.(get|post|head)\s*\(.*[/=]latest[/\s"']"#
);
re!(PY_SUBPROCESS_CURL, r"subprocess\b.*\bcurl\b");
re!(PY_SUBPROCESS_WGET, r"subprocess\b.*\bwget\b");
re!(
PY_URLLIB_URL,
r#"urllib\.request\.urlopen\s*\(\s*["']https?://"#
);
re!(
PY_REQUESTS_URL,
r#"requests\.(get|post|head)\s*\(\s*["']https?://"#
);
pub static PY_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &PY_URLLIB_LATEST,
severity: Severity::High,
category: Category::PythonFetch,
description: "urllib fetching from a 'latest' URL",
},
Pattern {
regex: &PY_REQUESTS_LATEST,
severity: Severity::High,
category: Category::PythonFetch,
description: "requests library fetching from a 'latest' URL",
},
Pattern {
regex: &PY_SUBPROCESS_CURL,
severity: Severity::High,
category: Category::PythonFetch,
description: "subprocess shelling out to curl — runtime fetch bypasses pinning",
},
Pattern {
regex: &PY_SUBPROCESS_WGET,
severity: Severity::High,
category: Category::PythonFetch,
description: "subprocess shelling out to wget — runtime fetch bypasses pinning",
},
]
});
pub static PY_URL_PATTERNS: LazyLock<Vec<Pattern>> = LazyLock::new(|| {
vec![
Pattern {
regex: &PY_URLLIB_URL,
severity: Severity::Medium,
category: Category::PythonFetch,
description: "urllib fetching external URL without version pinning",
},
Pattern {
regex: &PY_REQUESTS_URL,
severity: Severity::Medium,
category: Category::PythonFetch,
description: "requests library fetching external URL without version pinning",
},
]
});
re!(
CHECKSUM_VERIFY,
r"(?i)(sha256sum|sha512sum|shasum|openssl\s+dgst|gpg\s+--verify|Get-FileHash)"
);
pub fn has_checksum_verify(line: &str) -> bool {
CHECKSUM_VERIFY.is_match(line)
}
static VERSION_SEGMENT: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"[/=]v?\d+(\.\d+)+[/\s"]"#).unwrap());
pub fn url_has_version(s: &str) -> bool {
VERSION_SEGMENT.is_match(s)
}
const DATA_FORMAT_EXTENSIONS: &[&str] = &[
"json", "jsonl", "ndjson", "yaml", "yml", "toml", "xml", "csv", "tsv", "txt", "md", "rst",
];
pub fn url_extension(url: &str) -> Option<&str> {
let path = url.split(['?', '#']).next().unwrap_or(url);
let last = path.rsplit('/').next().unwrap_or("");
let dot = last.rfind('.')?;
Some(&last[dot + 1..])
}
pub fn url_host(url: &str) -> Option<&str> {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))?;
let after_userinfo = rest.split_once('@').map(|(_, r)| r).unwrap_or(rest);
let end = after_userinfo
.find(['/', ':', '?', '#'])
.unwrap_or(after_userinfo.len());
Some(&after_userinfo[..end])
}
pub fn url_is_data_format(url: &str) -> bool {
let Some(ext) = url_extension(url) else {
return false;
};
DATA_FORMAT_EXTENSIONS
.iter()
.any(|e| ext.eq_ignore_ascii_case(e))
}
pub fn extract_url(line: &str) -> Option<&str> {
static URL_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"https?://[^\s"'`)>]+"#).unwrap());
URL_RE.find(line).map(|m| m.as_str())
}
pub fn gh_release_has_tag(line: &str) -> bool {
static GH_RELEASE_TAG: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"gh\s+release\s+download\s+(v?\d|--tag\s+v?\d)").unwrap());
GH_RELEASE_TAG.is_match(line)
}
fn ref_looks_versioned(ref_name: &str) -> bool {
static VERSION_REF: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"v?\d+(\.\d+)+").unwrap());
VERSION_REF.is_match(ref_name)
}
pub fn git_clone_has_pinned_ref(line: &str) -> bool {
static GIT_CLONE_BRANCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"git\s+clone\s+.*(?:--branch|-b)\s+(\S+)").unwrap());
let Some(caps) = GIT_CLONE_BRANCH.captures(line) else {
return false;
};
ref_looks_versioned(&caps[1])
}
pub fn has_git_checkout_sha(line: &str) -> bool {
GIT_CHECKOUT_SHA.is_match(line)
}
pub fn pip_install_has_version(line: &str) -> bool {
static PIP_VERSION: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"pip3?\s+install\s+\S*[=>~]=|pip3?\s+install\s+-r\s").unwrap()
});
PIP_VERSION.is_match(line)
}
pub fn npm_install_has_version(line: &str) -> bool {
static NPM_VERSION: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"npm\s+install\s+\S+@\d").unwrap());
NPM_VERSION.is_match(line)
}
pub fn cargo_install_has_version(line: &str) -> bool {
static CARGO_VERSION: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"cargo\s+install\s+\S+@\d|cargo\s+install\s+.*--version\s").unwrap()
});
CARGO_VERSION.is_match(line)
}
pub fn gem_install_has_version(line: &str) -> bool {
static GEM_VERSION: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"gem\s+install\s+.*(-v\s+\d|--version\s)").unwrap());
GEM_VERSION.is_match(line)
}
pub fn category_str(c: &Category) -> &'static str {
match c {
Category::DockerUnpinned => "docker_unpinned",
Category::JavaScriptFetch => "javascript_fetch",
Category::PythonFetch => "python_fetch",
Category::ShellFetch => "shell_fetch",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn versioned_download_url() {
assert!(url_has_version(
"https://github.com/nicklockwood/SwiftFormat/releases/download/0.55.8/swiftformat"
));
}
#[test]
fn versioned_with_v_prefix() {
assert!(url_has_version(
"https://example.com/releases/download/v2.8.1/tool.tar.xz"
));
}
#[test]
fn unversioned_latest_url() {
assert!(!url_has_version(
"https://github.com/aquasecurity/trivy/releases/latest/download/trivy.tar.gz"
));
}
#[test]
fn unversioned_api_url() {
assert!(!url_has_version("https://api.example.com/data"));
}
#[test]
fn single_number_not_version() {
assert!(!url_has_version("https://example.com/v4/resource"));
}
#[test]
fn url_extension_simple() {
assert_eq!(url_extension("https://example.com/data.json"), Some("json"));
}
#[test]
fn url_extension_strips_query_string() {
assert_eq!(
url_extension("https://example.com/data.json?cache=false"),
Some("json")
);
}
#[test]
fn url_extension_strips_fragment() {
assert_eq!(
url_extension("https://example.com/doc.md#section"),
Some("md")
);
}
#[test]
fn url_extension_no_extension() {
assert_eq!(url_extension("https://api.github.com/user"), None);
}
#[test]
fn url_extension_dot_only_in_earlier_segment() {
assert_eq!(
url_extension("https://example.com/v1.2.3/config/settings"),
None
);
}
#[test]
fn url_host_simple_https() {
assert_eq!(
url_host("https://example.com/path/to/file"),
Some("example.com")
);
}
#[test]
fn url_host_simple_http() {
assert_eq!(url_host("http://example.com/"), Some("example.com"));
}
#[test]
fn url_host_with_port_strips_port() {
assert_eq!(
url_host("https://example.com:8080/api"),
Some("example.com")
);
}
#[test]
fn url_host_with_query() {
assert_eq!(url_host("https://example.com?foo=bar"), Some("example.com"));
}
#[test]
fn url_host_with_fragment() {
assert_eq!(url_host("https://example.com#section"), Some("example.com"));
}
#[test]
fn url_host_bare() {
assert_eq!(url_host("https://example.com"), Some("example.com"));
}
#[test]
fn url_host_strips_userinfo() {
assert_eq!(
url_host("https://user@example.com/path"),
Some("example.com")
);
}
#[test]
fn url_host_subdomain() {
assert_eq!(
url_host("https://api.example.com/data"),
Some("api.example.com")
);
}
#[test]
fn url_host_not_a_url() {
assert_eq!(url_host("example.com"), None);
assert_eq!(url_host("ftp://example.com"), None);
}
#[test]
fn data_format_json() {
assert!(url_is_data_format(
"https://formulae.brew.sh/api/analytics/install/homebrew-core/30d.json"
));
}
#[test]
fn data_format_yaml() {
assert!(url_is_data_format("https://example.com/config.yaml"));
assert!(url_is_data_format("https://example.com/config.yml"));
}
#[test]
fn data_format_toml() {
assert!(url_is_data_format("https://example.com/settings.toml"));
}
#[test]
fn data_format_csv_tsv_xml() {
assert!(url_is_data_format("https://example.com/data.csv"));
assert!(url_is_data_format("https://example.com/data.tsv"));
assert!(url_is_data_format("https://example.com/data.xml"));
}
#[test]
fn data_format_markdown() {
assert!(url_is_data_format(
"https://raw.githubusercontent.com/owner/repo/main/README.md"
));
}
#[test]
fn data_format_case_insensitive() {
assert!(url_is_data_format("https://example.com/DATA.JSON"));
}
#[test]
fn data_format_with_query_string() {
assert!(url_is_data_format(
"https://example.com/data.json?cache=false"
));
}
#[test]
fn data_format_with_fragment() {
assert!(url_is_data_format("https://example.com/doc.md#section"));
}
#[test]
fn data_format_jsonl_ndjson() {
assert!(url_is_data_format("https://example.com/events.jsonl"));
assert!(url_is_data_format("https://example.com/events.ndjson"));
}
#[test]
fn not_data_format_shell_script() {
assert!(!url_is_data_format("https://example.com/install.sh"));
}
#[test]
fn not_data_format_archive() {
assert!(!url_is_data_format("https://example.com/tool.tar.gz"));
assert!(!url_is_data_format("https://example.com/bundle.zip"));
}
#[test]
fn not_data_format_executable() {
assert!(!url_is_data_format("https://example.com/tool.exe"));
assert!(!url_is_data_format("https://example.com/tool"));
}
#[test]
fn not_data_format_html() {
assert!(!url_is_data_format("https://example.com/page.html"));
}
#[test]
fn not_data_format_no_extension() {
assert!(!url_is_data_format("https://api.github.com/user"));
}
#[test]
fn not_data_format_path_ends_with_dot_in_earlier_segment() {
assert!(!url_is_data_format(
"https://example.com/v1.2.3/config/settings"
));
}
#[test]
fn extract_url_from_curl() {
let line = r#"curl -L "https://example.com/file.tar.gz" -o out"#;
assert_eq!(extract_url(line), Some("https://example.com/file.tar.gz"));
}
#[test]
fn extract_url_single_quotes() {
let line = "wget 'https://example.com/file'";
assert_eq!(extract_url(line), Some("https://example.com/file"));
}
#[test]
fn no_url() {
assert!(extract_url("echo hello world").is_none());
}
#[test]
fn curl_latest_detected() {
assert!(
SH_CURL_LATEST.is_match(
r#"curl -L "https://github.com/owner/repo/releases/latest/download/tool""#
)
);
}
#[test]
fn curl_versioned_not_flagged_as_latest() {
assert!(
!SH_CURL_LATEST.is_match(
r#"curl -L "https://github.com/owner/repo/releases/download/v1.2.3/tool""#
)
);
}
#[test]
fn wget_latest_detected() {
assert!(
SH_WGET_LATEST.is_match(r#"wget "https://example.com/releases/latest/tool.tar.gz""#)
);
}
#[test]
fn gh_release_download_unversioned() {
assert!(SH_GH_RELEASE_LATEST.is_match("gh release download --pattern '*.tar.gz'"));
assert!(!gh_release_has_tag(
"gh release download --pattern '*.tar.gz'"
));
}
#[test]
fn gh_release_download_versioned() {
assert!(SH_GH_RELEASE_LATEST.is_match("gh release download v1.2.3 --pattern '*.tar.gz'"));
assert!(gh_release_has_tag(
"gh release download v1.2.3 --pattern '*.tar.gz'"
));
}
#[test]
fn gh_release_download_versioned_tag_flag() {
assert!(
SH_GH_RELEASE_LATEST.is_match("gh release download --tag v1.2.3 --pattern '*.tar.gz'")
);
assert!(gh_release_has_tag(
"gh release download --tag v1.2.3 --pattern '*.tar.gz'"
));
}
#[test]
fn go_install_latest_detected() {
assert!(
SH_GO_INSTALL_LATEST
.is_match("go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest")
);
}
#[test]
fn go_install_versioned_not_flagged() {
assert!(
!SH_GO_INSTALL_LATEST
.is_match("go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.0")
);
}
#[test]
fn npm_install_unversioned_detected() {
assert!(SH_NPM_UNVERSIONED.is_match("npm install typescript"));
assert!(SH_NPM_UNVERSIONED.is_match("npm install svelte-kit"));
}
#[test]
fn npm_install_scoped_unversioned_detected() {
assert!(SH_NPM_UNVERSIONED.is_match("npm install @babel/core"));
}
#[test]
fn npm_install_version_pinned_not_flagged() {
assert!(!SH_NPM_UNVERSIONED.is_match("npm install typescript@5.6.0"));
assert!(!SH_NPM_UNVERSIONED.is_match("npm install @babel/core@1.0.0"));
}
#[test]
fn npm_install_no_args_not_flagged() {
assert!(!SH_NPM_UNVERSIONED.is_match("npm install"));
}
#[test]
fn pip_install_unversioned_detected() {
assert!(SH_PIP_UNVERSIONED.is_match("pip install requests"));
assert!(SH_PIP_UNVERSIONED.is_match("pip3 install flask"));
}
#[test]
fn pip_install_version_pinned_not_flagged() {
assert!(!SH_PIP_UNVERSIONED.is_match("pip install requests==2.31.0"));
}
#[test]
fn pip_install_requirements_not_flagged() {
assert!(!SH_PIP_UNVERSIONED.is_match("pip install -r requirements.txt"));
}
#[test]
fn js_fetch_latest_detected() {
assert!(
JS_FETCH_LATEST
.is_match(r#"fetch("https://api.github.com/repos/o/r/releases/latest")"#)
);
}
#[test]
fn js_exec_curl_detected() {
assert!(JS_EXEC_CURL.is_match(r#"exec("curl -L https://example.com")"#));
}
#[test]
fn docker_from_latest_detected() {
assert!(DOCKER_FROM_LATEST.is_match("FROM ubuntu:latest"));
assert!(DOCKER_FROM_LATEST.is_match("FROM node:latest AS builder"));
}
#[test]
fn docker_from_untagged_detected() {
assert!(DOCKER_FROM_UNTAGGED.is_match("FROM ubuntu AS builder"));
assert!(DOCKER_FROM_UNTAGGED.is_match("FROM node "));
}
#[test]
fn docker_from_tagged_not_untagged() {
assert!(!DOCKER_FROM_UNTAGGED.is_match("FROM ubuntu:22.04"));
assert!(!DOCKER_FROM_UNTAGGED.is_match("FROM ubuntu:latest"));
assert!(!DOCKER_FROM_UNTAGGED.is_match(
"FROM ubuntu@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"
));
}
#[test]
fn docker_from_pinned_not_flagged() {
assert!(!DOCKER_FROM_LATEST.is_match("FROM ubuntu:22.04"));
assert!(DOCKER_FROM_DIGEST.is_match(
"FROM ubuntu@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"
));
}
#[test]
fn docker_run_curl_detected() {
assert!(DOCKER_RUN_CURL.is_match("RUN curl -L https://example.com/install.sh | bash"));
}
#[test]
fn docker_add_url_detected() {
assert!(DOCKER_ADD_URL.is_match("ADD https://example.com/install.tar.gz /tmp/"));
assert!(DOCKER_ADD_URL.is_match("ADD http://example.com/foo.zip /opt/"));
}
#[test]
fn docker_add_url_with_chown_detected() {
assert!(
DOCKER_ADD_URL.is_match("ADD --chown=user:group https://example.com/tool.tgz /opt/")
);
}
#[test]
fn docker_add_local_not_matched() {
assert!(!DOCKER_ADD_URL.is_match("ADD ./local.tar.gz /opt/"));
assert!(!DOCKER_ADD_URL.is_match("ADD context/* /app/"));
}
#[test]
fn docker_add_case_insensitive() {
assert!(DOCKER_ADD_URL.is_match("add https://example.com/tool.tgz /opt/"));
}
#[test]
fn powershell_iwr_latest_detected() {
assert!(
SH_IWR_LATEST
.is_match(r#"Invoke-WebRequest "https://example.com/releases/latest/tool""#)
);
}
#[test]
fn powershell_irm_latest_detected() {
assert!(SH_IWR_LATEST.is_match(r#"irm "https://example.com/releases/latest/tool""#));
}
#[test]
fn powershell_iwr_versioned_not_latest() {
assert!(
!SH_IWR_LATEST.is_match(
r#"Invoke-WebRequest "https://example.com/releases/download/v1.2.3/tool""#
)
);
}
#[test]
fn python_requests_latest_detected() {
assert!(
PY_REQUESTS_LATEST
.is_match(r#"requests.get("https://example.com/releases/latest/tool")"#)
);
}
#[test]
fn python_urllib_latest_detected() {
assert!(
PY_URLLIB_LATEST
.is_match(r#"urllib.request.urlopen("https://example.com/releases/latest/tool")"#)
);
}
#[test]
fn python_subprocess_curl_detected() {
assert!(PY_SUBPROCESS_CURL.is_match(r#"subprocess.run(["curl", "-L", url])"#));
}
#[test]
fn python_requests_versioned_not_latest() {
assert!(
!PY_REQUESTS_LATEST
.is_match(r#"requests.get("https://example.com/releases/download/v1.2.3/tool")"#)
);
}
#[test]
fn pipe_shell_curl_to_sh() {
assert!(SH_PIPE_SHELL.is_match("curl -sSL https://example.com/install.sh | sh"));
}
#[test]
fn pipe_shell_curl_to_sudo_bash() {
assert!(SH_PIPE_SHELL.is_match("curl -fsSL https://example.com/install.sh | sudo bash"));
}
#[test]
fn pipe_shell_wget_to_sh_with_args() {
assert!(
SH_PIPE_SHELL.is_match("wget -qO- https://example.com/install.sh | sh -s -- --yes")
);
}
#[test]
fn pipe_shell_curl_to_python3() {
assert!(SH_PIPE_SHELL.is_match("curl https://example.com/get.py | python3"));
}
#[test]
fn pipe_shell_versioned_url_still_matches() {
assert!(
SH_PIPE_SHELL
.is_match("curl -sSL https://example.com/releases/download/v1.2.3/install.sh | sh")
);
}
#[test]
fn pipe_shell_tee_not_matched() {
assert!(!SH_PIPE_SHELL.is_match("curl https://example.com/file.sh | tee out.sh"));
}
#[test]
fn pipe_shell_jq_not_matched() {
assert!(!SH_PIPE_SHELL.is_match("curl https://api.example.com/data | jq ."));
}
#[test]
fn proc_sub_bash_curl_matched() {
assert!(SH_PROC_SUB_FETCH.is_match("bash <(curl https://example.com/install.sh)"));
}
#[test]
fn proc_sub_sh_wget_matched() {
assert!(SH_PROC_SUB_FETCH.is_match("sh <(wget -qO- https://example.com/install.sh)"));
}
#[test]
fn proc_sub_not_fetch_not_matched() {
assert!(!SH_PROC_SUB_FETCH.is_match("bash <(cat local.sh)"));
}
#[test]
fn cmd_sub_bash_c_curl_matched() {
assert!(
SH_CMD_SUB_FETCH.is_match(r#"bash -c "$(curl -fsSL https://example.com/install.sh)""#)
);
}
#[test]
fn cmd_sub_eval_wget_matched() {
assert!(SH_CMD_SUB_FETCH.is_match(r#"eval "$(wget -qO- https://example.com/install.sh)""#));
}
#[test]
fn cmd_sub_local_not_matched() {
assert!(!SH_CMD_SUB_FETCH.is_match(r#"bash -c "$(pwd)""#));
}
#[test]
fn iex_iwr_matched() {
assert!(SH_IEX_FETCH.is_match("iex (iwr https://example.com/install.ps1)"));
}
#[test]
fn iex_downloadstring_matched() {
assert!(SH_IEX_FETCH.is_match(
r#"Invoke-Expression ((New-Object Net.WebClient).DownloadString("https://example.com/install.ps1"))"#
));
}
#[test]
fn iex_invoke_restmethod_matched() {
assert!(
SH_IEX_FETCH.is_match("iex (Invoke-RestMethod -Uri https://example.com/install.ps1)")
);
}
#[test]
fn iex_without_fetch_not_matched() {
assert!(!SH_IEX_FETCH.is_match("iex $scriptBlock"));
}
#[test]
fn checksum_sha256sum_detected() {
assert!(has_checksum_verify("sha256sum --check checksums.txt"));
}
#[test]
fn checksum_openssl_detected() {
assert!(has_checksum_verify("openssl dgst -sha256 file.tar.gz"));
}
#[test]
fn checksum_gpg_detected() {
assert!(has_checksum_verify("gpg --verify file.sig file.tar.gz"));
}
#[test]
fn checksum_powershell_detected() {
assert!(has_checksum_verify(
"Get-FileHash -Algorithm SHA256 file.tar.gz"
));
}
#[test]
fn no_checksum() {
assert!(!has_checksum_verify("echo done"));
}
#[test]
fn git_clone_basic_detected() {
assert!(SH_GIT_CLONE.is_match("git clone https://github.com/org/repo"));
}
#[test]
fn git_clone_with_depth_detected() {
assert!(SH_GIT_CLONE.is_match("git clone --depth 1 https://github.com/org/repo"));
}
#[test]
fn git_clone_with_branch_detected() {
assert!(SH_GIT_CLONE.is_match("git clone --branch main https://github.com/org/repo"));
}
#[test]
fn git_clone_pinned_versioned_branch() {
assert!(git_clone_has_pinned_ref(
"git clone --branch v1.2.3 https://github.com/org/repo"
));
}
#[test]
fn git_clone_pinned_short_flag() {
assert!(git_clone_has_pinned_ref(
"git clone -b v1.2.3 https://github.com/org/repo"
));
}
#[test]
fn git_clone_pinned_no_v_prefix() {
assert!(git_clone_has_pinned_ref(
"git clone --branch 2.0.1 https://github.com/org/repo"
));
}
#[test]
fn git_clone_pinned_release_branch_with_version() {
assert!(git_clone_has_pinned_ref(
"git clone --branch release/1.0.0 https://github.com/org/repo"
));
}
#[test]
fn git_clone_pinned_depth_one_versioned() {
assert!(git_clone_has_pinned_ref(
"git clone --depth 1 --branch v1.2.3 https://github.com/org/repo"
));
}
#[test]
fn git_clone_unpinned_main() {
assert!(!git_clone_has_pinned_ref(
"git clone --branch main https://github.com/org/repo"
));
}
#[test]
fn git_clone_unpinned_master() {
assert!(!git_clone_has_pinned_ref(
"git clone -b master https://github.com/org/repo"
));
}
#[test]
fn git_clone_unpinned_develop() {
assert!(!git_clone_has_pinned_ref(
"git clone -b develop https://github.com/org/repo"
));
}
#[test]
fn git_clone_unpinned_feature_branch() {
assert!(!git_clone_has_pinned_ref(
"git clone --branch feature/my-feature https://github.com/org/repo"
));
}
#[test]
fn git_clone_no_branch_flag() {
assert!(!git_clone_has_pinned_ref(
"git clone https://github.com/org/repo"
));
}
#[test]
fn git_checkout_sha_detected() {
assert!(has_git_checkout_sha(
"git checkout abcdef1234567890abcdef1234567890abcdef12"
));
}
#[test]
fn git_checkout_branch_not_sha() {
assert!(!has_git_checkout_sha("git checkout main"));
}
#[test]
fn git_checkout_short_sha_not_matched() {
assert!(!has_git_checkout_sha("git checkout abc1234"));
}
#[test]
fn cargo_install_unversioned_detected() {
assert!(SH_CARGO_INSTALL_UNVERSIONED.is_match("cargo install ripgrep"));
}
#[test]
fn cargo_install_with_flags_detected() {
assert!(SH_CARGO_INSTALL_UNVERSIONED.is_match("cargo install typos-cli --locked"));
assert!(SH_CARGO_INSTALL_UNVERSIONED.is_match("cargo install cargo-deny --locked"));
}
#[test]
fn cargo_install_no_args_not_flagged() {
assert!(!SH_CARGO_INSTALL_UNVERSIONED.is_match("cargo install"));
}
#[test]
fn gem_install_unversioned_detected() {
assert!(SH_GEM_INSTALL_UNVERSIONED.is_match("gem install rubocop"));
}
#[test]
fn gem_install_with_flags_detected() {
assert!(SH_GEM_INSTALL_UNVERSIONED.is_match("gem install rubocop --no-document"));
}
#[test]
fn gem_install_no_args_not_flagged() {
assert!(!SH_GEM_INSTALL_UNVERSIONED.is_match("gem install"));
}
#[test]
fn pip_install_with_flags_detected() {
assert!(SH_PIP_UNVERSIONED.is_match("pip install requests --quiet"));
assert!(SH_PIP_UNVERSIONED.is_match("pip3 install flask --user"));
}
#[test]
fn npm_install_with_flags_detected() {
assert!(SH_NPM_UNVERSIONED.is_match("npm install typescript --save-dev"));
assert!(SH_NPM_UNVERSIONED.is_match("npm install @babel/core --save-dev"));
}
#[test]
fn pip_version_pinned() {
assert!(pip_install_has_version("pip install requests==2.31.0"));
assert!(pip_install_has_version("pip install requests>=2.0"));
assert!(pip_install_has_version("pip install requests~=2.31"));
assert!(pip_install_has_version("pip install -r requirements.txt"));
}
#[test]
fn pip_version_not_pinned() {
assert!(!pip_install_has_version("pip install requests"));
assert!(!pip_install_has_version("pip install requests --quiet"));
}
#[test]
fn npm_version_pinned() {
assert!(npm_install_has_version("npm install typescript@5.6.0"));
assert!(npm_install_has_version("npm install @babel/core@1.0.0"));
}
#[test]
fn npm_version_not_pinned() {
assert!(!npm_install_has_version("npm install typescript"));
assert!(!npm_install_has_version("npm install @babel/core"));
assert!(!npm_install_has_version(
"npm install typescript --save-dev"
));
}
#[test]
fn cargo_version_pinned() {
assert!(cargo_install_has_version("cargo install ripgrep@14.0.0"));
assert!(cargo_install_has_version(
"cargo install ripgrep --version 14.0.0"
));
}
#[test]
fn cargo_version_not_pinned() {
assert!(!cargo_install_has_version("cargo install ripgrep"));
assert!(!cargo_install_has_version(
"cargo install typos-cli --locked"
));
assert!(!cargo_install_has_version(
"cargo install cargo-deny --locked"
));
}
#[test]
fn gem_version_pinned() {
assert!(gem_install_has_version("gem install rubocop -v 1.0.0"));
assert!(gem_install_has_version(
"gem install rubocop --version 1.0.0"
));
}
#[test]
fn gem_version_not_pinned() {
assert!(!gem_install_has_version("gem install rubocop"));
assert!(!gem_install_has_version(
"gem install rubocop --no-document"
));
}
}