mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-10-31 17:18:25 +09:00
test(mods): fleet adversarial probe for every plugin guard (W6)
scripts/probe-mod-guards.mjs bundles each hooks/guard.ts with esbuild and runs a 97-probe corpus through its verdict: invisible/bidi splits, nesting to 5000, wide/huge inputs, secrets in object keys, encodings, regex-DoS stress timed per call (50 ms gate), benign look-alikes. Calibrates each guard's real secret surface first so a miss is a real bypass. A fast subset runs last in smoke-all-plugins against a known-holes baseline. Findings (4 root causes across 32 guards, no ReDoS/crash/hang) are in v3/docs/validation/mod-guard-probe-2026-10.md. No plugin code changed. Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
1 parent
cd010ee76c
commit
7de1657db8
8 files changed
+1898
-1
No files matched your search
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Tests for scripts/probe-mod-guards.mjs against synthetic guards: a naive one (depth/size caps, values only, no invisible-character
|
||||
* stripping) that the probe must flag, and a screening one it must pass. Also pins the CLI contract (exit codes, baseline handling)
|
||||
* and that the corpus never carries a secret-shaped literal.
|
||||
*
|
||||
* Run via: node --test scripts/__tests__/probe-mod-guards.test.mjs
|
||||
*/
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { ALL_SECRETS, BENIGN, SECRET_TYPES, evasionProbes, stressProbes } from '../lib/guard-probe-corpus.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const CLI = join(HERE, '..', 'probe-mod-guards.mjs')
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'probe-mod-guards-test-'))
|
||||
process.on('exit', () => rmSync(tmp, { recursive: true, force: true }))
|
||||
|
||||
const SECRET_RE = String.raw`/(?:gh[pousr]_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|sk-(?:ant-)?[A-Za-z0-9_-]{24,}|xox[abprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}|Bearer\s+[A-Za-z0-9._~+/=-]{24,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|api_?key\s*[:=]\s*[A-Za-z0-9/+=_.-]{16,})/i`
|
||||
|
||||
const NAIVE = `
|
||||
const SECRET = ${SECRET_RE}
|
||||
const texts = (v: unknown, depth = 0): string[] =>
|
||||
depth > 3 ? [] : typeof v === 'string' ? [v.slice(0, 5000)] : Array.isArray(v) ? v.slice(0, 20).flatMap(x => texts(x, depth + 1)) :
|
||||
typeof v === 'object' && v !== null ? Object.values(v).slice(0, 20).flatMap(x => texts(x, depth + 1)) : []
|
||||
export function verdict(tool: string, input: unknown): string | undefined {
|
||||
if (tool !== 'memory_store') return undefined
|
||||
return texts(input).some(t => SECRET.test(t)) ? 'naive: secret' : undefined
|
||||
}
|
||||
`
|
||||
|
||||
const SCREENING = `
|
||||
const SECRET = ${SECRET_RE}
|
||||
const HIDDEN = /[\\p{Cf}\\u034f]/gu
|
||||
const MAX_NODES = 20000
|
||||
const MAX_CHARS = 8_000_000
|
||||
export function verdict(tool: string, input: unknown): string | undefined {
|
||||
if (tool !== 'memory_store') return undefined
|
||||
const stack: unknown[] = [input]
|
||||
let nodes = 0
|
||||
let chars = 0
|
||||
while (stack.length > 0) {
|
||||
const v = stack.pop()
|
||||
if (++nodes > MAX_NODES) return 'screening: too large to screen'
|
||||
if (typeof v === 'string') {
|
||||
chars += v.length
|
||||
if (chars > MAX_CHARS) return 'screening: too large to screen'
|
||||
if (SECRET.test(v.replace(HIDDEN, ''))) return 'screening: secret'
|
||||
} else if (Array.isArray(v)) {
|
||||
for (const x of v) stack.push(x)
|
||||
} else if (typeof v === 'object' && v !== null) {
|
||||
for (const k of Object.keys(v)) { stack.push(k); stack.push((v as Record<string, unknown>)[k]) }
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
`
|
||||
|
||||
function fixtures(dir, sources) {
|
||||
for (const [name, src] of Object.entries(sources)) {
|
||||
mkdirSync(join(dir, name, 'hooks'), { recursive: true })
|
||||
writeFileSync(join(dir, name, 'hooks', 'guard.ts'), src)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
const run = (...a) => spawnSync(process.execPath, [CLI, ...a], { encoding: 'utf8', timeout: 240_000 })
|
||||
|
||||
test('the corpus never carries a secret-shaped literal, only builders', () => {
|
||||
const src = ['guard-probe-corpus.mjs', 'guard-probe-worker.mjs'].map(f => readFileSync(join(HERE, '..', 'lib', f), 'utf8')).join('\n')
|
||||
assert.doesNotMatch(src, /gh[pousr]_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{24,}|xox[abprs]-[A-Za-z0-9-]{10,}|BEGIN RSA PRIVATE KEY/)
|
||||
})
|
||||
|
||||
test('every secret type builds a distinct, non-empty value and benign look-alikes are not secrets', () => {
|
||||
const built = Object.values(SECRET_TYPES).map(f => f())
|
||||
assert.equal(new Set(built).size, built.length)
|
||||
assert.ok(built.every(s => s.length > 10))
|
||||
assert.ok(ALL_SECRETS.includes(built[0]))
|
||||
const re = new RegExp(SECRET_RE.slice(1, SECRET_RE.lastIndexOf('/')), 'i')
|
||||
for (const b of BENIGN) assert.equal(re.test(typeof b === 'string' ? b : JSON.stringify(b)), false, `benign text must not look like a secret: ${String(b).slice(0, 40)}`)
|
||||
})
|
||||
|
||||
test('corpus ids are unique and cover every required class', () => {
|
||||
const ids = [...evasionProbes(1000), ...stressProbes(1000)].map(p => p.id)
|
||||
assert.equal(new Set(ids).size, ids.length)
|
||||
const classes = new Set(evasionProbes(1000).map(p => p.cls))
|
||||
for (const c of ['invisible', 'nesting', 'keys', 'json', 'encoding', 'confusable', 'huge', 'wide']) assert.ok(classes.has(c), c)
|
||||
assert.ok(ids.includes('nest-obj-5000') && ids.includes('s-a') && ids.includes('s-pem-spaces'))
|
||||
const deep = evasionProbes(1000).find(p => p.id === 'nest-obj-5000').build('x')
|
||||
let depth = 0
|
||||
for (let v = deep; typeof v === 'object'; v = v.k) depth++
|
||||
assert.equal(depth, 5000)
|
||||
assert.equal(stressProbes(1_048_576).find(p => p.id === 's-a').build().length, 1_048_576)
|
||||
})
|
||||
|
||||
test('flags the naive guard on nesting, keys, invisible characters and size; passes the screening guard', () => {
|
||||
const dir = fixtures(join(tmp, 'plugins'), { naive: NAIVE, screening: SCREENING })
|
||||
const r = run('--fast', '--plugins-dir', dir, '--format', 'json')
|
||||
assert.equal(r.status, 1, r.stderr)
|
||||
const out = JSON.parse(r.stdout)
|
||||
const by = Object.fromEntries(out.rows.map(x => [x.name, x]))
|
||||
const failed = name => new Set(by[name].results.filter(x => x.status === 'fail').map(x => x.id))
|
||||
|
||||
assert.equal(by.naive.meta.calibrated > 0, true)
|
||||
for (const id of ['nest-obj-12', 'nest-obj-5000', 'nest-arr-60', 'wide-array-250', 'key-name', 'key-name-nested-3', 'shy-split', 'bidi-isolate', 'after-25k-padding']) {
|
||||
assert.ok(failed('naive').has(id), `naive guard must fail ${id}`)
|
||||
}
|
||||
assert.ok(!failed('naive').has('plain'), 'the plain secret is caught: that is the control')
|
||||
assert.equal(by.naive.results.find(x => x.id === 'benign-lookalikes').status, 'pass')
|
||||
|
||||
assert.deepEqual([...failed('screening')], [], `screening guard failed: ${[...failed('screening')].join(', ')}`)
|
||||
const advisory = by.screening.results.filter(x => x.status === 'info').map(x => x.id)
|
||||
assert.ok(advisory.includes('base64'), 'encodings are advisory misses, not failures')
|
||||
assert.ok(out.newHoles.every(h => h.startsWith('naive:')))
|
||||
})
|
||||
|
||||
test('a throwing guard and a slow guard are failures of the stress probes', () => {
|
||||
const dir = fixtures(join(tmp, 'plugins-bad'), {
|
||||
thrower: `export function verdict(tool: string, input: unknown): string | undefined {
|
||||
if (JSON.stringify(input).length > 1000) throw new RangeError('boom')
|
||||
return tool === 'memory_store' ? undefined : undefined
|
||||
}`,
|
||||
slow: `export function verdict(tool: string, input: unknown): string | undefined {
|
||||
if (tool !== 'memory_store') return undefined
|
||||
const s = JSON.stringify(input) ?? ''
|
||||
const end = Date.now() + (s.length > 100000 ? 80 : 0)
|
||||
while (Date.now() < end) { /* busy */ }
|
||||
return undefined
|
||||
}`,
|
||||
})
|
||||
const r = run('--fast', '--plugins-dir', dir, '--format', 'json', '--budget-ms', '50')
|
||||
assert.equal(r.status, 1)
|
||||
const by = Object.fromEntries(JSON.parse(r.stdout).rows.map(x => [x.name, x]))
|
||||
const thrown = by.thrower.results.find(x => x.id === 's-a')
|
||||
assert.equal(thrown.status, 'fail')
|
||||
assert.match(thrown.note, /RangeError/)
|
||||
const slowed = by.slow.results.find(x => x.id === 's-a')
|
||||
assert.equal(slowed.status, 'fail')
|
||||
assert.ok(slowed.ms >= 50)
|
||||
})
|
||||
|
||||
test('a hung guard is killed by the watchdog and reported, not waited on forever', () => {
|
||||
const dir = fixtures(join(tmp, 'plugins-hang'), {
|
||||
hang: `export function verdict(tool: string, input: unknown): string | undefined {
|
||||
if (typeof input === 'object' && input !== null && JSON.stringify(input).length > 60000) for (;;) { /* never returns */ }
|
||||
return undefined
|
||||
}`,
|
||||
})
|
||||
const t0 = Date.now()
|
||||
const r = run('--fast', '--plugins-dir', dir, '--format', 'json', '--watchdog-ms', '1500')
|
||||
assert.equal(r.status, 1)
|
||||
assert.ok(Date.now() - t0 < 120_000)
|
||||
const row = JSON.parse(r.stdout).rows[0]
|
||||
assert.ok(row.results.some(x => x.status === 'fail' && /HANG/.test(x.note)), 'a probe must be marked HANG')
|
||||
})
|
||||
|
||||
test('a known-holes baseline makes only new holes fail, and reports holes that are now fixed', () => {
|
||||
const dir = fixtures(join(tmp, 'plugins-base'), { naive: NAIVE, screening: SCREENING })
|
||||
const base = join(tmp, 'known.json')
|
||||
const w = run('--fast', '--plugins-dir', dir, '--write-known-holes', base, '--format', 'json')
|
||||
assert.equal(w.status, 1)
|
||||
const known = JSON.parse(readFileSync(base, 'utf8'))
|
||||
assert.ok(known.naive.length > 10)
|
||||
assert.equal(known.screening, undefined)
|
||||
|
||||
const ok = run('--fast', '--plugins-dir', dir, '--known-holes', base)
|
||||
assert.equal(ok.status, 0, ok.stdout)
|
||||
assert.match(ok.stdout, /0 new/)
|
||||
|
||||
writeFileSync(base, JSON.stringify({ naive: known.naive.filter(id => id !== 'key-name'), screening: ['shy-split'] }))
|
||||
const regress = run('--fast', '--plugins-dir', dir, '--known-holes', base)
|
||||
assert.equal(regress.status, 1, 'a hole missing from the baseline is a new hole')
|
||||
assert.match(regress.stdout, /FAIL\s+key-name\b/)
|
||||
assert.match(regress.stdout, /known holes that now pass/)
|
||||
})
|
||||
|
||||
test('CLI contract: exit 2 on a bad flag, exit 3 when nothing is found', () => {
|
||||
assert.equal(run('--nope').status, 2)
|
||||
assert.equal(run('--budget-ms', '0', '--plugins-dir', tmp).status, 2)
|
||||
const empty = join(tmp, 'empty')
|
||||
mkdirSync(empty)
|
||||
assert.equal(run('--plugins-dir', empty).status, 3)
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
// Adversarial corpus for scripts/probe-mod-guards.mjs. Pure data builders: no I/O, no plugin knowledge.
|
||||
// Secrets are assembled from parts at runtime so no secret-shaped literal sits in the repo.
|
||||
|
||||
const cp = (...c) => String.fromCodePoint(...c)
|
||||
const rep = (s, n) => s.repeat(Math.max(1, Math.ceil(n / s.length))).slice(0, n)
|
||||
|
||||
export const INVISIBLE = {
|
||||
zwsp: cp(0x200b), zwnj: cp(0x200c), zwj: cp(0x200d), lrm: cp(0x200e), rlo: cp(0x202e), pdf: cp(0x202c),
|
||||
wj: cp(0x2060), bom: cp(0xfeff), shy: cp(0xad), rli: cp(0x2067), pdi: cp(0x2069), mvs: cp(0x180e), cgj: cp(0x34f),
|
||||
}
|
||||
|
||||
const A36 = 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8'
|
||||
/** Secret shapes the repo's guards are meant to catch, in the order a plugin's detector is tried. */
|
||||
export const SECRET_TYPES = {
|
||||
github: () => 'gh' + 'p_' + A36,
|
||||
aws: () => 'AK' + 'IA' + 'ABCDEFGHIJKLMNOP',
|
||||
anthropic: () => 'sk' + '-ant-' + rep('a1B2c3D4', 40),
|
||||
slack: () => 'xox' + 'b-' + '1234567890-abcdefghijkl',
|
||||
jwt: () => 'eyJ' + 'hbGciOiJIUzI1NiJ9' + '.eyJ' + 'zdWIiOiIxMjM0NTY3ODkwIn0' + '.' + 'abcdefgh12345678',
|
||||
bearer: () => 'Bear' + 'er ' + rep('Zy9x8W7v', 32),
|
||||
assign: () => 'api_' + 'key = ' + 'A1b2C3d4E5f6G7h8I9j0',
|
||||
pem: () => '-----BEGIN ' + 'RSA PRIVATE KEY-----',
|
||||
}
|
||||
export const ALL_SECRETS = Object.values(SECRET_TYPES).map(f => f()).join('\n')
|
||||
|
||||
export const BENIGN = [
|
||||
'Decided to rotate credentials quarterly; the runbook says where each token lives.',
|
||||
'pip install sk-learn is wrong; the package is scikit-learn. Version 1.2.3, commit 4f9c2a1.',
|
||||
'The password policy needs 12 characters. Bearer auth is described in section 4.',
|
||||
'ghp_ is the prefix GitHub uses; AKIA is the AWS prefix. Neither is followed by anything here.',
|
||||
'token budget: 4096, max_tokens=128, api key rotation ticket RUF-123',
|
||||
'sha256: ' + rep('0123456789abcdef', 64) + ' (a digest, not a key)',
|
||||
'id 123e4567-e89b-12d3-a456-426614174000 created by agent-7 at 2026-10-04T12:00:00Z',
|
||||
'function isKey(k) { return typeof k === "string" && k.length > 3 }\nconst token = nextToken()',
|
||||
{ title: 'Use JWT with refresh tokens', status: 'accepted', tags: ['auth', 'adr'], depth: { a: [1, 2, { b: 'c' }] } },
|
||||
['plain', 'list', 'of', 'ordinary', 'words', 42, true, null],
|
||||
]
|
||||
|
||||
const split = (s, ch, at) => s.slice(0, at) + ch + s.slice(at)
|
||||
const everyChar = (s, ch) => [...s].join(ch)
|
||||
const fullwidth = s => [...s].map(c => (c >= '!' && c <= '~' ? String.fromCodePoint(c.codePointAt(0) + 0xfee0) : c)).join('')
|
||||
const homoglyph = s => s.replace(/[aeopc]/g, c => ({ a: cp(0x430), e: cp(0x435), o: cp(0x43e), p: cp(0x440), c: cp(0x441) })[c])
|
||||
const nest = (leaf, depth, kind) => {
|
||||
let v = leaf
|
||||
for (let i = 0; i < depth; i++) v = kind === 'arr' ? [v] : kind === 'mix' ? (i % 2 ? [v] : { k: v }) : { k: v }
|
||||
return v
|
||||
}
|
||||
const pad = (n, s, where) => {
|
||||
const filler = rep('lorem ipsum dolor sit amet ', n)
|
||||
return where === 'start' ? s + ' ' + filler : where === 'end' ? filler + ' ' + s : filler.slice(0, n >> 1) + ' ' + s + ' ' + filler.slice(n >> 1)
|
||||
}
|
||||
const escFirst = s => '\\u00' + s.charCodeAt(0).toString(16) + s.slice(1)
|
||||
|
||||
/**
|
||||
* Evasion probes: the plain secret in the same slot is refused (calibration proves it), so any mutation that is NOT refused is a hole.
|
||||
* tier `must` fails the run; `advisory` is an encoding a pattern guard is not designed to decode (reported, fails only with --strict).
|
||||
*/
|
||||
export function evasionProbes(big) {
|
||||
const p = []
|
||||
const add = (id, cls, tier, build) => p.push({ id, cls, tier, kind: 'evade', build })
|
||||
add('plain', 'control', 'must', s => `deploy with ${s} now`)
|
||||
add('zwsp-prefix', 'invisible', 'must', s => split(s, INVISIBLE.zwsp, 2))
|
||||
add('zwj-mid', 'invisible', 'must', s => split(s, INVISIBLE.zwj, s.length >> 1))
|
||||
add('bom-mid', 'invisible', 'must', s => split(s, INVISIBLE.bom, s.length >> 1))
|
||||
add('shy-split', 'invisible', 'must', s => split(s, INVISIBLE.shy, 3))
|
||||
add('word-joiner', 'invisible', 'must', s => split(s, INVISIBLE.wj, 3))
|
||||
add('bidi-isolate', 'invisible', 'must', s => split(s, INVISIBLE.rli + INVISIBLE.pdi, 3))
|
||||
add('bidi-override-wrap', 'invisible', 'must', s => INVISIBLE.rlo + split(s, INVISIBLE.lrm, 4) + INVISIBLE.pdf)
|
||||
add('mongolian-vs', 'invisible', 'must', s => split(s, INVISIBLE.mvs, 3))
|
||||
add('every-char-zwsp', 'invisible', 'must', s => everyChar(s, INVISIBLE.zwsp))
|
||||
for (const kind of ['obj', 'arr', 'mix']) {
|
||||
for (const d of [3, 6, 12, 60, 5000]) add(`nest-${kind}-${d}`, 'nesting', 'must', s => nest(s, d, kind))
|
||||
}
|
||||
add('wide-array-250', 'wide', 'must', s => [...Array.from({ length: 250 }, (_, i) => `note ${i}`), s])
|
||||
add('wide-object-250', 'wide', 'must', s => ({ ...Object.fromEntries(Array.from({ length: 250 }, (_, i) => [`k${i}`, 'x'])), last: s }))
|
||||
add('wide-array-5000', 'wide', 'must', s => [...Array.from({ length: 5000 }, () => 'x'), s])
|
||||
add('key-name', 'keys', 'must', s => ({ [s]: 'value' }))
|
||||
add('key-name-nested-3', 'keys', 'must', s => nest({ [s]: 'value' }, 3, 'obj'))
|
||||
add('key-name-in-array', 'keys', 'must', s => [{ [s]: 'value' }])
|
||||
add('json-in-string', 'json', 'must', s => JSON.stringify({ token: s, note: 'x' }))
|
||||
add('json-double-encoded', 'json', 'must', s => JSON.stringify(JSON.stringify({ token: s })))
|
||||
add('json-unicode-escape', 'encoding', 'advisory', s => `{"token":"${escFirst(s)}"}`)
|
||||
add('base64', 'encoding', 'advisory', s => Buffer.from(s).toString('base64'))
|
||||
add('hex', 'encoding', 'advisory', s => Buffer.from(s).toString('hex'))
|
||||
add('url-encoded', 'encoding', 'advisory', s => '%' + s.charCodeAt(0).toString(16) + s.slice(1))
|
||||
add('fullwidth-confusable', 'confusable', 'advisory', s => fullwidth(s))
|
||||
add('cyrillic-confusable', 'confusable', 'advisory', s => homoglyph(s))
|
||||
add('huge-start', 'huge', 'must', s => pad(big, s, 'start'))
|
||||
add('huge-middle', 'huge', 'must', s => pad(big, s, 'middle'))
|
||||
add('huge-end', 'huge', 'must', s => pad(big, s, 'end'))
|
||||
add('after-25k-padding', 'huge', 'must', s => pad(25_000, s, 'end'))
|
||||
add('after-200k-padding', 'huge', 'must', s => pad(Math.min(big, 200_000), s, 'end'))
|
||||
return p
|
||||
}
|
||||
|
||||
/** Stress inputs (no secret needed): the guard must neither throw nor take > the budget. */
|
||||
export function stressProbes(big) {
|
||||
const p = []
|
||||
const add = (id, build) => p.push({ id, cls: 'stress', tier: 'must', kind: 'stress', build })
|
||||
const n = big
|
||||
add('s-a', () => 'a'.repeat(n))
|
||||
add('s-a-bang', () => 'a'.repeat(n) + '!')
|
||||
add('s-sk-near-miss', () => rep('sk' + '-' + 'a'.repeat(22) + ' ', n))
|
||||
add('s-ghp-near-miss', () => rep('gh' + 'p_' + 'a'.repeat(28) + ' ', n))
|
||||
add('s-akia-near-miss', () => rep('AK' + 'IA' + 'ABCDEFGHIJKLM ', n))
|
||||
add('s-assign-spaces', () => rep('api_' + 'key' + ' '.repeat(40), n))
|
||||
add('s-assign-long-value', () => 'pass' + 'word: ' + 'a'.repeat(n))
|
||||
add('s-bearer', () => 'Bear' + 'er ' + 'a'.repeat(n))
|
||||
add('s-pem-spaces', () => '-----BEGIN ' + 'A '.repeat(n >> 1))
|
||||
add('s-pem-upper', () => '-----BEGIN ' + 'A'.repeat(n))
|
||||
add('s-jwt-dots', () => rep('eyJ' + 'abcdefghi.', n))
|
||||
add('s-curl-repeat', () => 'curl '.repeat(n / 5))
|
||||
add('s-ignore-repeat', () => 'ignore previous '.repeat(n / 16))
|
||||
add('s-role-tags', () => '<system>'.repeat(n / 8))
|
||||
add('s-send-secrets', () => rep('send the ', n) + 'x')
|
||||
add('s-newlines', () => '\n'.repeat(n))
|
||||
add('s-spaces-x', () => ' '.repeat(n) + 'x')
|
||||
add('s-lt', () => '<'.repeat(n))
|
||||
add('s-brace', () => '{'.repeat(n))
|
||||
add('s-backslash', () => 'x:' + '\\'.repeat(n))
|
||||
add('s-zwsp', () => INVISIBLE.zwsp.repeat(n))
|
||||
add('s-zwsp-in-secret', () => rep('gh' + INVISIBLE.zwsp + 'p_' + INVISIBLE.zwsp, n))
|
||||
add('s-emoji', () => '\u{1F600}'.repeat(n >> 1))
|
||||
add('s-email', () => 'a@' + 'a.'.repeat(n >> 1))
|
||||
add('s-email-dots', () => 'a.'.repeat(n >> 1) + '@')
|
||||
add('s-url', () => 'http://' + 'a'.repeat(n))
|
||||
add('s-url-userinfo', () => 'http://' + 'a:'.repeat(n >> 1) + '@h')
|
||||
add('s-path-dots', () => '../'.repeat(n / 3))
|
||||
add('s-words', () => rep('alpha beta gamma ', n))
|
||||
add('s-eq-run', () => ('a'.repeat(200) + '=').repeat(n / 201))
|
||||
add('s-deep-obj-5000', () => nest('benign', 5000, 'obj'))
|
||||
add('s-deep-arr-5000', () => nest('benign', 5000, 'arr'))
|
||||
add('s-deep-obj-100000', () => nest('benign', 100_000, 'obj'))
|
||||
add('s-wide-array-100k', () => Array.from({ length: 100_000 }, () => 'x'))
|
||||
add('s-wide-keys-100k', () => Object.fromEntries(Array.from({ length: 100_000 }, (_, i) => ['k' + i, 'x'])))
|
||||
add('s-many-1kb-strings', () => Array.from({ length: Math.max(200, n >> 10) }, () => 'a'.repeat(1024)))
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Markdown rendering of a probe run for v3/docs/validation/mod-guard-probe-*.md. Pure: rows in, string out.
|
||||
|
||||
const count = (r, s) => r.results.filter(x => x.status === s).length
|
||||
const worstOf = r => r.results.reduce((m, x) => (x.status === 'skip' || x.ms <= m.ms ? m : x), { ms: 0, id: '-' })
|
||||
const cell = s => ({ pass: '✓', fail: '✗', info: '~', skip: '·' })[s] ?? '?'
|
||||
|
||||
export function renderReport({ rows, args, startedMs }) {
|
||||
const out = []
|
||||
const ids = [...new Set(rows.flatMap(r => r.results.map(x => x.id)))]
|
||||
const byId = id => rows.map(r => [r.name, r.results.find(x => x.id === id)]).filter(([, x]) => x)
|
||||
out.push(`Generated by \`node scripts/probe-mod-guards.mjs${args.fast ? ' --fast' : ''}\` in ${((Date.now() - startedMs) / 1000).toFixed(1)}s. Timing gate: ${args.budgetMs} ms per call (best of ${args.fast ? 1 : 3} runs).`)
|
||||
out.push('')
|
||||
out.push('## Per-plugin summary')
|
||||
out.push('')
|
||||
out.push('| plugin | secret surface found (tool[namespace].field) | pass | fail | advisory miss | skipped | worst ms (probe) |')
|
||||
out.push('|---|---|---:|---:|---:|---:|---|')
|
||||
for (const r of rows) {
|
||||
const w = worstOf(r)
|
||||
const surface = r.meta.chosen.length ? r.meta.chosen.map(c => `\`${c.at}\``).join('<br>') : r.fatal ? `**FATAL** ${r.fatal}` : 'none (policy guard)'
|
||||
out.push(`| ${r.name} | ${surface} | ${count(r, 'pass')} | ${count(r, 'fail')} | ${count(r, 'info')} | ${count(r, 'skip')} | ${w.ms.toFixed(1)} (${w.id}) |`)
|
||||
}
|
||||
out.push('')
|
||||
out.push('## Probe matrix (✓ pass · ✗ fail · ~ advisory miss · · not applicable)')
|
||||
out.push('')
|
||||
out.push('Columns are plugins in the order of the summary table above (1 = first row).')
|
||||
out.push('')
|
||||
out.push(`| probe | class | ${rows.map((_, i) => i + 1).join(' | ')} |`)
|
||||
out.push(`|---|---|${rows.map(() => ':-:').join('|')}|`)
|
||||
for (const id of ids) {
|
||||
const hit = byId(id)
|
||||
const cls = hit[0]?.[1].cls ?? ''
|
||||
const by = new Map(hit)
|
||||
out.push(`| ${id} | ${cls} | ${rows.map(r => (by.has(r.name) ? cell(by.get(r.name).status) : '·')).join(' | ')} |`)
|
||||
}
|
||||
out.push('')
|
||||
out.push('## Failures by probe')
|
||||
out.push('')
|
||||
for (const id of ids) {
|
||||
const bad = byId(id).filter(([, x]) => x.status === 'fail' || x.status === 'info')
|
||||
if (bad.length === 0) continue
|
||||
const tier = bad[0][1].status === 'info' ? 'advisory' : 'must'
|
||||
const applicable = byId(id).length
|
||||
const clean = byId(id).filter(([, x]) => x.status === 'pass').map(([n]) => n)
|
||||
const who = bad.length > applicable / 2 ? `all but ${clean.join(', ') || 'none'}` : bad.map(([n, x]) => `${n}${x.ms > args.budgetMs ? ` ${x.ms.toFixed(0)}ms` : ''}`).join(', ')
|
||||
out.push(`- **${id}** (${tier}) — ${bad.length}/${applicable}: ${who}`)
|
||||
}
|
||||
out.push('')
|
||||
return out.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Worker for scripts/probe-mod-guards.mjs: loads one bundled guard, finds the tool/input shapes it actually bites on
|
||||
// ("calibration"), then streams one result per probe to the parent. Runs in a worker thread so the parent can kill a hung regex.
|
||||
import { parentPort, workerData } from 'node:worker_threads'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { ALL_SECRETS, BENIGN, SECRET_TYPES, evasionProbes, stressProbes } from './guard-probe-corpus.mjs'
|
||||
|
||||
const { bundle, tools, servers, extra, fast, skip, budgetMs } = workerData
|
||||
const done = new Set(skip)
|
||||
const send = m => parentPort.postMessage(m)
|
||||
|
||||
const mod = await import(pathToFileURL(bundle).href)
|
||||
const verdictFn = mod.guard?.verdict
|
||||
if (typeof verdictFn !== 'function') {
|
||||
send({ type: 'fatal', error: 'guard.ts exports no verdict()' })
|
||||
process.exit(0)
|
||||
}
|
||||
const opts0 = (() => { try { return mod.options?.readOptions?.({}) ?? { guard: true } } catch { return { guard: true } } })()
|
||||
|
||||
const call = (tool, input) => {
|
||||
const stats = (() => { try { return mod.status?.newStats?.() ?? {} } catch { return {} } })()
|
||||
const t0 = performance.now()
|
||||
try {
|
||||
return { v: verdictFn(tool, input, { ...opts0 }, stats), ms: performance.now() - t0 }
|
||||
} catch (e) {
|
||||
return { err: `${e?.name ?? 'Error'}: ${String(e?.message ?? e).slice(0, 80)}`, ms: performance.now() - t0 }
|
||||
}
|
||||
}
|
||||
const refused = r => r.err === undefined && r.v !== undefined && r.v !== null && r.v !== 'pass' && r.v !== false
|
||||
|
||||
const FIELDS = ['value', 'content', 'text', 'message', 'command', 'script', 'args', 'data', 'description', 'query', 'prompt', 'body', 'url', 'title']
|
||||
const NAMESPACES = [undefined, 'adr', 'docs', 'document', 'federation', 'patterns', 'pattern', 'market-data', 'learning', 'agent', 'goals', 'trading', 'cost-tracking', 'observability', 'migrations', 'rvf', 'sparc', 'tests', 'security', 'knowledge-graph', 'workflows', 'iot-devices', 'ddd-model', 'music']
|
||||
const base = ns => ({ action: 'create', ...(ns === undefined ? {} : { namespace: ns, key: ns }) })
|
||||
const superInput = (ns, p) => ({ ...base(ns), ...Object.fromEntries(FIELDS.map(f => [f, p])), input: Object.fromEntries(FIELDS.map(f => [f, p])) })
|
||||
const carriers = ns => [
|
||||
...FIELDS.map(f => ({ field: f, make: p => ({ ...base(ns), [f]: p }) })),
|
||||
...['value', 'content', 'text', 'command'].map(f => ({ field: `input.${f}`, make: p => ({ ...base(ns), input: { [f]: p } }) })),
|
||||
]
|
||||
|
||||
const forms = (() => {
|
||||
const out = []
|
||||
for (const t of tools) {
|
||||
out.push(t, `mcp__plugin_ruflo-core_ruflo__${t}`)
|
||||
for (const s of servers) out.push(`mcp__${s}__${t}`)
|
||||
}
|
||||
return [...new Set(out)]
|
||||
})()
|
||||
|
||||
/** Every (tool, namespace, field) the guard refuses a secret on while letting benign text through. */
|
||||
function calibrate() {
|
||||
const found = []
|
||||
const pool = [...forms.map(tool => ({ tool })), ...extra.map(e => ({ tool: e.tool, fixed: e }))]
|
||||
for (const { tool, fixed } of pool) {
|
||||
for (const ns of fixed ? [fixed.namespace] : NAMESPACES) {
|
||||
if (!refused(call(tool, superInput(ns, ALL_SECRETS)))) continue
|
||||
for (const c of carriers(ns)) {
|
||||
if (!refused(call(tool, c.make(ALL_SECRETS)))) continue
|
||||
if (BENIGN.some(b => refused(call(tool, c.make(b))))) continue
|
||||
const types = Object.keys(SECRET_TYPES).filter(t => refused(call(tool, c.make(SECRET_TYPES[t]()))))
|
||||
if (types.length > 0) found.push({ tool, ns, field: c.field, types, make: c.make })
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
const pairs = calibrate()
|
||||
const bareOf = t => (t.startsWith('mcp__') ? t.slice(t.lastIndexOf('__') + 2) : t)
|
||||
const chosen = []
|
||||
for (const p of pairs) if (chosen.length < 3 && !chosen.some(c => bareOf(c.tool) === bareOf(p.tool))) chosen.push(p)
|
||||
const describe = p => `${bareOf(p.tool)}${p.ns ? `[${p.ns}]` : ''}.${p.field}`
|
||||
|
||||
// Timing/crash targets: the calibrated shapes (one field each, so a legitimate linear scan is not multiplied), else every
|
||||
// (tool, namespace) pair with the payload in every field at once.
|
||||
const timing = pairs.length > 0
|
||||
? pairs.slice(0, 24).map(p => ({ tool: p.tool, ns: p.ns, make: p.make }))
|
||||
: [...new Set(forms)].flatMap(tool => NAMESPACES.map(ns => ({ tool, ns, make: x => superInput(ns, x) })))
|
||||
send({ type: 'meta', calibrated: pairs.length, chosen: chosen.map(c => ({ at: describe(c), types: c.types })), timingTargets: timing.length })
|
||||
|
||||
const big = fast ? 64 * 1024 : 1024 * 1024
|
||||
const reps = 3
|
||||
const record = (probe, status, ms, note) => send({ type: 'result', id: probe.id, cls: probe.cls, tier: probe.tier, status, ms: Math.round(ms * 100) / 100, note })
|
||||
const begin = probe => send({ type: 'start', id: probe.id })
|
||||
|
||||
for (const probe of evasionProbes(big)) {
|
||||
if (done.has(probe.id)) continue
|
||||
begin(probe)
|
||||
if (chosen.length === 0) { record(probe, 'skip', 0, 'no secret surface found'); continue }
|
||||
const failures = []
|
||||
let worst = 0
|
||||
for (const c of chosen) {
|
||||
const payload = probe.build(SECRET_TYPES[c.types[0]]())
|
||||
const r = call(c.tool, c.make(payload))
|
||||
worst = Math.max(worst, r.ms)
|
||||
if (r.err) failures.push(`${describe(c)} threw ${r.err}`)
|
||||
else if (!refused(r)) failures.push(`${describe(c)} let ${c.types[0]} through`)
|
||||
}
|
||||
record(probe, failures.length === 0 ? 'pass' : probe.tier === 'advisory' ? 'info' : 'fail', worst, failures.join('; '))
|
||||
}
|
||||
|
||||
for (const probe of stressProbes(big)) {
|
||||
if (done.has(probe.id)) continue
|
||||
begin(probe)
|
||||
const payload = probe.build()
|
||||
let worst = 0
|
||||
const bad = []
|
||||
for (const t of timing) {
|
||||
const input = t.make(payload)
|
||||
let best = Infinity
|
||||
let err
|
||||
for (let i = 0; i < reps; i++) {
|
||||
const r = call(t.tool, input)
|
||||
if (r.err) { err = r.err; best = Math.min(best, r.ms); break }
|
||||
best = Math.min(best, r.ms)
|
||||
if (best * 2 < budgetMs) break // clearly inside the budget: re-measure only the calls that are near it
|
||||
}
|
||||
if (best > worst) worst = best
|
||||
if (err) bad.push(`${bareOf(t.tool)}${t.ns ? `[${t.ns}]` : ''} threw ${err}`)
|
||||
else if (best > budgetMs) bad.push(`${bareOf(t.tool)}${t.ns ? `[${t.ns}]` : ''} ${best.toFixed(0)}ms`)
|
||||
if (bad.length >= 3) break // three offenders prove the point; do not sweep every shape of a guard that is slow everywhere
|
||||
}
|
||||
record(probe, bad.length === 0 ? 'pass' : 'fail', worst, [...new Set(bad)].slice(0, 3).join('; '))
|
||||
}
|
||||
|
||||
// Benign look-alikes must pass on every calibrated shape.
|
||||
{
|
||||
const probe = { id: 'benign-lookalikes', cls: 'benign', tier: 'must' }
|
||||
begin(probe)
|
||||
if (chosen.length === 0) record(probe, 'skip', 0, 'no secret surface found')
|
||||
else {
|
||||
const bad = []
|
||||
let worst = 0
|
||||
for (const c of pairs.slice(0, 12)) {
|
||||
BENIGN.forEach((b, i) => {
|
||||
const r = call(c.tool, c.make(b))
|
||||
worst = Math.max(worst, r.ms)
|
||||
if (r.err || refused(r)) bad.push(`#${i} on ${describe(c)}`)
|
||||
})
|
||||
}
|
||||
record(probe, bad.length === 0 ? 'pass' : 'fail', worst, bad.slice(0, 3).join('; '))
|
||||
}
|
||||
}
|
||||
send({ type: 'done' })
|
||||
@@ -0,0 +1,886 @@
|
||||
{
|
||||
"ruflo-adr": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-agent": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-agentdb": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-ai-team": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-aidefence": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-autopilot": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-bbs-federation": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-daa": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-ddd": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-docs": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-federation": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-goals": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-graph-intelligence": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-intelligence": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-iot-cognitum": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-jujutsu": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-knowledge-graph": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-loop-workers": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-market-data": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-metaharness": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-migrations": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-music": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-neural-trader": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-observability": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-rag-memory": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-ruvector": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-ruvllm": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-rvf": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-security-audit": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-sparc": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-workflows": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
],
|
||||
"ruflo-x-gateway": [
|
||||
"shy-split",
|
||||
"bidi-isolate",
|
||||
"mongolian-vs",
|
||||
"nest-obj-6",
|
||||
"nest-obj-12",
|
||||
"nest-obj-60",
|
||||
"nest-obj-5000",
|
||||
"nest-arr-6",
|
||||
"nest-arr-12",
|
||||
"nest-arr-60",
|
||||
"nest-arr-5000",
|
||||
"nest-mix-6",
|
||||
"nest-mix-12",
|
||||
"nest-mix-60",
|
||||
"nest-mix-5000",
|
||||
"wide-array-250",
|
||||
"wide-object-250",
|
||||
"wide-array-5000",
|
||||
"key-name",
|
||||
"key-name-nested-3",
|
||||
"key-name-in-array",
|
||||
"huge-middle",
|
||||
"huge-end",
|
||||
"after-25k-padding",
|
||||
"after-200k-padding",
|
||||
"s-wide-keys-100k"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env node
|
||||
// probe-mod-guards — adversarial fleet probe for every plugins/*/hooks/guard.ts verdict function.
|
||||
//
|
||||
// Each guard is bundled with esbuild into a private temp dir, then run in a worker thread through a fixed corpus (see
|
||||
// scripts/lib/guard-probe-corpus.mjs): secrets split by invisible/bidi characters, nested 5000 levels, in object keys, 1 MB strings,
|
||||
// regex-DoS inputs (each call is timed; > --budget-ms fails), encodings, and benign look-alikes (must pass). A worker that hangs is
|
||||
// killed by a watchdog and reported as a failure of the probe it was in.
|
||||
//
|
||||
// USAGE
|
||||
// node scripts/probe-mod-guards.mjs # full corpus, table, exit 1 on any `must` failure
|
||||
// node scripts/probe-mod-guards.mjs --fast # 64 KB inputs, one timing run: the smoke-path subset
|
||||
// node scripts/probe-mod-guards.mjs --only ruflo-adr,ruflo-rvf --verbose
|
||||
// node scripts/probe-mod-guards.mjs --known-holes scripts/probe-mod-guards.known-holes.json # only NEW holes fail
|
||||
// node scripts/probe-mod-guards.mjs --write-known-holes <file> # record current holes
|
||||
// node scripts/probe-mod-guards.mjs --report v3/docs/validation/mod-guard-probe-2026-10.md # write the markdown report
|
||||
// node scripts/probe-mod-guards.mjs --strict # advisory (encoding/confusable) misses fail too
|
||||
// node scripts/probe-mod-guards.mjs --plugins-dir <dir> # probe <dir>/*/hooks/guard.ts (used by the tests)
|
||||
//
|
||||
// Exit codes: 0 ok · 1 a guard failed a `must` probe (beyond --known-holes) · 2 config error · 3 nothing to probe
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { tmpdir, cpus } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Worker } from 'node:worker_threads'
|
||||
import { renderReport } from './lib/guard-probe-report.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = dirname(HERE)
|
||||
const WORKER = join(HERE, 'lib', 'guard-probe-worker.mjs')
|
||||
|
||||
const args = (() => {
|
||||
const a = { fast: false, format: 'table', only: null, verbose: false, pluginsDir: join(REPO, 'plugins'), report: null, knownHoles: null, writeKnownHoles: null, strict: false, budgetMs: 50, watchdogMs: null, concurrency: Math.max(1, Math.min(4, cpus().length >> 1)) }
|
||||
const argv = process.argv.slice(2)
|
||||
const need = i => { if (argv[i + 1] === undefined) { console.error(`probe-mod-guards: ${argv[i]} needs a value`); process.exit(2) } return argv[i + 1] }
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const v = argv[i]
|
||||
if (v === '--fast') a.fast = true
|
||||
else if (v === '--verbose') a.verbose = true
|
||||
else if (v === '--strict') a.strict = true
|
||||
else if (v === '--format') a.format = need(i++)
|
||||
else if (v === '--only') a.only = new Set(need(i++).split(',').map(s => s.trim()).filter(Boolean))
|
||||
else if (v === '--plugins-dir') a.pluginsDir = resolve(need(i++))
|
||||
else if (v === '--report') a.report = resolve(need(i++))
|
||||
else if (v === '--known-holes') a.knownHoles = resolve(need(i++))
|
||||
else if (v === '--write-known-holes') a.writeKnownHoles = resolve(need(i++))
|
||||
else if (v === '--budget-ms') a.budgetMs = Number(need(i++))
|
||||
else if (v === '--watchdog-ms') a.watchdogMs = Number(need(i++))
|
||||
else if (v === '--concurrency') a.concurrency = Number(need(i++))
|
||||
else { console.error(`probe-mod-guards: unknown argument ${v}`); process.exit(2) }
|
||||
}
|
||||
if (!['table', 'json'].includes(a.format) || !(a.budgetMs > 0) || !(a.concurrency >= 1)) { console.error('probe-mod-guards: bad --format, --budget-ms or --concurrency'); process.exit(2) }
|
||||
a.watchdogMs ??= a.fast ? 10_000 : 30_000
|
||||
return a
|
||||
})()
|
||||
|
||||
/** esbuild from the repo, else from the main checkout when this is a worktree with no node_modules. */
|
||||
function findEsbuild() {
|
||||
if (process.env.ESBUILD) return process.env.ESBUILD
|
||||
const roots = [REPO]
|
||||
try { roots.push(dirname(resolve(REPO, execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: REPO, encoding: 'utf8' }).trim()))) } catch { /* not a git checkout */ }
|
||||
for (const r of roots) { const p = join(r, 'node_modules', '.bin', 'esbuild'); if (existsSync(p)) return p }
|
||||
console.error('probe-mod-guards: esbuild not found (set ESBUILD=/path/to/esbuild)')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
// Shapes the generic calibration cannot guess: servers a guard matches by substring, and tools matched by prefix/regex.
|
||||
const SERVERS = ['plugin_ruflo-music_cogmusic', 'plugin_ruflo-core_ruflo', 'ruflo', 'claude-flow', 'ruflo-federation', 'plugin_ruflo-chatgpt-federation_chatgpt']
|
||||
const STATIC_TOOLS = ['memory_store', 'agentdb_hierarchical-store', 'agentdb_causal-edge', 'agentdb_pattern-store', 'agentdb_batch', 'hooks_intelligence_pattern-store', 'hooks_transfer', 'hooks_worker-dispatch', 'sublinear_solve', 'channel_publish', 'workflow_create', 'federation_bbs_publish', 'browser_eval', 'browser_open', 'aidefence_learn', 'autopilot_log', 'analyze_diff', 'metaharness_flywheel', 'create_production', 'embeddings_generate', 'ruvllm_hnsw_add', 'neural_train', 'daa_knowledge_share', 'github_pr_manage', 'task_create', 'terminal_execute']
|
||||
|
||||
function toolsOf(guardSrc) {
|
||||
const out = new Set(STATIC_TOOLS)
|
||||
for (const m of guardSrc.matchAll(/['"`]([a-z][a-z0-9]*(?:[_-][a-z0-9]+)+)['"`]/g)) out.add(m[1])
|
||||
for (const m of guardSrc.matchAll(/(?:startsWith|includes)\((['"`])([a-z][a-z0-9_-]*[_-])\1\)/g)) out.add(`${m[2]}x`)
|
||||
for (const m of guardSrc.matchAll(/\^([a-z][a-z0-9]*)\[\/_-\]/g)) out.add(`${m[1]}_x`)
|
||||
for (const m of guardSrc.matchAll(/['"`]([a-z][a-z0-9]*_)['"`]/g)) out.add(`${m[1]}x`)
|
||||
return [...out]
|
||||
}
|
||||
|
||||
function discover() {
|
||||
if (!existsSync(args.pluginsDir)) return []
|
||||
return readdirSync(args.pluginsDir)
|
||||
.filter(n => existsSync(join(args.pluginsDir, n, 'hooks', 'guard.ts')) && (!args.only || args.only.has(n)))
|
||||
.sort()
|
||||
.map(name => ({ name, dir: join(args.pluginsDir, name, 'hooks') }))
|
||||
}
|
||||
|
||||
function bundle(esbuild, plugin, tmp) {
|
||||
const entry = join(tmp, `${plugin.name}.entry.ts`)
|
||||
const lines = [`export * as guard from ${JSON.stringify(join(plugin.dir, 'guard.ts'))}`]
|
||||
for (const f of ['options', 'status']) if (existsSync(join(plugin.dir, `${f}.ts`))) lines.push(`export * as ${f} from ${JSON.stringify(join(plugin.dir, `${f}.ts`))}`)
|
||||
writeFileSync(entry, lines.join('\n') + '\n')
|
||||
const out = join(tmp, `${plugin.name}.mjs`)
|
||||
execFileSync(esbuild, [entry, '--bundle', '--platform=node', '--format=esm', `--outfile=${out}`, '--log-level=error'], { stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
return out
|
||||
}
|
||||
|
||||
/** Run one plugin's probes in a worker; a hung or crashed probe becomes a failure and the worker is restarted after it. */
|
||||
async function runPlugin(plugin, bundled) {
|
||||
const guardSrc = readFileSync(join(plugin.dir, 'guard.ts'), 'utf8')
|
||||
const named = [...guardSrc.matchAll(/includes\((['"`])([a-z][^'"`]*)\1\)/g)].map(m => m[2])
|
||||
const job = { bundle: bundled, tools: toolsOf(guardSrc), servers: [...SERVERS, ...named], extra: [], fast: args.fast, budgetMs: args.budgetMs }
|
||||
const results = []
|
||||
const skip = []
|
||||
let meta = { calibrated: 0, chosen: [], timingTargets: 0 }
|
||||
for (let restarts = 0; restarts < 12; restarts++) {
|
||||
let current
|
||||
const outcome = await new Promise(res => {
|
||||
const w = new Worker(WORKER, { workerData: { ...job, skip }, resourceLimits: { maxOldGenerationSizeMb: 3072 } })
|
||||
let timer
|
||||
const arm = () => { clearTimeout(timer); timer = setTimeout(() => { void w.terminate(); res({ kind: 'hang' }) }, args.watchdogMs) }
|
||||
arm()
|
||||
w.on('message', m => {
|
||||
arm()
|
||||
if (m.type === 'start') current = m.id
|
||||
else if (m.type === 'result') { results.push(m); current = undefined }
|
||||
else if (m.type === 'meta') meta = m
|
||||
else if (m.type === 'fatal') { clearTimeout(timer); res({ kind: 'fatal', error: m.error }) }
|
||||
else if (m.type === 'done') { clearTimeout(timer); res({ kind: 'done' }) }
|
||||
})
|
||||
w.on('error', e => { clearTimeout(timer); res({ kind: 'crash', error: `${e?.name}: ${String(e?.message ?? e).slice(0, 100)}` }) })
|
||||
w.on('exit', () => { clearTimeout(timer); res({ kind: 'exit' }) })
|
||||
})
|
||||
if (outcome.kind === 'done') return { meta, results }
|
||||
if (outcome.kind === 'fatal') return { meta, results, fatal: outcome.error }
|
||||
if (current === undefined) return { meta, results, fatal: outcome.kind === 'hang' ? `worker hung > ${args.watchdogMs}ms before the first probe` : (outcome.error ?? 'worker exited early') }
|
||||
const note = outcome.kind === 'hang' ? `HANG: no answer within ${args.watchdogMs}ms (worker killed)` : `worker ${outcome.kind}: ${outcome.error ?? 'exited'}`
|
||||
results.push({ id: current, cls: current.startsWith('s-') ? 'stress' : 'evade', tier: 'must', status: 'fail', ms: args.watchdogMs, note })
|
||||
skip.push(current, ...results.map(r => r.id))
|
||||
}
|
||||
return { meta, results, fatal: 'too many worker restarts' }
|
||||
}
|
||||
|
||||
async function pool(items, n, fn) {
|
||||
const out = new Array(items.length)
|
||||
let next = 0
|
||||
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => { while (next < items.length) { const i = next++; out[i] = await fn(items[i]) } }))
|
||||
return out
|
||||
}
|
||||
|
||||
const plugins = discover()
|
||||
if (plugins.length === 0) { console.error(`probe-mod-guards: no ${args.pluginsDir}/*/hooks/guard.ts found`); process.exit(3) }
|
||||
|
||||
const esbuild = findEsbuild()
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'probe-mod-guards-'))
|
||||
const started = Date.now()
|
||||
let rows
|
||||
try {
|
||||
rows = await pool(plugins, args.concurrency, async p => {
|
||||
try { return { name: p.name, ...(await runPlugin(p, bundle(esbuild, p, tmp))) } } catch (e) {
|
||||
return { name: p.name, meta: { calibrated: 0, chosen: [], timingTargets: 0 }, results: [], fatal: `bundle failed: ${String(e?.stderr ?? e?.message ?? e).slice(0, 200)}` }
|
||||
}
|
||||
})
|
||||
} finally { rmSync(tmp, { recursive: true, force: true }) }
|
||||
|
||||
// A hole is a failing `must` probe (or any failing/info probe under --strict), identified as "<plugin>:<probe>".
|
||||
const isHole = r => r.status === 'fail' || (args.strict && r.status === 'info')
|
||||
const holes = rows.flatMap(r => r.results.filter(isHole).map(x => `${r.name}:${x.id}`))
|
||||
const known = args.knownHoles && existsSync(args.knownHoles) ? new Set(Object.entries(JSON.parse(readFileSync(args.knownHoles, 'utf8'))).flatMap(([p, ids]) => ids.map(i => `${p}:${i}`))) : new Set()
|
||||
const fresh = holes.filter(h => !known.has(h))
|
||||
const ran = (k) => { const i = k.indexOf(':'); return rows.find(r => r.name === k.slice(0, i))?.results.some(x => x.id === k.slice(i + 1) && !isHole(x)) }
|
||||
const fixed = [...known].filter(ran)
|
||||
const fatal = rows.filter(r => r.fatal)
|
||||
|
||||
if (args.writeKnownHoles) {
|
||||
const byPlugin = {}
|
||||
for (const h of holes) { const i = h.indexOf(':'); (byPlugin[h.slice(0, i)] ??= []).push(h.slice(i + 1)) }
|
||||
mkdirSync(dirname(args.writeKnownHoles), { recursive: true })
|
||||
writeFileSync(args.writeKnownHoles, JSON.stringify(byPlugin, null, 1) + '\n')
|
||||
}
|
||||
if (args.report) {
|
||||
mkdirSync(dirname(args.report), { recursive: true })
|
||||
writeFileSync(args.report, renderReport({ rows, args: { fast: args.fast, budgetMs: args.budgetMs }, startedMs: started }))
|
||||
}
|
||||
|
||||
if (args.format === 'json') {
|
||||
console.log(JSON.stringify({ rows, holes, newHoles: fresh, fixedKnownHoles: fixed, fatal: fatal.map(f => ({ plugin: f.name, error: f.fatal })) }, null, 1))
|
||||
} else {
|
||||
const pad = (s, n) => String(s).padEnd(n)
|
||||
console.log(`probe-mod-guards: ${rows.length} guards, ${args.fast ? 'fast' : 'full'} corpus, budget ${args.budgetMs}ms/call, ${((Date.now() - started) / 1000).toFixed(1)}s\n`)
|
||||
console.log(`${pad('plugin', 26)} ${pad('surface (tool.field)', 44)} ${pad('pass', 5)} ${pad('fail', 5)} ${pad('info', 5)} ${pad('skip', 5)} worst-ms`)
|
||||
for (const r of rows) {
|
||||
const c = s => r.results.filter(x => x.status === s).length
|
||||
const worst = r.results.reduce((m, x) => (x.status === 'skip' ? m : x.ms > m.ms ? x : m), { ms: 0, id: '-' })
|
||||
const surface = r.meta.chosen.length ? r.meta.chosen.map(x => x.at).join(', ') : r.fatal ? `FATAL ${r.fatal}` : 'none (policy guard / no secret surface)'
|
||||
console.log(`${pad(r.name, 26)} ${pad(surface.length > 43 ? `${surface.slice(0, 42)}…` : surface, 44)} ${pad(c('pass'), 5)} ${pad(c('fail'), 5)} ${pad(c('info'), 5)} ${pad(c('skip'), 5)} ${worst.ms.toFixed(1)} (${worst.id})`)
|
||||
for (const x of r.results) {
|
||||
if (args.verbose || (isHole(x) && !known.has(`${r.name}:${x.id}`))) console.log(` ${pad(x.status.toUpperCase(), 5)} ${pad(x.id, 24)} ${pad(x.ms.toFixed(1) + 'ms', 10)} ${(x.note ?? '').slice(0, 120)}`)
|
||||
}
|
||||
}
|
||||
const total = rows.reduce((n, r) => n + r.results.length, 0)
|
||||
console.log(`\n${total} probe runs · ${holes.length} holes (${holes.length - fresh.length} known, ${fresh.length} new) · ${fixed.length} known holes now fixed · ${fatal.length} fatal`)
|
||||
if (fixed.length) console.log(`known holes that now pass (remove from the baseline): ${fixed.slice(0, 10).join(', ')}${fixed.length > 10 ? ', …' : ''}`)
|
||||
if (fatal.length) for (const f of fatal) console.log(` FATAL ${f.name}: ${f.fatal}`)
|
||||
}
|
||||
process.exitCode = fresh.length > 0 || fatal.length > 0 ? 1 : 0 // not process.exit(): it can truncate a large JSON write to a pipe
|
||||
@@ -8,6 +8,11 @@
|
||||
// node scripts/smoke-all-plugins.mjs --format json # machine-readable
|
||||
// node scripts/smoke-all-plugins.mjs --only ruflo-agent,ruflo-cost-tracker
|
||||
// node scripts/smoke-all-plugins.mjs --skip ruflo-iot-cognitum
|
||||
// node scripts/smoke-all-plugins.mjs --skip-guard-probe # omit the final guard-probe step
|
||||
//
|
||||
// After the per-plugin smokes, `scripts/probe-mod-guards.mjs --fast` runs every hooks/guard.ts through the adversarial corpus. It
|
||||
// fails only on holes NOT listed in scripts/probe-mod-guards.known-holes.json, so a regression (or a new guard copied from a
|
||||
// capped template) fails the run while the recorded backlog does not.
|
||||
//
|
||||
// CI integration:
|
||||
// - name: All-plugin smoke contracts
|
||||
@@ -20,7 +25,7 @@
|
||||
// 3 no smoke scripts found (likely repo-layout drift — fail closed)
|
||||
|
||||
import { readdirSync, existsSync, statSync } from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -36,6 +41,7 @@ const ARGS = (() => {
|
||||
skip: new Set(),
|
||||
timeoutSec: 120, // per-plugin hard cap — prevents a hung smoke from deadlocking the run
|
||||
failFast: false, // if true, kill remaining smokes on first failure
|
||||
guardProbe: true, // final step: fast adversarial probe of every hooks/guard.ts
|
||||
};
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
const v = process.argv[i];
|
||||
@@ -43,6 +49,7 @@ const ARGS = (() => {
|
||||
else if (v === '--format') a.format = process.argv[++i];
|
||||
else if (v === '--timeout') a.timeoutSec = parseFloat(process.argv[++i]);
|
||||
else if (v === '--fail-fast') a.failFast = true;
|
||||
else if (v === '--skip-guard-probe') a.guardProbe = false;
|
||||
else if (v === '--only') {
|
||||
a.only = new Set((process.argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean));
|
||||
} else if (v === '--skip') {
|
||||
@@ -155,6 +162,27 @@ function runSmoke(plugin, abortSignal) {
|
||||
});
|
||||
}
|
||||
|
||||
/** The guard probe as one more smoke row: ok unless a guard has a hole that is not in the known-holes baseline. */
|
||||
function runGuardProbe() {
|
||||
const probe = join(SCRIPTS_DIR, 'probe-mod-guards.mjs');
|
||||
const baseline = join(SCRIPTS_DIR, 'probe-mod-guards.known-holes.json');
|
||||
const args = [probe, '--fast', '--format', 'json', '--known-holes', baseline];
|
||||
if (ARGS.only) args.push('--only', [...ARGS.only].join(','));
|
||||
const t0 = Date.now();
|
||||
const r = spawnSync(process.execPath, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, timeout: 10 * 60 * 1000 });
|
||||
const row = { name: 'guard-probe (fast)', exitCode: r.status ?? 1, ok: r.status === 0, timedOut: r.error?.code === 'ETIMEDOUT', aborted: false, terminationReason: null, passed: null, failed: null, durationMs: Date.now() - t0, failingSteps: [], stderrTail: (r.stderr || '').slice(-400) };
|
||||
try {
|
||||
const out = JSON.parse(r.stdout);
|
||||
row.passed = out.rows.reduce((n, x) => n + x.results.filter((y) => y.status === 'pass').length, 0);
|
||||
row.failed = out.newHoles.length + out.fatal.length;
|
||||
row.failingSteps = [...out.newHoles, ...out.fatal.map((f) => `${f.plugin}: ${f.error}`)].slice(0, 6).map((step) => ({ step }));
|
||||
} catch {
|
||||
if (r.status === 3) { row.ok = true; row.terminationReason = 'no guards found'; } // nothing to probe is not a failure here
|
||||
else if (!row.ok) row.terminationReason = 'probe produced no JSON';
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const plugins = discoverPlugins();
|
||||
if (plugins.length === 0) {
|
||||
@@ -195,6 +223,8 @@ async function main() {
|
||||
results = await Promise.all(pending);
|
||||
}
|
||||
|
||||
if (ARGS.guardProbe && !abortController?.signal.aborted) results.push(runGuardProbe());
|
||||
|
||||
const okCount = results.filter((r) => r.ok).length;
|
||||
const failCount = results.length - okCount;
|
||||
const totalSteps = results.reduce((s, r) => s + (r.passed || 0) + (r.failed || 0), 0);
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
# Mod guard fleet probe — 2026-10
|
||||
|
||||
Adversarial probe of every `plugins/*/hooks/guard.ts` verdict function (37 guards), run by `node scripts/probe-mod-guards.mjs`
|
||||
(full corpus; `--fast` is the subset wired into `scripts/smoke-all-plugins.mjs`). No plugin code was changed: this report records
|
||||
what the probe found. The section below the marker is generated; the **Findings** section is hand-written from it (regenerate with
|
||||
`--report <tmp>` and splice, do not overwrite this file blindly).
|
||||
|
||||
## Method
|
||||
|
||||
- Each guard is bundled with esbuild into a private temp dir and run in a worker thread; a watchdog kills a hung probe and reports it.
|
||||
- **Calibration** finds where each guard actually bites: it tries every tool name the guard source mentions (plus a static list) under
|
||||
several namespaces/fields, and keeps a (tool, namespace, field) only if a secret is refused **and** the benign corpus passes. Every
|
||||
evasion probe is therefore a mutation of a payload the same guard *does* refuse in the same slot, so a miss is a real bypass, not a
|
||||
guard that never looked there. Up to 3 distinct tools per plugin are probed.
|
||||
- Corpus (97 probes): secrets split by zero-width/bidi/soft-hyphen characters; nested 3 to 5000 levels (object, array, mixed); wide arrays
|
||||
and objects; secrets as object keys; 1 MB strings with the secret at the start, middle, end, and after 25 k / 200 k of padding;
|
||||
JSON-in-string (plain, double-encoded, `\u` escaped); base64, hex, URL-encoding; fullwidth and Cyrillic confusables; 36 stress inputs
|
||||
(1 MB near-miss secrets, `-----BEGIN` runs, `curl ` repeats, 100 000-deep nesting, 100 000-wide arrays and objects) timed per call.
|
||||
- Gate: a call over 50 ms (best of up to 3 runs) fails; a throw fails; a hang fails; benign look-alikes (10 shapes) must pass.
|
||||
- Tiers: `must` fails the run. `advisory` (base64, hex, URL-encoding, `\u`-escaped JSON, fullwidth/Cyrillic confusables) is an encoding a
|
||||
pattern guard is not designed to decode; it is reported and fails only under `--strict`.
|
||||
|
||||
## Headline numbers (full corpus, 37 guards, 2 997 probe runs)
|
||||
|
||||
| result | count |
|
||||
|---|---:|
|
||||
| pass | 1 760 |
|
||||
| fail (`must`) | 820 |
|
||||
| advisory miss | 192 |
|
||||
| skipped (no secret surface to mutate) | 225 |
|
||||
|
||||
- 32 of 37 guards have a secret surface; 5 are policy-only (`ruflo-browser`, `ruflo-business-pods`, `ruflo-chatgpt-federation`,
|
||||
`ruflo-deepseek-harness`) or observe-only (`ruflo-testgen`, never refuses). They get the stress and crash probes only (36 pass, 45 skipped
|
||||
each); their URL/path/command policy rules are **not** covered by the secret-evasion corpus.
|
||||
- **No regex-DoS, no crash, no hang.** Across all 37 guards the slowest stress call outside the wide-object case was 15.9 ms
|
||||
(`ruflo-browser`, 1 MB of zero-width spaces); no guard threw on 5000- or 100 000-deep input; no probe hung.
|
||||
- The 820 failures are 4 root causes, each present in all 32 guards that share the copied `textsOf` + `screen.ts` template.
|
||||
|
||||
## Findings
|
||||
|
||||
Reproducers run against the real `ruflo-adr` bundle (`namespace: 'adr'`, `memory_store`, value as shown; `tok` is a GitHub-token-shaped
|
||||
string built at runtime). `refused` is the correct answer; `LET THROUGH` is the hole.
|
||||
|
||||
| # | hole | reproducer (value) | result | guards |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **Depth cap fails open.** `textsOf` returns `[]` past depth 6 | `{k:{k:{k:{k:{k:{k:tok}}}}}}` (secret at depth 7); also 12, 60, 5000, arrays, mixed | LET THROUGH (3 levels: refused) | 32 |
|
||||
| 2 | **Sibling cap fails open.** `slice(0, 200)` on arrays and `Object.values` | `[...200 × 'x', tok]`; object with 250 keys, secret last; 5000-element array | LET THROUGH | 32 |
|
||||
| 3 | **Character budget fails open.** `budget.left = 20_000`, and `hasSecret` slices to 20 000 chars | `'lorem '.repeat(4200) + tok`; same at 1 MB (middle and end). Secret at the start of 1 MB is caught | LET THROUGH | 32 |
|
||||
| 4 | **Object keys are never read.** `Object.values` only | `{ [tok]: 'v' }`, the same nested 3 deep, or inside an array of objects | LET THROUGH | 28 (the four that scan keys: `ruflo-migrations`, `ruflo-music`, `ruflo-neural-trader`, `ruflo-observability`) |
|
||||
| 5 | **Invisible-character class is incomplete.** The strip set covers C0/C1 controls, U+200B-U+200F, U+2028-U+202E, U+2060-U+2064 and U+FEFF, and nothing else | the token with U+00AD (soft hyphen) inserted after its third character; with U+2067 U+2069 (bidi isolates); with U+180E (Mongolian vowel separator) | LET THROUGH (U+200B, U+200D, U+FEFF, U+2060, U+202E are all refused) | 32 |
|
||||
| 6 | **Object with 100 000 keys costs 60–184 ms.** `Object.values(input)` allocates every value before `.slice(0, 200)` | `Object.fromEntries(100 000 × ['k'+i,'x'])` | 63.6–68.3 ms on `ruflo-adr`; 60.5–184.2 ms across the 32 (worst `ruflo-bbs-federation`) | 32 |
|
||||
|
||||
Repro for 1 to 5 (about 12 lines, runs standalone after bundling `plugins/ruflo-adr/hooks/guard.ts` with esbuild):
|
||||
|
||||
```js
|
||||
import { verdict } from './adr.mjs'
|
||||
const tok = 'gh' + 'p_' + 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8'
|
||||
const call = value => verdict('mcp__plugin_ruflo-core_ruflo__memory_store', { namespace: 'adr', value }) === undefined ? 'LET THROUGH' : 'refused'
|
||||
const nest = (leaf, d) => { let v = leaf; for (let i = 0; i < d; i++) v = { k: v }; return v }
|
||||
call(tok) // refused (control)
|
||||
call(nest(tok, 3)) // refused
|
||||
call(nest(tok, 6)) // LET THROUGH (#1)
|
||||
call([...Array(200).fill('x'), tok]) // LET THROUGH (#2)
|
||||
call('lorem '.repeat(4200) + tok) // LET THROUGH (#3)
|
||||
call({ [tok]: 'v' }) // LET THROUGH (#4)
|
||||
call(tok.slice(0, 3) + String.fromCharCode(0xad) + tok.slice(3)) // LET THROUGH (#5)
|
||||
```
|
||||
|
||||
Why these matter: every cap is a fail-open. A caller (or a model told to work around the guard) pads, nests or widens the
|
||||
payload until the secret sits past the cap, and the write is allowed. The fix pattern is the same everywhere: scan iteratively with no
|
||||
depth/sibling cap, scan keys as well as values, and when a work limit is hit **refuse** (fail closed) instead of returning "nothing
|
||||
found"; strip `\p{Cf}` plus U+00AD, U+034F and U+180E rather than a hand-listed range; iterate keys with `for…in` and stop at the
|
||||
limit instead of materialising `Object.values`.
|
||||
|
||||
### Advisory (not failures; 192 misses, 6 probes × 32 guards)
|
||||
|
||||
`base64`, `hex`, `url-encoded`, `json-unicode-escape` (`"ghp_…"`), `fullwidth-confusable`, `cyrillic-confusable`. A pattern guard
|
||||
does not decode, so every guard misses these. They are only a risk when the destination decodes before use; decide per tool whether to
|
||||
add a decode pass. Run `--strict` to make them fail.
|
||||
|
||||
### What passed everywhere
|
||||
|
||||
Plain secrets, secrets in JSON-in-string and double-encoded JSON, zero-width space/joiner/non-joiner, BOM, word joiner, bidi override
|
||||
and LRM wrapping, depth ≤ 6 nesting, every stress input, and all 10 benign look-alikes (no false positives on `sk-learn`, a SHA-256
|
||||
digest, a UUID, "api key rotation ticket", prose about JWTs).
|
||||
|
||||
### Not covered
|
||||
|
||||
- The 5 policy-only guards' own rules (URL scheme, template path traversal, public channel names, destructive-command patterns).
|
||||
- Real host input shapes: calibration uses synthetic `{namespace, value}`-style carriers, not captured tool calls. A guard that reads a
|
||||
field the carrier list does not contain would show as "no secret surface" rather than a hole.
|
||||
- `ruflo-iot-cognitum`'s Bash-command path (it needs a `cognitum-iot` prefix the generic carriers do not produce); only its
|
||||
`memory_store` path is probed.
|
||||
|
||||
## Gate
|
||||
|
||||
`scripts/probe-mod-guards.known-holes.json` records the 820 current holes. `smoke-all-plugins.mjs` runs `--fast --known-holes` last, so a
|
||||
**new** hole (a regression, or a new guard copied from the capped template) fails the smoke while this backlog does not. Fixed holes
|
||||
print as "known holes that now pass"; remove them from the baseline. Fix a root cause, rerun `--write-known-holes`, commit the shrink.
|
||||
|
||||
<!-- generated below: node scripts/probe-mod-guards.mjs --report <path> -->
|
||||
|
||||
Generated by `node scripts/probe-mod-guards.mjs` in 17.4s. Timing gate: 50 ms per call (best of 3 runs).
|
||||
|
||||
## Per-plugin summary
|
||||
|
||||
| plugin | secret surface found (tool[namespace].field) | pass | fail | advisory miss | skipped | worst ms (probe) |
|
||||
|---|---|---:|---:|---:|---:|---|
|
||||
| ruflo-adr | `memory_store[adr].value`<br>`agentdb_hierarchical-store[adr].value`<br>`agentdb_causal-edge[adr].value` | 49 | 26 | 6 | 0 | 80.8 (s-wide-keys-100k) |
|
||||
| ruflo-agent | `wasm_agent_prompt.value`<br>`wasm_agent_tool.value`<br>`wasm_agent_create.value` | 49 | 26 | 6 | 0 | 77.9 (s-wide-keys-100k) |
|
||||
| ruflo-agentdb | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_causal-edge.value` | 49 | 26 | 6 | 0 | 83.7 (s-wide-keys-100k) |
|
||||
| ruflo-ai-team | `task_create.value`<br>`memory_remember.value`<br>`task_update.value` | 49 | 26 | 6 | 0 | 82.3 (s-wide-keys-100k) |
|
||||
| ruflo-aidefence | `aidefence_learn.value` | 49 | 26 | 6 | 0 | 79.6 (s-wide-keys-100k) |
|
||||
| ruflo-autopilot | `autopilot_log.value`<br>`autopilot_learn.value`<br>`autopilot_config.value` | 49 | 26 | 6 | 0 | 90.2 (s-wide-keys-100k) |
|
||||
| ruflo-bbs-federation | `federation_bbs_publish.value`<br>`federation_bbs_human_join.value` | 49 | 26 | 6 | 0 | 184.2 (s-wide-keys-100k) |
|
||||
| ruflo-browser | none (policy guard) | 36 | 0 | 0 | 45 | 15.9 (s-zwsp) |
|
||||
| ruflo-business-pods | none (policy guard) | 36 | 0 | 0 | 45 | 1.4 (s-emoji) |
|
||||
| ruflo-chatgpt-federation | none (policy guard) | 36 | 0 | 0 | 45 | 0.4 (s-assign-spaces) |
|
||||
| ruflo-daa | `daa_knowledge_share.value`<br>`daa_x.value` | 49 | 26 | 6 | 0 | 91.7 (s-wide-keys-100k) |
|
||||
| ruflo-ddd | `memory_store[ddd-model].value`<br>`agentdb_hierarchical-store[ddd-model].value` | 49 | 26 | 6 | 0 | 79.3 (s-wide-keys-100k) |
|
||||
| ruflo-deepseek-harness | none (policy guard) | 36 | 0 | 0 | 45 | 0.0 (s-pem-spaces) |
|
||||
| ruflo-docs | `memory_store[docs].value`<br>`hooks_worker-dispatch[document].value` | 49 | 26 | 6 | 0 | 85.3 (s-wide-keys-100k) |
|
||||
| ruflo-federation | `memory_store[federation].value`<br>`federation_bbs_publish.value` | 49 | 26 | 6 | 0 | 85.8 (s-wide-keys-100k) |
|
||||
| ruflo-goals | `memory_store[goals].value`<br>`agentdb_hierarchical-store[goals].value`<br>`agentdb_pattern-store[goals].value` | 49 | 26 | 6 | 0 | 84.5 (s-wide-keys-100k) |
|
||||
| ruflo-graph-intelligence | `sublinear_solve.value`<br>`sublinear_x.value` | 49 | 26 | 6 | 0 | 79.1 (s-wide-keys-100k) |
|
||||
| ruflo-intelligence | `memory_store[patterns].value`<br>`hooks_intelligence_pattern-store.value`<br>`hooks_transfer.value` | 49 | 26 | 6 | 0 | 92.4 (s-wide-keys-100k) |
|
||||
| ruflo-iot-cognitum | `memory_store[iot-devices].value` | 49 | 26 | 6 | 0 | 87.1 (s-wide-keys-100k) |
|
||||
| ruflo-jujutsu | `github_pr_manage.value` | 49 | 26 | 6 | 0 | 86.7 (s-wide-keys-100k) |
|
||||
| ruflo-knowledge-graph | `agentdb_hierarchical-store.value`<br>`agentdb_causal-edge.value`<br>`agentdb_pattern-store.value` | 49 | 26 | 6 | 0 | 90.0 (s-wide-keys-100k) |
|
||||
| ruflo-loop-workers | `hooks_worker-dispatch.value` | 49 | 26 | 6 | 0 | 88.7 (s-wide-keys-100k) |
|
||||
| ruflo-market-data | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 49 | 26 | 6 | 0 | 91.1 (s-wide-keys-100k) |
|
||||
| ruflo-metaharness | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_causal-edge.value` | 49 | 26 | 6 | 0 | 85.0 (s-wide-keys-100k) |
|
||||
| ruflo-migrations | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 52 | 23 | 6 | 0 | 98.0 (s-wide-keys-100k) |
|
||||
| ruflo-music | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 52 | 23 | 6 | 0 | 95.0 (s-wide-keys-100k) |
|
||||
| ruflo-neural-trader | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 52 | 23 | 6 | 0 | 90.6 (s-wide-keys-100k) |
|
||||
| ruflo-observability | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 52 | 23 | 6 | 0 | 87.6 (s-wide-keys-100k) |
|
||||
| ruflo-rag-memory | `memory_store.value`<br>`agentdb_hierarchical-store.value`<br>`agentdb_pattern-store.value` | 49 | 26 | 6 | 0 | 92.0 (s-wide-keys-100k) |
|
||||
| ruflo-ruvector | `memory_store.value` | 49 | 26 | 6 | 0 | 84.8 (s-wide-keys-100k) |
|
||||
| ruflo-ruvllm | `ruvllm_hnsw_add.value` | 49 | 26 | 6 | 0 | 79.6 (s-wide-keys-100k) |
|
||||
| ruflo-rvf | `memory_store.value`<br>`hooks_transfer.value` | 49 | 26 | 6 | 0 | 92.3 (s-wide-keys-100k) |
|
||||
| ruflo-security-audit | `memory_store[security].value`<br>`agentdb_hierarchical-store[security].value`<br>`agentdb_pattern-store[security].value` | 49 | 26 | 6 | 0 | 81.3 (s-wide-keys-100k) |
|
||||
| ruflo-sparc | `memory_store[sparc].value`<br>`agentdb_hierarchical-store[sparc].value`<br>`agentdb_pattern-store[sparc].value` | 49 | 26 | 6 | 0 | 77.4 (s-wide-keys-100k) |
|
||||
| ruflo-testgen | none (policy guard) | 36 | 0 | 0 | 45 | 0.3 (s-deep-arr-5000) |
|
||||
| ruflo-workflows | `memory_store[workflows].value`<br>`agentdb_hierarchical-store[workflows].value`<br>`agentdb_pattern-store[workflows].value` | 49 | 26 | 6 | 0 | 60.5 (s-wide-keys-100k) |
|
||||
| ruflo-x-gateway | `x_federation_publish.value`<br>`x_federation_channel_publish.value` | 49 | 26 | 6 | 0 | 67.8 (s-wide-keys-100k) |
|
||||
|
||||
## Probe matrix (✓ pass · ✗ fail · ~ advisory miss · · not applicable)
|
||||
|
||||
Columns are plugins in the order of the summary table above (1 = first row).
|
||||
|
||||
| probe | class | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
|
||||
|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| plain | control | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| zwsp-prefix | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| zwj-mid | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| bom-mid | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| shy-split | invisible | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| word-joiner | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| bidi-isolate | invisible | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| bidi-override-wrap | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| mongolian-vs | invisible | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| every-char-zwsp | invisible | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| nest-obj-3 | nesting | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| nest-obj-6 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-obj-12 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-obj-60 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-obj-5000 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-arr-3 | nesting | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| nest-arr-6 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-arr-12 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-arr-60 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-arr-5000 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-mix-3 | nesting | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| nest-mix-6 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-mix-12 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-mix-60 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| nest-mix-5000 | nesting | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| wide-array-250 | wide | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| wide-object-250 | wide | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| wide-array-5000 | wide | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| key-name | keys | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| key-name-nested-3 | keys | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| key-name-in-array | keys | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| json-in-string | json | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| json-double-encoded | json | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| json-unicode-escape | encoding | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| base64 | encoding | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| hex | encoding | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| url-encoded | encoding | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| fullwidth-confusable | confusable | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| cyrillic-confusable | confusable | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | · | · | ~ | ~ | · | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | · | ~ | ~ |
|
||||
| huge-start | huge | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
| huge-middle | huge | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| huge-end | huge | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| after-25k-padding | huge | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| after-200k-padding | huge | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | · | · | ✗ | ✗ | · | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | · | ✗ | ✗ |
|
||||
| s-a | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-a-bang | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-sk-near-miss | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-ghp-near-miss | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-akia-near-miss | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-assign-spaces | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-assign-long-value | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-bearer | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-pem-spaces | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-pem-upper | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-jwt-dots | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-curl-repeat | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-ignore-repeat | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-role-tags | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-send-secrets | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-newlines | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-spaces-x | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-lt | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-brace | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-backslash | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-zwsp | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-zwsp-in-secret | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-emoji | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-email | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-email-dots | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-url | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-url-userinfo | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-path-dots | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-words | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-eq-run | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-deep-obj-5000 | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-deep-arr-5000 | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-deep-obj-100000 | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-wide-array-100k | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| s-wide-keys-100k | stress | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
|
||||
| s-many-1kb-strings | stress | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| benign-lookalikes | benign | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | ✓ |
|
||||
|
||||
## Failures by probe
|
||||
|
||||
- **shy-split** (must) — 32/37: all but none
|
||||
- **bidi-isolate** (must) — 32/37: all but none
|
||||
- **mongolian-vs** (must) — 32/37: all but none
|
||||
- **nest-obj-6** (must) — 32/37: all but none
|
||||
- **nest-obj-12** (must) — 32/37: all but none
|
||||
- **nest-obj-60** (must) — 32/37: all but none
|
||||
- **nest-obj-5000** (must) — 32/37: all but none
|
||||
- **nest-arr-6** (must) — 32/37: all but none
|
||||
- **nest-arr-12** (must) — 32/37: all but none
|
||||
- **nest-arr-60** (must) — 32/37: all but none
|
||||
- **nest-arr-5000** (must) — 32/37: all but none
|
||||
- **nest-mix-6** (must) — 32/37: all but none
|
||||
- **nest-mix-12** (must) — 32/37: all but none
|
||||
- **nest-mix-60** (must) — 32/37: all but none
|
||||
- **nest-mix-5000** (must) — 32/37: all but none
|
||||
- **wide-array-250** (must) — 32/37: all but none
|
||||
- **wide-object-250** (must) — 32/37: all but none
|
||||
- **wide-array-5000** (must) — 32/37: all but none
|
||||
- **key-name** (must) — 28/37: all but ruflo-migrations, ruflo-music, ruflo-neural-trader, ruflo-observability
|
||||
- **key-name-nested-3** (must) — 28/37: all but ruflo-migrations, ruflo-music, ruflo-neural-trader, ruflo-observability
|
||||
- **key-name-in-array** (must) — 28/37: all but ruflo-migrations, ruflo-music, ruflo-neural-trader, ruflo-observability
|
||||
- **json-unicode-escape** (advisory) — 32/37: all but none
|
||||
- **base64** (advisory) — 32/37: all but none
|
||||
- **hex** (advisory) — 32/37: all but none
|
||||
- **url-encoded** (advisory) — 32/37: all but none
|
||||
- **fullwidth-confusable** (advisory) — 32/37: all but none
|
||||
- **cyrillic-confusable** (advisory) — 32/37: all but none
|
||||
- **huge-middle** (must) — 32/37: all but none
|
||||
- **huge-end** (must) — 32/37: all but none
|
||||
- **after-25k-padding** (must) — 32/37: all but none
|
||||
- **after-200k-padding** (must) — 32/37: all but none
|
||||
- **s-wide-keys-100k** (must) — 32/37: all but ruflo-browser, ruflo-business-pods, ruflo-chatgpt-federation, ruflo-deepseek-harness, ruflo-testgen
|
||||
Reference in new issue
Block a user