Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,12 @@ def _edit_distance(a: str, b: str) -> int:
def _is_typosquat(pkg_name: str, popular: set[str], max_distance: int = 2) -> str | None:
"""Return the popular package name if pkg_name is a close-but-not-exact match."""
normalized = pkg_name.lower().replace("_", "-")
# A known package must win over any earlier, similar name (e.g. gunicorn
# sorts before uvicorn). Apply the same normalization on both sides.
if any(normalized == name.lower().replace("_", "-") for name in popular):
return None
for popular_name in sorted(popular):
pop_norm = popular_name.lower().replace("_", "-")
if normalized == pop_norm:
return None
if len(normalized) < 3 or len(pop_norm) < 3:
continue
dist = _edit_distance(normalized, pop_norm)
Expand Down
21 changes: 19 additions & 2 deletions tests/unit/test_patterns_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -1989,9 +1989,12 @@ def test_sc6_typosquat_npm(self) -> None:
assert len(sc6) >= 1
assert "express" in sc6[0].message

def test_sc6_exact_match_not_flagged(self) -> None:
@pytest.mark.parametrize("package", ["requests", "uvicorn", "gunicorn", "UVICORN"])
def test_sc6_exact_match_not_flagged(self, package: str) -> None:
sc6 = [
f for f in _analyze_deps("requests==2.31.0\n", "requirements.txt") if f.rule_id == "SC6"
f
for f in _analyze_deps(f"{package}==1.0.0\n", "requirements.txt")
if f.rule_id == "SC6"
]
assert len(sc6) == 0

Expand Down Expand Up @@ -2220,6 +2223,20 @@ def test_is_typosquat_positive(self) -> None:
def test_is_typosquat_exact_match_returns_none(self) -> None:
assert sc_mod._is_typosquat("requests", {"requests"}) is None

@pytest.mark.parametrize(
"package,popular",
[
("uvicorn", {"gunicorn", "uvicorn"}),
("UVICORN", {"gunicorn", "uvicorn"}),
("demo_tools", {"demo-tool", "demo-tools"}),
("demo-tools", {"demo-tool", "DEMO_TOOLS"}),
],
)
def test_is_typosquat_exact_match_precedes_similar_names(
self, package: str, popular: set[str]
) -> None:
assert sc_mod._is_typosquat(package, popular) is None

def test_is_typosquat_too_distant_returns_none(self) -> None:
assert sc_mod._is_typosquat("completely_different", {"requests"}) is None

Expand Down
Loading