mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 04:29:27 +00:00
feat(repos): pinned known-vulnerable checkouts, offline by default
The pair corpus scores excerpts: a vulnerable function and its fixed twin, handed to the engine with the answer already narrowed down. Nothing in it exercises a repository -- region ranking, the budget, one CPG over thousands of files, or whether a verdict that holds on fifteen lines still holds when the source, the sink and the guard live in three modules. benchmarks/repos/<name>.toml names a real checkout: url, a full 40-char commit SHA, licence, and every known vulnerability with its file, function and line. `ousast repos` lists them; `--fetch` materialises one; CI does neither, and conftest clears the switch and redirects the cache so a developer with warm checkouts cannot read them by accident. Three rules the loader enforces rather than documents: - A branch or tag is rejected. Either would make the same recipe mean different code next month while the baseline it produced kept the old numbers. - `in_scope` must be stated per vulnerability, never defaulted. It decides whether "not found" is a miss or a non-question, which is the most important thing about the result. - A recipe that declares `detection` must name something in scope to detect. `ousast repos` does not trust a recipe either: against a materialised checkout it checks HEAD against the pin, that the licence file is there, and that each declared function appears above its declared line. That earned its place immediately -- the first libpng recipe named LICENSE.md, which is the filename at HEAD today and not at the pinned commit, where it is still LICENSE. Two checkouts, verified against the real repositories: - vampi (MIT, 520 lines) is the DETECTION checkout. Excerpts of it are already in the pair corpus, including the update_password miss diagnosed earlier, so a whole-repository run says directly whether a verdict survives the move off the excerpt. Its routes register through an OpenAPI operationId, so it exercises registration-based entry-point mapping rather than the decorator path. - libpng (libpng-2.0, 91k lines of C) is the ENVELOPE checkout, pinned at the parent of the commit that added the missing clamp. Its CVE is marked out of scope: an out-of-bounds read is not a family this engine arbitrates, and recording "not found" against it as a miss is the easiest way to make a measurement lie. Nothing is vendored; the recipe records the licence so an artifact can say what it ran against. 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
c35426e186
commit
8c387306be
@@ -60,6 +60,16 @@ from .provenance import fingerprint
|
||||
from .rank import rank_obligations, rank_targets, write_rankings
|
||||
from .regress import TRIGGERABLE, CandidateVerdict, run_regression, write_verdicts
|
||||
from .reports import scan_exit_code, write_manifest, write_markdown_report, write_sarif_report
|
||||
from .repos import (
|
||||
DEFAULT_REPO_DIR,
|
||||
RepoUnavailable,
|
||||
checkout_path,
|
||||
load_repo_recipes,
|
||||
recipe_payload,
|
||||
)
|
||||
from .repos import resolve as resolve_repo
|
||||
from .repos import select as select_repo
|
||||
from .repos import verify as verify_repo
|
||||
from .ruleset import DEFAULT_RULESET_DIR, load_ruleset
|
||||
from .run import ScanRun, create_scan_run
|
||||
from .sandbox import resolve_sandbox_probe, resolve_sandbox_runner
|
||||
@@ -159,6 +169,15 @@ def main(argv: list[str] | None = None) -> int:
|
||||
pairs.add_argument("--pointers", action="store_true", help="allow network for non-vendored pointer pairs this run (nightly; CI never)")
|
||||
pairs.add_argument("--json", action="store_true", help="print the pair scoreboard as JSON")
|
||||
|
||||
repos = subparsers.add_parser(
|
||||
"repos",
|
||||
help="pinned known-vulnerable checkouts: list them, and verify a recipe against the code it names",
|
||||
)
|
||||
repos.add_argument("--dir", type=Path, default=DEFAULT_REPO_DIR)
|
||||
repos.add_argument("--repo", default="all", help="one recipe by name, or all")
|
||||
repos.add_argument("--fetch", action="store_true", help="allow network to materialise missing checkouts (CI never)")
|
||||
repos.add_argument("--json", action="store_true")
|
||||
|
||||
subparsers.add_parser("mcp", help="run the narrow MCP server over stdio for OpenCode integration")
|
||||
model_cmd = subparsers.add_parser("model", help="maintainer: the model layer that arbitrates detector claims")
|
||||
model_sub = model_cmd.add_subparsers(dest="model_command", required=True)
|
||||
@@ -199,6 +218,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
split=args.split,
|
||||
pointers=args.pointers,
|
||||
)
|
||||
if args.command == "repos":
|
||||
return _repos(args.dir, args.repo, fetch=args.fetch, json_out=args.json)
|
||||
if args.command == "model":
|
||||
return _model_candidates(args)
|
||||
if args.command == "mcp":
|
||||
@@ -803,6 +824,45 @@ def _pairs(
|
||||
return 0
|
||||
|
||||
|
||||
def _repos(directory: Path, name: str, *, fetch: bool, json_out: bool) -> int:
|
||||
"""List the pinned checkouts and check each recipe against the code it claims to describe.
|
||||
|
||||
Offline by default: without ``--fetch`` a missing checkout is reported as missing, not fetched. Exit is
|
||||
non-zero only when a materialised checkout CONTRADICTS its recipe -- a recipe that merely has not been
|
||||
fetched is not a failure, or CI could never run this.
|
||||
"""
|
||||
recipes = select_repo(load_repo_recipes(directory), name)
|
||||
if not recipes:
|
||||
raise SystemExit(f"no repository recipe matches {name!r} in {directory}")
|
||||
|
||||
rows: list[dict[str, object]] = []
|
||||
contradicted = 0
|
||||
for recipe in recipes:
|
||||
problems: tuple[str, ...] = ()
|
||||
try:
|
||||
root = resolve_repo(recipe, fetch=True if fetch else None)
|
||||
except RepoUnavailable:
|
||||
state, path = "absent", checkout_path(recipe)
|
||||
else:
|
||||
problems = verify_repo(recipe, root)
|
||||
state, path = ("contradicted" if problems else "verified"), root
|
||||
contradicted += 1 if problems else 0
|
||||
rows.append({**recipe_payload(recipe), "state": state, "path": str(path), "problems": list(problems)})
|
||||
|
||||
if not json_out:
|
||||
in_scope = len(recipe.in_scope)
|
||||
measures = ",".join(recipe.measures)
|
||||
print(f"{recipe.name:10} {state:13} {recipe.language:8} measures={measures} known={len(recipe.known)} in_scope={in_scope}")
|
||||
for problem in problems:
|
||||
print(f" ! {problem}")
|
||||
if state == "absent":
|
||||
print(f" fetch with: ousast repos --repo {recipe.name} --fetch")
|
||||
|
||||
if json_out:
|
||||
print(json.dumps(rows, indent=2, sort_keys=True))
|
||||
return 1 if contradicted else 0
|
||||
|
||||
|
||||
def _model_skipped(reason: str) -> dict[str, object]:
|
||||
"""A skipped stage reports the same SHAPE as a run one, plus why it skipped.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user