Files
safetynet/scripts/vault-agent-controls-smoke.cjs

34 lines
9.0 KiB
JavaScript

'use strict';
const {app,BrowserWindow,session}=require('electron'),fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{X509Certificate}=require('node:crypto');
app.setPath('userData',process.env.SAFETYNET_SMOKE_USERDATA);app.disableHardwareAcceleration();require('../apps/browser/main.cjs');
const wait=ms=>new Promise(r=>setTimeout(r,ms)),base=process.env.SAFETYNET_SMOKE_SERVER,origin=process.env.SAFETYNET_SMOKE_ORIGIN,headers={authorization:`Bearer ${process.env.SAFETYNET_SMOKE_ADMIN}`,'content-type':'application/json'};
async function until(fn){for(let i=0;i<200;i++){const r=await fn();if(r)return r;await wait(100);}throw Error('Vault native fixture timed out');}
const overview=async()=> (await fetch(base+'/api/admin/overview',{headers})).json();
app.whenReady().then(async()=>{const pages=[];let stage='startup';try{
const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});
const fingerprint=new X509Certificate(fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem'))).fingerprint256;
// Only the exact synthetic certificate on loopback is trusted, in this isolated fixture partition.
session.fromPartition('persist:safetynet-managed').setCertificateVerifyProc((req,callback)=>callback(req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint?0:-2));
const ui=async(method,value)=>{const result=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);assert.equal(result.ok,true,method);return result.data;};
stage='enrollment';await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});const page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;pages.push(page);await until(()=>!page.isLoading()&&page.getURL()===origin+'/login');
const a=await ui('agentConnection','vault-a'),b=await ui('agentConnection','vault-b'),requestFile=path.join(app.getPath('userData'),'vault-request.json');
async function call(connection,action,args={}){fs.writeFileSync(requestFile,JSON.stringify({action,args}));const child=require('node:child_process').spawn(process.execPath,['scripts/agent-client.cjs','--connection',connection.credentialPath,'--request',requestFile],{windowsHide:true,stdio:['ignore','pipe','pipe']});let output='';child.stdout.on('data',v=>output+=v);child.stderr.on('data',()=>{});const exit=await new Promise(r=>{child.once('exit',r);child.once('error',()=>r(1));});return {exit,result:exit===0?JSON.parse(output).result:undefined};}
const sessionId=(await call(a,'sessions')).result[0].id;
stage='locked';assert.equal((await call(a,'vault:status',{sessionId})).result.locked,true);assert.equal((await call(a,'vault:list',{sessionId})).exit,1);
const master='Synthetic vault master for native proof only',username='native-vault-private-user',password='native-vault-private-password',label='native-vault-private-label';
stage='human create';await ui('vaultCreate',master);const saved=await ui('vaultSave',{origin,username,password,label});await ui('vaultSave',{origin:'https://127.0.0.2',username,password,label});
const inventory=async()=>{const r=await call(a,'vault:list',{sessionId});assert.equal(r.exit,0);assert.equal(r.result.length,1);assert.equal(r.result[0].origin,origin);for(const sentinel of [username,password,label,saved.id])assert.ok(!JSON.stringify(r.result).includes(sentinel));return r.result[0].credentialRef;};
stage='cross actor';let ref=await inventory();assert.equal((await call(b,'vault:fill',{sessionId,credentialRef:ref})).exit,1);
stage='actual fill';win.focus();page.focus();await wait(150);assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,0);assert.equal(await page.executeJavaScript(`document.querySelector('input[name=username]').value===${JSON.stringify(username)}&&document.querySelector('input[type=password]').value===${JSON.stringify(password)}`),true);assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,1);
stage='other tab';ref=await inventory();await ui('createTab',{url:origin+'/login'});const other=win.contentView.children.find(v=>v.webContents!==win.webContents&&v.webContents!==page).webContents;pages.push(other);await until(()=>!other.isLoading());const otherId=(await call(a,'sessions')).result.find(row=>row.id!==sessionId).id;assert.equal((await call(a,'vault:fill',{sessionId:otherId,credentialRef:ref})).exit,1);assert.equal(await other.executeJavaScript('document.querySelector("input[type=password]").value'), '');await ui('activateTab',sessionId);await ui('vaultUnlock',master);win.focus();page.focus();
stage='inventory after tab restore';await wait(300);ref=await inventory();stage='vault relock';await ui('vaultLock');await ui('vaultUnlock',master);assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,1);
stage='password replacement';ref=await inventory();await ui('vaultSave',{id:saved.id,origin,username,password:password+'-changed',label});assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,1);
const change=async modify=>{const p=(await overview()).policy;modify(p);p.version++;const proposed=await fetch(base+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(p)});assert.equal(proposed.status,202);const review=await proposed.json();const approval=await fetch(base+`/api/admin/policy-reviews/${review.reviewId}/approve`,{method:'POST',headers:{cookie:`sn_session=${process.env.SAFETYNET_SMOKE_REVIEWER}`,origin:base,'content-type':'application/json'},body:JSON.stringify({payloadHash:review.payloadHash,expectedVersion:review.expectedVersion,approve:true})});assert.equal(approval.status,200);await until(async()=>{const s=await ui('state');return s.ready&&s.policyVersion===p.version;});};
stage='central disable';ref=await inventory();await change(p=>{p.passwordManager.allowAutofill=false;});assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,1);await change(p=>{p.passwordManager.allowAutofill=true;});await ui('vaultUnlock',master);
stage='no submit';assert.equal((await page.executeJavaScript(`fetch('/count').then(r=>r.json())`)).submitted,0);stage='target revoke';ref=await inventory();await change(p=>{p.agents.grants.find(g=>g.id==='vault-a').enabled=false;});assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:ref})).exit,1);
stage='audit trace';const events=await until(async()=>{const e=(await overview()).events;return e.some(e=>e.metadata.agent?.action==='vault:fill'&&e.metadata.agent.outcome==='completed')&&e;});for(const event of events)for(const sentinel of [master,username,password,label,saved.id,ref])assert.ok(!JSON.stringify(event).includes(sentinel));
await wait(1500);const proofs=[];for(const action of ['vault:list','vault:fill']){const event=events.find(e=>e.metadata.agent?.action===action&&e.metadata.agent.outcome==='completed'&&e.metadata.traceId);assert.ok(event);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+event.metadata.traceId)).json(),spans=trace.spans||[];const request=spans.find(s=>s.operation==='agent.request'),dispatch=spans.find(s=>s.operation==='agent.dispatch'&&s.parentSpanId===request?.spanId),agent=spans.find(s=>s.name==='browser.agent'&&s.operation===action&&s.parentSpanId===dispatch?.spanId);assert.ok(agent);for(const operation of action==='vault:list'?['vault.inventory.inspect']:['vault.fill.visual','vault.fill.native'])assert.ok(spans.some(s=>s.name==='browser.vault'&&s.operation===operation&&spans.some(p=>p.spanId===s.parentSpanId)));proofs.push({action,traceId:event.metadata.traceId,stages:spans.map(({name,operation,parentSpanId,spanId,durationMs})=>({name,operation,parentSpanId,spanId,durationMs}))});}
const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);const summary=await(await fetch('http://127.0.0.1:4318/api/summary')).json();
fs.writeFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'vault-agent-controls-native-proof.json'),JSON.stringify({actualElectron:true,syntheticExactCertificateTrustOnly:true,humanUnlockOnly:true,maskedInventory:true,hostScopeFilter:true,crossActorDenied:true,actualNativeFill:true,fillNeverSubmits:true,replayDenied:true,differentNativeTabDenied:true,reunlockInvalidates:true,passwordReplacementInvalidates:true,centralAutofillDisable:true,targetedRevoke:true,ordinaryAuditSecretFree:true,health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites},summary,proofs},null,2));console.log('Vault agent controls native proof passed');for(const p of pages)require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(p);app.exit(0);
}catch(error){fs.writeFileSync(path.resolve('artifacts/vault-agent-controls-native-error.json'),JSON.stringify({stage,name:error.name,message:error.message},null,2));console.error(`Vault fixture failed at ${stage}: ${error.name}`);for(const p of pages)if(!p.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(p);app.exit(1);}});