Cisco AI Defense skill scanner: evaluation results

What changes when the LLM judge and the System One model are switched on.

Large-scale run on local GPUs

Both models were served locally with vLLM on four NVIDIA H200 GPUs (150 GB each), 96 CPU cores: OpenJev, the System One weights, on two cards, and Gemma 4 26B-A4B as the LLM judge on the other two, each as independent replicas because either model fits on one card. The deterministic scanner ran on the host CPUs. Nothing left the cluster.

The judge reproduces locally, once a decoding fault is fixed

JudgeF1PrecisionRecallFPR
Gemma 4 26B, local vLLM64.19%81.63%52.88%18.20%
Gemma 4 26B, Bedrock mantle (earlier)61.20%81.50%49.00%17.60%

MaliciousSkillBench source-disjoint split, single pass, core rules, detection at MEDIUM or above. The two routes agree, which is what licenses using the local run for everything below.

The first local run lost 46.5% of records (644 of 1,384) to finish_reason=length. Under grammar-constrained JSON decoding the model emitted a complete object and then padded it with whitespace until it reached the 8,192-token limit. Serving with disable_any_whitespace took that to 0.6% (8 records). On eight records that had all failed, the patched server answered 8 of 8 and an unpatched control 4 of 8.

A System One screen in front of the judge halves its false-positive rate

OpenJev was asked eight boolean threat questions per skill, with the probability of true read from its next-token distribution. Its prompt_injection answer screens every record, and the judge runs only on those above a threshold. The threshold was chosen on one hash-selected half of the records (655) to keep recall within two points of the judge alone while skipping the most benign records, and every figure below is from the other half (721).

Held-out halfF1PrecisionRecallFPRJudge calls
Judge alone61.15%76.22%51.05%23.13%100%
Jev screen, then judge62.59%86.07%49.18%11.56%60.7%
Judge alone against the cascade, held-out halfJudge alone against the cascade, held-out half25%50%75%100%PrecisionJudge alone: 76.2%76.2%Jev screen, then judge: 86.1%86.1%RecallJudge alone: 51.1%51.1%Jev screen, then judge: 49.2%49.2%False-positive rateJudge alone: 23.1%23.1%Jev screen, then judge: 11.6%11.6%
Judge alone Jev screen, then judge

The screen skips 70.7% of benign records. It is not only a cost saving: many records the judge flags wrongly are ones OpenJev scores as harmless, so the cascade vetoes judge false positives and precision rises while recall moves little. This is one corpus split in half; the threshold has not been measured on the other labelled corpora and should not be assumed to transfer.

What is not: this threshold was chosen on half of MaliciousSkillBench's frozen test split, and the benchmark's terms forbid selecting thresholds on test members. The reported half was unseen, but the half used for selection was test data. The screen is re-selected on train/validation in the next section, and those figures supersede these.

The OpenJev screen, re-selected on train/validation

A record is flagged only when the screen passes it and the judge flags it. Two screens: OpenJev's prompt_injection probability alone, and a logistic regression over all eight probe logits fitted on train/validation. For each judge prompt the threshold keeps at least 97% of the judge's own train/validation recall at the lowest train/validation false-positive rate; the frozen test split was then scored once, and nothing was chosen on it.

With the new judge prompt

Held-out test, MEDIUM+RecallFPRPrecisionF1Judge callsReal-skill flagsReal-skill judge calls
Judge alone65.7%12.5%89.0%75.6%100%10.45%100.0%
prompt_injection screen (p ≥ 0.04835)59.5%3.7%96.1%73.5%60%3.15%7.9%
Logistic, all eight probes (score ≥ -0.4536)61.3%2.4%97.5%75.3%58%1.75%3.1%

With the shipped judge prompt

Held-out test, MEDIUM+RecallFPRPrecisionF1Judge callsReal-skill flagsReal-skill judge calls
Judge alone51.3%16.1%82.9%63.4%100%12.80%100.0%
prompt_injection screen (p ≥ 0.06092)48.0%3.3%95.7%63.9%58%2.60%7.0%
Logistic, all eight probes (score ≥ -0.0264)48.8%2.0%97.4%65.0%56%1.30%2.5%

New prompt, low-confidence findings capped at LOW

Held-out test, MEDIUM+RecallFPRReal-skill flagsReal-skill judge calls
Judge alone62.1%10.5%7.60%100.0%
prompt_injection screen54.2%2.9%2.35%6.0%
Logistic, all eight probes56.8%1.7%1.35%2.5%

What is good: Selected on train/validation, a screen cuts the judge's test false-positive rate by 71% to 88% for 2.5 to 6.2 points of recall, and on real skills it sends only 2.5% to 7.9% of records to the judge at all. The eight-probe logistic screen is the stronger of the two on every population.

