Files
safetynet/tests/broker-package-resources.test.mjs

73 lines
7.8 KiB
JavaScript

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