82 lines
5.7 KiB
JavaScript
82 lines
5.7 KiB
JavaScript
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)); }
|
|
});
|