Files
timmy-talking-turd/tests/release-security-gate.test.js
timmy f0a9a734db
Quality gates / quality (pull_request) Canceled after 0s
feat(security): attack the RC before users do and gate approval on P0 findings (#40)
Add an executable abuse suite that drives the real server with synthetic hostile bytes while a
counting stub worker records every call that reaches the model boundary, plus the verdict rule that
turns those observations into a release decision.

- src/release-security-gate.js: nearest-rank latency percentiles, per-case pass/fail derivation that
  names the field that broke, and the rule that any failing P0 case blocks RC approval while a
  required class with no verified evidence blocks it too. Reports are redacted and fail closed.
- scripts/security_attack_suite.mjs: oversized body, HTML polyglot as JPEG, undecodable bytes,
  missing consent, non-conforming worker answer, byte-identical replay, concurrent burst, captured
  log scan, and an out-of-surface deletion case counted only against a real test file. Writes the
  report, prints the verdict, exits 1 when blocked.
- docs/RC-SECURITY-EVIDENCE.md: procedure, rule, and the current finding stated plainly.
- Demo: a real run of the suite on camera, printing the verdict it derived instead of a flattering
  one; CI regenerates the same evidence on every change.

The suite currently returns VERDICT: BLOCKED: bytes are trusted from the data-URL label, so an HTML
polyglot still reaches the worker and only fails afterwards on schema. That is a finding against
#16, not a passing check, and it is why the gate treats a failing case as a block.
2026-10-30 11:12:52 +00:00

105 lines
5.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { buildReport, containsImageBytes, evaluateCase, latencyPercentiles, redactImageBytes, releaseVerdict } from '../src/release-security-gate.js';
test('latency percentiles use nearest-rank so p95 is an observed sample, not an interpolation', () => {
const samples = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
assert.deepEqual(latencyPercentiles(samples), { count: 10, p50: 50, p95: 100, max: 100 });
});
// A case is a promise the suite makes before it runs: this input must land here, and the provider
// must (or must not) see exactly this many calls. The verdict is derived, never typed.
const polyglot = {
id: 'ingress-polyglot-html',
severity: 'P0',
expected: { status: 400, providerCalls: 0 },
observed: { status: 400, providerCalls: 0 },
};
test('a case whose observation matches its expectation passes', () => {
assert.equal(evaluateCase(polyglot).status, 'pass');
});
test('a case that reached the provider is a failure that names the field that broke', () => {
const result = evaluateCase({ ...polyglot, observed: { status: 200, providerCalls: 1 } });
assert.equal(result.status, 'fail');
assert.match(result.mismatch, /status 400 expected, observed 200/);
assert.match(result.mismatch, /providerCalls 0 expected, observed 1/);
});
test('a P0 failure blocks the release, and a P1 failure only holds it', () => {
const leaked = { ...polyglot, observed: { status: 200, providerCalls: 1 } };
const blocked = releaseVerdict({ cases: [leaked, { ...polyglot, id: 'flood', severity: 'P1', observed: { status: 200, providerCalls: 12 } }] });
assert.equal(blocked.verdict, 'BLOCKED');
assert.deepEqual(blocked.blocking.map(item => item.id), ['ingress-polyglot-html']);
assert.deepEqual(blocked.holds.map(item => item.id), ['flood']);
});
test('a suite with no failures is a GO, and counts say how many cases were proven', () => {
const verdict = releaseVerdict({ cases: [polyglot] });
assert.equal(verdict.verdict, 'GO');
assert.deepEqual(verdict.blocking, []);
assert.equal(verdict.proven, 1);
});
test('a required category that no case exercised blocks the release instead of passing by omission', () => {
const deletion = { id: 'deletion-withdrawal', severity: 'P0', surface: 'out-of-surface', evidence: 'tests/training-contribution.test.js', evidenceVerified: false };
const verdict = releaseVerdict({ cases: [polyglot, deletion] });
assert.equal(verdict.verdict, 'BLOCKED');
assert.deepEqual(verdict.unproven.map(item => item.id), ['deletion-withdrawal']);
});
test('coverage borrowed from another test file only counts once the runner verified that file exists', () => {
const verified = { id: 'deletion-withdrawal', severity: 'P0', surface: 'out-of-surface', evidence: 'tests/training-contribution.test.js', evidenceVerified: true };
const verdict = releaseVerdict({ cases: [polyglot, verified] });
assert.equal(verdict.verdict, 'GO');
assert.deepEqual(verdict.coveredElsewhere, ['deletion-withdrawal']);
});
// The report and the server log are shipped as release evidence. Neither may carry a photo.
const JPEG_DATA_URI = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAJ/9oACAEBAAA/AKp//9k=';
test('a data URI or a long base64 run is recognised as image bytes', () => {
assert.equal(containsImageBytes(`posted ${JPEG_DATA_URI} to the worker`), true);
assert.equal(containsImageBytes('chunk ' + 'A'.repeat(64) + 'yQ'.repeat(200)), true);
});
test('ordinary log lines, hex digests, and short tokens are not image bytes', () => {
assert.equal(containsImageBytes('Timmy is listening on http://127.0.0.1:4173 · vision configured · Hermes locked and ready'), false);
assert.equal(containsImageBytes('sha256 ' + 'a1b2c3d4'.repeat(8)), false);
assert.equal(containsImageBytes('case ingress-polyglot-html observed status 200'), false);
});
test('redaction removes the bytes and keeps the sentence around them honest', () => {
const redacted = redactImageBytes(`provider received ${JPEG_DATA_URI} at 12:01`);
assert.equal(containsImageBytes(redacted), false);
assert.match(redacted, /\[redacted image bytes\]/);
assert.match(redacted, /^provider received .* at 12:01$/);
});
test('the report carries the verdict, the target it ran against, and the measured load numbers', () => {
const report = buildReport({
generatedAt: '2026-10-30T10:48:00.000Z',
target: { baseUrl: 'http://127.0.0.1:4173', provider: 'stub counting worker' },
cases: [polyglot],
load: { requests: 8, errors: 0, abstentions: 8, p50: 12, p95: 40, max: 40 },
});
assert.equal(report.schema, 'timmy-rc-security-attack/1');
assert.equal(report.verdict, 'GO');
assert.equal(report.generatedAt, '2026-10-30T10:48:00.000Z');
assert.equal(report.target.provider, 'stub counting worker');
assert.equal(report.load.p95, 40);
assert.equal(containsImageBytes(JSON.stringify(report)), false);
});
test('a case that quotes a response body cannot smuggle the photo into the report', () => {
const report = buildReport({
generatedAt: '2026-10-30T10:48:00.000Z',
target: { baseUrl: 'http://127.0.0.1:4173', provider: 'stub' },
cases: [{ ...polyglot, observed: { status: 400, providerCalls: 0 }, snippet: `refused ${JPEG_DATA_URI}` }],
load: null,
});
assert.equal(containsImageBytes(JSON.stringify(report)), false);
assert.match(JSON.stringify(report), /redacted image bytes/);
});