This repository contains the datasets and replication package for the paper: Beyond Labels: Evaluating LLMs on Vulnerable-Path Reconstruction
It supports four main workflows:
- CVEPath runs: evaluate models on vulnerable multi-file CVE examples (the 105-CVE study set, the identifier-masked copy of that set, or the held-out 14-CVE post-cutoff set). Optionally append same-repo distractor files (
k= 1, 3, or 5). - Negative-sample runs: evaluate models on non-vulnerable single-file samples.
- Log analysis: analyze model run logs into CSV summaries and plots.
- Post-cutoff data collection: rebuild a held-out set from scratch, from GitHub advisories published after a chosen cutoff through the study-design stages up to PF-1.
CVEPath runs default to 4 repetitions per query (--runs 4, --temperature 0.2, --seed 1000). Use --runs 1 --temperature 0.0 to reproduce the original single-run baseline.
- Repository structure
- What the project does
- Requirements
- Setup
- Supported providers
- Datasets Layout
- How to run the project
- Rebuild a post-cutoff set
- CLI reference
- Outputs
.
├── data/
│ ├── CVEPath/
│ │ ├── Java/
│ │ └── Python/
│ ├── post-cutoff-cves/
│ │ └── Python/
│ ├── CVEPath_obf/
│ │ ├── Java/
│ │ └── Python/
│ └── negative_samples/
│ ├── Java/
│ └── Python/
├── docs/
│ └── appendix.pdf
├── prompt_templates/
│ ├── baseline_prompt.txt
│ ├── llmpath_prompt.txt
│ └── llm_multifile_labelling.txt
├── query_packs/
│ ├── base_queries/Java/
│ ├── base_queries/Python/
│ ├── custom-cwe-queries/Java/
│ └── custom-cwe-queries/Python/
├── requirements.txt
├── scripts/
│ ├── .env.example
│ ├── analyze_runs.py
│ ├── clone_cvepath_repos.py
│ ├── collect_post_cutoff.py
│ ├── run_detection_negatives.py
│ ├── run_detection_positives.py
│ ├── run_llms_on_negative_samples.py
│ ├── run_llms_on_cvepath.py
│ └── setup_codeql.py
└── src/
├── llm_runner/
├── log_analyzer/
├── log_analysis_pipeline.py
├── negative_pipeline.py
├── negative_pipeline_detect.py
├── post_cutoff/
├── cvepath_pipeline.py
├── cvepath_pipeline_detect.py
├── detection_parallel.py
└── utils/
query_packs/ holds the CodeQL queries the study design uses to construct candidate paths: base_queries/ are the shared library files and custom-cwe-queries/ has one folder per CWE. Python and Java packs are both shipped. The Java qlpack.yml pins cliVersion: 2.15.3; the Python pack pins codeql/python-all 0.11.3 (the library the post-cutoff run used).
The project writes outputs under:
output/
This folder is created automatically when needed.
python scripts/run_llms_on_cvepath.py runs an LLM on either:
- one selected CVE, or
- all CVEs in the CVEPath dataset
It loads prompt templates from prompt_templates/, reads source files from data/CVEPath/ (or data/CVEPath_obf/ / data/post-cutoff-cves/ if you pass --dataset-dir), queries the selected provider, and saves one JSON log per run under output/runs/. With --distractors k (k = 1, 3, or 5) it appends same-language files sampled from full vulnerable-commit checkouts under output/original_repos/ (built by scripts/clone_cvepath_repos.py). k = 0 (the default) does not need those checkouts.
python scripts/run_llms_on_negative_samples.py runs the same prompting flow on non-vulnerable single-file examples stored in data/negative_samples/.
python scripts/analyze_runs.py reads the saved JSON logs, matches them back to the datasets, and writes CSV tables and plots under output/analysis/.
python scripts/collect_post_cutoff.py rebuilds a held-out set the way data/post-cutoff-cves/ was built: it discovers advisories published after a cutoff date, applies the CVEPath selection and path-construction stages, and writes the survivors in CVEPath layout. It stops at PF-1; see Rebuild a post-cutoff set.
- Python 3.12 recommended
- A virtual environment
- An API key for the LLM provider(s) you want to use
The project already includes requirements.txt.
Install from it rather than trying to recreate dependencies manually.
Post-cutoff data collection additionally needs git, the CodeQL CLI 2.15.3 (installed by scripts/setup_codeql.py), and a GitHub token. Nothing else in the package uses CodeQL.
cd /path/to/repopython -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txtpy -3.12 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txtCopy the example file from scripts/ into the same folder under the name .env
cp scripts/.env.example scripts/.envCopy-Item scripts/.env.example .envThen open .env and fill in the key(s) you need:
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
DEEPSEEK_API_KEY=
OPENROUTER_API_KEY=The remaining entries are only read by scripts/collect_post_cutoff.py; leave them blank if you are not rebuilding a post-cutoff set.
GITHUB_TOKEN=
NVD_API_KEY=
CODEQL_PATH=The code supports these provider names:
openaianthropicdeepseekopenrouter
Examples:
--provider openai--provider anthropic--provider deepseek--provider openrouter
The CVEPath dataset is organized by programming language, then by CVE instance. Each CVE folder contains:
- an
annotations/directory with the structured metadata used by the project - a
source/directory with the vulnerable project version files that are passed to the LLM
The dataset can be visualized using our visualization and manual analysis webpage
Example:
data/CVEPath/
└── Java/
└── <CVE_ID>_<PROJECT_NAME>/
├── annotations/
│ ├── input_filenames.json
│ ├── vulnerable_paths.json
│ └── cve_metadata.json
└── source/
└── ...
This file defines the file combinations that the vulnerable paths traverse. These combinations are what the runner uses to decide which source files to concatenate and pass to the LLM as input.
Structure:
{
"files": [
[
<PATH1_FROM_REPOSITORY_ROOT>,
<PATH2_FROM_REPOSITORY_ROOT>,
...
],
...
]
}Meaning:
- the top-level key is
files - each item inside
filesis one candidate file combination - each file combination is a list of relative file paths under the CVE's
source/directory
In practice, the runner reads one of these combinations, loads the corresponding files from source/, adds line numbers, and inserts the result into the chosen prompt template.
This file stores the CVE metadata, taken from the ReposVul or CWE-Bench-Java datasets. It provides contextual information about the vulnerability, such as the CVE ID, CWE(s), language, description, severity, relevant commit hashes and changed file contents.
This file is mainly useful for metadata, bookkeeping, and downstream analysis.
This file stores the reference vulnerable paths for the CVE, generated by our CVEPath pipeline.
Structure example:
{
"<UNIQUE-SHA256-HASH>": [
{
"line_number": <LINE_NUMBER>,
"file_name": <PATH_FROM_REPOSITORY_ROOT>,
"code_snippet": <SOURCE_CODE_LINE>
},
...
]
}Meaning:
- each key is a unique identifier for one vulnerable path
- each value is the ordered sequence of nodes in that path
- every node records:
line_number: the number of the vulnerable line in the file (1-based)file_name: the relative path of the file containing that nodecode_snippet: the code line at that node
These paths act as the ground-truth reference during analysis, where model outputs are compared against the expected vulnerable flow.
This directory contains the actual vulnerable project files for that CVE instance. The relative file paths listed in input_filenames.json and vulnerable_paths.json are resolved against this folder.
For example, if input_filenames.json contains:
src/main/java/com/jamesmurty/utils/XMLBuilder.java
then the file is expected at:
data/CVEPath/Java/CVE-2021-41110_cwlviewer/source/src/main/java/com/jamesmurty/utils/XMLBuilder.java
data/post-cutoff-cves/ is a held-out reconstruction set in the same folder layout as CVEPath. It is not mixed into data/CVEPath/.
It contains 14 Python CVEs. Each has an NVD publish date and a fix-commit date strictly after the GPT-5.2 knowledge cutoff (31 August 2025), so the same 14 cases also post-date the other evaluated models. There is no Java split.
The annotation files (input_filenames.json, cve_metadata.json, vulnerable_paths.json) and source/ tree follow the CVEPath schema above. Metadata is a slim subset (CVE id, CWE, description, language, project, commit, dates, parents). Each input_filenames.json has one file combination: every source file that appears on any reference path.
data/post-cutoff-cves/
└── Python/
└── CVE-2025-10155_picklescan/
├── annotations/
│ ├── input_filenames.json
│ ├── vulnerable_paths.json
│ └── cve_metadata.json
└── source/
└── ...
The 14 instances are:
CVE-2025-10155_picklescanCVE-2025-12060_kerasCVE-2025-61622_foryCVE-2025-61677_datachainCVE-2025-61765_python-socketioCVE-2025-67502_taguetteCVE-2025-67729_lmdeployCVE-2026-11529_mysql_mcp_serverCVE-2026-11816_kerasCVE-2026-25632_EPyT-FlowCVE-2026-27645_changedetection.ioCVE-2026-27953_ormarCVE-2026-54499_stanzaCVE-2026-8838_amazon-redshift-python-driver
Point the CVEPath runner at this tree with --dataset-dir data/post-cutoff-cves and --language Python. Use a separate --out-dir so post-cutoff logs are not mixed with the 105-CVE runs.
To rebuild an equivalent set from scratch, including for a later cutoff date, see Rebuild a post-cutoff set.
data/CVEPath_obf/ is a semantics-preserving identifier-masked copy of the same 105 CVEs as data/CVEPath/ (43 Java + 62 Python). It is not mixed into the unmasked tree. Use it for the RQ1 contamination check: rename user-defined identifiers to opaque tokens (Cls_#### / Fn_#### / v_####), rename files to file_####.ext, and redact comments, Python docstrings, CVE IDs, and project-name tokens. Language keywords and a blocklist of standard-library / framework source/sink APIs stay intact.
The annotation files and source/ tree follow the CVEPath schema. Paths and snippets in input_filenames.json and vulnerable_paths.json already point at the masked files, so score these runs against data/CVEPath_obf, not data/CVEPath. Each CVE folder also has obf_map.json (file_map and ident_map) for audit; the runner does not read it. Metadata is stripped of CVE descriptions, project names, commits, and diffs.
data/CVEPath_obf/
├── Java/
│ └── CVE-2021-41110_cwlviewer/
│ ├── annotations/
│ │ ├── input_filenames.json
│ │ ├── vulnerable_paths.json
│ │ └── cve_metadata.json
│ ├── obf_map.json
│ └── source/
│ └── file_0001.java
└── Python/
└── ...
Point the CVEPath runner at this tree with --dataset-dir data/CVEPath_obf. Use a separate --out-dir so masked logs are not mixed with unmasked 105-CVE runs.
The negative-sample dataset is organized by language, with one folder per sample.
Example:
data/negative_samples/
└── Python/
├── file_1/
│ ├── file_1.py
│ └── file_1.json
└── file_2/
└── ...
└── Java/
...
Each negative-sample folder should contain:
- one source file (
.pyor.java) - optionally one
.jsonmetadata file
The negative pipeline reads the source file content and treats the sample as a non-vulnerable example by default.
python scripts/run_llms_on_cvepath.py \
--cve CVE-2021-41110 \
--language Java \
--model gpt-4o \
--provider openai \
--prompt-mode allpython scripts/run_llms_on_cvepath.py \
--all-cves \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode allSame runner as CVEPath. Pass --dataset-dir data/post-cutoff-cves and --language Python (all 14 cases are Python).
One CVE:
python scripts/run_llms_on_cvepath.py \
--cve CVE-2025-10155 \
--language Python \
--model gpt-4o \
--provider openai \
--prompt-mode llmpath \
--dataset-dir data/post-cutoff-cves \
--out-dir output/runs_post_cutoffAll 14 CVEs:
python scripts/run_llms_on_cvepath.py \
--all-cves \
--language Python \
--model gpt-4o \
--provider openai \
--prompt-mode llmpath \
--dataset-dir data/post-cutoff-cves \
--out-dir output/runs_post_cutoffScore those logs against the post-cutoff annotations (not data/CVEPath):
python scripts/analyze_runs.py \
--logs-dir output/runs_post_cutoff \
--cvepath-dataset-dir data/post-cutoff-cves \
--negative-dataset-dir data/negative_samples \
--output-dir output/analysis_post_cutoff \
--analysis-model claude-sonnet-4-5 \
--recursiveSame runner as CVEPath. Pass --dataset-dir data/CVEPath_obf. Score against the masked annotations (not data/CVEPath).
One CVE:
python scripts/run_llms_on_cvepath.py \
--cve CVE-2021-41110 \
--language Java \
--model gpt-4o \
--provider openai \
--prompt-mode llmpath \
--dataset-dir data/CVEPath_obf \
--out-dir output/runs_obfAll 105 CVEs:
python scripts/run_llms_on_cvepath.py \
--all-cves \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode llmpath \
--dataset-dir data/CVEPath_obf \
--out-dir output/runs_obfpython scripts/analyze_runs.py \
--logs-dir output/runs_obf \
--cvepath-dataset-dir data/CVEPath_obf \
--negative-dataset-dir data/negative_samples \
--output-dir output/analysis_obf \
--analysis-model claude-sonnet-4-5 \
--recursiveCVEPath ships only the oracle source files. Distractor sampling needs each project's full tree at the pre-fix commit. scripts/clone_cvepath_repos.py reads data/CVEPath/ metadata and writes:
output/original_repos/{Java,Python}/<CVE>_<repo>/
That path is the default --distractor-repos-dir. The clones are gitignored under output/ (they are large). Resume-safe: a dest already at the target commit is skipped.
python scripts/clone_cvepath_repos.pyOptional: --dry-run, --limit N, --language Python, --out PATH, --cvepath data/CVEPath. Set GITHUB_TOKEN if GitHub rate-limits anonymous clones.
Same runner as CVEPath. Sample k extra same-language files from output/original_repos/{Java,Python}/<CVE>_<repo>/ (after step 5). The sampler prefers files in the same directory as the shown code, skips tests and empty files, and never uses a file that appears on any reference path of that CVE. Distractors are appended after the oracle combination so reference line numbers stay comparable. With the default seed (1234), k = 1 is a prefix of k = 3 / k = 5.
--distractor-repos-dir defaults to output/original_repos. Required only when --distractors is not 0.
python scripts/run_llms_on_cvepath.py \
--all-cves \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode llmpath \
--distractors 3 \
--distractor-seed 1234 \
--out-dir output/runs_e3/k3Score as usual. Matching uses the CVE's reference paths (and the needed_files field in each log), not distractor file headers.
python scripts/analyze_runs.py \
--logs-dir output/runs_e3/k3 \
--cvepath-dataset-dir data/CVEPath \
--negative-dataset-dir data/negative_samples \
--output-dir output/analysis_e3/k3 \
--analysis-model claude-sonnet-4-5 \
--recursiveRQ4 positives reuse the CVEPath runner's file combinations and distractor sampler, and add --workers for parallel API calls. Default prompt mode is baseline (LLMPath positives can come from run_llms_on_cvepath.py).
python scripts/run_detection_positives.py \
--all-cves \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode baseline \
--out-dir output/runs_detectWith distractors (k > 0), clone first, then pass --distractors (repos default to output/original_repos):
python scripts/run_detection_positives.py \
--all-cves \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode baseline \
--distractors 3 \
--out-dir output/runs_detect_e3/k3Oracle RQ4 negatives (multi-run majority, k = 0, no extra files):
python scripts/run_detection_negatives.py \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode all \
--out-dir output/runs_detect_negWith k > 0, clone first. Extra files are test/docs-like paths from that negative's project checkout when it exists under output/original_repos/; otherwise other same-language cloned repos fill to k. Selection is deterministic (seed mixed with model, language, and sample id). Both prompts see the same extra files.
python scripts/clone_cvepath_repos.py
python scripts/run_detection_negatives.py \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode all \
--distractors 5 \
--out-dir output/runs_detect_neg_k5scripts/run_llms_on_negative_samples.py remains the original single-run negatives path.
python scripts/run_llms_on_negative_samples.py \
--language all \
--model gpt-4o \
--provider openai \
--prompt-mode allMinimal example:
python scripts/analyze_runs.py --analysis-model claude-sonnet-4-5Explicit paths example:
python scripts/analyze_runs.py \
--logs-dir output/runs \
--cvepath-dataset-dir data/CVEPath \
--negative-dataset-dir data/negative_samples \
--output-dir output/analysis \
--analysis-model claude-sonnet-4-5 \
--recursivePer-run scores stay in output/analysis/data/cvepath_results.csv. rq1_model_summary.csv is the median of runs per CVE, then the median across CVEs (Table I / E3 / masking). Malformed taint_path nodes are skipped rather than crashing the scorer.
scripts/collect_post_cutoff.py reproduces how data/post-cutoff-cves/ was built. It starts from GitHub advisories published after a cutoff date and applies the CVEPath study-design stages, writing the survivors in the same layout the runner already reads. Use it to regenerate the shipped 14 cases, or to build a fresh held-out set for a newer model by moving --cutoff and --since forward.
The pipeline stops at PF-1. PF-2, the manual review in which an author confirms that the source and sink match the CVE description and that the intermediate nodes form a valid taint sequence, is not automated and is not part of this script. The shipped 14 CVEs did go through PF-2, so a rebuilt set will generally keep more candidate paths per CVE than data/post-cutoff-cves/ does.
| Stage | What it does |
|---|---|
| Discovery | Reviewed GitHub advisories published on or after --since in the language's ecosystems (pip for Python, maven and gradle for Java), restricted to those carrying a CVE id. |
| Metadata | Fix commit from a GHSA commit reference, else an OSV FIX reference, else an NVD reference; the vulnerable revision is that commit's first parent. Kept only when both the NVD publish date and the fix-commit date fall strictly after --cutoff. |
| CS-1 | Keep CVEs whose NVD CWE has an @kind path-problem query in query_packs/custom-cwe-queries/<language>/, then drop addition-only fixes (a fix must delete or replace lines for a vulnerable hunk to exist). |
| CS-2 | Label the changed files of multi-file fix commits with an LLM and keep CVEs with at least one vulnerability-path-fixing file. Single-file fix commits skip the call. |
| Clone | Blobless clone of each repository, detached at the vulnerable parent commit. |
| PC-1 | Build a CodeQL database on that checkout, then generate MySources.qll, MySinks.qll and MySummaries.qll from the vulnerable hunks to augment the CWE queries. Python uses codeql database create --language=python with no --command. Java probes Maven/Gradle (wrappers first) × each --jdk / $JAVA_HOME with codeql database create --language=java --command … (tests skipped); there is no no-command Java fallback. |
| PC-2 | codeql database analyze with the augmented queries; SARIF code flows become candidate paths. |
| PF-1 | Keep the candidate paths with the maximum number of distinct vulnerable hunks touched. |
The paper used CodeQL CLI 2.15.3. The official CLI zip is extractors only; the post-cutoff run also needed codeql/python-all 0.11.3 and codeql/java-all 0.8.3 on the CLI search path (the Java qlpack.yml pins cliVersion: 2.15.3). The setup script downloads the CLI and those libraries:
python scripts/setup_codeql.pyThis unpacks the CLI into tools/codeql/ (gitignored), puts the packs in tools/codeql/qlpacks/, and runs codeql pack install on query_packs/base_queries/{Python,Java}. If you already have CLI 2.15.3, point CODEQL_PATH at it and still run the script so the packs are installed.
In scripts/.env:
GITHUB_TOKEN— required. Advisory listing, commit metadata and cloning all go through the GitHub API, which rate-limits unauthenticated traffic almost immediately.NVD_API_KEY— optional but strongly recommended. Without it NVD allows 5 requests per 30 seconds instead of 50, which turns discovery from minutes into hours.ANTHROPIC_API_KEY— needed for CS-2, which defaults to Claude Opus 5 (the model used for the shipped set). Override with--cs2-modeland--cs2-provider.
python scripts/collect_post_cutoff.py --language Python --n 14--n is the number of CVEs that must end up with non-empty PF-1 paths. Discovery and the selection stages always run over the full pool, since CS-2 has to leave enough repositories to work with; cloning, database building and querying then proceed smallest repository first and stop as soon as --n is reached. Omit --n to run every candidate.
Java PC-1 needs a JDK plus Maven and/or Gradle. Wrappers in the cloned repo (mvnw, gradlew) are enough; otherwise mvn/gradle must be on PATH, or pass --mvn / --gradle. Point --jdk at whichever JDKs you have (repeatable); $JAVA_HOME is also tried. Do not copy machine-specific sdkman paths. Example:
python scripts/collect_post_cutoff.py --language Java --n 14 \
--jdk /usr/lib/jvm/java-17-openjdkEvery HTTP response is cached under output/post_cutoff/cache/, and each stage skips work it has already done, so an interrupted run can simply be restarted. To rerun one part in isolation, pass a stage subset:
python scripts/collect_post_cutoff.py --stages cs2,clone,pc1For a quick end-to-end check that does not scan the whole advisory feed:
python scripts/collect_post_cutoff.py --max-advisories 200 --n 1To run later stages on one already-collected CVE (repeatable), pass --cve CVE-.... That does not rewrite the full-run funnel lists under funnel/.
Everything lands under output/post_cutoff/ (gitignored):
output/post_cutoff/
├── FUNNEL.md # stage-by-stage counts for the run
├── funnel/ # 01–11, one CVE id per line
├── candidates.jsonl # metadata for every CVE past the cutoff
├── summary.csv
├── metadata/<CVE>_<repo>/
├── hunks/ # removed-line hunks per CVE
├── cs2/ # per-CVE classifier output
├── cvepath_hunks/ # hunks restricted to CS-2 kept files (PC-1 input)
├── repos/Python/<CVE>_<repo>/ # checkouts at the vulnerable commit
├── dbs/<CVE>_<repo>/ # CodeQL databases
├── paths/<CVE>_<repo>/ # raw_paths_<cwe>.json, processed_paths_<cwe>.json
└── cvepath/Python/<CVE>_<repo>/
├── annotations/
└── source/
cvepath/ is the deliverable and uses the CVEPath schema, so it can be evaluated directly:
python scripts/run_llms_on_cvepath.py \
--all-cves \
--language Python \
--model claude-sonnet-4-5 \
--provider anthropic \
--dataset-dir output/post_cutoff/cvepath \
--out-dir output/runs_post_cutoff_newfunnel/11_pf1_hunk_overlap.txt lists the CVEs that reached PF-1. There is no 12_: that stage is manual.
python scripts/run_llms_on_cvepath.py [OPTIONS]Target selection:
--cve CVE-...--all-cves
Other options:
--language {Java,Python,all}--model MODEL_NAME--provider PROVIDER_NAME--prompt-mode {llmpath,baseline,all}--actual-label INTdefault:1--runs INTdefault:4(multi-run variance)--temperature FLOATdefault:0.2(auto-omitted for reasoning models)--seed INTdefault:1000(run i usesseed+ifor OpenAI chat models)--distractors STRdefault:0(E3 distractor count, orall-in-dir/all)--distractor-seed INTdefault:1234--distractor-repos-dir PATHdefault:output/original_repos--dataset-dir PATHdefault:data/CVEPath--out-dir PATHdefault:output/runs
Notes:
--cveand--all-cvesare mutually exclusive.- CVEPath runs default to a positive ground-truth label.
- Default
--runs 4and--temperature 0.2target variance estimation; use--runs 1 --temperature 0.0to reproduce the original single-run baseline. - For the held-out set use
--dataset-dir data/post-cutoff-cvesand--language Python. - For the identifier-masked set use
--dataset-dir data/CVEPath_obfand score with--cvepath-dataset-dir data/CVEPath_obf. - For
--distractors!=0, runpython scripts/clone_cvepath_repos.pyfirst.k = 0does not need those checkouts.
python scripts/clone_cvepath_repos.py [OPTIONS]Options:
--cvepath PATHdefault:data/CVEPath--out PATHdefault:output/original_repos--language {Java,Python,all}--limit N--dry-run
python scripts/run_detection_positives.py [OPTIONS]Same target and distractor flags as run_llms_on_cvepath.py, plus --workers (default 1). Default --prompt-mode is baseline.
python scripts/run_detection_negatives.py [OPTIONS]Oracle RQ4 negatives with --runs / --temperature / --seed / --workers / --out-dir. --distractors default 0 is the current single-file oracle. For k > 0, run python scripts/clone_cvepath_repos.py first; --distractor-repos-dir defaults to output/original_repos. Extra files are test/docs-like from that project's cloned tree when present, otherwise other same-language cloned repos fill to k.
python scripts/run_llms_on_negative_samples.py [OPTIONS]Options:
--language {Java,Python,all}--model MODEL_NAME--provider PROVIDER_NAME--prompt-mode {llmpath,baseline,all}--actual-label INTdefault:0
Notes:
- Negative-sample runs default to a negative ground-truth label.
python scripts/analyze_runs.py [OPTIONS]Options:
--logs-dir PATH--cvepath-dataset-dir PATH--negative-dataset-dir PATH--output-dir PATH--recursive--no-recursive--thresholds FLOAT [FLOAT ...]--analysis-model MODEL_NAME
Notes:
- You should use either
--recursiveor--no-recursive, not both. - The default analysis model in the code is
claude-sonnet-4-5. - Model-level NOR/LCNR are the median of per-CVE medians across runs, not the max-NOR run.
python scripts/setup_codeql.py [OPTIONS]Options:
--version STRdefault:2.15.3--dest PATHdefault:tools/codeql--platform {linux64,osx64,win64}default: detected from the host--forcere-download over an existing install--skip-packsCLI only; do not download Java/Python qlpacks
Notes:
- Downloads from the
github/codeql-cli-binariesreleases and verifies the unpackedcodeql version. - Then downloads
codeql/python-all@0.11.3andcodeql/java-all@0.8.3(and their 2.15.3-era dependencies) intotools/codeql/qlpacks/, which is what makes both languages compile. - Only the post-cutoff collection uses CodeQL; nothing else in the package needs it.
python scripts/collect_post_cutoff.py [OPTIONS]Scope:
--language {Python,Java}default:Python(Python: no-command extract; Java: Maven/Gradle × JDK probe)--n INTstop once this many CVEs have PF-1 paths; default: no limit--cve CVE-IDrestrict everything after discovery to these CVEs (repeatable)--cutoff YYYY-MM-DDdefault:2025-08-31--since YYYY-MM-DDGHSA published-on-or-after date; default:2025-09-01--stages STRcomma-separated subset ofcollect,cs1,cs2,clone,pc1,pc2,pack; default:all
Paths:
--out PATHdefault:output/post_cutoff--codeql PATHdefault:$CODEQL_PATH, elsetools/codeql/codeql--query-packs PATHdefault:query_packs(CS-1 path-problem CWE scan and PC-1/PC-2 query copy)
CS-2 classifier:
--cs2-model MODEL_NAMEdefault:claude-opus-5--cs2-provider PROVIDER_NAMEdefault:anthropic--cs2-prompt PATHdefault:prompt_templates/llm_multifile_labelling.txt--cs2-workers INTdefault:4--cs2-limit INTcap the number of LLM calls this run
Clone and CodeQL:
--clone-workers INTdefault:4--clone-retries INTdefault:5--threads INTCodeQL threads; default:6--ram INTCodeQL RAM in MB; default:24576--jdk PATHJAVA_HOME to try for Java PC-1 (repeatable); also uses$JAVA_HOME--mvn PATHextra Maven binary for Java PC-1 (repeatable); wrappers are tried first--gradle PATHextra Gradle binary for Java PC-1 (repeatable); wrappers are tried first
Smoke tests and caching:
--max-advisories INTcap the advisories scanned--limit INTcap the metadata records kept--no-resumeignore the HTTP cache and re-fetch everything
Notes:
- Needs
GITHUB_TOKEN, ideallyNVD_API_KEY, and a key for the CS-2 provider. - The pipeline stops at PF-1; PF-2 is a manual review and is not automated.
--cveis for smoke tests and reruns of a known ID. It does not overwritefunnel/01–11from a full collection.- Output in
<out>/cvepath/<language>/uses CVEPath layout and can be passed torun_llms_on_cvepath.py --dataset-dir.
The run scripts write JSON logs under:
output/runs/
The logs include fields such as:
- task
- language
- model
- provider
- prompt name
- prompt text
- input text
- output text
- reasoning content
- usage
- timestamp
The exact schema differs slightly between CVEPath and negative-sample runs.
The analysis script writes under:
output/analysis/
├── data/
└── plots/
Examples of generated outputs include CSV summaries and PDF plots. data/cvepath_results.csv keeps every run; data/rq1_model_summary.csv aggregates with median-of-runs per CVE.
docs/appendix.pdf is the replication-package appendix cited from the paper. It contains:
- Identifier-masking contamination check
- Post-cutoff held-out reconstruction check (14 Python CVEs after the GPT-5.2 cutoff)
- Claude fail-case analysis of the 10 NOR = 0 cases in original Figure 4