mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(contributor-scan): 1.1 regions from entry points, 1.2 the repository driver
Pair scoring could rely on a labelled function; a contributor cannot supply one -- which code is risky is the question they are asking. Regions now come from entry points, with a file-level fallback that says it is one, and each carries only the families its shape and language admit. Ranking uses what EntryPointRecord already decided (access_level, trust_boundary) rather than a second opinion computed from the same evidence. A family is offered only where its arbiter could answer: access_control needs a handler, so a library file with no entry point is not asked about obligations it has no siblings to be inconsistent with. The driver makes the two costs that were free at pair scale explicit. ONE CPG per repository, reused across regions -- a build is 5-30s on a forty-line excerpt and per-region builds would make this unusable. And a budget that is a TOTAL for the run rather than the enumerator's per-region cap of eight, spent highest-rank-first, with whatever it does not reach counted into regions_unjudged. A tool that silently analyses a tenth of a repository and reports a clean result is worse than one that says what it skipped. Two flaws the tests caught, both mine and both about ownership: the budget counted calls by reading a client's own usage log, so it silently never tripped for a client that keeps none -- the driver now counts its own spend through a wrapper; and the driver trusted its caller to have sorted, when the order the budget is spent in belongs to whoever holds the budget. The third was worse and only surfaced because a test failed for the right reason: the driver passed candidates=() and never enumerated, so a repository scan would have reported only what the arbiters establish unaided and never populated the suspicion band -- which supplied 15 of 26 findings in the pair measurement. It enumerates now. Sibling audit (Req 8.5) done before writing: family routing lives in exactly one production place, pipeline._arbitrate, dispatching on spec type. regions_for returns family names and the driver resolves specs, so that stays true. 750 passing, mypy clean over 96 files, gates byte-identical, zero orphans. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
64a27c4e30
commit
8663c1a5b3
@@ -0,0 +1,147 @@
|
||||
"""contributor-scan task 1.2: the repository driver (Req 3).
|
||||
|
||||
Pair scoring built one CPG per pair and asked one region. A repository has thousands of regions, and the two
|
||||
things that were free at pair scale are the two that dominate here:
|
||||
|
||||
* **CPG construction.** A build is 5-30s on a 40-line excerpt. Building one per region would make a
|
||||
repository scan unusable, so the driver builds ONE and reuses it.
|
||||
* **Model calls.** The enumerator caps candidates at 8 per region, which says nothing about a scan with two
|
||||
thousand regions. The budget is therefore a total for the run, spent on the highest-ranked regions first,
|
||||
and whatever it does not reach is COUNTED rather than quietly dropped.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _region(path="a.py", function="run", families=("injection",), rank=1.0): # type: ignore[no-untyped-def]
|
||||
from openultrasast.model.regions import ScanRegion
|
||||
|
||||
return ScanRegion(path=path, function=function, language="python", families=families, rank=rank, source="entry_point")
|
||||
|
||||
|
||||
class _Backend:
|
||||
"""Counts builds, so the one-CPG-per-scan promise is testable rather than asserted."""
|
||||
|
||||
def __init__(self, fail: bool = False) -> None:
|
||||
self.builds = 0
|
||||
self._fail = fail
|
||||
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
def build(self, root): # type: ignore[no-untyped-def]
|
||||
from openultrasast.cpg.backend import CpgResult
|
||||
|
||||
self.builds += 1
|
||||
if self._fail:
|
||||
return None
|
||||
return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: [])
|
||||
|
||||
|
||||
def test_one_cpg_is_built_for_the_whole_repository() -> None:
|
||||
from openultrasast.model.scan import scan_repository
|
||||
|
||||
backend = _Backend()
|
||||
regions = tuple(_region(path=f"f{i}.py") for i in range(20))
|
||||
scan_repository(Path("/repo"), regions, backend=backend, client=None, model="")
|
||||
assert backend.builds == 1, "a build per region would make a repository scan unusable"
|
||||
|
||||
|
||||
def test_a_failed_build_yields_an_empty_result_not_an_exception() -> None:
|
||||
from openultrasast.model.scan import scan_repository
|
||||
|
||||
result = scan_repository(Path("/repo"), (_region(),), backend=_Backend(fail=True), client=None, model="")
|
||||
assert result.findings == ()
|
||||
assert any("cpg_build_failed" in str(d) for d in result.degradations)
|
||||
|
||||
|
||||
def test_the_budget_is_a_total_for_the_run_and_the_remainder_is_counted(tmp_path) -> None:
|
||||
"""Req 3.2, 3.3: silently truncating is the failure this exists to prevent.
|
||||
|
||||
Real files on disk, because the driver enumerates candidates from them. A region pointing at a path that
|
||||
does not exist yields no candidates, no model calls, and a budget that never trips -- which would make
|
||||
this test pass for the wrong reason.
|
||||
"""
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
|
||||
class _Client:
|
||||
def complete(self, **kw): # type: ignore[no-untyped-def]
|
||||
from openultrasast.tool_hunter import ChatResponse
|
||||
|
||||
return ChatResponse(content='{"vulnerable": true}')
|
||||
|
||||
source = "import os\n\n\ndef run(cmd):\n os.system(cmd)\n"
|
||||
for i in range(50):
|
||||
(tmp_path / f"f{i}.py").write_text(source)
|
||||
regions = tuple(_region(path=f"f{i}.py") for i in range(50))
|
||||
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
regions,
|
||||
backend=_Backend(),
|
||||
client=_Client(),
|
||||
model="m",
|
||||
budget=ScanBudget(max_model_calls=5, max_regions=50),
|
||||
)
|
||||
assert result.model_calls > 0, "the driver must enumerate candidates and ask the judge about them"
|
||||
assert result.regions_unjudged > 0
|
||||
assert result.regions_scanned + result.regions_unjudged == 50
|
||||
assert any("budget_exhausted" in str(d) for d in result.degradations)
|
||||
|
||||
|
||||
def test_the_budget_is_spent_on_the_highest_ranked_regions_first() -> None:
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
|
||||
seen: list[str] = []
|
||||
|
||||
class _Backend2(_Backend):
|
||||
def build(self, root): # type: ignore[no-untyped-def]
|
||||
from openultrasast.cpg.backend import CpgResult
|
||||
|
||||
self.builds += 1
|
||||
return CpgResult(cpg_path=Path("c.bin"), run=lambda q, p: (seen.append(str(p.get("function"))), [])[1])
|
||||
|
||||
regions = (_region(path="low.py", function="low", rank=0.1), _region(path="high.py", function="high", rank=1.0))
|
||||
scan_repository(Path("/repo"), regions, backend=_Backend2(), client=None, model="", budget=ScanBudget(max_regions=1))
|
||||
assert seen and seen[0] == "high", f"budget spent on the wrong region first: {seen}"
|
||||
|
||||
|
||||
def test_findings_are_ordered_by_rung_before_rank() -> None:
|
||||
"""Req 3.1: what the model established outranks what the LLM merely proposed."""
|
||||
from openultrasast.model.ladder import Rung
|
||||
from openultrasast.model.pipeline import ModelFinding
|
||||
from openultrasast.model.scan import _ordered
|
||||
|
||||
findings = [
|
||||
(ModelFinding(site="a", family="injection", rung=Rung.SUSPICION), 1.0),
|
||||
(ModelFinding(site="b", family="injection", rung=Rung.ENTAILED), 0.1),
|
||||
(ModelFinding(site="c", family="injection", rung=Rung.CORROBORATED), 0.5),
|
||||
]
|
||||
assert [f.site for f in _ordered(findings)] == ["b", "c", "a"]
|
||||
|
||||
|
||||
def test_the_result_reports_what_the_model_arbitrated() -> None:
|
||||
"""Req 1.5: a user should see how much of the result the model established, not have to infer it."""
|
||||
from openultrasast.model.scan import scan_repository
|
||||
|
||||
result = scan_repository(Path("/repo"), (_region(),), backend=_Backend(), client=None, model="")
|
||||
assert set(result.by_rung) >= {"model_entailed", "model_corroborated", "suspicion"}
|
||||
assert result.regions_scanned == 1
|
||||
|
||||
|
||||
def test_a_region_is_only_asked_about_families_it_carries() -> None:
|
||||
from openultrasast.model.scan import scan_repository
|
||||
|
||||
asked: list[str] = []
|
||||
|
||||
class _B(_Backend):
|
||||
def build(self, root): # type: ignore[no-untyped-def]
|
||||
from openultrasast.cpg.backend import CpgResult
|
||||
|
||||
self.builds += 1
|
||||
return CpgResult(cpg_path=Path("c.bin"), run=lambda q, p: (asked.append(q), [])[1])
|
||||
|
||||
scan_repository(Path("/repo"), (_region(families=("injection",)),), backend=_B(), client=None, model="")
|
||||
assert "dominance" not in asked and "config" not in asked
|
||||
Reference in New Issue
Block a user