import assert from 'node:assert/strict'; import { test } from 'node:test'; import { createServer } from 'node:http'; import { randomUUID, createHash } from 'node:crypto'; import { readFile, readdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { configuration, decodeBody, readResponse, acquire, privateDirectory, PATHS } from './chr-recursive-observation.mjs'; test('source/transport allowlist refuses arbitrary hosts, plaintext external, missing tunnel attestation and option injection', () => { const args = ['--source', '194.226.121.150', '--transport', 'https', '--output', 'tmp/acquisition-test']; assert.equal(configuration(args).endpoint, 'https://194.226.121.150:443'); for (const changes of [args.map(x => x === '194.226.121.150' ? 'example.org' : x), args.map(x => x === 'https' ? 'http' : x), args.map(x => x === 'https' ? 'ssh-loopback' : x), [...args, '--password', 'forbidden'], [...args, '--source', '104.165.244.7']]) assert.throws(() => configuration(changes)); const tunnel = configuration([...args.map(x => x === 'https' ? 'ssh-loopback' : x), '--owned-tunnel-attestation', 'verified-known-host-161.104.46.222-owned-forward']); assert.equal(tunnel.endpoint, 'http://127.0.0.1:19081'); }); test('full own fixture saves nine exact immutable captures and metadata; reflected errors never enter reports', async () => { const output = fileURLToPath(new URL('../../../tmp/chr-recursive-own-fixture-' + randomUUID(), import.meta.url)), seen = []; let reflect = false; const server = createServer((req, res) => { seen.push(req.url); res.setHeader('Content-Type', 'application/json'); if (reflect) { res.statusCode = 401; res.end('{"message":"own-fixture-secret-only"}'); return; } res.end(req.url === '/rest/system/resource' ? '[{"version":"7.26"}]\n' : '[{".id":"*1"}]\n'); }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(19081, '127.0.0.1', resolve); }); const makeConfig = destination => configuration(['--source', '194.226.121.150', '--transport', 'ssh-loopback', '--output', destination, '--owned-tunnel-attestation', 'verified-known-host-161.104.46.222-owned-forward']); try { const report = await acquire(makeConfig(output), { username: 'own-fixture', password: 'own-fixture-secret-only' }); assert.equal(report.complete, true); assert.equal(report.semanticProof, false); assert.deepEqual(seen, PATHS.map(resource => '/rest' + resource)); for (const record of report.records) { const body = await readFile(output + '/' + record.file); assert.equal(createHash('sha256').update(body).digest('hex'), record.rawSha256); assert.equal(body.length, record.bytes); assert.equal(record.version, '7.26'); } await assert.rejects(acquire(makeConfig(output), { username: 'own-fixture', password: 'own-fixture-secret-only' })); assert.equal(seen.length, 9); await assert.rejects(privateDirectory('../outside-tmp')); reflect = true; const failedOutput = output + '-failed'; const failed = await acquire(makeConfig(failedOutput), { username: 'own-fixture', password: 'own-fixture-secret-only' }); assert.equal(failed.complete, false); assert.equal(failed.records.length, 0); assert.equal(failed.failedResource, '/system/resource'); assert.equal(failed.failedHttpStatus, 401); assert.deepEqual((await readdir(failedOutput)).sort(), ['capture.claim', 'report.json']); assert(!String(await readFile(failedOutput + '/report.json')).includes('own-fixture-secret-only')); } finally { await new Promise(resolve => server.close(resolve)); } }); test('exact UTF8 and row bounds fail closed', () => { assert.equal(decodeBody(Buffer.from('[{"version":"7.26"}]')).rows.length, 1); for (const body of [Buffer.from([0xff]), Buffer.from('null'), Buffer.from('[[]]'), Buffer.from(JSON.stringify(Array.from({ length: 1001 }, () => ({}))))]) assert.throws(() => decodeBody(body)); }); test('own loopback fixture observes exact GET paths, refuses redirect/error/encoding/bytes, no retry or follow', async () => { let mode = 'normal', requests = 0; const server = createServer((req, res) => { requests++; assert.equal(req.method, 'GET'); assert.equal(req.url, '/rest/system/resource'); res.setHeader('Content-Type', 'application/json'); if (mode === 'redirect') { res.statusCode = 302; res.setHeader('Location', 'http://example.invalid'); res.end('[]'); } else if (mode === 'error') { res.statusCode = 401; res.end('{"error":"reflected-private-input"}'); } else if (mode === 'encoding') { res.setHeader('Content-Encoding', 'gzip'); res.end('[]'); } else if (mode === 'oversized') res.end(' '.repeat(1048577)); else res.end('[{"version":"7.26"}]\n'); }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(19081, '127.0.0.1', resolve); }); const budget = () => ({ requests: 0, bytes: 0, rows: 0, deadline: Date.now() + 5000 }); try { const first = await readResponse('http://127.0.0.1:19081', PATHS[0], 'Basic own-fixture', budget()); assert.equal(first.body.toString(), '[{"version":"7.26"}]\n'); for (mode of ['redirect', 'error', 'encoding', 'oversized']) await assert.rejects(readResponse('http://127.0.0.1:19081', PATHS[0], 'Basic own-fixture', budget())); assert.equal(requests, 5); assert.throws(() => readResponse('http://194.226.121.150', PATHS[0], '', budget())); assert.throws(() => readResponse('http://127.0.0.1:19081', '/system/reboot', '', budget())); assert.throws(() => readResponse('http://127.0.0.1:19081', PATHS[0], '', { ...budget(), requests: 256 })); assert.throws(() => readResponse('http://127.0.0.1:19081', PATHS[0], '', { ...budget(), deadline: 0 })); assert.equal(requests, 5); } finally { await new Promise(resolve => server.close(resolve)); } });