Files
safetynet/scripts/verify-package.mjs
T

89 lines
12 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
import {createRequire} from 'node:module';
import {spawn} from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import assert from 'node:assert/strict';
const require=createRequire(import.meta.url);
const {startAgentControl}=require('../apps/browser/agent-control.cjs');
const {startInspector}=require('../apps/browser/inspection.cjs');
const root=path.resolve('dist/win-unpacked');
if(process.platform!=='win32')throw Error('Windows package verification requires Windows');
const {extractFile}=require('@electron/asar');
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize('packages/core/agent-audit.cjs')),await fs.readFile('packages/core/agent-audit.cjs'),'Packaged structured agent audit schema is stale');
for(const file of ['apps/browser/upload-lease.cjs','apps/browser/file-upload-gate.cjs','apps/browser/request-source.cjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged upload binding source is stale: ${file}`);
assert.deepEqual(await fs.readFile(path.join(root,'resources/inspector/upload_lease.py')),await fs.readFile('apps/inspector/upload_lease.py'),'Packaged Windows upload lease helper is stale');
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize('apps/browser/header-inspection.cjs')),await fs.readFile('apps/browser/header-inspection.cjs'),'Packaged outgoing header DLP source is stale');
for(const file of ['apps/browser/agent-visual-export.cjs','apps/browser/agent-visual-probe.cjs','apps/browser/credential-submission.cjs','packages/core/credential-policy.mjs','apps/browser/session-snapshot.cjs','apps/browser/agent-session-adapters.cjs','apps/browser/session-restore.cjs','apps/browser/vault-agent-controls.cjs','apps/browser/vault.cjs','apps/browser/window-controls.cjs','apps/browser/history-snapshot.cjs','apps/browser/human-interaction.cjs','packages/core/siem.mjs','packages/core/evidence.mjs','packages/core/evidence-payload.cjs','packages/core/interaction-facts.cjs','apps/browser/evidence-capture.cjs','apps/browser/evidence-delivery.cjs','packages/core/request-transfer-facts.cjs','packages/core/transfer-facts.cjs','packages/core/inspection-stages.cjs','apps/browser/history-navigation.cjs','apps/browser/agent-transport-client.cjs','apps/browser/agent-client.cjs','apps/browser/library-organization.cjs','packages/core/library.mjs','packages/core/branding.mjs','packages/core/file-transfer.cjs','packages/core/file-transfer.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged new security/branding source is stale: ${file}`);
for(const file of ['apps/browser/agent-grants.cjs','apps/browser/policy-assignment.cjs','packages/core/policy-groups.mjs','apps/browser/tab-registry.cjs','apps/browser/agent-adapters.cjs','apps/browser/agent-control.cjs','apps/browser/password-fill.cjs','apps/browser/diagnostics.cjs','packages/core/diagnostics.mjs','packages/core/security.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged device/session policy source is stale: ${file}`);
2026-10-13 10:33:31 +02:00
for(const file of ['apps/browser/main.cjs','apps/browser/preload.cjs','apps/browser/site-preload.cjs','apps/browser/clipboard-deny.cjs','apps/browser/clipboard-guard.cjs','apps/browser/clipboard-dom.cjs','apps/browser/page-clipboard-guard.cjs','apps/browser/startup-security.cjs','apps/browser/visual-capture.cjs','apps/browser/renderer.js','apps/browser/index.html','apps/browser/style.css','packages/core/clipboard.mjs']){
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged source is stale: ${file}`);
}
for(const file of ['apps/browser/inspection.cjs','apps/browser/managed-settings.cjs','apps/browser/management-channel.cjs','apps/browser/enterprise-inspection.cjs','apps/browser/icap-client.cjs','apps/browser/network-posture.cjs','apps/browser/download-gate.cjs','packages/core/enterprise.mjs','packages/core/semantic.mjs','packages/core/managed-settings.mjs','packages/core/policy-channel.cjs','packages/core/policy.mjs'])assert.ok(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)).equals(await fs.readFile(file)),`Packaged source is stale: ${file}`);
for(const file of ['stage_timings.py','office_macros.py','semantic.py','engine.py','server.py','windows_containment.py','windows_appcontainer.py','sandbox_worker.py','provision_runtime_acl.py'])assert.deepEqual(await fs.readFile(path.join(root,'resources/inspector',file)),await fs.readFile(path.join('apps/inspector',file)),`Packaged inspector is stale: ${file}`);
2026-10-13 10:33:31 +02:00
const temporary=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-package-'));
for(const file of ['apps/browser/bookmarks.cjs','packages/core/bookmarks.mjs'])assert.ok(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)).equals(await fs.readFile(file)),`Packaged bookmark source is stale: ${file}`);
function run(executable,args,input,timeoutMs=15000){return new Promise((resolve,reject)=>{
const child=spawn(executable,args,{stdio:['pipe','pipe','pipe'],windowsHide:true,env:{...process.env,PYTHONDONTWRITEBYTECODE:'1'}});let output='',errors='';
const timer=setTimeout(()=>{child.kill();reject(Error('Package verification timed out'));},timeoutMs);
child.on('error',reject);child.stdout.on('data',data=>output+=data);child.stderr.on('data',data=>errors+=data);
child.on('close',code=>{clearTimeout(timer);resolve({code,output,errors});});child.stdin.end(input);
});}
let service;
try{
const uploadFile=path.join(temporary,'packaged-upload.txt'),uploadBytes=Buffer.from('Synthetic packaged upload lease verification');
await fs.writeFile(uploadFile,uploadBytes);
const uploadLease=await require('../apps/browser/upload-lease.cjs').acquireUploadLease({paths:[uploadFile],maxBytes:4096,
pythonPath:path.join(root,'resources/python/python.exe'),helperPath:path.join(root,'resources/inspector/upload_lease.py')});
try{
assert.deepEqual(uploadLease.files[0].bytes,uploadBytes,'Packaged upload helper snapshot differs');
await assert.rejects(fs.writeFile(uploadFile,'Unapproved replacement'),'Packaged upload handles did not prevent overwrite');
await assert.rejects(fs.rename(uploadFile,path.join(temporary,'moved-upload.txt')),'Packaged upload handles did not prevent rename');
}finally{uploadLease.files[0].bytes.fill(0);await uploadLease.release();}
await fs.writeFile(uploadFile,'Released after terminal-equivalent verifier cleanup');
2026-10-13 10:33:31 +02:00
const inspectorBytecode=path.join(root,'resources/inspector/__pycache__');
const bytecodeExists=()=>fs.stat(inspectorBytecode).then(()=>true,error=>{if(error.code==='ENOENT')return false;throw error;});
assert.equal(await bytecodeExists(),false,'Package must start with no inspector bytecode cache');
const packagedStages=[];
const packagedInspector=startInspector(path.join(root,'resources'),true,{onStages:batch=>packagedStages.push(batch)});
2026-10-13 10:33:31 +02:00
assert.ok(packagedInspector,'Packaged inspector runtime is missing');
try{
const verdict=await packagedInspector.inspect(Buffer.from('Public package verification text'),{diagnostics:{enabled:true,mode:'debug'},dlp:{maxBytes:1048576,rules:[],semantic:{enabled:false}}});
2026-10-13 10:33:31 +02:00
assert.equal(verdict.decision,'allow','Packaged contained inspector failed');
assert.equal(verdict.coverage.complete,true);
const macro=Buffer.from(await fs.readFile('tests/fixtures/office/benign-vba-project.base64.txt','utf8'),'base64');
for(const blocked of [false,true]){
const result=await packagedInspector.scanDownload(macro,{diagnostics:{enabled:true,mode:'debug'},security:{yara:{enabled:true,scanOfficeMacros:true,rules:[{id:'packaged-macro-marker',source:blocked?'rule source_marker { strings: $m = "Hello from Python!" condition: $m }':'rule clean { condition: false }'}]}}});
assert.equal(result.decision,blocked?'block':'allow','Packaged VBA source scanning failed');
assert.equal(result.officeMacros?.modules,5,'Packaged VBA extraction missing');
}
const stages=packagedStages.flatMap(batch=>batch?.records?.map(row=>row.stage)||[]);for(const stage of ['inspect','parse','yara','macros'])assert.ok(stages.includes(stage),`Packaged internal stage missing: ${stage}`);
2026-10-13 10:33:31 +02:00
}finally{packagedInspector.stop();}
const semanticPath=path.join(root,'resources/semantic');
const actual=await run(path.join(root,'resources/python/python.exe'),['-c','import sys; sys.path.insert(0,sys.argv[1]); from semantic import NativeSemanticModel; m=NativeSemanticModel(sys.argv[2],sys.argv[3]); vectors=m.text_vectors("Public information about weather forecasting."); assert len(vectors)>=1 and len(vectors[0])==768; m.close(); print("Offline packaged semantic CPU inference verified.")',path.join(root,'resources/inspector'),path.join(semanticPath,'models'),path.join(semanticPath,'searchi-gemma-worker.exe')],undefined,45000);
assert.equal(actual.code,0,'Packaged offline semantic inference failed');assert.ok(actual.output.includes('Offline packaged semantic CPU inference verified.'));
service=await startAgentControl({authorize:async()=>({allowed:true,role:'agent',scopes:['sessions:list']}),audit:async()=>{},actions:{sessions:async()=>[{id:'managed',url:'https://example.test/'}]}});
const encrypted=await run(path.join(root,'resources/python/python.exe'),[path.join(root,'resources/inspector/dpapi.py'),'protect'],JSON.stringify({address:service.address,token:service.token,edPublic:service.edPublic,pqPublic:service.pqPublic,protocol:service.protocol}));
2026-10-13 10:33:31 +02:00
assert.equal(encrypted.code,0,'Packaged DPAPI failed');
const connection=path.join(temporary,'connection.json'),request=path.join(temporary,'request.json');
await fs.writeFile(connection,JSON.stringify({format:'windows-dpapi-v1',protectedConnection:encrypted.output}));
await fs.writeFile(request,JSON.stringify({action:'sessions'}));
const result=await run(path.join(root,'SafetyNet.exe'),[`--agent-connection=${connection}`,`--agent-action=${request}`]);
assert.equal(result.code,0,'Packaged agent helper failed');assert.equal(JSON.parse(result.output).result[0].id,'managed');assert.equal(result.output.includes(service.token),false);
await service.close();
const expiresAt=new Date(Date.now()+60000).toISOString(),individualEvents=[];
service=await startAgentControl({principalMode:'individual',principals:[{id:'packaged-reader',generation:1,enabled:true,expiresAt,scopes:['sessions'],allowedHosts:[]}],authorize:async({principalId,generation})=>({allowed:principalId==='packaged-reader'&&generation===1,role:'agent',scopes:['sessions:list']}),audit:async event=>individualEvents.push(event),actions:{sessions:async()=>[{id:'individual-managed'}]}});
const individual=service.issuePrincipal('packaged-reader',1,expiresAt);
const protectedIndividual=await run(path.join(root,'resources/python/python.exe'),[path.join(root,'resources/inspector/dpapi.py'),'protect'],JSON.stringify(individual));
assert.equal(protectedIndividual.code,0,'Packaged individual DPAPI failed');
const individualConnection=path.join(temporary,'individual.json');await fs.writeFile(individualConnection,JSON.stringify({format:'windows-dpapi-v1',protectedConnection:protectedIndividual.output}));
const individualResult=await run(path.join(root,'SafetyNet.exe'),[`--agent-connection=${individualConnection}`,`--agent-action=${request}`]);
assert.equal(individualResult.code,0,'Packaged individual agent helper failed');assert.equal(JSON.parse(individualResult.output).result[0].id,'individual-managed');assert.equal(individualResult.output.includes(individual.token),false);assert.ok(individualEvents.some(event=>event.principalId==='packaged-reader'&&event.generation===1&&event.outcome==='completed'));
2026-10-13 10:33:31 +02:00
assert.equal(await bytecodeExists(),false,'Cold packaged startup must not create inspector bytecode files');
console.log('Packaged security sources, Windows upload handle lease, Windows-contained inspector, offline EmbeddingGemma 2 CPU inference, legacy and individual Windows agent helpers and bundled DPAPI runtime passed.');
2026-10-13 10:33:31 +02:00
}finally{
await service?.close();
// eslint-disable-next-line no-unsafe-finally -- Cleanup failure must fail closed rather than report successful protected cleanup.
2026-10-13 10:33:31 +02:00
if(!temporary.startsWith(path.join(os.tmpdir(),'safetynet-package-')))throw Error('Unexpected temporary directory');
await fs.rm(temporary,{recursive:true,force:true});
}