52 lines
12 KiB
JavaScript
52 lines
12 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);
|
|
stage='human observer installation';await until(()=>page.executeJavaScriptInIsolatedWorld(998,[{code:"!!globalThis.__safetynetHumanInteraction"}],false));await page.executeJavaScript("window.__fixtureTrustedClick=false;window.addEventListener('click',e=>{window.__fixtureTrustedClick=e.isTrusted},true)");
|
|
stage='synthetic human exclusion';await page.executeJavaScript("document.getElementById('public-button').click();document.getElementById('public-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));document.getElementById('public-form').requestSubmit()");await wait(700);assert.equal((await records()).length,4);
|
|
async function physicalClick(id){const point=await page.executeJavaScript(`(()=>{const r=document.getElementById(${JSON.stringify(id)}).getBoundingClientRect();return {x:Math.round(r.left+r.width/2),y:Math.round(r.top+r.height/2)}})()`);page.focus();page.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...point});page.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,...point});await wait(900);}
|
|
stage='physical click';await physicalClick('public-button');assert.equal(await page.executeJavaScript('window.__fixtureTrustedClick'),true);assert.equal(await page.executeJavaScript("document.getElementById('public-button').dataset.clicked"),'3');await until(async()=>(await records()).length===5);
|
|
stage='physical submit';await physicalClick('submit-button');assert.equal(await page.executeJavaScript("document.getElementById('public-form').dataset.submitted"),'3');await until(async()=>(await records()).length===7);
|
|
stage='private form exclusion';await physicalClick('private-submit');await wait(500);assert.equal((await records()).length,7);
|
|
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','click','click','copy','cut','paste','submit']);assert.ok(payloads.every(p=>p.sessionId===sessionId&&p.sensitiveField===false));assert.equal(payloads.filter(p=>p.captureSource==='human-observer'&&p.outcome==='observed').length,3);assert.ok(payloads.every(p=>p.pageUrl===new URL(p.pageUrl).origin+'/'));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','Synthetic private form field','#public-button','#private-input']){assert.equal(serialized.includes(marker),false);assert.equal(JSON.stringify(eventsBefore).includes(marker),false);}
|
|
assert.equal(eventsBefore.filter(e=>e.kind==='evidence.accessed').length,7);
|
|
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);await physicalClick('public-button');assert.equal((await records()).length,7);
|
|
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));sources.sort((a,b)=>Number(b.kind==='agent.action')-Number(a.kind==='agent.action'));assert.equal(sources.filter(e=>e.kind==='browser.interaction').length,3);
|
|
fs.writeFileSync(path.resolve('artifacts/interaction-evidence-native-proof.json'),JSON.stringify({actualElectron:true,physicalTrustedClickAndSubmit:true,syntheticHumanEventsExcluded:true,passwordFormsExcluded:true,humanObservationNotTransmission: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),humanTraceIds:sources.filter(e=>e.kind==='browser.interaction').map(e=>e.metadata.traceId),humanEventIds:sources.filter(e=>e.kind==='browser.interaction').map(e=>e.id)},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);let checkpoint={};try{if(page&&!page.isDestroyed())checkpoint.observer=await page.executeJavaScriptInIsolatedWorld(998,[{code:"(()=>{const s=globalThis.__safetynetHumanInteraction;return {installed:!!s,pending:s?.queue?.length||0,dropped:s?.dropped||0}})()"}],false);const queuePath=path.join(process.env.SAFETYNET_SMOKE_USERDATA,'audit-queue.bin');if(fs.existsSync(queuePath)){const queue=JSON.parse(require('electron').safeStorage.decryptString(fs.readFileSync(queuePath)));checkpoint.pendingHumanSources=queue.filter(e=>e.kind==='browser.interaction').slice(0,16).map(({eventId,traceId,spanId,policyVersion})=>({eventId,traceId,spanId,policyVersion}));}}catch{}fs.writeFileSync(path.resolve('artifacts/interaction-evidence-native-error.json'),JSON.stringify({stage,name:error.name,message:error.message,checkpoint},null,2));console.error(`Interaction evidence fixture failed at ${stage}: ${error.name}`);app.exit(1);}});
|