Files
safetynet/scripts/controlled-login-smoke.mjs
T

25 lines
8.7 KiB
JavaScript
Raw Normal View History

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-controlled-login-'));mkdirSync('artifacts',{recursive:true});
if(!resolve(temporary).startsWith(resolve(tmpdir())+sep)||!temporary.split(/[\\/]/).at(-1).startsWith('safetynet-controlled-login-'))throw Error('Unsafe temporary fixture path');
const icapFacts={scans:0,blocks:0,credentialObserved:false},icapSockets=new Set();const icap=net.createServer(socket=>{icapSockets.add(socket);socket.on('close',()=>icapSockets.delete(socket));socket.on('error',()=>{});let input=Buffer.alloc(0),done=false;socket.on('data',chunk=>{input=Buffer.concat([input,chunk]);if(input.length>262144){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: "controlled-fixture"\r\nEncapsulated: null-body=0\r\n\r\n');return;}icapFacts.scans++;if(input.includes(Buffer.from('sk-syntheticControlledLoginCredential0123456789'))||input.includes(Buffer.from('public-native-user'))||input.includes(Buffer.from('fixtureControlledCredentialDelta0123456789')))icapFacts.credentialObserved=true;const blocked=input.includes(Buffer.from('EXTRA_ICAP_RESIDUAL_SECRET'));if(blocked)icapFacts.blocks++;input.fill(0);socket.end(blocked?'ICAP/1.0 204 No Content\r\nISTag: "controlled-fixture"\r\nX-Infection-Found: Type=0; Threat=synthetic;\r\n\r\n':'ICAP/1.0 204 No Content\r\nISTag: "controlled-fixture"\r\n\r\n');});}).listen(0,'127.0.0.1');await new Promise(r=>icap.once('listening',r));
let mode='200',submitted=0,totalPosts=0,replayed=0,finalGets=0,credentialUrlLeaks=0,credentialHeaderLeaks=0,publicPosts=0;const sink=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.url.includes('fixtureControlledCredentialDelta0123456789'))credentialUrlLeaks++;const values=Object.values(req.headers).flat().map(value=>String(value)),known='fixtureControlledCredentialDelta0123456789';if(values.some(value=>value.includes(known))||values.some((a,i)=>values.some((b,j)=>i!==j&&a+b===known)))credentialHeaderLeaks++;if(req.url==='/stats'){res.setHeader('content-type','application/json');res.end(JSON.stringify({submitted,totalPosts,replayed,finalGets,credentialUrlLeaks,credentialHeaderLeaks,publicPosts,icap:icapFacts}));return;}if(req.method==='POST'){totalPosts++;if(req.url==='/public')publicPosts++;const chunks=[];let size=0;req.on('data',bytes=>{size+=bytes.length;if(size>65536){req.destroy();return;}chunks.push(bytes);});req.on('end',()=>{const body=Buffer.concat(chunks);const credentialPresent=body.includes(Buffer.from('sk-syntheticControlledLoginCredential0123456789'))||body.includes(Buffer.from('fixtureControlledCredentialDelta0123456789'));body.fill(0);for(const bytes of chunks)bytes.fill(0);if(req.url==='/replay'){replayed++;res.end('unexpected replay');return;}if(credentialPresent)submitted++;if(mode==='302'||mode==='307'){res.writeHead(Number(mode),{location:mode==='302'?'/final':'/replay'});res.end();return;}res.setHeader('content-type','text/html');if(mode==='slow'){setTimeout(()=>res.end('<!doctype html><title>Login accepted</title>Done'),17000);return;}res.end('<!doctype html><title>Login accepted</title>Done');});return;}if(req.url==='/final'){finalGets++;res.setHeader('content-type','text/html');res.end('<!doctype html><title>Login accepted</title>Done');return;}mode=req.url.endsWith('/302')?'302':req.url.endsWith('/307')?'307':req.url.endsWith('/slow')?'slow':'200';res.setHeader('content-type','text/html');res.end('<!doctype html><title>Approved login fixture</title><form method="post" action="/signin"><input name="username" autocomplete="username"><input name="password" type="password" autocomplete="current-password"><button id="submit">Sign in</button></form>');}).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+'/login';p.passwordManager={enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300,controlledSubmission:{enabled:true,forms:[{id:'approved-login',actionUrl:origin+'/signin',usernameField:'username',passwordField:'password'}]}};p.dlp.rules.push({id:'fixture-credential',label:'Synthetic controlled credential',action:'block',kind:'literal',value:'sk-syntheticControlledLoginCredential0123456789'});p.dlp.rules.push({id:'fixture-confidential',label:'Synthetic residual secret',action:'block',kind:'literal',value:'synthetic-company-confidential-value'});
p.security.yara.enabled=true;p.security.yara.rules=[{id:'controlled-residual-yara',name:'Synthetic residual YARA',source:'rule controlled_residual_yara { strings: $a = "EXTRA_YARA_RESIDUAL_SECRET" condition: $a }'}];p.enterprise.icap={...p.enterprise.icap,enabled:true,url:`icap://127.0.0.1:${icap.address().port}/scan`,scanUploads:true,scanDownloads:false,scanClipboard:false,scanAI:false};
p.agents={...p.agents,enabled:true,identityMode:'individual',scopes:['sessions','vault:status','vault:list','vault:fill','vault:prepare-submit'],allowedHosts:['127.0.0.1','127.0.0.2'],grants:[{id:'vault-a',name:'Vault A',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions','vault:status','vault:list','vault:fill','vault:prepare-submit'],allowedHosts:['127.0.0.1'],generation:1},{id:'vault-b',name:'Vault B',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['vault:status','vault:list'],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/controlled-login-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}});const redact=chunk=>chunk.toString('utf8').replaceAll('sk-syntheticControlledLoginCredential0123456789','[synthetic-credential]').replaceAll('fixtureControlledCredentialDelta0123456789','[synthetic-credential]');child.stdout.on('data',chunk=>process.stdout.write(redact(chunk)));child.stderr.on('data',chunk=>process.stderr.write(redact(chunk)));const deadline=setTimeout(()=>child.kill(),180000);const exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);
for(const socket of icapSockets)socket.destroy();await new Promise(r=>icap.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&&(submitted!==4||replayed!==0||finalGets!==1||icapFacts.scans<4||icapFacts.blocks!==1||icapFacts.credentialObserved||credentialUrlLeaks||credentialHeaderLeaks||publicPosts||totalPosts!==4))throw Error('Unexpected controlled login receiver counts');process.exit(exit);