Files
safetynet/scripts/windows-dlp-mvp-smoke.mjs
T

28 lines
9.8 KiB
JavaScript

// A bounded, real Windows browser DLP acceptance receiver; never a production bypass.
import {createServer} from 'node:https';
import {createRequire} from 'node:module';
import {mkdtempSync,mkdirSync,rmSync,readFileSync,writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join,resolve,sep} from 'node:path';
import {spawn} from 'node:child_process';
import {randomBytes} from 'node:crypto';
import net from 'node:net';
import {createApp} from '../apps/server/app.mjs';
import {openStore,digest} from '../apps/server/store.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
const collector='http://127.0.0.1:4318';if(!(await fetch(collector+'/health')).ok)throw Error('Protected collector unavailable');process.env.OTEL_EXPORTER_OTLP_ENDPOINT=collector;const telemetry=startTelemetry();
const temporary=mkdtempSync(join(tmpdir(),'safetynet-windows-dlp-'));mkdirSync('artifacts',{recursive:true});if(!resolve(temporary).startsWith(resolve(tmpdir())+sep)||!temporary.split(/[\\/]/).at(-1).startsWith('safetynet-windows-dlp-'))throw Error('Unsafe temporary fixture path');
const secret='MVP Company Forecast 2030',yaraMarker='MVP_YARA_ONLY_BLOCK',icapMarker='MVP_ICAP_ONLY_BLOCK',publicFile=Buffer.from('Public Windows DLP file content\n'),files={public:join(temporary,'public.txt'),secret:join(temporary,'secret.txt')};writeFileSync(files.public,publicFile);writeFileSync(files.secret,secret);
const receiver={posts:0,secretPosts:0,filePosts:0,exactPublicFile:0,kinds:{}},scanner={scans:0,blocks:0,failures:0,pending:false};let avMode='normal',pendingAV=null;const sockets=new Set();
const av=net.createServer(socket=>{sockets.add(socket);socket.on('close',()=>sockets.delete(socket));socket.on('error',()=>{});let input=Buffer.alloc(0),done=false;socket.on('data',chunk=>{input=Buffer.concat([input,chunk]);if(input.length>4194304){input.fill(0);socket.destroy();return;}if(done||input.indexOf('\r\n\r\n')<0)return;const options=input.subarray(0,7).toString()==='OPTIONS';if(!options&&!input.subarray(-5).equals(Buffer.from('0\r\n\r\n')))return;done=true;if(options){input.fill(0);socket.end('ICAP/1.0 200 OK\r\nMethods: RESPMOD, REQMOD\r\nAllow: 204\r\nISTag: "mvp-fixture"\r\nEncapsulated: null-body=0\r\n\r\n');return;}scanner.scans++;const block=input.includes(Buffer.from(icapMarker)),gate=avMode==='gate'&&input.includes(Buffer.from('Public pending scan'));input.fill(0);if(avMode==='fail'){scanner.failures++;socket.end('ICAP/1.0 500 Server Error\r\n\r\n');return;}const reply=()=>socket.end(block?'ICAP/1.0 204 No Content\r\nISTag: "mvp-fixture"\r\nX-Infection-Found: Type=0; Threat=synthetic-mvp;\r\n\r\n':'ICAP/1.0 204 No Content\r\nISTag: "mvp-fixture"\r\n\r\n');if(block)scanner.blocks++;if(gate){scanner.pending=true;pendingAV=()=>{scanner.pending=false;pendingAV=null;reply();};}else reply();});}).listen(0,'127.0.0.1');await new Promise(r=>av.once('listening',r));
const page=`<!doctype html><meta charset="utf-8"><title>Public AI workspace</title><style>body{font:18px Arial;padding:24px}input,textarea,[contenteditable]{display:block;width:80%;min-height:44px;border:1px solid #aaa;margin:15px}button{padding:12px}</style><h1>Public AI prompt workspace</h1><form id="prompt-form" method="post" action="/receiver/form"><input id="input" name="prompt" type="text"><button id="form-submit">Send prompt</button></form><textarea id="textarea"></textarea><div id="editor" role="textbox" contenteditable="true"><p><br></p></div><input id="file" type="file"><pre id="copy-source"></pre><script>window.mvpSubmit=(id)=>{const e=document.getElementById(id),text=e.isContentEditable?e.innerText:e.value;return fetch('/receiver/'+id,{method:'POST',headers:{'content-type':'text/plain'},body:text}).then(r=>r.ok).catch(()=>false)};</script>`;
// eslint-disable-next-line no-useless-assignment -- Explicitly clear sensitive transient text after use.
const sink=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.url==='/facts'){res.setHeader('content-type','application/json');res.end(JSON.stringify({receiver,scanner}));return;}if(req.url.startsWith('/scanner/')){const mode=req.url.split('/').at(-1);if(mode==='release')pendingAV?.();else avMode=mode;res.end('ok');return;}if(req.method==='POST'){let chunks=[],size=0;req.on('data',chunk=>{size+=chunk.length;if(size>1048576){req.destroy();return;}chunks.push(chunk);});req.on('end',()=>{receiver.posts++;const body=Buffer.concat(chunks);let decoded=body.toString('utf8');try{decoded=decodeURIComponent(decoded.replace(/\+/g,' '));}catch{}if(decoded.includes(secret)||body.includes(Buffer.from(secret))||decoded.includes(yaraMarker)||decoded.includes(icapMarker))receiver.secretPosts++;const kind=/^\/receiver\/(input|textarea|editor|form|typed|file|race|scanner|yara|icap)$/.exec(req.url)?.[1]||'other';receiver.kinds[kind]=(receiver.kinds[kind]||0)+1;if(kind==='file'){receiver.filePosts++;const boundary=/boundary=(?:"([^"]+)"|([^;]+))/.exec(req.headers['content-type']||'');if(boundary){const start=body.indexOf(Buffer.from('\r\n\r\n')),end=body.indexOf(Buffer.from('\r\n--'+(boundary[1]||boundary[2])),start+4);if(start>=0&&end>=0&&body.subarray(start+4,end).equals(publicFile))receiver.exactPublicFile++;}}decoded='';body.fill(0);for(const chunk of chunks)chunk.fill(0);chunks=[];res.end('accepted');});return;}res.setHeader('content-type','text/html');res.end(page);}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));
const origin=`https://127.0.0.1:${sink.address().port}`,store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');const p=store.get('policy');p.browser.homeUrl=origin+'/workspace';p.browser.allowAgentBackgroundWorkers=false;p.dlp.rules.push({id:'mvp-company-secret',label:'Synthetic company confidential forecast',kind:'literal',value:secret,action:'block'});p.security.yara.enabled=true;p.security.yara.rules=[{id:'mvp-yara-only',name:'Synthetic MVP YARA',source:`rule mvp_yara_only { strings: $a = "${yaraMarker}" condition: $a }`}];p.enterprise.icap={...p.enterprise.icap,enabled:true,url:`icap://127.0.0.1:${av.address().port}/scan`,scanUploads:true,scanDownloads:false,scanClipboard:false,scanAI:false};p.agents.enabled=false;p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:collector,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'mvp-fixture');
const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement:true,requireIndependentPolicyApproval:true};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.once('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;const reviewer=randomBytes(32).toString('base64url');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewer),Date.now()+600000,JSON.stringify({sub:'independent-mvp-reviewer',role:'admin'}));
const executable=createRequire(import.meta.url)('electron'),environment={...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ORIGIN:origin,SAFETYNET_SMOKE_PUBLIC_FILE:files.public,SAFETYNET_SMOKE_SECRET_FILE:files.secret};delete environment.SAFETYNET_SMOKE_CODE;
async function runNative(script,extra={}){const child=spawn(executable,[script],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...environment,...extra}});child.stdout.on('data',v=>process.stdout.write(v));child.stderr.on('data',v=>process.stderr.write(v));const deadline=setTimeout(()=>child.kill(),240000),exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);return {exit,pid:child.pid};}
let exit;try{const first=await runNative('scripts/windows-dlp-mvp-smoke.cjs',{SAFETYNET_SMOKE_CODE:code});exit=first.exit;
if(exit===0){const proof=JSON.parse(readFileSync('artifacts/windows-dlp-mvp-native-proof.json','utf8'));writeFileSync('artifacts/windows-dlp-mvp-first-process-native-proof.json',JSON.stringify(proof,null,2));const cursor=store.db.prepare('SELECT COALESCE(MAX(id),0) AS id FROM events').get().id,devicesBefore=store.db.prepare('SELECT COUNT(*) AS count FROM devices').get().count;const restarted=await runNative('scripts/windows-dlp-mvp-restart-smoke.cjs',{SAFETYNET_SMOKE_FIRST_PID:String(proof.processId),SAFETYNET_SMOKE_EXPECT_DEVICE:proof.deviceId,SAFETYNET_SMOKE_EXPECT_POLICY:String(store.get('policy').version),SAFETYNET_SMOKE_AFTER_EVENT:String(cursor)});exit=restarted.exit;if(exit===0){const second=JSON.parse(readFileSync('artifacts/windows-dlp-mvp-restart-native-proof.json','utf8'));if(first.pid===restarted.pid||proof.processId===second.restartedProcessId||devicesBefore!==1||store.db.prepare('SELECT COUNT(*) AS count FROM devices').get().count!==devicesBefore)throw Error('Native restart enrollment isolation failed');writeFileSync('artifacts/windows-dlp-mvp-process-restart-summary.json',JSON.stringify({actualTwoProcessRuns:true,sameProtectedUserData:true,sameManagementBackend:true,deviceCount:devicesBefore,enrollmentNotRepeated:true,firstProcessId:proof.processId,restartedProcessId:second.restartedProcessId,policyVersion:second.policyVersion,firstProof:'windows-dlp-mvp-first-process-native-proof.json',restartProof:'windows-dlp-mvp-restart-native-proof.json'},null,2));}}
}catch(error){console.error('Native restart harness failed: '+error.name);exit=1;}finally{pendingAV?.();for(const socket of sockets)socket.destroy();await new Promise(r=>av.close(r));for(const server of [management,sink])server.closeAllConnections();await Promise.all([management,sink].map(server=>new Promise(r=>server.close(r))));store.db.close();await telemetry?.shutdown();rmSync(temporary,{recursive:true,force:true});}process.exit(exit);