tracegrpo / src /data /load_secvuleval.py
Dhruv1000's picture
Add files using upload-large-folder tool
5bfc214 verified
Raw History Blame Contribute Delete
3.96 kB
"""Loads SecVulEval (HF: arag0rn/SecVulEval) and normalizes to the unified
corpus schema (spec §3.2).
SecVulEval ships statement-level `changed_statements` (line, code) pairs,
but **its line numbers are relative to the original source FILE, not the
extracted function body** (verified by inspection: a sample with an 80-line
func_body had a changed_statements line number of 877). Silently trusting
them would map every patch line to nothing inside func_vuln. We therefore
recompute `patch_lines_vuln` ourselves via the same diff-based approach as
PrimeVul (`src/data/diff_utils.py`), using `changed_statements` only to
sanity-check CWE/pairing — not for line numbers. See DECISIONS.md
"SecVulEval patch-line numbers were file-absolute, not function-relative".
Pairing is via `fixed_func_idx`, which points from a vulnerable row's `idx`
to its patched counterpart's `idx` in the same table.
`commit_date` is not provided by this dataset directly; recovering it would
require one GitHub API call per of its 6,050 distinct commits, which the
unauthenticated API's 60 req/hour limit makes impractical in one session
(~100+ hours). Left as None here, consistent with PrimeVul rows that didn't
match Big-Vul; see DECISIONS.md. Revisit with a GitHub token if the
chronological split's date coverage needs improving.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
import pandas as pd
from src.data.diff_utils import diff_and_patch_lines
PARQUET_PATH = Path("data/raw/SecVulEval/secvuleval.parquet")
def _func_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:16]
def _first_or_none(arr) -> str | None:
try:
return arr[0] if arr is not None and len(arr) > 0 else None
except TypeError:
return None
def normalize_secvuleval(path: Path = PARQUET_PATH) -> tuple[list[dict], dict]:
df = pd.read_parquet(path)
by_idx = {int(row.idx): row for row in df.itertuples()}
rows: list[dict] = []
n_pairs = 0
seen_pairs: set[frozenset[int]] = set()
for row in df.itertuples():
if not row.is_vulnerable:
continue
fixed_idx = row.fixed_func_idx
if fixed_idx is None or pd.isna(fixed_idx):
continue
fixed_idx = int(fixed_idx)
if fixed_idx not in by_idx:
continue
patch_row = by_idx[fixed_idx]
pair_key = frozenset((int(row.idx), fixed_idx))
if pair_key in seen_pairs:
continue
seen_pairs.add(pair_key)
n_pairs += 1
cve = _first_or_none(row.cve_list)
cwe = _first_or_none(row.cwe_list)
pair_id = f"secvuleval:{row.commit_id}:{row.idx}"
diff_text, patch_lines_vuln = diff_and_patch_lines(row.func_body, patch_row.func_body)
# sample_id includes the row's own idx, not just a content hash: two
# different pairs from the same commit can converge to an identical
# func text (verified in PrimeVul; guarding against it here too).
for target, func_text in ((1, row.func_body), (0, patch_row.func_body)):
sample_id = f"secvuleval:{cve or 'NOCVE'}:{row.commit_id}:{row.idx}:{_func_hash(func_text)}"
rows.append({
"sample_id": sample_id,
"source": "secvuleval",
"project": row.project,
"cve": cve,
"cwe": cwe,
"commit_id": row.commit_id,
"func_vuln": row.func_body,
"func_patch": patch_row.func_body,
"diff": diff_text,
"patch_lines_vuln": patch_lines_vuln if target == 1 else [],
"commit_date": None,
"target": target,
"pair_id": pair_id,
})
stats = {
"n_pairs": n_pairs,
"n_rows": len(rows),
"n_with_date": 0,
"date_coverage": 0.0,
}
return rows, stats