mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(pair-corpus-honesty): score non-vendored pointer pairs through a local cache
Task 8.3 (Req 10.1-10.5). - pairs.py: catalog rows with vendored = false carry their harvest recipe and no excerpts; evaluate_catalog harvests them into OPENULTRASAST_PAIR_CACHE (default ~/.cache/openultrasast/pairs) only when `pairs --pointers` or OPENULTRASAST_PAIRS_NETWORK=1 allows the network, otherwise records pointer_pair_skipped (a failed fetch is pointer_pair_fetch_failed with detail); a warm cache never scores without the switch. select_vendored for tests and gates. - harvest.py: materialize_pointer harvests into the cache without the license requirement (nothing is redistributed); redaction applies. - catalog_gen.py: emits pointer rows with recipe fields; training manifests skip them. - pair_gate and improve/evolve.evaluate_profiles score vendored pairs only; cli `pairs --pointers`; .gitignore covers the cache path. - tests: loader, skip/degradation, cache hit, fetch failure, warm cache with network off, profile gate exclusion, harvest and generator paths; conftest keeps every test offline whatever the shell exports. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7d238f2400
commit
4102404442
@@ -67,6 +67,18 @@ Defaults per slice: local `reviewed`, vibe-py `seeded`, sast/vfc/vfc-js/github
|
||||
`advisory`, agent-vfc `title`. `pairs --json` reports `per_tier`; `evolve.evaluate_profiles` scores only `seeded`
|
||||
and `reviewed` pairs, so `advisory` and `title` pairs never gate the improve loop (task 8.2).
|
||||
|
||||
## Pointer pairs (not vendored)
|
||||
|
||||
A row with `vendored = false` carries its harvest recipe (`repo`, `parent`, `commit`,
|
||||
`path`, `mode`, `line`/`fix_path`/`fix_line`, ...) and no excerpt files (Req 10). The
|
||||
repository never redistributes that code: `pairs --pointers` (or
|
||||
`OPENULTRASAST_PAIRS_NETWORK=1`) harvests both sides into
|
||||
`OPENULTRASAST_PAIR_CACHE` (default `~/.cache/openultrasast/pairs/<slice>/<name>/`,
|
||||
git-ignored) and scores them exactly like a vendored pair; without network the pair is
|
||||
skipped with a `pointer_pair_skipped` degradation and a failed fetch is
|
||||
`pointer_pair_fetch_failed`, never an error. The default `pytest` run and every gate
|
||||
score vendored pairs only (`select_vendored`); the pointer run is the nightly command.
|
||||
|
||||
## Labels, profiles, and mechanisms
|
||||
|
||||
Every `[[pair.expected]]` row names a `mechanism` from the closed vocabulary in
|
||||
|
||||
@@ -52,6 +52,7 @@ def catalog_text(slice_name: str, recipes: list[dict[str, object]]) -> str:
|
||||
for recipe in recipes:
|
||||
vuln, fixed = excerpt_rel(recipe)
|
||||
lang = LANG_TO_PAIR.get(str(recipe.get("language", "c")), "c_cpp")
|
||||
pointer = recipe.get("vendored") is False
|
||||
lines += [
|
||||
"[[pair]]",
|
||||
f'name = "{_q(recipe["name"])}"',
|
||||
@@ -65,8 +66,13 @@ def catalog_text(slice_name: str, recipes: list[dict[str, object]]) -> str:
|
||||
f'commit = "{_q(recipe.get("commit", ""))}"',
|
||||
f'parent = "{_q(recipe.get("parent", ""))}"',
|
||||
f'commit_url = "{_q(recipe.get("commit_url", ""))}"',
|
||||
f'vuln = "{vuln}"',
|
||||
f'fixed = "{fixed}"',
|
||||
]
|
||||
if pointer:
|
||||
lines.append("vendored = false") # Req 10.1: the recipe travels in the row, no excerpt in the repository
|
||||
lines += _pointer_lines(recipe)
|
||||
else:
|
||||
lines += [f'vuln = "{vuln}"', f'fixed = "{fixed}"']
|
||||
lines += [
|
||||
f'relpath = "{_q(recipe.get("relpath", recipe.get("path", "")))}"',
|
||||
"min_recall = 1.0",
|
||||
'fix_policy = "silent"',
|
||||
@@ -114,9 +120,21 @@ def _review_tier(slice_name: str, recipe: dict[str, object]) -> str:
|
||||
return _DEFAULT_TIER.get(slice_name, "advisory")
|
||||
|
||||
|
||||
_POINTER_STR = ("host", "path", "fix_repo", "fix_path", "mode")
|
||||
_POINTER_INT = ("line", "fix_line", "line_start", "line_end")
|
||||
|
||||
|
||||
def _pointer_lines(recipe: dict[str, object]) -> list[str]:
|
||||
out = [f'{key} = "{_q(recipe[key])}"' for key in _POINTER_STR if recipe.get(key)]
|
||||
out += [f"{key} = {int(recipe[key])}" for key in _POINTER_INT if isinstance(recipe.get(key), int)]
|
||||
return out
|
||||
|
||||
|
||||
def training_rows(slice_name: str, recipes: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
for recipe in recipes:
|
||||
if recipe.get("vendored") is False:
|
||||
continue # nothing vendored to train on
|
||||
vuln, fixed = excerpt_rel(recipe)
|
||||
base = {
|
||||
"pair": recipe["name"],
|
||||
@@ -353,7 +371,12 @@ def generate(slice_name: str, *, prune: bool = False) -> tuple[Path, int]:
|
||||
recipes: list[dict[str, object]] = []
|
||||
for recipe in loaded:
|
||||
vuln, fixed = excerpt_rel(recipe)
|
||||
if (slice_root / vuln).is_file() and (slice_root / fixed).is_file():
|
||||
if recipe.get("vendored") is False:
|
||||
if not recipe.get("function"):
|
||||
print(f"skip {recipe['name']}: pointer recipe without a function label (nothing to derive from)")
|
||||
continue
|
||||
recipes.append(recipe)
|
||||
elif (slice_root / vuln).is_file() and (slice_root / fixed).is_file():
|
||||
if not recipe.get("function"):
|
||||
derived = derive_function(slice_root, recipe)
|
||||
if not derived:
|
||||
|
||||
@@ -373,7 +373,7 @@ def provenance_header(recipe: dict[str, Any], *, side: str, sha: str, upstream_s
|
||||
return "/* " + "\n * ".join(lines) + "\n */\n"
|
||||
|
||||
|
||||
def validate_recipe(recipe: dict[str, Any]) -> None:
|
||||
def validate_recipe(recipe: dict[str, Any], *, require_license: bool = True) -> None:
|
||||
name = str(recipe.get("name", ""))
|
||||
if any(token in str(recipe).lower() for token in BLOCKED_TOKENS):
|
||||
raise RecipeError(f"{name}: embargoed or blocked dataset")
|
||||
@@ -390,7 +390,7 @@ def validate_recipe(recipe: dict[str, Any]) -> None:
|
||||
raise RecipeError(f"{name}: line_range mode needs integer line_start and line_end")
|
||||
if mode == "enclosing" and not isinstance(recipe.get("line"), int):
|
||||
raise RecipeError(f"{name}: enclosing mode needs an integer line")
|
||||
if not recipe.get("license"):
|
||||
if require_license and not recipe.get("license"):
|
||||
raise RecipeError(f"{name}: license required")
|
||||
|
||||
|
||||
@@ -514,8 +514,8 @@ def extract_pair(recipe: dict[str, Any], parent_src: str, fix_src: str) -> tuple
|
||||
return extract_function(parent_src, function, language), extract_function(fix_src, function, language)
|
||||
|
||||
|
||||
def harvest_recipe(recipe: dict[str, Any], root: Path) -> tuple[Path, Path]:
|
||||
validate_recipe(recipe)
|
||||
def harvest_recipe(recipe: dict[str, Any], root: Path, *, require_license: bool = True) -> tuple[Path, Path]:
|
||||
validate_recipe(recipe, require_license=require_license)
|
||||
if str(recipe.get("host", "github")) != "github":
|
||||
raise RecipeError(f"{recipe.get('name')}: live fetch currently implemented for host=github only")
|
||||
parent_src = fetch_url(github_raw_url(str(recipe["repo"]), str(recipe["parent"]), str(recipe["path"])))
|
||||
@@ -531,6 +531,19 @@ def harvest_recipe(recipe: dict[str, Any], root: Path) -> tuple[Path, Path]:
|
||||
return vuln_path, fixed_path
|
||||
|
||||
|
||||
def materialize_pointer(recipe: dict[str, Any], cache_root: Path, *, slice_name: str) -> tuple[Path, Path]:
|
||||
"""Harvest a non-vendored pair into ``cache_root/<slice>/<name>/`` (Req 10.2).
|
||||
|
||||
The cache lives outside the repository; a missing upstream license is allowed here because nothing is redistributed,
|
||||
the excerpts stay on the operator's machine. Redaction applies as for vendored excerpts.
|
||||
"""
|
||||
root = Path(cache_root) / slice_name
|
||||
vuln_path, fixed_path = excerpt_paths(recipe, root)
|
||||
if vuln_path.is_file() and fixed_path.is_file():
|
||||
return vuln_path, fixed_path
|
||||
return harvest_recipe(recipe, root, require_license=False)
|
||||
|
||||
|
||||
def fetch_url(url: str, timeout: int = 60) -> str:
|
||||
host = urlparse(url).hostname or ""
|
||||
if host not in ALLOWED_HOSTS:
|
||||
|
||||
Reference in New Issue
Block a user