What is not: The logistic screen needs all eight probes per skill, which were scored on the labelled splits and the 2,000-skill sample; the full-corpus run scored prompt_injection only, so the full-corpus cascade uses that screen. Real-skill rates are flag rates, not false-positive rates.

Each probe on its own

ScreenTrain/val AUCTest AUC
logistic0.9730.915
obfuscation0.9670.908
prompt_injection0.9590.881
security_control_change0.9350.859
supply_chain0.8970.793
destructive_command0.8300.791
remote_hidden_execution0.8770.726
data_exfiltration0.7920.648
sensitive_data_access0.7340.588

Where the deterministic false positives come from

Every finding was recorded with the analyzer that produced it and stored as Parquet, queried with DuckDB. The record-level rates reproduce the published deterministic baseline (7.71% false-positive rate at HIGH or above), which checks that the new pipeline measures the same thing as the old one.

AnalyzerMEDIUM+ on harmlessMEDIUM+ on maliciousHarmless per malicious
correlation35291.21
static10300.33
pipeline10330.30
AnalyzerRuleHarmlessMaliciousPrecision
correlationCORRELATED_NETWORK_EXECUTION_FLOW31718.4%
pipelinePIPELINE_TAINT_FLOW10947.4%
correlationCORRELATED_SENSITIVE_NETWORK_FLOW42284.6%
staticACTIVE_DYNAMIC_EXECUTION41881.8%
staticACTIVE_REMOTE_ACQUIRE_EXECUTE1150.0%
staticFILE_MAGIC_MISMATCH100.0%

One rule change removes most of the deterministic false-positive rate

ConfigurationF1PrecisionRecallFPR
Shipped, all rules13.49%58.18%7.63%8.44%
Demote CORRELATED_NETWORK_EXECUTION_FLOW13.29%77.22%7.27%3.30%
Also demote PIPELINE_TAINT_FLOW11.50%80.00%6.20%2.39%

correlation is the only analyzer that fires more often on harmless records than malicious ones, and one rule accounts for almost all of it. The earlier conclusion that rule-level suppression could buy at most 0.2 F1 points came from data that recorded only a record's set of rule ids, with no analyzer and no per-finding severity; it was a limit of the data, not of the rules. F1 barely moves because deterministic recall is low either way, so precision and false-positive rate are the metrics to read.

Every usable skill: deterministic, judge and cascade

1,876,662 real published skills, the whole usable gitskills corpus, each scanned in full by every deterministic analyzer with the shipped scanner, again after the first tuning pass, and a third time with the final tree. Between the shipped and the final scanner 38,275 records left MEDIUM or above and 7 entered it. These are unlabelled, so a flag rate bounds the false-positive rate from above rather than measuring it.

ThresholdShipped (95% interval)First passFinal (95% interval)Relative change
CRITICAL0.629% 0.617 to 0.6400.598%0.425% 0.416 to 0.434-32.4%
HIGH+2.469% 2.447 to 2.4912.312%1.987% 1.967 to 2.007-19.5%
MEDIUM+4.184% 4.155 to 4.2122.466%2.144% 2.124 to 2.165-48.7%
LOW+5.213% 5.181 to 5.2453.770%3.451% 3.425 to 3.477-33.8%
INFO+90.365% 90.323 to 90.40789.639%89.616% 89.572 to 89.659-0.8%

The later passes were first measured by rescanning only the records each change could touch and overlaying them on the first-pass scan. That estimate was 2.153%; the full rescan gives 2.144%. Every record the full scan flags was in the estimate, which over-counted 168 records (130 by YARA_jailbreak_generic, 38 by FIND_EXEC_PATTERN) whose changes landed after, or outside, the rescanned sets.

The same change on labelled corpora

Shipped scanner against the final scanner, static arm, MEDIUM or above. Every corpus the harness holds is shown, including those where the tuning costs recall. The rules were designed on the train/validation split and on real skills; the test split was not used to design them.

CorpusRecordsRecallFPRUnlabelled flag rate
msb-trainval development split (rules were designed on it)6,59433.14% → 31.45%2.17% → 1.05%— → —
msb-source-disjoint frozen test split (held out)1,3847.99% → 7.99%8.44% → 4.22%— → —
msb-balanced-800 overlaps the test split (137 records)80033.00% → 31.00%2.00% → 1.25%— → —
openskillrisk-corpus37431.55% → 29.95%— → —— → —
mcp-tool-poisoning5040.00% → 40.00%0.00% → 0.00%— → —
harmfulskillbench-corpus unlabelled for this purpose200— → —— → —4.00% → 3.50%
real-world-skills12,498— → —— → —3.76% → 2.49%
gitskills-sample199,998— → —— → —2.23% → 0.52%

