Files
safetynet/tests/broker-package-contract.test.mjs
T

86 lines
5.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtemp,mkdir,writeFile,rm,symlink,open} from 'node:fs/promises';
import {writeFileSync,utimesSync} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {buildBrokerPackageManifest,verifyBrokerPackage} from '../scripts/broker-package-contract.mjs';
const release={version:'0.1.0',protocol:1,entrypoint:'broker.exe'};
async function fixture(t){
const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-broker-contract-'));
t.after(()=>rm(root,{recursive:true,force:true}));
await mkdir(path.join(root,'runtime'));
// Synthetic bytes deliberately are not a broker executable or launch proof.
await writeFile(path.join(root,'broker.exe'),'synthetic packaged executable');
await writeFile(path.join(root,'runtime','scanner.dll'),'synthetic scanner');
const manifest=await buildBrokerPackageManifest(root,release);
return {root,manifest};
}
test('complete bundled broker inventory hashes and verifies without external runtime',async t=>{
const {root,manifest}=await fixture(t);
assert.equal(manifest.files.length,2);
assert.equal((await verifyBrokerPackage(root,manifest,release)).files,2);
});
for(const operation of ['missing','tampered','extra'])test(`rejects ${operation} broker payload`,async t=>{
const {root,manifest}=await fixture(t);
if(operation==='missing')await rm(path.join(root,'broker.exe'));
if(operation==='tampered')await writeFile(path.join(root,'runtime','scanner.dll'),'replacement');
if(operation==='extra')await writeFile(path.join(root,'runtime','injected.dll'),'injection');
await assert.rejects(verifyBrokerPackage(root,manifest,release),{code:operation==='tampered'?'broker_package_integrity_mismatch':'broker_package_inventory_mismatch'});
});
test('runtime protocol and version must match integrity-protected expectations',async t=>{
const {root,manifest}=await fixture(t);
for(const expected of [{version:'0.2.0',protocol:1},{version:'0.1.0',protocol:2}])
await assert.rejects(verifyBrokerPackage(root,manifest,expected),{code:'broker_package_incompatible'});
});
test('rejects ambiguous paths and manifests before package traversal',async t=>{
const {root,manifest}=await fixture(t);
for(const unsafe of ['../broker.exe','C:/broker.exe','runtime\\scanner.dll','CON.exe','broker.exe:stream','./broker.exe']){
const changed=structuredClone(manifest);changed.files[0].path=unsafe;
await assert.rejects(verifyBrokerPackage(root,changed,release),{code:'broker_manifest_invalid'});
}
const duplicate=structuredClone(manifest);duplicate.files.push({...duplicate.files[0],path:duplicate.files[0].path.toUpperCase()});
await assert.rejects(verifyBrokerPackage(root,duplicate,release),{code:'broker_manifest_invalid'});
});
test('rejects absent entrypoint and malformed hashes',async t=>{
const {root,manifest}=await fixture(t);
await assert.rejects(verifyBrokerPackage(root,{...manifest,entrypoint:'missing.exe'},release),{code:'broker_manifest_invalid'});
const changed=structuredClone(manifest);changed.files[0].sha256='invalid';
await assert.rejects(verifyBrokerPackage(root,changed,release),{code:'broker_manifest_invalid'});
});
test('directory junctions cannot redirect broker payload outside installation',async t=>{
const {root,manifest}=await fixture(t);
const target=path.join(root,'runtime'),alias=path.join(root,'linked');
await symlink(target,alias,process.platform==='win32'?'junction':'dir');
await assert.rejects(verifyBrokerPackage(root,manifest,release),{code:'broker_path_unsafe'});
});
test('unavailable package errors contain no filesystem or raw exception content',async()=>{
await assert.rejects(verifyBrokerPackage(path.join(os.tmpdir(),'absent-sensitive-company-path'),{...release,format:'safetynet-broker-package-v1',files:[{path:'broker.exe',bytes:0,sha256:'0'.repeat(64)}]},release),error=>error.code==='broker_package_unavailable'&&error.message==='broker_package_unavailable');
});
test('same-size mutation during actual read rejects with bounded fixed invariant facts',async t=>{
const {root,manifest}=await fixture(t),target=path.join(root,'broker.exe');
const probe=await open(target,'r'),prototype=Object.getPrototypeOf(probe);
const original=prototype.createReadStream;await probe.close();
let mutated=false;
t.mock.method(prototype,'createReadStream',function(options){
const stream=original.call(this,options);
stream.once('end',()=>{
if(mutated)return;mutated=true;
writeFileSync(target,'X'.repeat(manifest.files.find(file=>file.path==='broker.exe').bytes));
const future=new Date(Date.now()+5000);utimesSync(target,future,future);
});
return stream;
});
await assert.rejects(verifyBrokerPackage(root,manifest,release),error=>{
assert.equal(error.code,'broker_package_changed');assert.equal(error.message,error.code);
assert.ok(error.failureFacts.changed.includes('modification-time'));
assert.ok(error.failureFacts.changed.length<=5);
assert.ok(error.failureFacts.changed.every(value=>['size-before-read','size-after-read','modification-time','change-time','file-identity'].includes(value)));
assert.ok(Object.isFrozen(error.failureFacts)&&Object.isFrozen(error.failureFacts.changed));
assert.ok(!JSON.stringify(error.failureFacts).includes(root));return true;
});
assert.equal(mutated,true);
});