Picking a model to drive a security scanner ought to be a benchmark question, and there are public benchmarks for it. The trouble is what they measure.
CyberGym is the careful version of the genre. It builds 1,507 instances out of real OSS-Fuzz vulnerabilities across 188 projects, and the task is genuinely hard: the agent reads a vulnerability description, gets the codebase at its pre-patch commit, and has to write a proof-of-concept that crashes the unpatched build but not the patched one.
It’s also a poor guide for application security. The corpus is C and C++, so it says nothing about the Python, Java, PHP, and JavaScript most product code is written in. The bugs are overwhelmingly memory safety, and a sanitizer crash is not the same thing as a vulnerability someone can exploit in a web app. And it has been public long enough that the models we’d be grading have most likely trained on it.
Corgea runs an AI engine (“fusion”) that scans code, triages findings, and writes fixes. The hardest question we get asked is which model should drive the scan, and nothing public answered it for the kind of code our customers actually ship. So we built our own: a fixed corpus, ground truth that’s curated and versioned, and mechanical scoring.
We ran 12 model profiles against the same corpus. Which model won turned out to be less interesting than which column of the table people usually read first.
Model profiles ranked by F1
Across 1,913 security cases with standard metrics — an acceptable-CWE alternate (TP*) counts as a hit. Click a metric to re-rank. Profiles that scored under 90% of cases are held out of the ranking.
| # | Tool | ||||
|---|---|---|---|---|---|
| 1 | GPT-5.6 Luna | 56.4% | 95.9% | 40.0% | $0.133 |
| 2 | gpt52 | 55.9% | 94.8% | 39.7% | $0.207 |
| 3 | GPT-5.4 | 54.7% | 95.1% | 38.4% | $0.251 |
| 4 | GLM 5.2 | 54.0% | 95.8% | 37.6% | n/a |
| 5 | gpt-5.5 | 53.5% | 95.6% | 37.2% | $0.685 |
| 6 | GPT-5.6 Sol | 53.2% | 94.8% | 37.0% | $0.686 |
| 7 | Kimi K2.7 Code | 52.7% | 96.8% | 36.3% | $0.237 |
| 8 | Kimi K2.5 | 52.5% | 95.0% | 36.3% | $0.216 |
| 9 | claude-opus-4-8 | 51.1% | 95.4% | 34.9% | $2.203 |
| 10 | GPT-5.6 Terra | 50.3% | 95.9% | 34.1% | $0.300 |
| 11 | Baseline | 50.2% | 96.2% | 34.0% | $0.182 |
| 12 | DeepSeek V4 Flash | 48.5% | 95.8% | 32.5% | $0.018 |
The standard leaderboard, ranked by F1. GPT-5.6 Luna tops the board on one of the cheapest bills; the flagship Claude Opus 4.8 lands ninth.
What we ran
Only one variable changes between runs: the model bundle that drives the vulnerability scanner. The fusion code is pinned to a single commit, and the harness hard-aborts a run if the checkout has drifted, because a moving engine would turn an apples-to-apples comparison into noise.
Each profile is a flat list of roles mapped to model IDs. The vulnerability scanner is the role under test:
# Swap the detector, leave every triage role on the default bundle
vuln_scan = "gpt-5.6-luna"
# Flagship single-model profile
vuln_scan = "claude-opus-4-8"
Each profile runs across the full corpus. We collect every finding and score it against ground truth, and we fix the list of files that count before the run instead of trimming it once we’ve seen the results.
The corpus
The benchmark scores source code rather than config. The harness owns an allowlist of extensions (.py, .java, .js, .php, .html, .twig, .c, .h, .cs, .cshtml) and drops anything else from both sides of the join. IaC and config files (.tf, .yaml, .properties) belong to a separate scanner and stay out of scope. We say so up front because “we found 4,000 issues” is easy when you count every YAML key.
There are 1,913 ground-truth cases across 55 fixtures, 8 languages, and 95 distinct CWEs. The fixtures come from three places. Some are apps we built ourselves, like an insecure payment API and a coffee-shop app ported across Java, Node, PHP, and C#, where we know exactly what was planted and where. Others are real open-source vulnerable apps: pygoat, DVGA, VAmPI, deliberately-vulnerable Flask and Tornado training apps, and dozens more, all messy in the way production code is messy. The rest are imported CVE-derived cases, woven from public vulnerability datasets.
Every expected case is one row in a versioned inventory with a stable ID, a canonical CWE, the file path, the target line, and whether it’s a true positive (a real vulnerability the scanner should flag) or a hard negative (a spot that looks vulnerable but isn’t, bait for false positives).
That last column is what keeps the benchmark honest. A scanner that flags everything scores perfect recall and is still useless. By design the corpus holds 1,562 expected true positives (82%) and 351 hard negatives (18%), which gives us something to measure precision against.
The CWE spread covers the bugs that actually ship: cross-site scripting, SQL injection, missing authorization, CSRF, SSRF, path traversal, command injection, hardcoded secrets, open redirects.
The headline numbers use only the independent slice of the corpus. 565 cases were minted while reviewing the scanners’ own output, and we exclude all of them, because ground truth derived from the tools you’re grading is circular.
How we score
We match each scanner finding against the expected cases with a strict join. A finding credits a case only when all four of these hold:
- Same fixture. A finding in
insecure-api/main.pycan’t match a case ininsecure-payment-api/main.py, so basename collisions never cross-match. - The finding’s CWE equals the case’s canonical CWE, or one of its listed acceptable alternates.
- The finding’s file matches the case’s path.
- The finding lands within 25 lines of the target, or its line span brackets the target.
Matching is a 1:1 assignment. Each finding credits at most one case; each case is claimed by at most one finding. Exact-CWE matches always win over alternate-CWE matches, and the closest finding wins. The result is deterministic: re-run it and you get a byte-identical scoreboard.
Per-run confusion matrix
Per-profile counts, strict precision / recall / F1, total cost, and total scan time. Strict counts TP* as a missed CWE (right location, wrong class).
| Run | TP | TP* | FP | FN | TN | strict P | strict R | strict F1 | Cost (total) | Total time |
|---|---|---|---|---|---|---|---|---|---|---|
| GPT-5.6 Luna | 568 | 56 | 27 | 937 | 324 | 0.955 | 0.364 | 0.527 | $7.1733 | 7.4h |
| gpt52 | 542 | 77 | 34 | 942 | 315 | 0.941 | 0.347 | 0.507 | $11.1578 | 7.5h |
| GPT-5.4 | 548 | 52 | 31 | 961 | 319 | 0.946 | 0.351 | 0.512 | $13.5433 | 5.2h |
| GLM 5.2 | 535 | 52 | 26 | 974 | 325 | 0.954 | 0.343 | 0.504 | n/a | 15.0h |
| gpt-5.5 | 516 | 64 | 27 | 981 | 324 | 0.950 | 0.331 | 0.490 | $37.0032 | 13.4h |
| GPT-5.6 Sol | 516 | 62 | 32 | 983 | 319 | 0.942 | 0.331 | 0.489 | $37.0678 | 7.1h |
| Kimi K2.7 Code | 510 | 56 | 19 | 995 | 332 | 0.964 | 0.327 | 0.488 | $12.8198 | 8.7h |
| Kimi K2.5 | 503 | 63 | 30 | 995 | 321 | 0.944 | 0.322 | 0.480 | $11.6435 | 8.2h |
| claude-opus-4-8 | 490 | 55 | 26 | 1016 | 325 | 0.950 | 0.314 | 0.472 | $118.9686 | 8.9h |
| GPT-5.6 Terra | 494 | 38 | 23 | 1029 | 328 | 0.956 | 0.316 | 0.475 | $16.1893 | 5.9h |
| Baseline | 479 | 51 | 21 | 1031 | 330 | 0.958 | 0.307 | 0.465 | $9.8173 | 11.9h |
| DeepSeek V4 Flash | 461 | 46 | 22 | 1054 | 329 | 0.954 | 0.295 | 0.451 | $0.9937 | 8.2h |
Per-profile confusion-matrix counts feed precision, recall, and F1.
Every case lands in one of four cells:
| Cell | Meaning |
|---|---|
| TP | Expected vulnerability, scanner found it |
| FP | Hard negative, scanner flagged it anyway |
| FN | Expected vulnerability, scanner missed it |
| TN | Hard negative, scanner correctly stayed silent |
A scan that runs and finds nothing writes a sentinel row, so we can tell “scanned, found nothing” (a real FN or TN) from “never scanned” (blank). That way we never have to infer coverage.
Scanners also disagree on taxonomy. A SQL injection labeled CWE-89 is still a real catch if the scanner calls it CWE-943. So each case can list acceptable alternate CWEs, and we mark those matches with an asterisk (TP* / FP*, meaning right location but alternate CWE) to keep the drift visible. The lenient view counts TP* as a hit; the strict view reclassifies every asterisked match as a miss. The leaderboard above is lenient.
From the cells we compute precision, recall, and F1. Non-overlapping confidence intervals aren’t enough for us to call a difference significant, since the cases cluster by fixture. For significance we run pairwise McNemar tests over each pair’s common coverage, adjust for the fixture-cluster design effect, apply a Holm correction, and confirm the ranking survives leaving any single fixture out. We publish a ranking claim only when it clears that bar.
The scoreboard
Lenient scoring, independent corpus slice. Profiles below 90% coverage are held out of the ranking.
Start with the precision column. Every ranked profile clears 90%, and all twelve sit between 94.8% and 96.8%, which is far too narrow a range to separate them. On this corpus, precision is effectively a solved problem.
What does separate the profiles is recall and cost, and the top one isn’t the flagship model. It’s GPT-5.6 Luna (gpt-5-6-luna-vuln-scan, vuln-scan only), which hits 40.0% recall at $0.133 per fixture, one of the cheapest bills on the board. Claude Opus 4.8 lands ninth, at 34.9% recall. The whole ranked field fits in a 7.5-point recall band, from DeepSeek V4 Flash at 32.5% to Luna at 40.0%.
baseline and claude-opus-4-8 are a statistical tie on recall. Over their common coverage of 1,174 cases across 54 fixtures, the two differ by 0.003. If you’re paying for the flagship expecting it to beat the default bundle, this corpus doesn’t show that.
Cost
Spend against detection quality
One dot per profile — average total USD (input + output) per fixture (x, log scale) against detection quality (y). The green upper-left quadrant is the sweet spot: cheaper than the median profile and above median quality. Dashed guides split the median-cost / median-quality quadrants. Profiles with no verified rate are left off the plot.
One dot per model: total cost per fixture against quality. The cheap profiles sit in the same quality band as the expensive one.
Per fixture, GPT-5.6 Luna costs $0.133 and claude-opus-4-8 costs $2.203, about 17× more. Across the full corpus that works out to $118.97 for Claude Opus 4.8 against $7.17 for GPT-5.6 Luna.
For that 17× premium you get lower recall (34.9% versus Luna’s 40.0%) and precision that’s a rounding error apart. The floor goes lower still. DeepSeek V4 Flash scans a fixture for $0.018, two orders of magnitude under Opus, and still finishes within 2.4 points of it on recall. The cheap profiles and the most expensive one land in the same quality band, so on this corpus the extra spend isn’t buying extra catches.
Limitations
We’d rather state these than have you find them.
- These are SAST detection scores on a curated corpus, so don’t read them as field guarantees. Strong recall here does not promise strong recall on your code, because vulnerable-by-design apps bury the same bug under far less noise than production does. Treat these numbers as a ceiling on what you’d see in your own repo.
- 565 of the 1,913 cases were minted from the scanners’ own output. Grading a tool on ground truth it helped create is circular, so those stay out of the headline numbers.
- Our cost figures cover input and output spend, stamped with that basis. Token counts can be approximate for backends that don’t return a usage block, and GLM 5.2 reads
n/abecause no verified rate is configured for it. - IaC and config files are out of scope here by design.
- Ground-truth labels are human judgments, and a curated label is still a label. We version the inventory, so relabeling a case leaves a trail instead of quietly moving a score.
- These results describe one pinned version of fusion. A prompt change can move every number in the table, which is exactly why we pin the commit and snapshot the run.
What’s next
We want a wider corpus: more languages, more real CVE-derived cases, and more hard negatives. The hard negatives are the expensive part to build, and also the part that keeps a benchmark honest.
Detection is only the first leg of what fusion does, so we’re extending the harness to score triage accuracy (did the model correctly call a finding a false positive?) and fix quality (did the patch close the bug without breaking the build?). A model that finds bugs but writes bad patches is a different tool than the scoreboard above suggests.