guests/static-scan/tests/names.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Typosquat and dependency confusion, judged by
//! modgunn::scip::typosquat_findings on the snapshot the caller sent. Heimdall
//! is a defensive sandbox.
mod common;
use common::*;
use heimdall_scan::report::unknown;
use heimdall_scan::{Check, CheckResult, DataKind, Severity, Verdict};
const NPM_POPULAR: &[u8] =
b"# top npm names, snapshot 2026-11-01\nlodash\nexpress\nreact\nrequest\n\n";
const PYPI_POPULAR: &[u8] = b"requests\nnumpy\npython-dateutil\nurllib3\n";
fn typo(purl: &str, data: Option<&[u8]>) -> CheckResult {
let d: Vec<(DataKind, &[u8])> = data
.map(|d| vec![(DataKind::PopularNames, d)])
.unwrap_or_default();
check(
&envelope(b"", &d),
&[("purl", purl), ("checks", "typosquat")],
Check::Typosquat,
)
}
fn confusion(purl: &str, data: Option<&[u8]>, origin: Option<&str>) -> CheckResult {
let d: Vec<(DataKind, &[u8])> = data
.map(|d| vec![(DataKind::InternalNamespaces, d)])
.unwrap_or_default();
let mut p = vec![("purl", purl), ("checks", "dependency-confusion")];
if let Some(o) = origin {
p.push(("origin", o));
}
check(&envelope(b"", &d), &p, Check::DependencyConfusion)
}
#[test]
fn a_name_one_edit_from_a_popular_npm_package_is_a_high_typosquat_finding() {
for (purl, pop) in [
("pkg:npm/lodashs@1.0.0", "lodash"),
("pkg:npm/expres@4.0.0", "express"),
("pkg:npm/reat@1.0.0", "react"),
("pkg:npm/reqiest@2.0.0", "request"),
] {
let r = typo(purl, Some(NPM_POPULAR));
assert_eq!(r.verdict, Verdict::Findings, "{purl}");
assert_eq!(r.findings[0].severity, Severity::High);
assert!(
r.findings[0].summary.contains(pop),
"{purl}: {:?}",
r.findings
);
assert!(r.findings[0].id.starts_with("MODGUNN-TYPOSQUAT:"));
}
}
#[test]
fn the_popular_package_itself_is_not_a_typosquat_of_itself() {
assert_clean(&typo("pkg:npm/lodash@4.17.21", Some(NPM_POPULAR)));
}
#[test]
fn a_name_far_from_every_popular_name_is_clean() {
assert_clean(&typo("pkg:npm/my-internal-tool@1.0.0", Some(NPM_POPULAR)));
}
#[test]
fn pypi_names_compare_after_pep_503_normalization() {
// The same project spelled another way is not a look-alike.
assert_clean(&typo("pkg:pypi/Python_Dateutil@2.9.0", Some(PYPI_POPULAR)));
assert_clean(&typo("pkg:pypi/Requests@2.32.0", Some(b"REQUESTS\n")));
// A real one-letter look-alike is.
let r = typo("pkg:pypi/reqests@1.0", Some(PYPI_POPULAR));
assert!(has(&r, "MODGUNN-TYPOSQUAT"), "{r:?}");
}
#[test]
fn a_missing_popular_names_snapshot_is_unknown_never_clean() {
assert_unknown(&typo("pkg:npm/lodashs@1.0.0", None), unknown::DATA_MISSING);
}
#[test]
fn an_empty_popular_names_snapshot_is_unknown() {
assert_unknown(
&typo("pkg:npm/lodashs@1.0.0", Some(b"# nothing\n\n")),
unknown::DATA_INVALID,
);
}
#[test]
fn a_popular_names_snapshot_that_is_not_utf8_is_unknown() {
assert_unknown(
&typo("pkg:npm/lodashs@1.0.0", Some(b"lodash\n\xff\n")),
unknown::DATA_INVALID,
);
}
#[test]
fn an_internal_namespace_name_resolved_publicly_is_a_critical_confusion_finding() {
let r = confusion(
"pkg:npm/%40acme/billing@9.9.9",
Some(b"@acme/\nacme-\n"),
Some("public"),
);
assert_eq!(r.verdict, Verdict::Findings);
assert_eq!(r.findings[0].severity, Severity::Critical);
assert!(r.findings[0].id.starts_with("MODGUNN-DEP-CONFUSION:"));
let r = confusion(
"pkg:pypi/acme-internal@1.0",
Some(b"acme-\n"),
Some("public"),
);
assert!(has(&r, "MODGUNN-DEP-CONFUSION"));
}
#[test]
fn an_internal_name_resolved_from_the_internal_registry_is_clean() {
assert_clean(&confusion(
"pkg:npm/%40acme/billing@1.0.0",
Some(b"@acme/\n"),
Some("internal"),
));
}
#[test]
fn a_public_name_outside_every_internal_prefix_is_clean() {
assert_clean(&confusion(
"pkg:npm/lodash@4.17.21",
Some(b"@acme/\n"),
Some("public"),
));
}
#[test]
fn an_explicitly_empty_namespace_list_means_none_and_is_clean() {
assert_clean(&confusion(
"pkg:npm/lodash@4.17.21",
Some(b""),
Some("public"),
));
}
#[test]
fn dependency_confusion_without_its_snapshot_is_unknown() {
assert_unknown(
&confusion("pkg:npm/%40acme/x@1.0.0", None, Some("public")),
unknown::DATA_MISSING,
);
}
#[test]
fn dependency_confusion_without_the_origin_parameter_is_unknown() {
assert_unknown(
&confusion("pkg:npm/%40acme/x@1.0.0", Some(b"@acme/\n"), None),
unknown::ORIGIN_MISSING,
);
}
#[test]
fn a_large_popular_list_is_judged() {
let mut list = String::new();
for i in 0..20_000 {
list.push_str(&format!("package-number-{i}\n"));
}
list.push_str("express\n");
let r = typo("pkg:npm/expresss@1.0.0", Some(list.as_bytes()));
assert!(has(&r, "MODGUNN-TYPOSQUAT"));
}