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