41 lines
8.6 KiB
JavaScript
41 lines
8.6 KiB
JavaScript
'use strict';
|
|||
|
|
const {app,BrowserWindow}=require('electron'),fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
||
|
|
app.setPath('userData',process.env.SAFETYNET_SMOKE_USERDATA);app.disableHardwareAcceleration();require('../apps/browser/main.cjs');
|
||
|
|
const wait=ms=>new Promise(r=>setTimeout(r,ms)),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('Extended native fixture timed out');}
|
||
|
|
const overview=async()=> (await fetch(process.env.SAFETYNET_SMOKE_SERVER+'/api/admin/overview',{headers})).json();
|
||
|
|
app.whenReady().then(async()=>{let page;try{
|
||
|
|
const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});
|
||
|
|
assert.equal((await win.webContents.executeJavaScript(`window.safetynet.enroll(${JSON.stringify({server:process.env.SAFETYNET_SMOKE_SERVER,code:process.env.SAFETYNET_SMOKE_CODE})})`)).ok,true);
|
||
|
|
page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading());
|
||
|
|
const pair=async id=>{const r=await win.webContents.executeJavaScript(`window.safetynet.agentConnection(${JSON.stringify(id)})`);assert.equal(r.ok,true);return r.data;};const a=await pair('reader-a'),b=await pair('state-b');
|
||
|
|
const requestFile=path.join(app.getPath('userData'),'extended-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,base=process.env.SAFETYNET_SMOKE_SINK;
|
||
|
|
assert.equal((await win.webContents.executeJavaScript(`window.safetynet.navigate(${JSON.stringify(process.env.SAFETYNET_SMOKE_FORBIDDEN)})`)).ok,true);await until(()=>!page.isLoading());
|
||
|
|
for(const route of ['/old','/protected-title','/?code=synthetic-private','/current']){assert.equal((await call(a,'navigate',{sessionId,url:base+route})).exit,0);await until(()=>!page.isLoading());}
|
||
|
|
let list=await call(a,'history:list',{sessionId});assert.equal(list.exit,0);const entries=list.result.entries;
|
||
|
|
assert.ok(entries.some(e=>e.title==='Protected page'&&!e.url),'Confidential/credential history was not masked');assert.ok(!entries.some(e=>e.url?.includes('127.0.0.2')),'Out-of-host native history exposed');assert.ok(!JSON.stringify(entries).includes('synthetic-private'));assert.ok(!JSON.stringify(entries).includes('Synthetic confidential inventory title'));
|
||
|
|
assert.equal((await call(b,'history:go',{sessionId,snapshotId:list.result.snapshotId,entryId:entries[0].entryId})).exit,1,'Other actor reused snapshot');
|
||
|
|
const currentIndex=entries.findIndex(e=>e.active),credential=entries[currentIndex-1];assert.equal(credential.title,'Protected page');assert.equal(credential.url,undefined);assert.equal((await call(a,'history:go',{sessionId,snapshotId:list.result.snapshotId,entryId:credential.entryId})).exit,1);
|
||
|
|
// The failed credential selection does not consume another entry. Obtain fresh
|
||
|
|
// grant so the exact public native target can be proved independently.
|
||
|
|
list=await call(a,'history:list',{sessionId});const old=list.result.entries.find(e=>e.url===base+'/old');assert.ok(old);
|
||
|
|
let navigations=0;const observe=(_e,url)=>{if(url===base+'/old')navigations++;};page.on('did-navigate',observe);
|
||
|
|
assert.equal((await call(a,'history:go',{sessionId,snapshotId:list.result.snapshotId,entryId:old.entryId})).exit,0);await until(()=>page.getURL()===base+'/old'&&!page.isLoading());assert.equal(navigations,1);
|
||
|
|
assert.equal((await call(a,'history:go',{sessionId,snapshotId:list.result.snapshotId,entryId:old.entryId})).exit,1);assert.equal(navigations,1);page.removeListener('did-navigate',observe);
|
||
|
|
list=await call(a,'history:list',{sessionId});assert.equal((await call(a,'navigate',{sessionId,url:base+'/replacement'})).exit,0);await until(()=>!page.isLoading());assert.equal((await call(a,'history:go',{sessionId,snapshotId:list.result.snapshotId,entryId:list.result.entries[0].entryId})).exit,1);
|
||
|
|
const initial=(await call(b,'window:state')).result;assert.equal(typeof initial.width,'number');assert.equal((await call(a,'window:control',{command:'maximize'})).exit,1);
|
||
|
|
for(const command of ['maximize','restore','minimize','restore','focus']){if(command==='focus'){win.blur();await until(()=>!win.isFocused());}assert.equal((await call(b,'window:control',{command})).exit,0);await wait(150);if(command==='maximize')assert.equal(win.isMaximized(),true);if(command==='minimize')assert.equal(win.isMinimized(),true);if(command==='restore')await until(()=>!win.isMinimized()&&!win.isMaximized());if(command==='focus')await until(()=>win.isFocused());}
|
||
|
|
assert.equal(win.isMinimized(),false);
|
||
|
|
const change=async modify=>{const p=(await overview()).policy;modify(p);p.version++;assert.equal((await fetch(process.env.SAFETYNET_SMOKE_SERVER+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(p)})).status,200);await until(async()=>{const s=await win.webContents.executeJavaScript('window.safetynet.state()');return s.data.ready&&s.data.policyVersion===p.version;});};
|
||
|
|
await change(p=>{p.agents.grants.find(g=>g.id==='state-b').enabled=false;});assert.equal((await call(b,'window:control',{command:'maximize'})).exit,1);assert.equal(win.isMaximized(),false);
|
||
|
|
await change(p=>{p.agents.enabled=false;});assert.equal((await call(a,'history:list',{sessionId})).exit,1);assert.equal((await call(a,'window:state')).exit,1);
|
||
|
|
const events=await until(async()=>{const e=(await overview()).events.filter(e=>e.kind==='agent.action');return e.some(e=>e.metadata.agent?.action==='history:go')&&e.some(e=>e.metadata.agent?.action==='window:control')&&e;});
|
||
|
|
for(const event of events){const encoded=JSON.stringify(event);for(const text of ['synthetic-private','Synthetic confidential inventory title','pageState',base+'/old'])assert.ok(!encoded.includes(text),'Ordinary audit contains protected history content');}
|
||
|
|
await wait(1500);const traceProof=[];
|
||
|
|
for(const action of ['history:go','window:control']){const event=events.find(e=>e.metadata.agent?.action===action&&e.metadata.agent.outcome==='completed'&&e.metadata.traceId);assert.ok(event);const response=await fetch('http://127.0.0.1:4318/api/trace/'+event.metadata.traceId);assert.equal(response.ok,true);const trace=await response.json(),spans=trace.spans||trace,root=spans.find(s=>s.operation==='agent.request');assert.ok(root);const dispatch=spans.find(s=>s.operation==='agent.dispatch'&&s.parentSpanId===root.spanId);assert.ok(dispatch);const agentAction=spans.find(s=>s.name==='browser.agent'&&s.operation===action&&s.parentSpanId===dispatch.spanId);assert.ok(agentAction,'Missing named agent action');assert.ok(spans.some(s=>s.name===(action==='history:go'?'browser.navigation':'browser.window')&&s.operation===action&&s.parentSpanId===agentAction.spanId),'Missing named native stage');traceProof.push({action,principalId:event.metadata.agent.principalId,traceId:event.metadata.traceId,stages:spans.map(s=>({name:s.name,operation:s.operation,parentSpanId:s.parentSpanId,spanId:s.spanId,durationMs:s.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,'extended-agent-controls-native-proof.json'),JSON.stringify({actualElectron:true,twoActors:true,historyMetadataMasked:true,outOfHostHistoryExcluded:true,crossActorGrantDenied:true,indexedNavigationExactlyOnce:true,snapshotReplayDenied:true,staleSnapshotDenied:true,nativeWindowCommands:true,nativeRestorePostcondition:true,nativeFocusAfterDeliberateBlur:true,targetedRevokeBeforeEffect:true,globalDisable:true,ordinaryAuditContentFree:true,health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites,counts:health.counts},summary,traceProof},null,2));
|
||
|
|
console.log('Extended agent controls native proof passed');require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(0);
|
||
|
|
}catch(error){console.error(error.stack);if(page)require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(1);}});
|