What is not: the recall cost on train/validation is 89 malicious packages that no longer reach MEDIUM. The findings that carried them: 44 by SUPPLY_CHAIN_UNPINNED_DEPENDENCY, 19 by SOCIAL_ENG_ANTHROPIC_IMPERSONATION, 12 by PIPELINE_TAINT_FLOW, 6 by FILE_MAGIC_MISMATCH, 4 by CORRELATED_NETWORK_EXECUTION_FLOW + PIPELINE_TAINT_FLOW, 3 by CORRELATED_SENSITIVE_NETWORK_FLOW, 1 by PROMPT_INJECTION_IGNORE_INSTRUCTIONS. Most are hygiene or mention findings rather than the malicious behaviour — an unpinned dependency, a mention of Anthropic, a text label in Markdown — and the judge is the layer meant to catch these skills. Keeping a rule firing on the wrong evidence for its incidental recall would keep the false positives that come with it.

The LLM judge, with the shipped prompt, read 1,876,206 of them (563 could not be analysed and are excluded, not counted as clean; 96,519 were read with partial coverage, the prompt budget leaving a file out) and flagged 12.70% 12.65 to 12.75 at MEDIUM or above.

Its package verdicts: SAFE 80.7%, SUSPICIOUS 17.3%, MALICIOUS 2.0%. Almost all of its false positives read as findings about what a skill is for — a browser-automation skill can run JavaScript, a payments skill can move money — which is what the new prompt below addresses.

