mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
228 lines
7.8 KiB
Python
228 lines
7.8 KiB
Python
import json
|
|||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from openultrasast.complexity.hints import GAP_COVERED, KIND_FUZZ_HARNESS
|
||
|
|
from openultrasast.complexity.map import build_complexity_map
|
||
|
|
from openultrasast.findings import StaticFinding
|
||
|
|
from openultrasast.preprocess import FileTarget
|
||
|
|
|
||
|
|
|
||
|
|
def _target(
|
||
|
|
path: str,
|
||
|
|
*,
|
||
|
|
loc: int,
|
||
|
|
tags: list[str] | None = None,
|
||
|
|
language: str = "python",
|
||
|
|
hints: list[dict[str, object]] | None = None,
|
||
|
|
has_fuzz_entry_point: bool = False,
|
||
|
|
absolute_path: str | None = None,
|
||
|
|
) -> FileTarget:
|
||
|
|
return FileTarget(
|
||
|
|
path=path,
|
||
|
|
absolute_path=absolute_path or f"/repo/{path}",
|
||
|
|
language=language,
|
||
|
|
loc=loc,
|
||
|
|
tags=list(tags or []),
|
||
|
|
has_fuzz_entry_point=has_fuzz_entry_point,
|
||
|
|
reachability_hints=list(hints or []),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _hit(path: str, finding_id: str = "rule:helper:1") -> StaticFinding:
|
||
|
|
return StaticFinding(
|
||
|
|
finding_id=finding_id,
|
||
|
|
path=path,
|
||
|
|
title="pattern hit",
|
||
|
|
severity="low",
|
||
|
|
confidence="medium",
|
||
|
|
evidence_level="static_corroboration",
|
||
|
|
rationale="inventory",
|
||
|
|
line=1,
|
||
|
|
function_name=None,
|
||
|
|
reachability_status="unknown",
|
||
|
|
reachability_evidence=[],
|
||
|
|
reachability_conditions=[],
|
||
|
|
tags=[],
|
||
|
|
ranking_priority=1.0,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _tree_snapshot(root: Path) -> dict[str, tuple[int, bytes]]:
|
||
|
|
snapshot: dict[str, tuple[int, bytes]] = {}
|
||
|
|
for path in sorted(root.rglob("*")):
|
||
|
|
if path.is_file():
|
||
|
|
snapshot[path.relative_to(root).as_posix()] = (path.stat().st_mtime_ns, path.read_bytes())
|
||
|
|
return snapshot
|
||
|
|
|
||
|
|
|
||
|
|
def test_covered_hotspot_emits_covered_and_no_test_kind(tmp_path: Path) -> None:
|
||
|
|
widget = _target(
|
||
|
|
"src/widget.py",
|
||
|
|
loc=12,
|
||
|
|
hints=[{"kind": "route", "access_level": "public", "function_name": "render"}],
|
||
|
|
)
|
||
|
|
artifact = tmp_path / "complexity_map.json"
|
||
|
|
|
||
|
|
complexity_map = build_complexity_map(
|
||
|
|
[widget],
|
||
|
|
[_hit("src/widget.py")],
|
||
|
|
artifact,
|
||
|
|
repo_files=("src/widget.py", "tests/test_widget.py"),
|
||
|
|
sources={
|
||
|
|
"src/widget.py": "def render():\n return 1\n",
|
||
|
|
"tests/test_widget.py": "from widget import render\n\ndef test_render():\n assert render() == 1\n",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
hotspot = complexity_map.hotspots[0]
|
||
|
|
assert hotspot.test_hint is not None
|
||
|
|
assert hotspot.test_hint.gap == GAP_COVERED
|
||
|
|
assert hotspot.test_hint.gap == "covered"
|
||
|
|
assert hotspot.test_hint.test_kind is None
|
||
|
|
assert hotspot.test_hint.path == "src/widget.py"
|
||
|
|
assert hotspot.test_hint.function_name == "render"
|
||
|
|
payload = json.loads(artifact.read_text())
|
||
|
|
hint = payload["hotspots"][0]["test_hint"]
|
||
|
|
assert hint["gap"] == "covered"
|
||
|
|
assert hint["test_kind"] is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_fuzzable_c_parser_without_harness_emits_fuzz_harness(tmp_path: Path) -> None:
|
||
|
|
parser = _target(
|
||
|
|
"src/parser.c",
|
||
|
|
loc=400,
|
||
|
|
language="c",
|
||
|
|
tags=["parser", "memory_unsafe"],
|
||
|
|
hints=[{"kind": "parser", "access_level": "public", "function_name": "parse_header"}],
|
||
|
|
has_fuzz_entry_point=False,
|
||
|
|
)
|
||
|
|
artifact = tmp_path / "complexity_map.json"
|
||
|
|
|
||
|
|
complexity_map = build_complexity_map(
|
||
|
|
[parser],
|
||
|
|
[],
|
||
|
|
artifact,
|
||
|
|
repo_files=("src/parser.c", "tests/test_parser.c"),
|
||
|
|
sources={
|
||
|
|
"src/parser.c": "int parse_header(const char *buf) { return 0; }\n",
|
||
|
|
"tests/test_parser.c": 'int test_parse_header(void) { return parse_header(""); }\n',
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
hotspot = complexity_map.hotspots[0]
|
||
|
|
assert hotspot.test_hint is not None
|
||
|
|
assert hotspot.test_hint.gap == "no_fuzz_entry"
|
||
|
|
assert hotspot.test_hint.test_kind == KIND_FUZZ_HARNESS
|
||
|
|
assert hotspot.test_hint.test_kind == "fuzz-harness"
|
||
|
|
assert hotspot.test_hint.path == "src/parser.c"
|
||
|
|
assert hotspot.test_hint.function_name == "parse_header"
|
||
|
|
assert hotspot.test_hint.reason
|
||
|
|
payload = json.loads(artifact.read_text())
|
||
|
|
hint = payload["hotspots"][0]["test_hint"]
|
||
|
|
assert hint["gap"] == "no_fuzz_entry"
|
||
|
|
assert hint["test_kind"] == "fuzz-harness"
|
||
|
|
|
||
|
|
|
||
|
|
def test_attach_test_hints_leaves_scanned_tree_unchanged(tmp_path: Path) -> None:
|
||
|
|
repo = tmp_path / "repo"
|
||
|
|
src = repo / "src"
|
||
|
|
tests_dir = repo / "tests"
|
||
|
|
src.mkdir(parents=True)
|
||
|
|
tests_dir.mkdir()
|
||
|
|
parser = src / "parser.c"
|
||
|
|
helper = src / "helper.py"
|
||
|
|
parser_test = tests_dir / "test_parser.c"
|
||
|
|
parser.write_text("int parse_header(const char *buf) {\n return buf[0];\n}\n")
|
||
|
|
helper.write_text("def ping():\n return 1\n")
|
||
|
|
parser_test.write_text("int test_parse_header(void) { return 0; }\n")
|
||
|
|
before = _tree_snapshot(repo)
|
||
|
|
|
||
|
|
artifact = tmp_path / "out" / "complexity_map.json"
|
||
|
|
targets = [
|
||
|
|
_target(
|
||
|
|
"src/parser.c",
|
||
|
|
loc=3,
|
||
|
|
language="c",
|
||
|
|
tags=["parser", "memory_unsafe"],
|
||
|
|
hints=[{"kind": "parser", "access_level": "public", "function_name": "parse_header"}],
|
||
|
|
absolute_path=str(parser),
|
||
|
|
),
|
||
|
|
_target("src/helper.py", loc=2, tags=[], absolute_path=str(helper)),
|
||
|
|
]
|
||
|
|
|
||
|
|
complexity_map = build_complexity_map(
|
||
|
|
targets,
|
||
|
|
[_hit("src/helper.py")],
|
||
|
|
artifact,
|
||
|
|
repo_files=("src/parser.c", "src/helper.py", "tests/test_parser.c"),
|
||
|
|
sources={
|
||
|
|
"src/parser.c": parser.read_text(),
|
||
|
|
"src/helper.py": helper.read_text(),
|
||
|
|
"tests/test_parser.c": parser_test.read_text(),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert complexity_map.hotspots
|
||
|
|
assert all(hotspot.test_hint is not None for hotspot in complexity_map.hotspots)
|
||
|
|
assert _tree_snapshot(repo) == before
|
||
|
|
assert set(_tree_snapshot(repo)) == {"src/parser.c", "src/helper.py", "tests/test_parser.c"}
|
||
|
|
assert not any(path.as_posix().endswith(".py") and "test" in path.name for path in src.rglob("*"))
|
||
|
|
assert artifact.is_file()
|
||
|
|
assert artifact.is_relative_to(tmp_path / "out")
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_adjacent_test_recommends_closed_test_kind(tmp_path: Path) -> None:
|
||
|
|
helper = _target("src/helper.py", loc=4, tags=[])
|
||
|
|
route = _target(
|
||
|
|
"src/api.py",
|
||
|
|
loc=40,
|
||
|
|
tags=["network_entry"],
|
||
|
|
hints=[{"kind": "route", "access_level": "public", "function_name": "handle"}],
|
||
|
|
)
|
||
|
|
artifact = tmp_path / "complexity_map.json"
|
||
|
|
|
||
|
|
complexity_map = build_complexity_map(
|
||
|
|
[helper, route],
|
||
|
|
[],
|
||
|
|
artifact,
|
||
|
|
repo_files=("src/helper.py", "src/api.py"),
|
||
|
|
sources={
|
||
|
|
"src/helper.py": "def ping():\n return 1\n",
|
||
|
|
"src/api.py": "def handle():\n return {}\n",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
by_path = {hotspot.path: hotspot for hotspot in complexity_map.hotspots}
|
||
|
|
|
||
|
|
assert by_path["src/helper.py"].test_hint is not None
|
||
|
|
assert by_path["src/helper.py"].test_hint.gap == "no_adjacent_test"
|
||
|
|
assert by_path["src/helper.py"].test_hint.test_kind == "unit"
|
||
|
|
assert by_path["src/api.py"].test_hint is not None
|
||
|
|
assert by_path["src/api.py"].test_hint.gap == "no_adjacent_test"
|
||
|
|
assert by_path["src/api.py"].test_hint.test_kind == "http-contract"
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_function_reference_when_adjacent_test_omits_function(tmp_path: Path) -> None:
|
||
|
|
widget = _target(
|
||
|
|
"src/widget.py",
|
||
|
|
loc=12,
|
||
|
|
hints=[{"kind": "route", "access_level": "public", "function_name": "render"}],
|
||
|
|
)
|
||
|
|
artifact = tmp_path / "complexity_map.json"
|
||
|
|
|
||
|
|
complexity_map = build_complexity_map(
|
||
|
|
[widget],
|
||
|
|
[],
|
||
|
|
artifact,
|
||
|
|
repo_files=("src/widget.py", "tests/test_widget.py"),
|
||
|
|
sources={
|
||
|
|
"src/widget.py": "def render():\n return 1\n",
|
||
|
|
"tests/test_widget.py": "def test_placeholder():\n assert True\n",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
hint = complexity_map.hotspots[0].test_hint
|
||
|
|
assert hint is not None
|
||
|
|
assert hint.gap == "no_function_reference"
|
||
|
|
assert hint.test_kind == "unit"
|
||
|
|
assert hint.function_name == "render"
|