73 lines
7.8 KiB
JavaScript
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'});
|
|
});
|