The final deterministic scanner and the judge agree on 16,321 flagged records; 23,829 are flagged only by the rules and 221,924 only by the judge (Cohen's κ 0.084). The two layers look at different things, which is why neither is ground truth for the other.

Deterministic flags alone: 2.14%. Deterministic flags plus the judge behind the OpenJev screen: 5.19%.

Which deterministic rules the judge disagrees with, after tuning

For the rules that flag the most real skills at MEDIUM or above: how many records each rule alone drives to that tier (removing the rule would clear them), and the share the judge clears. The judge is not ground truth — on the labelled split its own false-positive rate is about 18% and its recall 53% — so this ranks rules for review rather than measuring their error.

AnalyzerRuleRecordsSole driverJudgedJudge clears
correlationCORRELATED_NETWORK_EXECUTION_FLOW7,5564,3807,54943.6% 42.5 to 44.7
staticACTIVE_DYNAMIC_EXECUTION6,9685,7346,93763.8% 62.7 to 64.9
pipelinePIPELINE_TAINT_FLOW5,4382,1845,41554.0% 52.6 to 55.3
correlationCORRELATED_SENSITIVE_NETWORK_FLOW4,2373,5904,22659.7% 58.3 to 61.2
pipelineCOMPOUND_FIND_EXEC2,3271,4012,32670.2% 68.3 to 72.0
staticACTIVE_REMOTE_ACQUIRE_EXECUTE2,2361,7152,22568.4% 66.4 to 70.3
staticPROMPT_INJECTION_IGNORE_INSTRUCTIONS2,0451,3192,03839.7% 37.6 to 41.9
staticSECRET_CONNECTION_STRING1,4951,3121,49371.0% 68.6 to 73.2
staticFIND_EXEC_PATTERN1,3954741,39368.8% 66.3 to 71.2
staticYARA_jailbreak_generic1,3651,0471,35670.4% 67.9 to 72.8
staticYARA_sql_injection_generic1,1851,0491,17859.7% 56.8 to 62.4
pipelineCOMPOUND_FETCH_EXECUTE1,0303911,02853.4% 50.3 to 56.4
staticACTIVE_OS_PERSISTENCE_DIRECTIVE1,0208121,01764.0% 61.0 to 66.9
staticYARA_tool_chaining_abuse_generic94951694642.0% 38.9 to 45.1
staticYARA_autonomy_abuse_generic93280893188.2% 86.0 to 90.1
staticYARA_prompt_injection_generic87237987260.6% 57.3 to 63.7
staticPROMPT_INJECTION_CONCEALMENT85940685973.2% 70.2 to 76.1
staticYARA_command_injection_generic75147074446.2% 42.7 to 49.8
staticYARA_system_manipulation_generic68450068255.3% 51.5 to 59.0
staticYARA_credential_harvesting_generic67052066558.5% 54.7 to 62.2

The cascade over every skill

OpenJev screens each skill and the judge runs only above a threshold on its prompt_injection probability. The threshold, 0.0609, was selected on MaliciousSkillBench train/validation for the prompt this corpus was judged with: keep at least 97% of the judge's recall at the lowest false-positive rate. On real skills it sends far fewer records to the judge than it did on the labelled split, so the whole trade-off is shown.

Screen thresholdJudge callsJudge flags keptMEDIUM+ flag rate
0.00534.6%70.9%9.00%
0.0123.2%56.7%7.20%
0.0215.4%43.3%5.50%
0.03511.1%34.4%4.36%
0.04849.1%29.9%3.80%
0.06097.8%26.8%3.40%
0.15.5%20.6%2.62%
0.23.2%13.3%1.69%
0.351.7%8.3%1.05%
0.51.0%5.8%0.73%

OpenJev's score ranks the judge's flags with AUC 0.778 over 1,876,206 skills both models read. Judge alone: 12.70%.

Policy packs instead of knobs

The scan policy has hundreds of settings; most users need one decision, how much review capacity they have. Two presets sit beside the default. Every rule was scored by how many real skills it alone drives to MEDIUM or above that the judge also cleared, against how many malicious train/validation packages it alone detects; rules were demoted greedily in that order and the path was cut at two points. A demoted rule is reported at LOW, still visible but not gating. Real-skill rates below are exact on the final full scan, because a pack only lowers the severity of listed rules.

PresetRules reported at LOWReal-skill MEDIUM+ (rules)Train/val recallTrain/val FPR
balanced02.144% 2.124 to 2.16531.45%1.05%
low-noise111.930% 1.911 to 1.95031.35%1.05%
quiet191.331% 1.314 to 1.34729.78%0.15%

Use them with --policy low-noise or --policy quiet. low-noise also caps judge findings the model itself rates low-confidence; quiet additionally caps findings it labels contextual risk.

A judge prompt that separates a skill's purpose from misuse

The threat-analysis prompt now lists what is not a finding on its own — the capability the skill exists to provide, the breadth of its declared permissions, installing dependencies from a registry, requiring API keys, hypothetical injection, quality issues — and, just as explicitly, the misuse to report even when it is phrased as routine: a bundled or downloaded script the agent must run automatically, instructions to hide actions or override other instructions, and data sent where the stated purpose does not need it. It was written from real skills and MaliciousSkillBench train/validation, then scored once on the frozen test split.

Judge, MEDIUM+Train/val recallTrain/val FPRTest recallTest FPRTest precisionTest F1Real-skill flags
Shipped prompt86.2%28.2%51.6%16.1%83.1%63.7%12.80%
New prompt88.8%24.0%65.9%12.5%89.0%75.8%10.45%

Operating points on the new prompt

Each finding carries the model's own verdict (TRUE_POSITIVE or CONTEXTUAL_RISK) and confidence. Capping either below MEDIUM is an operating point, not a free win: it cuts false positives on every population and costs recall, more on test than on train/validation.

New prompt, MEDIUM+Train/val recallTrain/val FPRTest recallTest FPRReal-skill flags
No cap88.8%24.0%65.9%12.5%10.45%
Low-confidence findings at LOW86.3%16.6%62.3%10.5%7.60%
Contextual-risk findings at LOW83.8%6.9%49.6%4.2%3.00%

Gemma 4 26B-A4B via Bedrock mantle, verdict repair on (the default). Train/validation: 600 malicious and 1,338 benign MaliciousSkillBench packages. Test: the 1,384-package source-disjoint split. Real skills: 2,000 sampled uniformly from gitskills, a flag rate that bounds the false-positive rate from above.

Measurement notes

The data behind this page

Every figure on this page, and the per-rule tables behind it, is in results/large-scale.json: counts, rates, thresholds and rule identifiers only. No skill content, record identifier, finding text, prompt or model rationale is published, and the file is refused at build time if one appears.

results/SHA256SUMS holds its SHA-256, so a copy can be checked against the file this page was rendered from.

Scanner trees: shipped 5b696a1, first tuning pass f3a42f3, final 9c08673; report built at 5807c36de84b.

Reproducing it

StepScript
Score OpenJev's eight probesevals/experiments/c3_openjev_local.py
Run the judge alone over a corpusevals/runners/judge_only.py
Scan a corpus with one treeevals/runners/cross_tool_benchmark.py
Build the storeevals/experiments/f1_full_corpus_store.py
Analyse every real skillevals/experiments/f2_full_corpus_analysis.py
Check the overlay against the full scanevals/experiments/f3_overlay_check.py
Shipped against final on labelled corporaevals/experiments/f4_labelled_ab.py
The preset demotion pathevals/experiments/f5_policy_pack_path.py
Judge prompt and capsevals/experiments/a2_judge_prompt_caps.py
Select the OpenJev screenevals/experiments/c4_openjev_screen.py
Assemble and check this reportevals/publish/large_scale_report.py