2026-06-18 10:16:25 +02:00
|
|
|
import json
|
2026-09-03 23:14:00 +02:00
|
|
|
import subprocess
|
2026-06-18 10:16:25 +02:00
|
|
|
from pathlib import Path
|
2026-09-03 19:44:04 +02:00
|
|
|
|
|
|
|
|
import pytest
|
2026-06-18 10:16:25 +02:00
|
|
|
|
|
|
|
|
from openultrasast.cli import main
|
2026-09-04 00:18:33 +02:00
|
|
|
from openultrasast.complexity.hints import TestHint
|
|
|
|
|
from openultrasast.complexity.map import ComplexityMap, Hotspot
|
2026-06-18 10:16:25 +02:00
|
|
|
from openultrasast.findings import StaticFinding
|
|
|
|
|
from openultrasast.reports import scan_exit_code, write_manifest, write_markdown_report, write_sarif_report
|
|
|
|
|
from openultrasast.run import ScanRun
|
2026-09-03 23:14:00 +02:00
|
|
|
from openultrasast.stages import Stage, plan_for_mode, record_completed, stages_payload
|
2026-06-18 10:16:25 +02:00
|
|
|
from openultrasast.verification import verify_finding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sarif_report_uses_finding_ids_and_evidence_properties(tmp_path: Path) -> None:
|
|
|
|
|
finding = _finding()
|
|
|
|
|
verification = verify_finding(finding)
|
|
|
|
|
output = tmp_path / "report.sarif"
|
|
|
|
|
|
|
|
|
|
write_sarif_report([finding], [verification], output)
|
|
|
|
|
|
|
|
|
|
payload = json.loads(output.read_text())
|
|
|
|
|
result = payload["runs"][0]["results"][0]
|
|
|
|
|
|
|
|
|
|
assert payload["version"] == "2.1.0"
|
|
|
|
|
assert result["partialFingerprints"]["openultrasastFindingId"] == finding.finding_id
|
|
|
|
|
assert result["properties"]["finding_id"] == finding.finding_id
|
|
|
|
|
assert result["properties"]["evidence_level"] == "static_corroboration"
|
|
|
|
|
assert result["properties"]["verification"]["status"] == "accepted"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manifest_links_shared_artifacts_by_finding_id(tmp_path: Path) -> None:
|
|
|
|
|
run = ScanRun(scan_id="scan-1", root=tmp_path / "run", target=tmp_path / "repo")
|
|
|
|
|
run.root.mkdir()
|
|
|
|
|
finding = _finding()
|
|
|
|
|
verification = verify_finding(finding)
|
|
|
|
|
artifacts = {
|
|
|
|
|
"findings": run.root / "findings.json",
|
|
|
|
|
"verification": run.root / "verification.json",
|
|
|
|
|
"markdown": run.root / "report.md",
|
|
|
|
|
"sarif": run.root / "report.sarif",
|
|
|
|
|
}
|
|
|
|
|
output = run.root / "manifest.json"
|
|
|
|
|
|
|
|
|
|
write_manifest(run=run, findings=[finding], verifications=[verification], artifact_paths=artifacts, path=output)
|
|
|
|
|
|
|
|
|
|
payload = json.loads(output.read_text())
|
|
|
|
|
manifest_finding = payload["findings"][0]
|
|
|
|
|
|
|
|
|
|
assert payload["scan_id"] == "scan-1"
|
|
|
|
|
assert manifest_finding["finding_id"] == finding.finding_id
|
|
|
|
|
assert manifest_finding["verification_status"] == "accepted"
|
|
|
|
|
assert manifest_finding["artifact_refs"]["sarif"] == "report.sarif"
|
2026-09-03 23:14:00 +02:00
|
|
|
assert "stages" not in payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manifest_includes_requested_completed_skipped_stages(tmp_path: Path) -> None:
|
|
|
|
|
run = ScanRun(scan_id="scan-1", root=tmp_path / "run", target=tmp_path / "repo")
|
|
|
|
|
run.root.mkdir()
|
|
|
|
|
finding = _finding()
|
|
|
|
|
artifacts = {
|
|
|
|
|
"findings": run.root / "findings.json",
|
|
|
|
|
"verification": run.root / "verification.json",
|
|
|
|
|
"markdown": run.root / "report.md",
|
|
|
|
|
"sarif": run.root / "report.sarif",
|
|
|
|
|
}
|
|
|
|
|
output = run.root / "manifest.json"
|
|
|
|
|
plan = record_completed(plan_for_mode("quick"), Stage.STATIC)
|
|
|
|
|
|
|
|
|
|
write_manifest(
|
|
|
|
|
run=run,
|
|
|
|
|
findings=[finding],
|
|
|
|
|
verifications=[verify_finding(finding)],
|
|
|
|
|
artifact_paths=artifacts,
|
|
|
|
|
path=output,
|
|
|
|
|
stages=stages_payload(plan),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
payload = json.loads(output.read_text())
|
|
|
|
|
assert payload["stages"] == {"requested": ["static"], "completed": ["static"], "skipped": []}
|
|
|
|
|
assert payload["findings"][0]["finding_id"] == finding.finding_id
|
2026-06-18 10:16:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_markdown_report_includes_verification_status(tmp_path: Path) -> None:
|
|
|
|
|
finding = _finding()
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report([finding], output, [verify_finding(finding)])
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
2026-09-03 23:14:00 +02:00
|
|
|
assert "## Inventory" in text
|
2026-06-18 10:16:25 +02:00
|
|
|
assert "Verification: `accepted`" in text
|
|
|
|
|
assert finding.finding_id in text
|
2026-09-03 23:14:00 +02:00
|
|
|
assert "worth-fixing" not in text.lower()
|
|
|
|
|
assert "worth_fixing" not in text.lower()
|
|
|
|
|
|
|
|
|
|
|
2026-10-13 10:50:01 +02:00
|
|
|
def test_markdown_report_includes_complexity_map(tmp_path: Path) -> None:
|
2026-09-04 00:18:33 +02:00
|
|
|
finding = _finding()
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
hint = TestHint(
|
|
|
|
|
path="app.py",
|
|
|
|
|
function_name="admin",
|
|
|
|
|
gap="no_adjacent_test",
|
|
|
|
|
test_kind="http-contract",
|
|
|
|
|
reason="no adjacent test file for app.py::admin; recommend http-contract.",
|
|
|
|
|
)
|
|
|
|
|
hotspot = Hotspot(
|
|
|
|
|
path="app.py",
|
|
|
|
|
function_name="admin",
|
|
|
|
|
score=6.5,
|
|
|
|
|
band="high",
|
|
|
|
|
signals={"loc": 12, "has_adjacent_test": False},
|
|
|
|
|
rationale="high band: nested public route",
|
|
|
|
|
test_hint=hint,
|
|
|
|
|
inventory_finding_ids=(finding.finding_id,),
|
|
|
|
|
)
|
|
|
|
|
write_markdown_report(
|
|
|
|
|
[finding],
|
|
|
|
|
output,
|
|
|
|
|
[verify_finding(finding)],
|
|
|
|
|
complexity_map=ComplexityMap(hotspots=(hotspot,), heuristic_only=True),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
|
|
|
|
inventory_at = text.index("## Inventory")
|
|
|
|
|
map_at = text.index("## Complexity map")
|
2026-10-13 10:50:01 +02:00
|
|
|
assert inventory_at < map_at
|
|
|
|
|
map_section = text[map_at:]
|
2026-09-04 00:18:33 +02:00
|
|
|
assert "app.py::admin" in map_section
|
|
|
|
|
assert "Gap: `no_adjacent_test`" in map_section
|
|
|
|
|
assert "Recommended test: `http-contract`" in map_section
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 23:14:00 +02:00
|
|
|
def test_markdown_report_labels_hits_as_inventory_not_worth_fixing(tmp_path: Path) -> None:
|
|
|
|
|
finding = _finding()
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report([finding], output, [verify_finding(finding)])
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
|
|
|
|
inventory_at = text.index("## Inventory")
|
|
|
|
|
finding_at = text.index(f"## {finding.title}")
|
|
|
|
|
assert inventory_at < finding_at
|
|
|
|
|
assert "worth-fixing" not in text.lower()
|
2026-06-18 10:16:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_scan_exit_code_policy() -> None:
|
|
|
|
|
finding = _finding()
|
|
|
|
|
verification = verify_finding(finding)
|
|
|
|
|
|
|
|
|
|
assert scan_exit_code([finding], [verification], "never") == 0
|
|
|
|
|
assert scan_exit_code([finding], [verification], "findings") == 1
|
2026-09-03 19:44:04 +02:00
|
|
|
assert scan_exit_code([], [], "findings") == 0
|
2026-06-18 10:16:25 +02:00
|
|
|
assert scan_exit_code([finding], [verification], "verified") == 1
|
|
|
|
|
assert scan_exit_code([], [], "verified") == 0
|
2026-09-03 19:44:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_scan_exit_code_unknown_policy() -> None:
|
|
|
|
|
with pytest.raises(ValueError, match="unknown fail policy"):
|
|
|
|
|
scan_exit_code([], [], "maybe")
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 10:16:25 +02:00
|
|
|
def test_cli_scan_writes_sarif_manifest_and_honors_fail_policy(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
|
|
|
repo = tmp_path / "repo"
|
|
|
|
|
repo.mkdir()
|
|
|
|
|
(repo / "app.py").write_text("@app.route('/admin')\ndef admin():\n return eval(request.data)\n")
|
|
|
|
|
monkeypatch.setenv("OPENULTRASAST_RUNS_DIR", ".runs")
|
|
|
|
|
|
|
|
|
|
assert main(["scan", str(repo), "--mode", "quick", "--fail-on", "verified"]) == 1
|
|
|
|
|
|
|
|
|
|
run_dir = sorted((repo / ".runs").iterdir())[-1]
|
|
|
|
|
sarif = json.loads((run_dir / "report.sarif").read_text())
|
|
|
|
|
manifest = json.loads((run_dir / "manifest.json").read_text())
|
|
|
|
|
|
|
|
|
|
finding_id = manifest["findings"][0]["finding_id"]
|
|
|
|
|
assert sarif["runs"][0]["results"][0]["properties"]["finding_id"] == finding_id
|
|
|
|
|
assert manifest["findings"][0]["artifact_refs"]["verification_json"] == "verification.json"
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 23:14:00 +02:00
|
|
|
def test_quick_scan_report_is_inventory_without_map_or_docker(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
from openultrasast import tool_hunter
|
|
|
|
|
from openultrasast.complexity import map as complexity_map
|
|
|
|
|
|
|
|
|
|
repo = tmp_path / "repo"
|
|
|
|
|
repo.mkdir()
|
|
|
|
|
(repo / "app.py").write_text("@app.route('/admin')\ndef admin():\n return eval(request.data)\n")
|
|
|
|
|
monkeypatch.setenv("OPENULTRASAST_RUNS_DIR", ".runs")
|
|
|
|
|
|
|
|
|
|
docker_argv: list[list[str]] = []
|
|
|
|
|
original_run = subprocess.run
|
|
|
|
|
|
|
|
|
|
def guarded_run(command, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
|
|
|
argv = list(command) if isinstance(command, list | tuple) else [command]
|
|
|
|
|
if argv and Path(str(argv[0])).name == "docker":
|
|
|
|
|
docker_argv.append(argv)
|
|
|
|
|
raise AssertionError(f"quick scan must not start docker: {argv}")
|
|
|
|
|
return original_run(command, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def fail(message: str): # type: ignore[no-untyped-def]
|
|
|
|
|
def _fail(*args, **kwargs): # type: ignore[no-untyped-def]
|
|
|
|
|
raise AssertionError(message)
|
|
|
|
|
|
|
|
|
|
return _fail
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(subprocess, "run", guarded_run)
|
|
|
|
|
monkeypatch.setattr(complexity_map, "build_complexity_map", fail("quick scan must not build a complexity map"))
|
|
|
|
|
monkeypatch.setattr(tool_hunter, "run_tool_hunter", fail("quick scan must not start a hunter model"))
|
|
|
|
|
|
|
|
|
|
assert main(["scan", str(repo), "--mode", "quick"]) == 0
|
|
|
|
|
|
|
|
|
|
run_dir = sorted((repo / ".runs").iterdir())[-1]
|
|
|
|
|
report = (run_dir / "report.md").read_text()
|
|
|
|
|
manifest = json.loads((run_dir / "manifest.json").read_text())
|
|
|
|
|
|
|
|
|
|
assert "## Inventory" in report
|
|
|
|
|
assert "worth-fixing" not in report.lower()
|
|
|
|
|
assert not (run_dir / "complexity_map.json").exists()
|
|
|
|
|
assert docker_argv == []
|
|
|
|
|
assert manifest["stages"]["requested"] == ["static"]
|
|
|
|
|
assert manifest["stages"]["completed"] == ["static"]
|
|
|
|
|
assert manifest["stages"]["skipped"] == []
|
2026-09-03 23:26:54 +02:00
|
|
|
assert "complexity" not in manifest
|
|
|
|
|
assert "complexity_map" not in manifest.get("artifacts", {})
|
|
|
|
|
assert not any(entry.get("reason") == "hunter_model_unavailable" for entry in manifest.get("degradations", []))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_standard_scan_writes_complexity_map_without_docker(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
|
|
|
|
|
repo = tmp_path / "repo"
|
|
|
|
|
repo.mkdir()
|
|
|
|
|
(repo / "app.py").write_text("@app.route('/admin')\ndef admin():\n return eval(request.data)\n")
|
|
|
|
|
monkeypatch.setenv("OPENULTRASAST_RUNS_DIR", ".runs")
|
|
|
|
|
|
|
|
|
|
docker_argv: list[list[str]] = []
|
|
|
|
|
original_run = subprocess.run
|
|
|
|
|
|
|
|
|
|
def guarded_run(command, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
|
|
|
argv = list(command) if isinstance(command, list | tuple) else [command]
|
|
|
|
|
if argv and Path(str(argv[0])).name == "docker":
|
|
|
|
|
docker_argv.append(argv)
|
|
|
|
|
raise AssertionError(f"standard scan must not start docker: {argv}")
|
|
|
|
|
return original_run(command, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(subprocess, "run", guarded_run)
|
|
|
|
|
|
|
|
|
|
assert main(["scan", str(repo), "--mode", "standard"]) == 0
|
|
|
|
|
|
|
|
|
|
run_dir = sorted((repo / ".runs").iterdir())[-1]
|
|
|
|
|
map_path = run_dir / "complexity_map.json"
|
|
|
|
|
manifest = json.loads((run_dir / "manifest.json").read_text())
|
|
|
|
|
complexity_map = json.loads(map_path.read_text())
|
|
|
|
|
|
|
|
|
|
assert map_path.is_file()
|
|
|
|
|
assert complexity_map["heuristic_only"] is True
|
|
|
|
|
assert isinstance(complexity_map["hotspots"], list)
|
|
|
|
|
assert complexity_map["hotspots"]
|
|
|
|
|
assert docker_argv == []
|
|
|
|
|
assert manifest["stages"]["requested"] == ["static", "map"]
|
|
|
|
|
assert manifest["stages"]["completed"] == ["static", "map"]
|
|
|
|
|
assert manifest["stages"]["skipped"] == []
|
|
|
|
|
assert manifest["complexity"]["hotspot_count"] == len(complexity_map["hotspots"])
|
|
|
|
|
assert manifest["complexity"]["heuristic_only"] is True
|
|
|
|
|
assert manifest["artifacts"]["complexity_map"] == "complexity_map.json"
|
2026-09-04 00:18:33 +02:00
|
|
|
report = (run_dir / "report.md").read_text()
|
|
|
|
|
assert "## Inventory" in report
|
|
|
|
|
assert "## Complexity map" in report
|
|
|
|
|
assert "Gap:" in report
|
|
|
|
|
assert "## Worth fixing" not in report
|
2026-09-03 23:26:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_standard_scan_records_hunter_model_unavailable_when_model_unset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
|
|
|
|
|
repo = tmp_path / "repo"
|
|
|
|
|
repo.mkdir()
|
|
|
|
|
(repo / "app.py").write_text("@app.route('/admin')\ndef admin():\n return eval(request.data)\n")
|
|
|
|
|
monkeypatch.setenv("OPENULTRASAST_RUNS_DIR", ".runs")
|
|
|
|
|
|
|
|
|
|
assert main(["scan", str(repo), "--mode", "standard"]) == 0
|
|
|
|
|
|
|
|
|
|
run_dir = sorted((repo / ".runs").iterdir())[-1]
|
|
|
|
|
manifest = json.loads((run_dir / "manifest.json").read_text())
|
|
|
|
|
degradations = manifest.get("degradations", [])
|
|
|
|
|
|
|
|
|
|
assert any(entry.get("reason") == "hunter_model_unavailable" for entry in degradations)
|
|
|
|
|
hunter_skip = next(entry for entry in degradations if entry.get("reason") == "hunter_model_unavailable")
|
|
|
|
|
assert hunter_skip["stage"] in {"map", "hunter_pool"}
|
|
|
|
|
assert manifest["complexity"]["heuristic_only"] is True
|
|
|
|
|
assert (run_dir / "complexity_map.json").is_file()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manifest_includes_complexity_summary(tmp_path: Path) -> None:
|
|
|
|
|
run = ScanRun(scan_id="scan-1", root=tmp_path / "run", target=tmp_path / "repo")
|
|
|
|
|
run.root.mkdir()
|
|
|
|
|
finding = _finding()
|
|
|
|
|
artifacts = {
|
|
|
|
|
"findings": run.root / "findings.json",
|
|
|
|
|
"verification": run.root / "verification.json",
|
|
|
|
|
"markdown": run.root / "report.md",
|
|
|
|
|
"sarif": run.root / "report.sarif",
|
|
|
|
|
"complexity_map": run.root / "complexity_map.json",
|
|
|
|
|
}
|
|
|
|
|
output = run.root / "manifest.json"
|
|
|
|
|
|
|
|
|
|
write_manifest(
|
|
|
|
|
run=run,
|
|
|
|
|
findings=[finding],
|
|
|
|
|
verifications=[verify_finding(finding)],
|
|
|
|
|
artifact_paths=artifacts,
|
|
|
|
|
path=output,
|
|
|
|
|
complexity={"hotspot_count": 3, "heuristic_only": True},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
payload = json.loads(output.read_text())
|
|
|
|
|
assert payload["complexity"] == {"hotspot_count": 3, "heuristic_only": True}
|
|
|
|
|
assert payload["artifacts"]["complexity_map"] == "complexity_map.json"
|
2026-09-03 23:14:00 +02:00
|
|
|
|
|
|
|
|
|
2026-06-18 10:16:25 +02:00
|
|
|
def _finding() -> StaticFinding:
|
|
|
|
|
return StaticFinding(
|
|
|
|
|
finding_id="python-unsafe-eval:app.py:3",
|
|
|
|
|
path="app.py",
|
|
|
|
|
title="Dynamic Python execution needs review",
|
|
|
|
|
severity="high",
|
|
|
|
|
confidence="medium",
|
|
|
|
|
evidence_level="static_corroboration",
|
|
|
|
|
rationale="Static pattern matched.",
|
|
|
|
|
line=3,
|
|
|
|
|
function_name="admin",
|
|
|
|
|
reachability_status="reachable",
|
|
|
|
|
reachability_evidence=[
|
|
|
|
|
{
|
|
|
|
|
"kind": "route",
|
|
|
|
|
"access_level": "public",
|
|
|
|
|
"line": 1,
|
|
|
|
|
"end_line": 3,
|
|
|
|
|
"function_name": "admin",
|
|
|
|
|
"conditions": [],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
reachability_conditions=[],
|
|
|
|
|
tags=["syscall_entry"],
|
|
|
|
|
ranking_priority=3.0,
|
|
|
|
|
)
|
2026-09-09 13:29:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pattern_finding(rule: str, path: str, line: int) -> StaticFinding:
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
base = _finding()
|
|
|
|
|
return replace(base, finding_id=f"{rule}:{path}:{line}", path=path, line=line, title=f"{rule} needs review")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_pattern_rule_with_many_sites_is_summarised_not_repeated(tmp_path: Path) -> None:
|
|
|
|
|
"""contributor-scan 2.11. libpng emitted 198 sections, 163 of them asserting that `memcpy` and `strcpy`
|
|
|
|
|
are PRESENT in a library that uses them correctly throughout. That is the noise developers have learned
|
|
|
|
|
to skip in every other tool, and shipping it costs the findings beside it their credibility."""
|
|
|
|
|
crowded = [_pattern_finding("c-unsafe-copy", f"src/f{i}.c", i) for i in range(20)]
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report(crowded, output)
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
2026-09-09 15:23:57 +02:00
|
|
|
assert "## Proposed, not corroborated" in text
|
2026-09-09 13:29:26 +02:00
|
|
|
assert "20 site(s) across 20 file(s)" in text
|
|
|
|
|
assert text.count("## c-unsafe-copy needs review") == 0, "twenty sections for one rule is the noise"
|
|
|
|
|
assert "`findings.json`" in text, "counted, never dropped"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_pattern_rule_with_few_sites_keeps_its_sections(tmp_path: Path) -> None:
|
|
|
|
|
"""One `python-flask-debug` is a finding a contributor can act on, and must not be summarised away."""
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report([_pattern_finding("python-flask-debug", "app.py", 17)], output)
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
|
|
|
|
assert "## python-flask-debug needs review" in text
|
2026-09-09 15:23:57 +02:00
|
|
|
assert "## Proposed, not corroborated" not in text
|
2026-09-09 13:29:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_reasoned_claim_keeps_its_section_whatever_its_rung(tmp_path: Path) -> None:
|
|
|
|
|
"""An obligation or model finding sits at `suspicion` too, but it is a claim about a site rather than an
|
|
|
|
|
observation that an API exists. Twenty of them are twenty findings, not one property of the codebase."""
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
base = _finding()
|
|
|
|
|
reasoned = [
|
|
|
|
|
replace(base, finding_id=f"obligation:protected_read:api/u.py:{i}", evidence_level="suspicion", path="api/u.py", line=i)
|
|
|
|
|
for i in range(20)
|
|
|
|
|
]
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report(reasoned, output)
|
|
|
|
|
|
|
|
|
|
assert output.read_text().count(f"## {base.title}") == 20
|
2026-09-09 15:08:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_report_says_what_was_looked_for_and_what_cannot_be_decided(tmp_path: Path) -> None:
|
|
|
|
|
"""contributor-scan 2.16, Req 5.6. `c/memory` scored zero true positives across 89k lines of C for a
|
|
|
|
|
structural reason -- it models string CALLS and libpng overflows through arithmetic. A silent scan
|
|
|
|
|
reading as a clean bill of health is worse than a noisy one, because the reader acts on it."""
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
coverage = [
|
|
|
|
|
{
|
|
|
|
|
"language": "c",
|
|
|
|
|
"family": "memory",
|
|
|
|
|
"regions": 121,
|
|
|
|
|
"modelled": 9,
|
|
|
|
|
"description": "Misuse of unbounded string and format APIs",
|
|
|
|
|
"limits": "Models CALLS, not arithmetic. It cannot decide an overflow expressed as an index.",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
write_markdown_report([], output, coverage=coverage)
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
|
|
|
|
assert "## What was analysed" in text
|
|
|
|
|
assert "| c | `memory` | 121 | 9 |" in text, "what it looked for, counted from the fact table itself"
|
|
|
|
|
assert "### Stated limits" in text
|
|
|
|
|
assert "cannot decide an overflow expressed as an index" in text
|
|
|
|
|
assert "silence here means no fact table covered the code, not that the code is safe" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_scan_that_made_no_claims_states_no_limits(tmp_path: Path) -> None:
|
|
|
|
|
"""A report with no model layer has nothing to disclaim; the section would be noise."""
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report([_finding()], output)
|
|
|
|
|
|
|
|
|
|
assert "## What was analysed" not in output.read_text()
|
2026-09-09 15:23:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_model_suspicion_is_grouped_like_a_pattern_match(tmp_path: Path) -> None:
|
|
|
|
|
"""contributor-scan 2.17, Req 5.5's other half.
|
|
|
|
|
|
|
|
|
|
A model suspicion is what the enumerator proposed at a site the graph could NOT decide, affirmed by a
|
|
|
|
|
judge with no coverage there -- a pattern match with an opinion attached. It reads like a reasoned claim
|
|
|
|
|
because a model produced it, and on vampi it was 38 of 60 findings on a 520-line application.
|
|
|
|
|
"""
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
base = _finding()
|
|
|
|
|
suspicions = [
|
|
|
|
|
replace(
|
|
|
|
|
base,
|
|
|
|
|
finding_id=f"model:injection:api/f{i}.py:{i}:jsonify",
|
|
|
|
|
evidence_level="suspicion",
|
|
|
|
|
rung="suspicion",
|
|
|
|
|
path=f"api/f{i}.py",
|
|
|
|
|
line=i,
|
|
|
|
|
rationale="the model resolved no flow here; its sink table may not cover this API",
|
|
|
|
|
)
|
|
|
|
|
for i in range(20)
|
|
|
|
|
]
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report(suspicions, output)
|
|
|
|
|
|
|
|
|
|
text = output.read_text()
|
|
|
|
|
assert "`model:injection`" in text, "grouped by family, since the rule is always `model`"
|
|
|
|
|
assert "20 site(s)" in text
|
|
|
|
|
assert text.count(f"## {base.title}") == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_an_obligation_finding_is_not_grouped_away(tmp_path: Path) -> None:
|
|
|
|
|
"""It sits at `suspicion` too, but it comes from a structural analysis rather than a guess. Collapsing
|
|
|
|
|
it would hide a detector -- which an earlier attempt at this rule did, and six tests caught it."""
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
base = _finding()
|
|
|
|
|
obligations = [
|
|
|
|
|
replace(base, finding_id=f"obligation:protected_read:api/u.py:{i}", evidence_level="suspicion", rung="suspicion", line=i)
|
|
|
|
|
for i in range(20)
|
|
|
|
|
]
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report(obligations, output)
|
|
|
|
|
|
|
|
|
|
assert output.read_text().count(f"## {base.title}") == 20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_an_entailed_model_finding_keeps_its_section(tmp_path: Path) -> None:
|
|
|
|
|
"""The rung is the product. Whatever the model layer DECIDED is never summarised away."""
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
base = _finding()
|
|
|
|
|
entailed = [
|
|
|
|
|
replace(base, finding_id=f"model:injection:app.py:{i}:execute", evidence_level="suspicion", rung="model_entailed", line=i)
|
|
|
|
|
for i in range(20)
|
|
|
|
|
]
|
|
|
|
|
output = tmp_path / "report.md"
|
|
|
|
|
|
|
|
|
|
write_markdown_report(entailed, output)
|
|
|
|
|
|
|
|
|
|
assert output.read_text().count(f"## {base.title}") == 20
|
2026-09-10 07:32:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_sharded_graph_is_stated_in_the_report(tmp_path) -> None: # type: ignore[no-untyped-def]
|
|
|
|
|
"""A graph built in parts cannot see a flow that crosses the parts, and the reader has no way to know
|
|
|
|
|
that from the findings alone."""
|
|
|
|
|
from openultrasast.reports import write_markdown_report
|
|
|
|
|
|
|
|
|
|
path = tmp_path / "report.md"
|
|
|
|
|
write_markdown_report(
|
|
|
|
|
[],
|
|
|
|
|
path,
|
|
|
|
|
[],
|
|
|
|
|
coverage=[{"language": "php", "family": "injection", "regions": 3, "modelled": 12, "limits": ""}],
|
|
|
|
|
degradations=[
|
|
|
|
|
{"stage": "model", "reason": "cpg_sharded", "shards": 2},
|
|
|
|
|
{"stage": "model", "reason": "files_unparsed", "count": 1, "files": ["big.php"]},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
text = path.read_text()
|
|
|
|
|
assert "### What could not be analysed" in text
|
|
|
|
|
assert "built in 2 parts" in text
|
|
|
|
|
assert "is not visible to any question asked here" in text
|
|
|
|
|
assert "`big.php`" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_clean_scan_states_no_coverage_gaps(tmp_path) -> None: # type: ignore[no-untyped-def]
|
|
|
|
|
from openultrasast.reports import write_markdown_report
|
|
|
|
|
|
|
|
|
|
path = tmp_path / "report.md"
|
|
|
|
|
write_markdown_report(
|
|
|
|
|
[],
|
|
|
|
|
path,
|
|
|
|
|
[],
|
|
|
|
|
coverage=[{"language": "php", "family": "injection", "regions": 3, "modelled": 12, "limits": ""}],
|
|
|
|
|
degradations=[],
|
|
|
|
|
)
|
|
|
|
|
assert "### What could not be analysed" not in path.read_text()
|
2026-10-02 22:10:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_recorded_scan_degradation_is_rendered(tmp_path) -> None: # type: ignore[no-untyped-def]
|
|
|
|
|
"""pre-push 17.2 / Req 9.2: the eight degradations cli.py records and the report used to drop."""
|
|
|
|
|
from openultrasast.reports import write_markdown_report
|
|
|
|
|
|
|
|
|
|
reasons = [
|
|
|
|
|
{"stage": "model", "reason": "learning_endpoint_unavailable"},
|
|
|
|
|
{"stage": "model", "reason": "cpg_unavailable"},
|
|
|
|
|
{"stage": "overlay", "reason": "facts_unavailable", "detail": "missing table"},
|
|
|
|
|
{"stage": "obligations", "reason": "facts_unavailable"},
|
|
|
|
|
{"stage": "obligations", "reason": "policy_invalid", "detail": "bad toml"},
|
|
|
|
|
{"stage": "model", "reason": "model_withheld_execution_budget"},
|
|
|
|
|
{"stage": "budget", "reason": "max_findings_exceeded", "requested": 5, "actual": 9},
|
|
|
|
|
{"stage": "map", "reason": "hunter_model_unavailable"},
|
2026-10-13 10:50:01 +02:00
|
|
|
{"stage": "regress", "reason": "regress_retired"},
|
2026-10-02 22:10:33 +02:00
|
|
|
{"stage": "x", "reason": "not_yet_described"},
|
|
|
|
|
]
|
|
|
|
|
path = tmp_path / "report.md"
|
|
|
|
|
write_markdown_report([], path, [], degradations=reasons)
|
|
|
|
|
text = path.read_text()
|
|
|
|
|
for needle in (
|
|
|
|
|
"No model endpoint is configured",
|
|
|
|
|
"Joern engine is not installed",
|
|
|
|
|
"fact tables for the `overlay` stage",
|
|
|
|
|
"fact tables for the `obligations` stage",
|
|
|
|
|
"`obligations` policy could not be loaded",
|
|
|
|
|
"time budget leaves no room",
|
|
|
|
|
"first 5 of 9 findings",
|
|
|
|
|
"needs a model and none is available",
|
2026-10-13 10:50:01 +02:00
|
|
|
"the sandboxed snippet stage was retired on 2026-10-13",
|
|
|
|
|
"execution proof comes from the search-with-proof arm",
|
2026-10-02 22:10:33 +02:00
|
|
|
"`not_yet_described`",
|
|
|
|
|
):
|
|
|
|
|
assert needle in text, needle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_quick_scan_names_a_language_nothing_covers(tmp_path, capsys) -> None: # type: ignore[no-untyped-def]
|
|
|
|
|
"""pre-push 17.2 / Req 9.2: a Go-only tree is not a silent zero."""
|
|
|
|
|
(tmp_path / "run.go").write_text('package main\n\nimport "os/exec"\n\nfunc run(c string) { exec.Command("sh", "-c", c).Run() }\n')
|
|
|
|
|
assert main(["scan", str(tmp_path), "--mode", "quick"]) == 0
|
|
|
|
|
assert "notice: language not covered: go (1 files)" in capsys.readouterr().out
|
|
|
|
|
report = next((tmp_path / ".openultrasast" / "runs").iterdir()) / "report.md"
|
|
|
|
|
assert "Not a clean result for go (1 files)" in report.read_text()
|
2026-10-13 10:50:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("fail_on, expected", [("never", 0), ("findings", 1), ("verified", 1)])
|
|
|
|
|
def test_deep_scan_records_retirement_without_running_snippets(tmp_path, monkeypatch, fail_on, expected):
|
|
|
|
|
(tmp_path / "app.py").write_text("@app.route('/admin')\ndef admin():\n return eval(request.data)\n")
|
|
|
|
|
|
|
|
|
|
def guard(command, *args, **kwargs):
|
|
|
|
|
argv = list(command) if isinstance(command, list | tuple) else [command]
|
|
|
|
|
assert not argv or Path(str(argv[0])).name != "docker", "deep scan must not start Docker"
|
|
|
|
|
return original_run(command, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
original_run = subprocess.run
|
|
|
|
|
monkeypatch.setattr(subprocess, "run", guard)
|
|
|
|
|
assert main(["scan", str(tmp_path), "--mode", "deep", "--fail-on", fail_on]) == expected
|
|
|
|
|
run_dir = next((tmp_path / ".openultrasast" / "runs").iterdir())
|
|
|
|
|
manifest = json.loads((run_dir / "manifest.json").read_text())
|
|
|
|
|
retired = {"stage": "regress", "reason": "regress_retired"}
|
|
|
|
|
assert manifest["stages"] == {
|
|
|
|
|
"requested": ["static", "map", "regress"],
|
|
|
|
|
"completed": ["static", "map"],
|
|
|
|
|
"skipped": [retired],
|
|
|
|
|
}
|
|
|
|
|
assert manifest["degradations"].count(retired) == 1
|
|
|
|
|
assert "verdicts" not in manifest["artifacts"]
|
|
|
|
|
assert "worth_fixing" not in manifest
|
|
|
|
|
assert not (run_dir / "verdicts.json").exists()
|
|
|
|
|
assert "execution proof comes from the search-with-proof arm" in (run_dir / "report.md").read_text()
|