Files
MikrotikManager/backend/scripts/acceptance/chr-recursive-observation.test.mjs
T

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)); }
});