x402-prompt-injection-scan / tree / scripts/benchmark.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
#!/usr/bin/env node
// Benchmark InjectScan on public, permissively licensed data. Nothing is vendored; data is fetched and cached.
//   node scripts/benchmark.js [--write bench/results.json] [--cache .bench-cache]
//
// Labeled sets (precision / recall), from the Hugging Face datasets-server API:
//   deepset/prompt-injections (Apache-2.0)            train 546 / test 116, English + German, label 1 = injection
//   jackhhao/jailbreak-classification (Apache-2.0)    train 1044 / test 262, "jailbreak" vs "benign"
// Attack-only set (recall):
//   Lakera/gandalf_ignore_instructions (MIT)          test 112, real "ignore your instructions" attacks on Gandalf
// Benign-only corpora (false-positive rate on the kind of content agents read):
//   bench/benign-wikipedia.json  300 random English Wikipedia articles (REST HTML -> Markdown, fetched at run time)
//   bench/benign-readmes.json    150 GitHub READMEs pinned to a commit (technical docs: install commands, badges, keys)
//
// Honesty notes: rules were written while looking at the deepset and jackhhao TRAIN splits and at a different sample of
// Wikipedia articles and popular READMEs. The TEST splits, the Lakera set and the two benign corpora above were not
// used for tuning; they are the held-out numbers.
import { scan, THRESHOLDS } from '../src/detect.js';
import { RULESET_VERSION } from '../src/rules.js';
import { htmlToMarkdown } from '../src/html2md.js';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';

const arg = (k, d) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : d; };
const CACHE = arg('--cache', '.bench-cache'); mkdirSync(CACHE, { recursive: true });
const UA = 'InjectScanBench/0.1 (+https://injectscan.agentgate.workers.dev)';
async function cached(key, fn) {
  const p = `${CACHE}/${key.replace(/[^a-z0-9._-]/gi, '_')}.json`;
  if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8'));
  const v = await fn(); writeFileSync(p, JSON.stringify(v)); return v;
}
async function hfRows(dataset, split) {
  return cached(`hf-${dataset}-${split}`, async () => {
    const out = [];
    for (let offset = 0; ; offset += 100) {
      const j = await (await fetch(`https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(dataset)}&config=default&split=${split}&offset=${offset}&length=100`)).json();
      out.push(...j.rows.map((x) => x.row));
      if (out.length >= j.num_rows_total || !j.rows.length) break;
    }
    return out;
  });
}
async function pool(items, n, fn) { const out = new Array(items.length); let i = 0; await Promise.all(Array.from({ length: n }, async () => { while (i < items.length) { const k = i++; out[k] = await fn(items[k]); } })); return out; }
async function wikipedia() {
  // Article HTML from the Wikimedia REST API, converted to Markdown with the same converter the url= mode uses.
  const { titles } = JSON.parse(readFileSync(new URL('../bench/benign-wikipedia.json', import.meta.url)));
  return cached('wikipedia', async () => (await pool(titles, 2, async (t) => {
    const u = `https://en.wikipedia.org/api/rest_v1/page/html/${encodeURIComponent(t.replace(/ /g, '_'))}`;
    for (let attempt = 0; attempt < 6; attempt++) {
      const r = await fetch(u, { headers: { 'user-agent': UA } });
      if (r.ok) return htmlToMarkdown(await r.text(), { baseUrl: 'https://en.wikipedia.org/wiki/' }).markdown;
      if (r.status === 404) return '';
      await new Promise((res) => setTimeout(res, 2000 * (attempt + 1)));
    }
    throw new Error(`Wikipedia fetch failed for ${t}`);
  })).filter((t) => t.length >= 200));
}
async function readmes() {
  const { repos } = JSON.parse(readFileSync(new URL('../bench/benign-readmes.json', import.meta.url)));
  return cached('readmes', async () => (await pool(repos, 6, async ({ repo, sha }) => {
    for (const f of ['README.md', 'readme.md', 'Readme.md']) { const r = await fetch(`https://raw.githubusercontent.com/${repo}/${sha}/${f}`); if (r.ok) return r.text(); }
    return '';
  })).filter((t) => t.length >= 300));
}

const metrics = (pairs, th) => {
  let tp = 0, fp = 0, tn = 0, fn = 0;
  for (const [score, y] of pairs) { const p = score >= th; if (p && y) tp++; else if (p) fp++; else if (y) fn++; else tn++; }
  const precision = tp + fp ? tp / (tp + fp) : null, recall = tp + fn ? tp / (tp + fn) : null;
  const f1 = precision != null && recall != null && precision + recall ? 2 * precision * recall / (precision + recall) : null;
  const r3 = (x) => (x == null ? null : +x.toFixed(3));
  return { n: pairs.length, tp, fp, tn, fn, precision: r3(precision), recall: r3(recall), f1: r3(f1), fpr: tn + fp ? r3(fp / (tn + fp)) : null };
};
const run = (texts) => { const t0 = performance.now(); const s = texts.map((t) => scan(t, { detail: false }).risk_score / 100); return { s, avg_ms: +((performance.now() - t0) / texts.length).toFixed(3) }; };

const results = { ruleset: RULESET_VERSION, generated_at: new Date().toISOString(), thresholds: THRESHOLDS, rows: [] };
const add = (row) => { results.rows.push(row); const f = (x) => (x == null ? '  -  ' : x.toFixed(3)); console.log(`${row.dataset.padEnd(38)} ${row.split.padEnd(6)} ${row.role.padEnd(9)} ${row.threshold.padEnd(10)} P=${f(row.precision)} R=${f(row.recall)} F1=${f(row.f1)} FPR=${f(row.fpr)} n=${row.n} tp=${row.tp} fp=${row.fp} fn=${row.fn} ${row.avg_ms}ms`); };
const both = (base, pairs, avg_ms) => { for (const [name, th] of [['flagged', THRESHOLDS.suspicious], ['malicious', THRESHOLDS.malicious]]) add({ ...base, threshold: name, ...metrics(pairs, th), avg_ms }); };

for (const [dataset, label, text] of [
  ['deepset/prompt-injections', (r) => r.label === 1, (r) => r.text],
  ['jackhhao/jailbreak-classification', (r) => r.type === 'jailbreak', (r) => r.prompt],
]) for (const split of ['train', 'test']) {
  const data = await hfRows(dataset, split); const { s, avg_ms } = run(data.map(text));
  both({ dataset, split, role: split === 'train' ? 'dev' : 'held-out' }, data.map((r, i) => [s[i], label(r)]), avg_ms);
}
{ const data = await hfRows('Lakera/gandalf_ignore_instructions', 'test'); const { s, avg_ms } = run(data.map((r) => r.text)); both({ dataset: 'Lakera/gandalf_ignore_instructions', split: 'test', role: 'held-out' }, s.map((x) => [x, true]), avg_ms); }
{ const t = await wikipedia(); const { s, avg_ms } = run(t); both({ dataset: 'benign: Wikipedia (300 random)', split: '-', role: 'held-out' }, s.map((x) => [x, false]), avg_ms); }
{ const t = await readmes(); const { s, avg_ms } = run(t); both({ dataset: 'benign: GitHub READMEs (150)', split: '-', role: 'held-out' }, s.map((x) => [x, false]), avg_ms); }

const out = arg('--write'); if (out) writeFileSync(out, JSON.stringify(results, null, 2) + '\n');

agentgate-source: code mirror on Cloudflare Pages. All repos