27 lines
7.0 KiB
JavaScript
27 lines
7.0 KiB
JavaScript
import {createServer} from 'node:https';
|
|
import {createRequire} from 'node:module';
|
|
import {mkdtempSync,mkdirSync,rmSync,readFileSync} 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-agent-visual-'));mkdirSync('artifacts',{recursive:true});
|
|
if(!resolve(temporary).startsWith(resolve(tmpdir())+sep)||!temporary.split(/[\\/]/).at(-1).startsWith('safetynet-agent-visual-'))throw Error('Unsafe temporary fixture path');
|
|
let blockPNG=false;const avFacts={scans:0,pngScans:0,pngBlocks:0},avSockets=new Set(),pngSignature=Buffer.from([137,80,78,71,13,10,26,10]);const av=net.createServer(socket=>{avSockets.add(socket);socket.on('close',()=>avSockets.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){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: "visual-fixture"\r\nEncapsulated: null-body=0\r\n\r\n');return;}avFacts.scans++;const png=input.indexOf(pngSignature)>=0;if(png)avFacts.pngScans++;const blocked=png&&blockPNG;if(blocked)avFacts.pngBlocks++;input.fill(0);socket.end(blocked?'ICAP/1.0 204 No Content\r\nISTag: "visual-fixture"\r\nX-Infection-Found: Type=0; Threat=synthetic-visual;\r\n\r\n':'ICAP/1.0 204 No Content\r\nISTag: "visual-fixture"\r\n\r\n');});}).listen(0,'127.0.0.1');await new Promise(r=>av.once('listening',r));
|
|
const sink=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.url==='/av-stats'){res.setHeader('content-type','application/json');res.end(JSON.stringify(avFacts));return;}if(req.url==='/av-block')blockPNG=true;else if(['/public','/canvas','/sensitive','/closed-shadow','/open-shadow','/frames'].includes(req.url))blockPNG=false;res.setHeader('content-type','text/html');const base='<!doctype html><meta name="viewport" content="width=device-width"><style>body{margin:0;background:white;color:#123;font:28px Arial}h1{margin:70px}canvas{display:block}</style><title>Public visual fixture</title>';if(req.url==='/canvas')res.end(base+'<canvas width="1200" height="360"></canvas><script>const c=document.querySelector("canvas").getContext("2d");c.fillStyle="white";c.fillRect(0,0,1200,360);c.fillStyle="black";c.font="bold 48px Arial";c.fillText("CLASSIFIED ORCHID DOCUMENT",40,170);</script>');else if(req.url==='/sensitive')res.end(base+'<h1>Public form</h1><input type="password" value="synthetic-hidden-field" style="position:absolute;left:-9999px"><script>document.querySelectorAll=()=>[]</script>');else if(req.url==='/closed-shadow')res.end(base+`<div id=host></div><script>document.getElementById("host").attachShadow({mode:"closed"}).innerHTML='<input type="password" value="synthetic-closed-shadow-password">';document.querySelectorAll=()=>[]</script>`);else if(req.url==='/open-shadow')res.end(base+`<div id=host></div><script>document.getElementById("host").attachShadow({mode:"open"}).innerHTML='<p style="margin:70px;font:28px Arial">Public shadow content</p>'</script>`);else if(req.url==='/frames')res.end(base+'<h1>Public parent</h1><iframe src="/public"></iframe>');else res.end(base+'<h1>Public approved visual page</h1><p style="margin:70px">Research and documentation workspace.</p>');}).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+'/public';p.inspection.documentAnalysisEnabled=false;p.dlp.rules.push({id:'visual-confidential-fixture',label:'Synthetic visual confidential rule',kind:'literal',value:'CLASSIFIED ORCHID DOCUMENT',action:'block'});
|
|
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={...p.agents,enabled:true,identityMode:'individual',scopes:['sessions'],allowedHosts:['127.0.0.1'],grants:[{id:'visual-a',name:'Visual A',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions'],allowedHosts:['127.0.0.1'],generation:1},{id:'visual-b',name:'Visual B',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions'],allowedHosts:['127.0.0.1'],generation:1}]};
|
|
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,'vault-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-vault-reviewer',role:'admin'}));
|
|
const child=spawn(createRequire(import.meta.url)('electron'),['scripts/agent-visual-export-smoke.cjs'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ORIGIN:origin}});child.stdout.on('data',v=>process.stdout.write(v));child.stderr.on('data',v=>process.stderr.write(v));const deadline=setTimeout(()=>child.kill(),240000);const exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);
|
|
for(const socket of avSockets)socket.destroy();await new Promise(r=>av.close(r));for(const server of [management,sink])server.closeAllConnections();await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r))]);store.db.close();await telemetry?.shutdown();rmSync(temporary,{recursive:true,force:true});if(exit===0&&(avFacts.pngScans<3||avFacts.pngBlocks!==1))throw Error('Actual visual AV exchanges incomplete');process.exit(exit);
|