Files
safetynet/scripts/interaction-evidence-electron.cjs
T

46 lines
9.3 KiB
JavaScript

'use strict';
const {app,BrowserWindow,clipboard,dialog}=require('electron');
const 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));
let stage='startup',page,originalClipboard;
async function until(fn){const end=Date.now()+45000;while(Date.now()<end){if(await fn())return;await wait(100);}throw Error(`Fixture deadline: ${stage}`);}
app.whenReady().then(async()=>{try{
let win;await until(()=>{win=BrowserWindow.getAllWindows()[0];return win&&!win.webContents.isLoading()&&win.webContents.getURL().endsWith('index.html');});await wait(300);
const result=await win.webContents.executeJavaScript(`window.safetynet.enroll(${JSON.stringify({server:process.env.SAFETYNET_SMOKE_SERVER,code:process.env.SAFETYNET_SMOKE_CODE})})`);assert.equal(result.ok,true);await wait(600);
page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;
const state=()=>win.webContents.executeJavaScript('window.safetynet.state()').then(r=>r.data),base=process.env.SAFETYNET_SMOKE_SERVER;
const headers={authorization:`Bearer ${process.env.SAFETYNET_SMOKE_ADMIN}`,'content-type':'application/json'};
const overview=()=>fetch(base+'/api/admin/overview',{headers}).then(r=>r.json());
const records=async()=>((await overview()).events||[]).filter(e=>e.kind==='evidence.recorded'&&e.metadata.evidenceType==='interaction');
async function key(code){page.focus();page.sendInputEvent({type:'keyDown',keyCode:code,modifiers:['control']});page.sendInputEvent({type:'keyUp',keyCode:code,modifiers:['control']});await wait(800);await until(async()=>!(await state()).clipboardPending);}
originalClipboard=await clipboard.readText();
stage='copy';await page.executeJavaScript("(()=>{const r=document.createRange();r.selectNodeContents(document.getElementById('clipboard-source'));const s=getSelection();s.removeAllRanges();s.addRange(r)})()");await key('C');assert.equal(await clipboard.readText(),'Synthetic public selection');await until(async()=>(await records()).length===1);
stage='paste';await clipboard.writeText('Synthetic pasted text');await page.executeJavaScript("document.getElementById('public-input').focus()");await key('V');assert.equal(await page.executeJavaScript("document.getElementById('public-input').value"),'Synthetic pasted text');await until(async()=>(await records()).length===2);
stage='cut';await page.executeJavaScript("(()=>{const e=document.getElementById('public-input');e.focus();e.select()})()");await key('X');assert.equal(await page.executeJavaScript("document.getElementById('public-input').value"),'');assert.equal(await clipboard.readText(),'Synthetic pasted text');await until(async()=>(await records()).length===3);
stage='password exclusion';await page.executeJavaScript("document.getElementById('private-input').focus()");await clipboard.writeText('Synthetic password attempt');await key('V');assert.equal(await page.executeJavaScript("document.getElementById('private-input').value"),'never-export-this');await wait(600);assert.equal((await records()).length,3);
stage='agent click';const connection=await win.webContents.executeJavaScript('window.safetynet.agentConnection()');assert.equal(connection.ok,true);
const sessionId=(await state()).activeTabId,requestFile=path.join(process.env.SAFETYNET_SMOKE_USERDATA,'interaction-request.json');
fs.writeFileSync(requestFile,JSON.stringify({action:'click',args:{sessionId,selector:'#public-button'}}));
const child=require('node:child_process').spawn(process.execPath,['scripts/agent-client.cjs','--connection',connection.data.credentialPath,'--request',requestFile],{windowsHide:true,stdio:['ignore','pipe','pipe']});let output='';child.stdout.on('data',c=>output+=c);child.stderr.on('data',()=>{});const exit=await new Promise(r=>{child.once('exit',r);child.once('error',()=>r(1));});assert.equal(exit,0);assert.equal(JSON.parse(output).result.completed,true);assert.equal(await page.executeJavaScript("document.getElementById('public-button').dataset.clicked"),'1');await until(async()=>(await records()).length===4);
const recorded=await records(),payloads=[];
stage='analyst reveal';for(const e of recorded){const response=await fetch(base+`/api/admin/evidence/${e.metadata.evidenceId}?reason=investigation`,{headers});assert.equal(response.status,200);const item=await response.json();payloads.push(item.payload);}
assert.deepEqual(payloads.map(p=>p.action).sort(),['click','copy','cut','paste']);assert.ok(payloads.every(p=>p.sessionId===sessionId&&p.outcome==='completed'&&p.sensitiveField===false));assert.ok(payloads.some(p=>p.action==='click'&&p.captureSource==='agent-adapter'&&p.target?.tag==='BUTTON'));
const serialized=JSON.stringify(payloads),eventsBefore=(await overview()).events;
for(const marker of ['Synthetic public selection','Synthetic pasted text','Synthetic control label','Synthetic password attempt','never-export-this','#public-button','#private-input']){assert.equal(serialized.includes(marker),false);assert.equal(JSON.stringify(eventsBefore).includes(marker),false);}
assert.ok(eventsBefore.filter(e=>e.kind==='evidence.accessed').length>=4);
stage='admin policy screenshot';const adminWindow=new BrowserWindow({width:1440,height:1000,show:false,webPreferences:{nodeIntegration:false,contextIsolation:true,sandbox:true}});
await adminWindow.webContents.session.cookies.set({url:base,name:'sn_session',value:process.env.SAFETYNET_SMOKE_REVIEWER,httpOnly:true,sameSite:'strict'});await adminWindow.loadURL(base+'/');
await until(()=>adminWindow.webContents.executeJavaScript("(()=>{const e=document.getElementById('evidence-policy-interactions');return !!e&&e.checked&&!e.disabled})()"));
const crop=await adminWindow.webContents.executeJavaScript("(()=>{const section=document.getElementById('evidence-policy-controls');section.scrollIntoView({block:'start'});const r=section.getBoundingClientRect();return {x:Math.max(0,Math.floor(r.x)),y:Math.max(0,Math.floor(r.y)),width:Math.min(innerWidth-Math.max(0,Math.floor(r.x)),Math.ceil(r.width)),height:Math.min(innerHeight-Math.max(0,Math.floor(r.y)),Math.ceil(r.height))}})()");
await wait(200);fs.writeFileSync(path.resolve('artifacts/interaction-evidence-admin-controls.png'),(await adminWindow.webContents.capturePage(crop)).toPNG());adminWindow.destroy();
stage='independent policy review';const policy=(await overview()).policy;policy.version++;policy.evidence.captureInteractions=false;
const proposed=await fetch(base+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(policy)});assert.equal(proposed.status,202);const review=await proposed.json();
const approved=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(approved.status,200);await until(async()=>(await state()).policyVersion===policy.version&&(await state()).ready);
stage='disabled interaction';await page.executeJavaScript("document.getElementById('public-input').focus()");await clipboard.writeText('Synthetic post-disable text');await key('V');assert.equal(await page.executeJavaScript("document.getElementById('public-input').value"),'Synthetic post-disable text');await wait(600);assert.equal((await records()).length,4);
stage='download independently enabled';dialog.showSaveDialog=async()=>({canceled:false,filePath:path.join(process.env.SAFETYNET_SMOKE_USERDATA,'safe.bin')});await page.executeJavaScript("document.getElementById('safe-download').click()");await until(async()=>{const s=await state();return s.ready&&s.message==='Download scanned and saved.';});assert.deepEqual(fs.readFileSync(path.join(process.env.SAFETYNET_SMOKE_USERDATA,'safe.bin')),Buffer.from([0,1,2,3,255]));await until(async()=>((await overview()).events||[]).some(e=>e.kind==='evidence.recorded'&&e.metadata.evidenceType==='download'));
const after=(await overview()).events,sources=recorded.map(e=>after.find(source=>source.id===e.metadata.sourceEventId));assert.ok(sources.every(Boolean));assert.ok(sources.every(e=>e.metadata.traceId));
fs.writeFileSync(path.resolve('artifacts/interaction-evidence-native-proof.json'),JSON.stringify({actualElectron:true,nativeCopyPasteCut:true,agentClickExactlyOnce:true,passwordControlExcluded:true,noRawContentLabelsSelectors:true,completedActionsBoundToSession:true,analystAccessAudited:true,independentSignedDisable:true,actualAdminControlsScreenshot:true,disabledInteractionLeavesDownloadCapture:true,eventIds:sources.map(e=>e.id),traceIds:sources.map(e=>e.metadata.traceId)},null,2));
console.log('Native protected interaction evidence proof passed.');await clipboard.writeText(originalClipboard);require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(0);
}catch(error){if(originalClipboard!==undefined)await clipboard.writeText(originalClipboard);if(page)require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);fs.writeFileSync(path.resolve('artifacts/interaction-evidence-native-error.json'),JSON.stringify({stage,name:error.name,message:error.message},null,2));console.error(`Interaction evidence fixture failed at ${stage}: ${error.name}`);app.exit(1);}});