27 lines
5.5 KiB
JavaScript
27 lines
5.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|||
|
|
import {mkdtemp,readFile,writeFile,copyFile,mkdir,readdir,rm,access} from 'node:fs/promises';
|
||
|
|
import path from 'node:path';
|
||
|
|
import {spawn} from 'node:child_process';
|
||
|
|
import {verifyPackagedFuses} from './package-fuses.mjs';
|
||
|
|
const root=path.resolve('dist/win-unpacked'),executable=path.join(root,'SafetyNet.exe');
|
||
|
|
let currentStage='initialization';
|
||
|
|
function run(stage,file,args,extra={}){currentStage=stage;console.log(`Package fuse proof: ${stage}`);return new Promise((resolve,reject)=>{const env={...process.env,...extra};delete env.NODE_OPTIONS;if(extra.NODE_OPTIONS)env.NODE_OPTIONS=extra.NODE_OPTIONS;delete env.ELECTRON_RUN_AS_NODE;if(extra.ELECTRON_RUN_AS_NODE)env.ELECTRON_RUN_AS_NODE=extra.ELECTRON_RUN_AS_NODE;const child=spawn(file,args,{env,windowsHide:true,stdio:['ignore','pipe','pipe']});let output='',timedOut=false;const timer=setTimeout(()=>{timedOut=true;child.kill();},15000);child.on('error',error=>{clearTimeout(timer);reject(error);});const append=b=>{output=(output+b).slice(-16384);};child.stdout.on('data',append);child.stderr.on('data',append);child.on('close',async code=>{clearTimeout(timer);const diagnostic={stage,code,timedOut,output};await writeFile('artifacts/package-fuses-last-stage.json',JSON.stringify(diagnostic,null,2));if(timedOut)reject(Error(`Finite package fuse proof timed out during ${stage}; bounded diagnostic persisted`));else resolve({code,output});});});}
|
||
|
|
|
||
|
|
const fixtureRoot=path.resolve('artifacts/package-fuse-fixtures');await mkdir(fixtureRoot,{recursive:true});const directory=await mkdtemp(path.join(fixtureRoot,'case-'));
|
||
|
|
try{
|
||
|
|
currentStage='fuses-and-integrity';const fuses=await verifyPackagedFuses(executable,path.join(root,'resources/app.asar'));
|
||
|
|
const args=[`--agent-connection=${path.join(directory,'missing-connection.json')}`,'--agent-action={"action":"sessions","args":{}}'];
|
||
|
|
const baseline=await run('baseline-cli',executable,args);assert.equal(baseline.code,1);assert.match(baseline.output,/Local agent action failed/);
|
||
|
|
const nodeMode=await run('node-mode',executable,args,{ELECTRON_RUN_AS_NODE:'1'});assert.equal(nodeMode.code,1);assert.match(nodeMode.output,/Local agent action failed/,'RUN_AS_NODE altered normal entrypoint');
|
||
|
|
const marker=path.join(directory,'node-options-ran'),preload=path.join(directory,'injection.cjs');await writeFile(preload,`require('node:fs').writeFileSync(${JSON.stringify(marker)},'injected')`);
|
||
|
|
const injected=await run('node-options-injection',executable,args,{NODE_OPTIONS:`--require=${JSON.stringify(preload)}`});assert.equal(injected.code,1);assert.match(injected.output,/Local agent action failed/);await assert.rejects(access(marker),{code:'ENOENT'});
|
||
|
|
const inspector=await run('cli-inspector',executable,[...args,'--inspect=127.0.0.1:0']);assert.notEqual(inspector.code,0);assert.doesNotMatch(inspector.output,/Debugger listening|DevTools listening/);
|
||
|
|
for(const flag of ['--ignore-certificate-errors','--ignore-certificate-errors-spki-list=synthetic-fixture','--allow-insecure-localhost','--disable-features=ConnectionAllowlists','-disable-features=ConnectionAllowlists','/disable-features=ConnectionAllowlists','/DISABLE-FEATURES=ConnectionAllowlists',' --disable-features=ConnectionAllowlists ','\u0085/disable-features=ConnectionAllowlists\u0085','--disable-blink-features=WebTransport','--enable-experimental-web-platform-features']){const rejected=await run('unsafe-startup-'+flag.split('=')[0].slice(2),executable,[...args,flag]);assert.equal(rejected.code,1);assert.match(rejected.output,/Managed browser startup denied/);assert.doesNotMatch(rejected.output,/Local agent action failed|Debugger listening|DevTools listening/);}
|
||
|
|
const copied=path.join(directory,'copied');await mkdir(path.join(copied,'resources'),{recursive:true});for(const entry of await readdir(root,{withFileTypes:true}))if(entry.isFile())await copyFile(path.join(root,entry.name),path.join(copied,entry.name));
|
||
|
|
const archive=await readFile(path.join(root,'resources/app.asar'));const original=Buffer.from(archive);const index=archive.indexOf(Buffer.from('apps'));assert.ok(index>16&&index<archive.readUInt32LE(12)+16,'No safe header tamper fixture offset');archive[index]^=1;await writeFile(path.join(copied,'resources/app.asar'),archive);
|
||
|
|
const tampered=await run('tampered-asar',path.join(copied,'SafetyNet.exe'),args);assert.notEqual(tampered.code,0);assert.match(tampered.output,/integrity|hash.*mismatch/i,'Tampered archive did not fail with integrity evidence');assert.doesNotMatch(tampered.output,/Local agent action failed/,'Tampered application reached its entrypoint');
|
||
|
|
assert.ok(original.equals(await readFile(path.join(root,'resources/app.asar'))),'Real packaged archive changed');
|
||
|
|
const report={fuses,nodeModeDisabled:true,nodeOptionsInjectionIgnored:true,noInspectorListener:true,unsafeStartupOverridesRejected:true,embeddedAsarHeaderIntegrity:true,tamperedCopiedArchiveRejected:true,originalPackageUnmodified:true};await writeFile('artifacts/package-fuses-proof.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report));
|
||
|
|
}catch(error){await writeFile('artifacts/package-fuses-failure.json',JSON.stringify({stage:currentStage,message:error.message},null,2));throw error;
|
||
|
|
// eslint-disable-next-line no-unsafe-finally -- Unsafe cleanup paths must fail closed before deletion.
|
||
|
|
}finally{const target=path.resolve(directory);if(!target.startsWith(fixtureRoot+path.sep)||path.dirname(target)!==fixtureRoot)throw Error('Unsafe fuse fixture cleanup');await rm(target,{recursive:true,force:true,maxRetries:10,retryDelay:100});}
|