mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
224 lines
8.3 KiB
Python
224 lines
8.3 KiB
Python
"""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 == ()
|