Files
safetynet/scripts/smoke.mjs
T
2026-10-13 10:34:48 +02:00

55 lines
6.2 KiB
JavaScript

import { createServer } from 'node:http';
import { createRequire } from 'node:module';
import { mkdtempSync,rmSync,mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join,resolve } from 'node:path';
import { spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { createApp } from '../apps/server/app.mjs';
import { openStore,digest } from '../apps/server/store.mjs';
const require=createRequire(import.meta.url),electron=require('electron');
const temporary=mkdtempSync(join(tmpdir(),'safetynet-smoke-'));mkdirSync('artifacts',{recursive:true});
let posts=0,proxyRequests=0,secondaryRequests=0;
const visualLogin='<!doctype html><meta charset="utf-8"><title>Visual identity fixture</title><style>body{font-family:Arial;background:#fff;color:#202124;margin:0}form{margin:60px auto;padding:32px;border:2px solid #3267cb;border-radius:12px;width:340px;height:330px;box-sizing:content-box}h1{color:#3267cb;font-size:27px}input{display:block;margin:20px 0;padding:12px;border:1px solid #aaa;width:260px}button{background:#3267cb;color:white;padding:12px}</style><form method="post" action="/sink"><h1>SafetyNet Identity Lab</h1><h2>Sign in</h2><p>Use your laboratory account</p><input name="username" type="email" autocomplete="username"><input type="password" autocomplete="current-password"><button type="submit">Continue</button></form>';
const receiver=(req,res)=>{
res.setHeader('Access-Control-Allow-Origin','*');
if(req.url==='/count'){res.setHeader('content-type','application/json');res.end(JSON.stringify({posts,proxyRequests,secondaryRequests}));return;}
if(req.url==='/visual-login'){res.setHeader('content-type','text/html');res.end(visualLogin);return;}
if(req.url==='/download-safe'||req.url==='/download-blocked'){
res.setHeader('Content-Type','application/octet-stream');res.setHeader('Content-Disposition',`attachment; filename="${req.url==='/download-safe'?'safe.bin':'blocked.bin'}"`);
res.end(req.url==='/download-safe'?Buffer.from([0,1,2,3,255]):Buffer.from('SAFETYNET_BLOCK_TEST'));return;
}
if(req.url.startsWith('http://proxy-check.invalid/'))proxyRequests++;
if(req.method==='POST'){posts++;req.resume();res.end('received');return;}
res.setHeader('content-type','text/html');res.end('<!doctype html><title>SafetyNet enforcement fixture</title><h1>Managed browsing is active</h1><p>This controlled receiver verifies that blocked payloads never arrive.</p><input id="public-input" type="text"><input id="private-input" type="password" value="never-export-this"><a id="safe-download" href="/download-safe">Download safe sample</a><a id="blocked-download" href="/download-blocked">Download blocked sample</a>');
};
const sink=createServer(receiver).listen(0,'127.0.0.1');await new Promise(r=>sink.on('listening',r));
const secondary=createServer((req,res)=>{secondaryRequests++;res.setHeader('Access-Control-Allow-Origin','*');res.end('secondary');}).listen(0,'127.0.0.2');await new Promise(r=>secondary.on('listening',r));
const store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');
const inspectorToken=randomBytes(32).toString('base64url');
const inspectorProcess=spawn(resolve('.runtime/windows/python/python.exe'),['apps/inspector/server.py'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...process.env,INSPECTOR_TOKEN:inspectorToken,INSPECTOR_PORT:'0',INSPECTOR_PARENT_PID:String(process.pid)}});
const inspectorPort=await new Promise((resolve,reject)=>{
let output='';const deadline=setTimeout(()=>{inspectorProcess.kill();reject(Error('Fixture inspector did not start'));},40000);
inspectorProcess.once('exit',()=>{clearTimeout(deadline);reject(Error('Fixture inspector exited before startup'));});
inspectorProcess.on('error',reject);
inspectorProcess.stdout.on('data',chunk=>{
output+=chunk;
if(output.includes('\n')){
clearTimeout(deadline);
try{resolve(JSON.parse(output.split('\n')[0]).port);}catch{reject(Error('Fixture inspector startup rejected'));}
}
});
inspectorProcess.stderr.on('data',()=>{});
});
const p=store.get('policy');p.browser.homeUrl=`http://127.0.0.1:${sink.address().port}`;p.brand={name:'Acme Secure Browser',color:'#245fbc'};p.dlp.rules=[{id:'secret-roadmap',label:'Roadmap',kind:'literal',value:'Secret Roadmap',action:'block'}];store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'smoke-user');
p.browser.allowDownloads=true;p.security.yara={enabled:true,rules:[{id:'test-marker',name:'Controlled malicious marker',source:'rule Block_Test { strings: $a = "SAFETYNET_BLOCK_TEST" condition: $a }'}],maxScanBytes:1048576};p.agents={enabled:true,scopes:['sessions','navigate','read','click','type','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group'],allowedHosts:['127.0.0.1'],allowSensitiveRead:false};store.put('policy',p);
const config={publicUrl:'http://127.0.0.1',secure:false,adminToken,inspectorUrl:`http://127.0.0.1:${inspectorPort}`,inspectorToken};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.on('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
const child=spawn(electron,['scripts/electron-smoke.cjs'],{stdio:'inherit',windowsHide:true,env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_SINK:p.browser.homeUrl,SAFETYNET_SMOKE_SECONDARY:`http://127.0.0.2:${secondary.address().port}`,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ADMIN:adminToken}});
const timeout=setTimeout(()=>child.kill(),240000);
const exitCode=await new Promise(resolve=>{child.on('exit',code=>resolve(code??1));child.on('error',()=>resolve(1));});clearTimeout(timeout);
for(const server of [management,sink,secondary])server.closeAllConnections();
await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r)),new Promise(r=>secondary.close(r))]);store.db.close();
inspectorProcess.kill();
// Temporary paths originate from mkdtemp under the OS temp directory.
rmSync(temporary,{recursive:true,force:true});process.exit(exitCode);