Files
Open_Ultra_SAST/benchmarks/valid/catalog.py
T

143 lines
5.5 KiB
Python
Raw Normal View History

"""Build fix-site pairs from resolved advisories and local Git objects only."""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import re
import subprocess
from dataclasses import asdict, is_dataclass
from functools import cache
from pathlib import Path
from benchmarks.unseen import extract
from benchmarks.unseen.draft import toml
from benchmarks.unseen.eligibility import normalize
LANGUAGES = {
".py": "python",
".js": "javascript",
".jsx": "javascript",
".mjs": "javascript",
".cjs": "javascript",
".ts": "typescript",
".tsx": "typescript",
}
def local_git(path: Path):
"""Disable partial-clone lazy fetches while reading existing local objects."""
def runner(argv, **kwargs):
return subprocess.run(argv, env={**os.environ, "GIT_NO_LAZY_FETCH": "1", "GIT_TERMINAL_PROMPT": "0"}, **kwargs)
return extract.Git(path, runner=runner)
@cache
def pair_builder():
path = Path(__file__).resolve().parents[1] / "pairs/advisory-fixes/materialize.py"
spec = importlib.util.spec_from_file_location("valid_pair_builder", path)
if spec is None or spec.loader is None:
raise RuntimeError("pair builder could not be loaded")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def read_functions(git, case: dict, *, report=None) -> dict:
"""Use the pair builder's function_bounds cutter on both pinned snapshots."""
result = {}
for side, sha in (("pre_fix", case["parent"]), ("post_fix", case["commit"])):
source = git.blob(sha, case["path"])
cut = pair_builder().extract(source, case["language"], case["function"]) if source else None
if cut is None and report:
report(side, 0)
if cut is None:
raise extract.InstrumentFailure(f"{side} function unread or undeclared")
lines, (start, end) = cut
function = "\n".join(lines[start:end])
if report:
report(side, len(function.encode("utf-8")))
if not function.strip():
raise extract.InstrumentFailure(f"{side} function empty")
result[side] = function
result[side + "_lines"] = [start + 1, end]
result[side + "_context"] = "\n".join(lines[max(0, start - 30) : min(len(lines), end + 30)])
return result
def build_cases(git, source, *, cwe: str | None = None, advisory_text: str = "") -> list[dict]:
"""Accept source.Candidate or its resolved dictionary, with advisory metadata."""
source = asdict(source) if is_dataclass(source) else dict(source)
repo = normalize(source.get("repository") or source["repo"])
fix, parent = source.get("fix") or source["commit"], source["parent"]
if not extract.SHA.fullmatch(fix) or not extract.SHA.fullmatch(parent) or fix == parent:
raise ValueError("fix and parent must be distinct full SHAs")
if git.parents(fix) != [parent]:
raise ValueError("fix must have exactly the supplied parent")
cwe = cwe or source.get("cwe")
if not cwe or not re.fullmatch(r"CWE-\d+", cwe) or not source.get("family"):
raise ValueError("advisory CWE and family are required")
rows = []
for site in extract.fix_sites(git, fix, parent):
language = LANGUAGES.get(Path(site["path"]).suffix)
if language is None:
continue
identity = [repo, source["advisory"], fix, site["path"], site["function"]]
from benchmarks.unseen.journal import digest
case = dict(
id=repo.replace("/", "-") + "-" + digest(identity)[:16],
repo=repo,
url=source["url"],
advisory=source["advisory"],
cwe=cwe,
family=source["family"],
language=language,
commit=fix,
parent=parent,
path=site["path"],
function=site["function"],
method="fix_site",
)
sides = read_functions(git, case)
if extract.normalized(sides["pre_fix"]) == extract.normalized(sides["post_fix"]):
continue
case["function_lines"] = [sides["pre_fix_lines"]]
case.update(pre_fix=sides["pre_fix"], post_fix=sides["post_fix"], advisory_text=advisory_text or source.get("advisory_text", ""))
rows.append(case)
return sorted(rows, key=lambda row: row["id"])
def write_catalog(path: Path, cases: list[dict]) -> None:
if len({row["id"] for row in cases}) != len(cases):
raise ValueError("duplicate case id")
text = "\n".join(
"[[case]]\n" + "\n".join(f"{key} = {toml(value)}" for key, value in sorted(row.items())) + "\n"
for row in sorted(cases, key=lambda row: row["id"])
)
path.write_text(text, encoding="utf-8")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("sources", type=Path, help="JSON array of resolved Candidate dictionaries with CWE and advisory_text")
parser.add_argument("--repos", type=Path, required=True, help="JSON map of repository names to local Git directories")
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args(argv)
repos = {normalize(k): local_git(Path(v)) for k, v in json.loads(args.repos.read_text()).items()}
cases = [
case
for source in json.loads(args.sources.read_text())
for case in build_cases(repos[normalize(source.get("repository") or source["repo"])], source)
]
write_catalog(args.out, cases)
return 0
if __name__ == "__main__":
raise SystemExit(main())