Files

224 lines
8.3 KiB
Python
Raw Permalink Normal View History

"""Pinned known-vulnerable checkouts (Req 4.1).
Every test here is offline. The one that fetches builds a git repository in ``tmp_path`` and uses it as the
recipe's url, which exercises the real fetch path -- ``git init``, ``fetch``, ``checkout`` -- with no network.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from openultrasast.repos import (
DEFAULT_REPO_DIR,
RepoRecipeError,
RepoUnavailable,
checkout_path,
load_repo_recipe,
load_repo_recipes,
network_allowed,
recipe_payload,
resolve,
select,
verify,
)
RECIPE = """
name = "sample"
url = "{url}"
commit = "{commit}"
language = "python"
license = "MIT"
license_file = "LICENSE"
measures = ["detection"]
[[known]]
id = "SAMPLE-1"
family = "injection"
weakness = "the parameter is interpolated into the query"
file = "app.py"
function = "handler"
line = 3
reference = "README"
in_scope = true
"""
def _git(cwd: Path, *args: str) -> str:
return subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True).stdout.strip()
@pytest.fixture
def origin(tmp_path: Path) -> tuple[Path, str]:
"""A real git repository standing in for a remote, and the SHA a recipe would pin."""
root = tmp_path / "origin"
(root / "sub").mkdir(parents=True)
(root / "LICENSE").write_text("MIT")
(root / "app.py").write_text("def handler(name):\n q = f'select {name}'\n execute(q)\n")
_git(root, "init", "--quiet", "-b", "main")
_git(root, "config", "user.email", "[email protected]")
_git(root, "config", "user.name", "t")
_git(root, "add", "-A")
_git(root, "commit", "--quiet", "-m", "sample")
return root, _git(root, "rev-parse", "HEAD")
@pytest.fixture
def recipe_path(tmp_path: Path, origin: tuple[Path, str]) -> Path:
root, commit = origin
path = tmp_path / "sample.toml"
path.write_text(RECIPE.format(url=root.as_uri(), commit=commit))
return path
def test_recipe_round_trips(recipe_path: Path) -> None:
recipe = load_repo_recipe(recipe_path)
assert recipe.name == "sample"
assert recipe.measures == ("detection",)
assert [item.id for item in recipe.in_scope] == ["SAMPLE-1"]
assert recipe_payload(recipe)["known"] == [
{
"id": "SAMPLE-1",
"family": "injection",
"weakness": "the parameter is interpolated into the query",
"location": "app.py:handler:3",
"in_scope": True,
}
]
def test_a_branch_is_not_a_pin(tmp_path: Path) -> None:
"""A tag or branch would make the same recipe mean different code next month."""
path = tmp_path / "loose.toml"
path.write_text(RECIPE.format(url="https://example.invalid/x.git", commit="main"))
with pytest.raises(RepoRecipeError, match="full 40-character SHA"):
load_repo_recipe(path)
def test_in_scope_must_be_stated(tmp_path: Path, origin: tuple[Path, str]) -> None:
"""Defaulting it would silently decide whether 'not found' is a miss or a non-question."""
_, commit = origin
path = tmp_path / "quiet.toml"
path.write_text(RECIPE.format(url="https://example.invalid/x.git", commit=commit).replace("in_scope = true\n", ""))
with pytest.raises(RepoRecipeError, match="must state in_scope explicitly"):
load_repo_recipe(path)
def test_detection_needs_something_in_scope(tmp_path: Path, origin: tuple[Path, str]) -> None:
_, commit = origin
path = tmp_path / "empty.toml"
path.write_text(RECIPE.format(url="https://example.invalid/x.git", commit=commit).replace("in_scope = true", "in_scope = false"))
with pytest.raises(RepoRecipeError, match="names no in-scope vulnerability"):
load_repo_recipe(path)
def test_unknown_measure_is_rejected(tmp_path: Path, origin: tuple[Path, str]) -> None:
_, commit = origin
path = tmp_path / "odd.toml"
path.write_text(RECIPE.format(url="https://example.invalid/x.git", commit=commit).replace('["detection"]', '["vibes"]'))
with pytest.raises(RepoRecipeError, match="unknown measures"):
load_repo_recipe(path)
def test_offline_by_default(recipe_path: Path, tmp_path: Path) -> None:
"""No switch, no fetch -- this is what keeps CI from reaching the network."""
assert network_allowed() is False
with pytest.raises(RepoUnavailable, match="--fetch"):
resolve(load_repo_recipe(recipe_path), cache_root=tmp_path / "cache")
def test_the_switch_is_read_from_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("OPENULTRASAST_REPOS_NETWORK", "1")
assert network_allowed() is True
assert network_allowed(False) is False, "an explicit flag beats the environment"
def test_resolve_fetches_then_serves_from_cache(recipe_path: Path, tmp_path: Path) -> None:
recipe = load_repo_recipe(recipe_path)
cache = tmp_path / "cache"
root = resolve(recipe, fetch=True, cache_root=cache)
assert root == checkout_path(recipe, cache)
assert (root / "app.py").is_file()
assert verify(recipe, root) == (), "the recipe describes what was actually fetched"
# A warm cache spares the fetch; it never turns an offline run into a networked one.
assert resolve(recipe, cache_root=cache) == root
def test_checkout_is_keyed_by_commit(recipe_path: Path, tmp_path: Path) -> None:
"""Re-pinning must not overwrite the source an older baseline was measured against."""
recipe = load_repo_recipe(recipe_path)
other = checkout_path(recipe, tmp_path)
assert other.name == recipe.commit[:12]
assert other.parent.name == "sample"
def test_verify_catches_the_wrong_commit(recipe_path: Path, tmp_path: Path, origin: tuple[Path, str]) -> None:
root_repo, _ = origin
recipe = load_repo_recipe(recipe_path)
root = resolve(recipe, fetch=True, cache_root=tmp_path / "cache")
(root_repo / "app.py").write_text("def handler(name):\n pass\n")
_git(root_repo, "commit", "--quiet", "-am", "moved on")
moved = _git(root_repo, "rev-parse", "HEAD")
_git(root, "fetch", "--quiet", "origin", moved)
_git(root, "checkout", "--quiet", "FETCH_HEAD")
assert any("HEAD is" in problem for problem in verify(recipe, root))
def test_verify_catches_a_recipe_that_describes_absent_code(recipe_path: Path, tmp_path: Path) -> None:
recipe = load_repo_recipe(recipe_path)
root = resolve(recipe, fetch=True, cache_root=tmp_path / "cache")
(root / "app.py").unlink()
(root / "LICENSE").unlink()
problems = verify(recipe, root)
assert any("LICENSE" in problem for problem in problems)
assert any("app.py is not in the checkout" in problem for problem in problems)
def test_verify_catches_a_line_outside_the_file(recipe_path: Path, tmp_path: Path) -> None:
recipe = load_repo_recipe(recipe_path)
root = resolve(recipe, fetch=True, cache_root=tmp_path / "cache")
(root / "app.py").write_text("only one line\n")
assert any("outside" in problem for problem in verify(recipe, root))
def test_verify_catches_a_function_that_is_not_there(recipe_path: Path, tmp_path: Path) -> None:
recipe = load_repo_recipe(recipe_path)
root = resolve(recipe, fetch=True, cache_root=tmp_path / "cache")
(root / "app.py").write_text("a\nb\nc\n")
assert any("handler is not within" in problem for problem in verify(recipe, root))
def test_select_by_name(recipe_path: Path) -> None:
recipes = (load_repo_recipe(recipe_path),)
assert select(recipes, "all") == recipes
assert select(recipes, "sample") == recipes
assert select(recipes, "absent") == ()
def test_the_committed_recipes_parse() -> None:
"""The corpus itself, offline: every field present, every commit pinned, every scope decided."""
recipes = load_repo_recipes(DEFAULT_REPO_DIR)
assert {recipe.name for recipe in recipes} >= {"vampi", "libpng"}
for recipe in recipes:
assert recipe.license, f"{recipe.name} names no licence"
assert recipe.known, f"{recipe.name} names no known vulnerability"
assert all(item.line > 0 for item in recipe.known)
def test_the_envelope_checkout_claims_no_detection() -> None:
"""libpng's CVE is a memory-safety bug: recording 'not found' as a miss would make the number lie."""
libpng = next(recipe for recipe in load_repo_recipes(DEFAULT_REPO_DIR) if recipe.name == "libpng")
assert "detection" not in libpng.measures
assert libpng.in_scope == ()