01 · Methodology
Benchmark design & scoring
Every design choice optimizes for one property: results that can be audited line by line and reproduced exactly. This page documents the taxonomy, labeling, matching, and metrics.
1.3
Target-type taxonomy
Five target types classify the code under test on a code-realism axis. Version 1.0 covers Type 1: high vulnerability density, auditable labels, and resistance to training-data contamination. Types 2–5 are planned. A second axis records code authorship (human-authored, LLM-assisted, LLM-generated): the v1.0 corpus is human-authored, and v2.0 adds a vibe-coded (LLM-generated) subset, labeled per repository for authorship analysis.
1
Intentionally vulnerable apps
Educational & CTF projects — PyGoat, DVPWA, VAmPI. High vulnerability density, diverse CWE coverage.
v1.0 · live
2
Previously-vulnerable platforms
Production apps pinned to pre-patch commits with disclosed CVEs, at realistic vulnerability density.
v2 · planned
3
Previously-vulnerable libraries
Open-source libraries pinned to known-vulnerable versions, patched for disclosed CVEs.
v2 · planned
4
Benchmark roll-ups
Existing benchmarks (OWASP Benchmark, NIST Juliet) imported and re-scored under RealVuln metrics.
v2 · planned
5
Academic reproductions
Published scanner evaluations encoded as reproducible configurations.
future
1.4
Ground-truth labeling
Every finding was produced by manual review. Each labeled entry records the following fields:
- id · is_vulnerable
- A unique identifier and a boolean flag distinguishing real vulnerabilities from false-positive traps.
- primary_cwe
- The most precise weakness, plus an acceptable_cwes list of alternative CWE identifiers a scanner may reasonably report for the same flaw.
- location
- File path and a start_line/end_line range pinpointing the code.
- severity
- One of critical, high, medium, or low.
- evidence
- The annotation source (manual review, CVE id, or published walkthrough) and a free-text rationale for why the code is, or is not, vulnerable.
False-positive traps. 280 of the 4,418 entries (6.3%) are code patterns that appear suspicious but are demonstrably safe — for example, a login function passing user input to an ORM's filter_by(), which auto-parameterizes the query. Flagging a trap is penalized as a false positive, and the traps double as true negatives for false-positive-rate computation.
Non-scoring entries. A small number of reviewed locations are highly discretionary: whether they count as a vulnerability turns on intent or deployment context the source alone does not settle — an endpoint the code itself documents as deliberately public, for instance. Calling such a location vulnerable would penalize scanners for our uncertainty; calling it safe would reward them for it. These entries stay in ground truth, marked scoring: non_scoring with a written reason, and are excluded from scoring in both directions: reporting one is not a false positive and missing one is not a false negative. They take no part in matching, so they can never claim a finding away from a co-located real vulnerability, and they are never included in the published vulnerability or trap totals.
1.6
Scoring
Two base metrics underpin scoring. Precision is the fraction of flagged findings that were real; recall is the fraction of real vulnerabilities found. The Fβ family combines them as a weighted harmonic mean, where β controls the trade-off.
In security the costs are asymmetric: a single missed vulnerability can lead to a breach, while a false positive costs an analyst minutes. F1 (β=1) weights both equally, implicitly assuming those costs are equal. RealVuln's primary metric is F3 (β=3), which weights recall nine times over precision. F1 and F2 are reported throughout so results can be re-ranked under any preference.
Aggregation. The conservative strict_micro mode pools confusion-matrix counts across repositories and treats any repository a scanner failed to complete as all-false-negatives. All headline scores use this strict mode.
F3 = 10 ·
P · R9P + R
× 100
β = 3 · recall weighted 9× · scaled to [0, 100]
F2 = 5·PR / (4P+R) · F3 = 10·PR / (9P+R)
Per-CWE-family and per-severity breakdowns are computed alongside the aggregates.
$ python score.py --repo realvuln-pygoat --all-scanners