import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {mkdtemp,mkdir,writeFile,readFile,cp,rm} from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import {brokerResourceConfiguration,prepareBrokerResources,verifyPackagedBroker,embeddedBrokerManifestReader,snapshotScannerResources,verifyScannerResources} from '../scripts/package-broker-resources.mjs'; import {buildBrokerPackageManifest} from '../scripts/broker-package-contract.mjs'; import {BROKER_PAYLOAD} from '../scripts/stage-broker-payload.mjs'; const require=createRequire(import.meta.url),asar=require('@electron/asar'); const {getFileMatchers,copyFiles}=require('app-builder-lib/out/fileMatcher.js'); const build={extraResources:[{from:'apps/inspector',to:'inspector'},{from:'.runtime/windows/broker-payload',to:'broker'}],files:['apps/browser/**/*',{from:'.runtime/windows/broker-payload.manifest.json',to:'apps/browser/broker-package-manifest.json'}]}; async function fixture(t){const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-package-resources-'));t.after(()=>rm(root,{recursive:true,force:true}));return root;} test('declared single broker and protected ASAR manifest mapping is mandatory',()=>{ const payloadRoot=path.resolve('.runtime/windows/broker-payload-fixture'),manifestPath=payloadRoot+'.manifest.json'; const configured=brokerResourceConfiguration(build,{payloadRoot,manifestPath}); assert.equal(configured.extraResources[1].from,payloadRoot);assert.equal(configured.files[1].from,manifestPath);assert.equal(configured.files[1].to,'apps/browser/broker-package-manifest.json'); for(const bad of [{...build,extraResources:build.extraResources.slice(0,1)},{...build,extraResources:[...build.extraResources,build.extraResources[1]]},{...build,extraResources:[...build.extraResources,{from:'unexpected',to:'BROKER'}]},{...build,extraResources:[...build.extraResources,{from:'unexpected',to:'broker/injected.dll'}]},{...build,files:[...build.files,build.files[1]]},{...build,files:build.files.slice(0,1)}])assert.throws(()=>brokerResourceConfiguration(bad,{payloadRoot,manifestPath}),{code:'broker_package_configuration_missing'}); assert.equal(build.extraResources[1].from,'.runtime/windows/broker-payload'); }); test('actual ASAR reader verifies embedded manifest and rejects tampered external broker bytes',async t=>{ const root=await fixture(t),resources=path.join(root,'resources'),broker=path.join(resources,'broker'),app=path.join(root,'app'); await mkdir(broker,{recursive:true});await mkdir(path.join(app,'apps/browser'),{recursive:true}); await writeFile(path.join(broker,'node.exe'),'synthetic payload, not executable proof'); const manifest=await buildBrokerPackageManifest(broker,BROKER_PAYLOAD); const manifestPath=path.join(root,'broker.manifest.json');await writeFile(manifestPath,JSON.stringify(manifest)); const configured=brokerResourceConfiguration(build,{payloadRoot:broker,manifestPath}); const matchers=getFileMatchers(configured,'files',app,{defaultSrc:root,globalOutDir:path.join(root,'output'),macroExpander:value=>value,customBuildOptions:{}}); const manifestMatcher=matchers.find(matcher=>matcher.from===manifestPath); assert.ok(manifestMatcher);assert.equal(manifestMatcher.to,path.join(app,'apps/browser/broker-package-manifest.json')); await copyFiles([manifestMatcher],null,false); const archive=path.join(resources,'app.asar');await asar.createPackage(app,archive); await verifyPackagedBroker({resources,expectedManifest:manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)}); await writeFile(path.join(broker,'node.exe'),'tampered'); await assert.rejects(verifyPackagedBroker({resources,expectedManifest:manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)}),{code:'broker_package_integrity_mismatch'}); }); test('adjacent modified metadata cannot replace expected protected manifest',async()=>{ const expected={format:'reviewed'},changed={format:'attacker-adjacent'}; await assert.rejects(verifyPackagedBroker({resources:path.resolve('resources'),expectedManifest:expected,readEmbeddedManifest:async()=>Buffer.from(JSON.stringify(changed))}),{code:'broker_package_manifest_mismatch'}); await assert.rejects(verifyPackagedBroker({resources:path.resolve('resources'),expectedManifest:expected}),{code:'broker_package_manifest_reader_required'}); }); test('scanner resource inventory verifies copied bytes and rejects missing altered and extra files',async t=>{ const root=await fixture(t),resources=path.join(root,'output'); for(const directory of ['apps/inspector','.runtime/windows/python','.runtime/windows/semantic']){await mkdir(path.join(root,directory),{recursive:true});await writeFile(path.join(root,directory,'payload.dat'),'synthetic '+directory);} await mkdir(path.join(root,'apps/inspector/__pycache__'));await writeFile(path.join(root,'apps/inspector/__pycache__/ignored.pyc'),'not shipped'); const expected=await snapshotScannerResources(root);assert.equal(expected.length,3); for(const [from,to] of [['apps/inspector','inspector'],['.runtime/windows/python','python'],['.runtime/windows/semantic','semantic']]){await mkdir(path.join(resources,to),{recursive:true});await cp(path.join(root,from,'payload.dat'),path.join(resources,to,'payload.dat'));} await verifyScannerResources(resources,expected); await writeFile(path.join(resources,'python/payload.dat'),'changed');await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'}); await cp(path.join(root,'.runtime/windows/python/payload.dat'),path.join(resources,'python/payload.dat')); await writeFile(path.join(resources,'inspector/injected.dll'),'extra');await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'}); await rm(path.join(resources,'inspector/injected.dll'));await rm(path.join(resources,'semantic/payload.dat'));await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'}); assert.equal((await readFile(path.join(root,'apps/inspector/payload.dat'))).toString(),'synthetic apps/inspector'); }); test('real prepared standalone payload keeps source identities pinned through subsequent packaging',{ skip:process.platform!=='win32'||process.env.SAFETYNET_BROKER_PACKAGE_PREPARE!=='1',timeout:60000 },async t=>{ const root=await fixture(t),repository=path.resolve('.'); // Explicit integration gate copies reviewed real sources and the pinned // official runtime. Zod is fetched and integrity checked against the lock. for(const directory of ['apps/network-broker','packages/core'])await cp(path.join(repository,directory),path.join(root,directory),{recursive:true}); await mkdir(path.join(root,'apps/browser'),{recursive:true}); for(const name of ['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs'])await cp(path.join(repository,'apps/browser',name),path.join(root,'apps/browser',name)); await cp(path.join(repository,'package-lock.json'),path.join(root,'package-lock.json')); await cp(path.join(repository,'.runtime/windows/broker'),path.join(root,'.runtime/windows/broker'),{recursive:true}); const prepared=await prepareBrokerResources({root,build}); assert.ok(prepared.manifest.files.some(file=>file.path==='apps/network-broker/audit-gate.cjs')); assert.ok(prepared.manifest.files.some(file=>file.path==='node_modules/zod/LICENSE')); assert.ok(prepared.sourceSnapshot.length>20);await prepared.verifySources(); const changed=path.join(root,'apps/network-broker/policy-authority.cjs');await writeFile(changed,(await readFile(changed))+'\n// synthetic post-stage change\n'); await assert.rejects(prepared.verifySources(),{code:'broker_stage_source_changed'}); });