mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
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
115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
"""Where to look, and what to look for there (contributor-scan, Req 2).
|
|
|
|
Pair scoring could rely on a labelled function: the corpus declares which function carries the bug and which
|
|
family it belongs to. A contributor has neither — *which code is risky* is the question they are asking. So a
|
|
repository scan derives its regions from the code itself: entry points where the mapper found them, whole
|
|
files where it did not, and each region carries only the families its shape and language actually admit.
|
|
|
|
Two restraints are deliberate:
|
|
|
|
* **The rank comes from what ``EntryPointRecord`` already carries**, not from a new signal invented here. The
|
|
mapper has already decided whether a handler is public or local-only and whether it sits at a trust
|
|
boundary; a second opinion computed from the same evidence would be a way of disagreeing with ourselves.
|
|
* **A family is offered only where its arbiter could answer.** An obligation needs a reachable operation, so
|
|
``access_control`` is offered where there is a handler and withheld from a library file that has none.
|
|
Asking every family of every region is the unbounded version of this, and at repository scale the budget is
|
|
the thing being spent.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
|
|
from .specs import config_specs, dominance_specs, taint_specs
|
|
|
|
# From `mapping.AccessLevel`. A publicly reachable handler at a trust boundary is where a missing check costs
|
|
# most, so it is looked at first when the budget is finite.
|
|
_ACCESS_RANK: dict[str, float] = {
|
|
"public": 1.0,
|
|
"authenticated": 0.8,
|
|
"role-restricted": 0.6,
|
|
"contract-only/callback": 0.4,
|
|
"review-required": 0.3,
|
|
"local-only": 0.2,
|
|
}
|
|
|
|
# Languages whose facts the model layer carries. Anything else yields no region rather than an empty scan
|
|
# that looks like a clean bill of health.
|
|
_LANGUAGES = {"python", "javascript", "typescript", "java", "c", "c_cpp"}
|
|
_NORMALISE = {"typescript": "javascript", "c_cpp": "c"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScanRegion:
|
|
"""A place to arbitrate, and the families worth arbitrating there."""
|
|
|
|
path: str
|
|
function: str | None # None for a file-level fallback: there is no labelled function on a scan path
|
|
language: str
|
|
families: tuple[str, ...]
|
|
rank: float
|
|
source: str # "entry_point" | "file_fallback"
|
|
|
|
|
|
def regions_for(entries: Sequence[object], targets: Sequence[object]) -> tuple[ScanRegion, ...]:
|
|
"""Regions for a repository, strongest first.
|
|
|
|
A file with an entry point yields regions for its handlers only — a fallback region beside them would
|
|
scan the same code twice and spend the budget on duplicates.
|
|
"""
|
|
by_path: dict[str, str] = {}
|
|
for target in targets:
|
|
language = _language_of(target)
|
|
if language:
|
|
by_path[str(getattr(target, "path", ""))] = language
|
|
|
|
regions: list[ScanRegion] = []
|
|
covered: set[str] = set()
|
|
for entry in entries:
|
|
path = str(getattr(entry, "path", ""))
|
|
language = by_path.get(path)
|
|
if language is None:
|
|
continue
|
|
families = _families(language, has_handler=True)
|
|
if not families:
|
|
continue
|
|
covered.add(path)
|
|
regions.append(
|
|
ScanRegion(
|
|
path=path,
|
|
function=(getattr(entry, "function_name", None) or None),
|
|
language=language,
|
|
families=families,
|
|
rank=_ACCESS_RANK.get(str(getattr(entry, "access_level", "")), 0.5),
|
|
source="entry_point",
|
|
)
|
|
)
|
|
|
|
for path, language in by_path.items():
|
|
if path in covered:
|
|
continue
|
|
families = _families(language, has_handler=False)
|
|
if not families:
|
|
continue
|
|
regions.append(ScanRegion(path=path, function=None, language=language, families=families, rank=0.1, source="file_fallback"))
|
|
|
|
# A total order: rank descending, then path and function, so two runs of one repository agree.
|
|
return tuple(sorted(regions, key=lambda r: (-r.rank, r.path, r.function or "")))
|
|
|
|
|
|
def _language_of(target: object) -> str | None:
|
|
language = str(getattr(target, "language", "") or "")
|
|
if language not in _LANGUAGES:
|
|
return None
|
|
return _NORMALISE.get(language, language)
|
|
|
|
|
|
def _families(language: str, *, has_handler: bool) -> tuple[str, ...]:
|
|
"""The families this language has facts for, minus those whose arbiter needs a handler and has none."""
|
|
families = set(taint_specs(language=language))
|
|
families |= set(config_specs(language=language))
|
|
if has_handler:
|
|
families |= set(dominance_specs(language=language))
|
|
return tuple(sorted(families))
|