Files
gstack/lib/cso/verifier.ts
T
garrytan d93d61f7ba style(cso): format lib/cso TypeScript with pinned Prettier
Mechanical reformat only. Minified transpile output is byte-identical for
21 of 22 files; witness.ts differs only in three regex flag orders
(/mi -> /im), which JavaScript canonicalizes. Source-text assertions over
lib/cso now compare whitespace-insensitively with the same tokens.
2026-09-29 14:22:48 +00:00

162 lines
5.0 KiB
TypeScript

#!/usr/bin/env bun
import * as fs from 'node:fs';
import { connect } from 'node:net';
import { HttpAssertion, VerificationObservation, object } from './contracts';
interface Config {
phase: 'before' | 'after';
port: number;
legitimate: HttpAssertion[];
security: HttpAssertion;
}
function matches(status: number, body: string, oracle: HttpAssertion['expected']): boolean {
return (
status === oracle.status &&
(oracle.includes === undefined || body.includes(oracle.includes)) &&
(oracle.excludes === undefined || !body.includes(oracle.excludes))
);
}
export async function boundedResponseBody(response: Response, limit = 65536): Promise<string> {
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('invalid response limit');
const declared = response.headers.get('content-length');
if (declared !== null && (/^\d+$/.test(declared) ? Number(declared) > limit : true)) {
await response.body?.cancel();
throw new Error('response too large');
}
if (!response.body) return '';
const reader = response.body.getReader(),
chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const next = await reader.read();
if (next.done) break;
if (!next.value) continue;
total += next.value.byteLength;
if (total > limit) {
await reader.cancel();
throw new Error('response too large');
}
chunks.push(next.value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
async function request(a: HttpAssertion, port: number): Promise<{ status: number; body: string }> {
const controller = new AbortController(),
timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`http://127.0.0.1:${port}${a.path}`, {
method: a.method,
headers: a.headers,
body: ['GET'].includes(a.method) ? undefined : a.body,
redirect: 'manual',
signal: controller.signal,
});
return { status: response.status, body: await boundedResponseBody(response) };
} finally {
clearTimeout(timer);
}
}
async function ready(port: number): Promise<boolean> {
return await new Promise((resolve) => {
const socket = connect({ host: '127.0.0.1', port }),
done = (value: boolean) => {
socket.removeAllListeners();
socket.destroy();
resolve(value);
},
timer = setTimeout(() => done(false), 500);
socket.once('connect', () => {
clearTimeout(timer);
done(true);
});
socket.once('error', () => {
clearTimeout(timer);
done(false);
});
});
}
async function main() {
const file = process.argv[2];
if (!file || !file.startsWith('/policy/')) throw new Error('trusted policy path required');
const raw = fs.readFileSync(file, 'utf8');
if (Buffer.byteLength(raw) > 1024 * 1024) throw new Error('policy too large');
const v = object(JSON.parse(raw), 'verifier policy') as any;
if (
!['before', 'after'].includes(v.phase) ||
!Number.isInteger(v.port) ||
v.port < 1024 ||
v.port > 65535 ||
!Array.isArray(v.legitimate) ||
!v.security
)
throw new Error('invalid verifier policy');
const config = v as Config;
let booted = false;
for (let attempt = 0; attempt < 60; attempt++) {
if (await ready(config.port)) {
booted = true;
break;
}
await Bun.sleep(250);
}
let legitimate = false,
security: VerificationObservation['security'] = 'inconclusive',
summary = 'application did not answer a legitimate control';
if (booted) {
try {
legitimate = (
await Promise.all(
config.legitimate.map(async (a) => {
const r = await request(a, config.port);
return matches(r.status, r.body, a.expected);
}),
)
).every(Boolean);
const r = await request(config.security, config.port),
fixed = matches(r.status, r.body, config.security.expected),
vulnerable = matches(r.status, r.body, config.security.vulnerable!);
security =
config.phase === 'before'
? vulnerable && !fixed
? 'intended_failure'
: fixed && !vulnerable
? 'pass'
: 'inconclusive'
: fixed && !vulnerable
? 'pass'
: 'inconclusive';
summary = `boot=true legitimate=${legitimate} security=${security}`;
} catch {
summary = 'bounded verifier request failed';
}
}
process.stdout.write(
JSON.stringify({ booted, legitimate, security, existingTests: false, output: summary, inputHash: '' }) +
'\n',
);
}
if (import.meta.main)
main().catch(() => {
process.stdout.write(
JSON.stringify({
booted: false,
legitimate: false,
security: 'inconclusive',
existingTests: false,
output: 'verifier setup failed',
inputHash: '',
}) + '\n',
);
process.exitCode = 1;
});