Add protected interaction evidence and measured debugging phases

This commit is contained in:
Karl committed 2026-10-13 18:04:02 +02:00
1 parent ea448de2d4
commit 244f01630b
42 files changed
+640 -87

No files matched your search

+11 -3
View File
@@ -3,10 +3,13 @@ const {app,BrowserWindow}=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)),headers={authorization:`Bearer ${process.env.SAFETYNET_SMOKE_ADMIN}`,'content-type':'application/json'};
async function until(fn){for(let n=0;n<200;n++){const result=await fn();if(result)return result;await wait(100);}throw Error('Individual native fixture timed out');}
const startupAt=Date.now();
function safeSnapshot(stage){return {stage,elapsedMs:Date.now()-startupAt,phase:process.env.SAFETYNET_SMOKE_PHASE,windows:BrowserWindow.getAllWindows().map(w=>({destroyed:w.isDestroyed(),loading:w.webContents.isLoading(),urlKind:w.webContents.getURL().endsWith('index.html')?'shell':w.webContents.getURL()===''?'empty':'other',children:w.contentView.children.length})),metrics:app.getAppMetrics().map(m=>({type:m.type,cpu:m.cpu.percentCPUUsage,idleWakeupsPerSecond:m.cpu.idleWakeupsPerSecond,memoryWorkingSetSize:m.memory.workingSetSize}))};}
async function until(fn,stage='native-wait'){const started=Date.now();for(let n=0;n<200;n++){const result=await fn();if(result)return result;await wait(100);}fs.writeFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'individual-agent-timeout-'+process.env.SAFETYNET_SMOKE_PHASE+'.json'),JSON.stringify({...safeSnapshot(stage),waitElapsedMs:Date.now()-started},null,2));throw Error('Individual native fixture timed out: '+stage);}
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;});
const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;},'initial-shell-load');
fs.writeFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'individual-agent-startup-'+process.env.SAFETYNET_SMOKE_PHASE+'.json'),JSON.stringify(safeSnapshot('shell-ready'),null,2));
if(process.env.SAFETYNET_SMOKE_PHASE==='restart'){
await until(async()=> (await win.webContents.executeJavaScript('window.safetynet.state()')).data.ready);
page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;
@@ -17,6 +20,7 @@ app.whenReady().then(async()=>{let page;try{
const proofFile=path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'individual-agent-native-proof.json'),proof=JSON.parse(fs.readFileSync(proofFile,'utf8'));proof.durableGenerationFloorAfterRestart=true;proof.oldDescriptorRejectedAfterRestart=true;fs.writeFileSync(proofFile,JSON.stringify(proof,null,2));
require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);console.log('Individual generation floor restart proof passed');app.exit(0);return;
}
page=win.contentView.children.find(v=>v.webContents!==win.webContents)?.webContents;
const enroll=await win.webContents.executeJavaScript(`window.safetynet.enroll(${JSON.stringify({server:process.env.SAFETYNET_SMOKE_SERVER,code:process.env.SAFETYNET_SMOKE_CODE})})`);assert.equal(enroll.ok,true);
page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading());
assert.equal((await win.webContents.executeJavaScript('window.safetynet.agentConnection()')).ok,false);
@@ -32,6 +36,10 @@ app.whenReady().then(async()=>{let page;try{
const requestFile=path.join(app.getPath('userData'),'individual-fixture-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 sessions=await call(a,'sessions');assert.equal(sessions.exit,0);const sessionId=sessions.result[0].id;
assert.equal((await call(a,'navigate',{sessionId,url:process.env.SAFETYNET_SMOKE_SINK+'/protected-title'})).exit,0);
await until(()=>page.getTitle()==='Synthetic confidential inventory title','protected-title-load');
const protectedInventory=await call(a,'sessions');assert.equal(protectedInventory.exit,0);assert.equal(protectedInventory.result.find(row=>row.id===sessionId)?.title,'Protected page','Page-controlled confidential title escaped inventory classification');
assert.equal((await call(a,'navigate',{sessionId,url:process.env.SAFETYNET_SMOKE_SINK+'/allowed'})).exit,0);
assert.equal((await call(a,'read',{sessionId})).exit,0);assert.equal((await call(a,'application:state')).exit,1);assert.equal((await call(b,'application:state')).exit,0);assert.equal((await call(b,'read',{sessionId})).exit,1);
assert.equal((await call(a,'navigate',{sessionId,url:process.env.SAFETYNET_SMOKE_SINK+'/allowed'})).exit,0,'Approved host navigation failed');
assert.equal((await call(a,'navigate',{sessionId,url:process.env.SAFETYNET_SMOKE_FORBIDDEN})).exit,1,'Actor reached forbidden host');
@@ -51,6 +59,6 @@ app.whenReady().then(async()=>{let page;try{
assert.ok(traceProof,'Actual agent request trace missing');
const root=traceProof.stages.find(s=>s.operation==='agent.request');assert.equal(root.parentSpanId,null);
for(const operation of ['agent.decrypt','agent.authorize','agent.dispatch','agent.project','agent.audit','agent.encrypt'])assert.ok(traceProof.stages.some(s=>s.operation===operation&&s.parentSpanId===root.spanId&&Number.isFinite(s.durationMs)&&s.durationMs>=0),'Missing correlated stage '+operation);
fs.writeFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'individual-agent-native-proof.json'),JSON.stringify({actualElectron:true,explicitGrantSelection:true,renderedGrantChoices:true,uiPairingBothActors:true,separateDPAPIDescriptors:true,noLegacyDescriptor:true,twoActors:true,scopeIntersection:true,approvedHostNavigation:true,forbiddenHostDeniedBeforeTransmission:true,forbiddenRedirectDeniedBeforeTransmission:true,forbiddenReceiverRequests:0,humanNavigationPreserved:true,ownedTabFetchAfterHumanNavigation:true,ownerlessSessionFetchBlockedBeforeTransmission:true,ownerlessReceiverRequests:0,targetedRevocation:true,unaffectedPeer:true,liveGenerationRollbackDenied:true,attributedAudit:true,traceProof},null,2));console.log('Individual agent native proof passed');
fs.writeFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'individual-agent-native-proof.json'),JSON.stringify({actualElectron:true,classifiedInventoryRedactsTitlePreservesId:true,explicitGrantSelection:true,renderedGrantChoices:true,uiPairingBothActors:true,separateDPAPIDescriptors:true,noLegacyDescriptor:true,twoActors:true,scopeIntersection:true,approvedHostNavigation:true,forbiddenHostDeniedBeforeTransmission:true,forbiddenRedirectDeniedBeforeTransmission:true,forbiddenReceiverRequests:0,humanNavigationPreserved:true,ownedTabFetchAfterHumanNavigation:true,ownerlessSessionFetchBlockedBeforeTransmission:true,ownerlessReceiverRequests:0,targetedRevocation:true,unaffectedPeer:true,liveGenerationRollbackDenied:true,attributedAudit:true,traceProof},null,2));console.log('Individual agent 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);}});
+2 -1
View File
@@ -17,9 +17,10 @@ function requireSeparator(){return process.platform==='win32'?'\\':'/';}
const store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');
let forbiddenRequests=0,ownerlessRequests=0;const forbidden=createServer((_req,res)=>{forbiddenRequests++;res.end('Forbidden receiver');}).listen(0,'127.0.0.2');await new Promise(r=>forbidden.once('listening',r));
const forbiddenUrl=`http://127.0.0.2:${forbidden.address().port}/forbidden`;
const sink=createServer((req,res)=>{if(req.url==='/count'){res.setHeader('content-type','application/json');res.end(JSON.stringify({forbiddenRequests,ownerlessRequests}));return;}if(req.url==='/ownerless')ownerlessRequests++;if(req.url==='/redirect'){res.writeHead(302,{location:forbiddenUrl});res.end();return;}res.setHeader('content-type','text/html');res.end('<!doctype html><title>Individual actor fixture</title><h1>Public actor fixture</h1>');}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));
const sink=createServer((req,res)=>{if(req.url==='/count'){res.setHeader('content-type','application/json');res.end(JSON.stringify({forbiddenRequests,ownerlessRequests}));return;}if(req.url==='/ownerless')ownerlessRequests++;if(req.url==='/redirect'){res.writeHead(302,{location:forbiddenUrl});res.end();return;}res.setHeader('content-type','text/html');res.end(req.url==='/protected-title'?'<!doctype html><title>Synthetic confidential inventory title</title><h1>Public actor fixture</h1>':'<!doctype html><title>Individual actor fixture</title><h1>Public actor fixture</h1>');}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));
const p=store.get('policy');p.browser.homeUrl=`http://127.0.0.1:${sink.address().port}`;
p.agents={...p.agents,enabled:true,identityMode:'individual',scopes:['sessions','navigate','read','application:state','settings:get'],allowedHosts:['127.0.0.1','127.0.0.2'],grants:[{id:'reader-a',name:'Reader A',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions','navigate','read'],allowedHosts:['127.0.0.1'],generation:1},{id:'state-b',name:'State B',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['application:state'],allowedHosts:['127.0.0.1'],generation:1}]};
p.dlp.rules.push({id:'inventory-confidential-fixture',label:'Synthetic inventory fixture',action:'block',kind:'literal',value:'Synthetic confidential inventory title'});
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,'individual-fixture');
const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement: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}`;
+45
View File
@@ -0,0 +1,45 @@
'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);}});
+53
View File
@@ -0,0 +1,53 @@
import { createServer } from 'node:http';
import { createRequire } from 'node:module';
import { mkdtempSync,rmSync,mkdirSync,writeFileSync,readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join,resolve } from 'node:path';
import { spawn,execFileSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { createApp } from '../apps/server/app.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
import { openStore,digest } from '../apps/server/store.mjs';
const require=createRequire(import.meta.url),electron=require('electron');
if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)process.env.OTEL_EXPORTER_OTLP_ENDPOINT=process.env.SAFETYNET_TEST_OTLP_ENDPOINT;
const smokeTelemetry=startTelemetry();
const temporary=mkdtempSync(join(tmpdir(),'safetynet-smoke-'));mkdirSync('artifacts',{recursive:true});
const evidenceKeyFile=join(temporary,'evidence.key');writeFileSync(evidenceKeyFile,randomBytes(32),{mode:0o600});
if(process.platform==='win32')execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_EVIDENCE_KEY -AclObject $acl"],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_EVIDENCE_KEY:evidenceKeyFile},stdio:'pipe'});
const sink=createServer((req,res)=>{if(req.url==='/download-safe'){res.setHeader('Content-Type','application/octet-stream');res.setHeader('Content-Disposition','attachment; filename="safe.bin"');res.end(Buffer.from([0,1,2,3,255]));return;}res.setHeader('Content-Type','text/html');res.end('<!doctype html><title>Interaction fixture</title><pre id="clipboard-source">Synthetic public selection</pre><input id="public-input" type="text"><input id="private-input" type="password" value="never-export-this"><button id="public-button" onclick="this.dataset.clicked=String(Number(this.dataset.clicked||0)+1)">Synthetic control label</button><a id="safe-download" href="/download-safe">Download</a>');}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));
const store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');
const inspectorToken=randomBytes(32).toString('base64url');
const inspectorProcess=spawn(resolve('.runtime/windows/python/python.exe'),['apps/inspector/server.py'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...process.env,INSPECTOR_TOKEN:inspectorToken,INSPECTOR_PORT:'0',INSPECTOR_PARENT_PID:String(process.pid)}});
const inspectorPort=await new Promise((resolve,reject)=>{
let output='';const deadline=setTimeout(()=>{inspectorProcess.kill();reject(Error('Fixture inspector did not start'));},40000);
inspectorProcess.once('exit',()=>{clearTimeout(deadline);reject(Error('Fixture inspector exited before startup'));});
inspectorProcess.on('error',reject);
inspectorProcess.stdout.on('data',chunk=>{
output+=chunk;
if(output.includes('\n')){
clearTimeout(deadline);
try{resolve(JSON.parse(output.split('\n')[0]).port);}catch{reject(Error('Fixture inspector startup rejected'));}
}
});
inspectorProcess.stderr.on('data',()=>{});
});
const p=store.get('policy');p.browser.homeUrl=`http://127.0.0.1:${sink.address().port}`;p.brand={name:'Acme Secure Browser',color:'#245fbc'};p.dlp.rules=[{id:'secret-roadmap',label:'Roadmap',kind:'literal',value:'Secret Roadmap',action:'block'}];if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:process.env.SAFETYNET_TEST_OTLP_ENDPOINT,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'smoke-user');
p.evidence={enabled:true,captureDownloads:true,captureInteractions:true};p.browser.allowDownloads=true;p.security.yara={enabled:true,rules:[{id:'test-marker',name:'Controlled malicious marker',source:'rule Block_Test { strings: $a = "SAFETYNET_BLOCK_TEST" condition: $a }'}],maxScanBytes:1048576};p.agents={enabled:true,scopes:['sessions','navigate','back','forward','reload','read','inspect','scroll','click','type','select','check','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group'],allowedHosts:['127.0.0.1'],allowSensitiveRead:false};if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:process.env.SAFETYNET_TEST_OTLP_ENDPOINT,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);
const config={publicUrl:'http://127.0.0.1',secure:false,adminToken,evidenceKeyFile,requirePQManagement:true,requireIndependentPolicyApproval:true,inspectorUrl:`http://127.0.0.1:${inspectorPort}`,inspectorToken};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.on('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
const reviewerToken=randomBytes(32).toString('base64url');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewerToken),Date.now()+600000,JSON.stringify({sub:'independent-fixture-reviewer',role:'admin'}));
const child=spawn(electron,['scripts/interaction-evidence-electron.cjs'],{stdio:'inherit',windowsHide:true,env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewerToken}});
const timeout=setTimeout(()=>child.kill(),180000);const exitCode=await new Promise(r=>{child.once('exit',code=>r(code??1));child.once('error',()=>r(1));});clearTimeout(timeout);
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();inspectorProcess.kill();
// Only this mkdtemp-created fixture directory is removed.
if(!temporary.startsWith(join(tmpdir(),'safetynet-smoke-')))throw Error('Fixture cleanup boundary invalid');rmSync(temporary,{recursive:true,force:true});await smokeTelemetry?.shutdown();
if(exitCode===0&&process.env.SAFETYNET_TEST_OTLP_ENDPOINT){
const proof=JSON.parse(readFileSync(resolve('artifacts/interaction-evidence-native-proof.json'),'utf8')),endpoint=process.env.SAFETYNET_TEST_OTLP_ENDPOINT;
const health=await fetch(endpoint+'/health').then(r=>r.json());if(health.rejectedWrites!==0)throw Error('Collector reports rejected writes');
await new Promise(resolve=>setTimeout(resolve,3000));
const trace=await fetch(endpoint+'/api/trace/'+proof.traceIds[0]).then(r=>r.json()),spans=trace.spans||[],root=spans.find(s=>s.operation==='agent.request');
if(!root||!spans.some(s=>s.operation==='agent.dispatch'&&s.parentSpanId===root.spanId)||!['evidence.challenge','evidence.decrypt','evidence.store','evidence.encrypt'].every(name=>spans.some(s=>s.name===name&&spans.some(parent=>parent.spanId===s.parentSpanId))))throw Error('Actual interaction evidence trace correlation missing');
const browserStages=['evidence.capture','evidence.challenge','evidence.encrypt','evidence.send','evidence.ack'];if(!browserStages.every(operation=>spans.some(s=>s.name==='browser.evidence'&&s.operation===operation&&spans.some(parent=>parent.spanId===s.parentSpanId))))throw Error('Browser evidence stages missing or uncorrelated');
writeFileSync(resolve('artifacts/interaction-evidence-trace-proof.json'),JSON.stringify({traceId:trace.traceId,linkedNativeEventId:proof.eventIds[0],spanCount:spans.length,agentRootMs:root.durationMs,browserEvidenceStages:spans.filter(s=>s.name==='browser.evidence').map(({operation,durationMs,parentSpanId})=>({operation,durationMs,parentSpanId})),protectedEvidenceStages:spans.filter(s=>s.name.startsWith('evidence.')).map(({name,durationMs,parentSpanId})=>({name,durationMs,parentSpanId})),parentCorrelation:true,collectorRejectedWrites:health.rejectedWrites,collectorStoredBytes:health.storedBytes},null,2));
}
process.exit(exitCode);
+1 -1
View File
@@ -30,6 +30,6 @@ app.whenReady().then(async()=>{try{
const collectorStatus=async()=>await (await fetch(process.env.SAFETYNET_TABS_COLLECTOR+'/status')).json();await until(async()=>{const status=await collectorStatus();return ['browser.ipc','browser.navigation','browser.dns','browser.request'].every(name=>status.names.includes(name))&&status.logs>0&&status.parentChild&&status.correlatedLogs;},'Actual native diagnostics export');
const enrolled=(await state()).deviceId;const response=await fetch(`${process.env.SAFETYNET_TABS_SERVER}/api/admin/devices/${enrolled}/revoke`,{method:'POST',headers:{Authorization:`Bearer ${process.env.SAFETYNET_TABS_ADMIN}`,'content-type':'application/json'},body:'{}'});assert.equal(response.status,200);await until(async()=>!(await state()).ready,'Central revocation');
for(const page of [revokePageA,revokePageB]){assert.equal(page.isDestroyed(),false,'Revocation proof lost native view before request assertion');assert.equal(await page.executeJavaScript(`fetch(${JSON.stringify(base+'/revoked-public')},{method:'POST',body:'Public after revocation'}).then(()=>false).catch(()=>true)`),true,'Revoked background/foreground page sent request');}
await wait(500);const stoppedExports=(await collectorStatus()).exports;for(let i=0;i<5;i++)await state();await wait(600);assert.equal((await collectorStatus()).exports,stoppedExports,'Revoked browser continued diagnostic export');
await wait(500);const stoppedExports=await collectorStatus();for(let i=0;i<5;i++)await state();await wait(600);const afterRevocation=await collectorStatus();assert.ok(stoppedExports.exportsByService.browser>0,'Browser export attribution missing');assert.equal(afterRevocation.exportsByService.browser,stoppedExports.exportsByService.browser,'Revoked browser continued diagnostic export');fs.writeFileSync('artifacts/native-tabs-revocation-diagnostics.json',JSON.stringify({before:stoppedExports.exportsByService,after:afterRevocation.exportsByService,aggregateBefore:stoppedExports.exports,aggregateAfter:afterRevocation.exports},null,2));
const report={actualNativeWebContents:true,actualChromeUi:true,independentDomAndScroll:true,noReloadOnSwitch:true,independentHistory:true,allTabsDlp:true,siteClipboardDenied:true,remoteNodeUnavailable:true,closedContentsDestroyed:true,lastTabFreshGuard:true,centralRevocationAllTabs:true,actualBrowserDiagnostics:true,diagnosticsStoppedOnRevocation:true};fs.mkdirSync('artifacts',{recursive:true});fs.writeFileSync('artifacts/native-tabs-proof.json',JSON.stringify(report,null,2));console.log('NATIVE_TABS_PROOF_OK '+JSON.stringify(report));finish(0);
}catch(error){console.error('NATIVE_TABS_PROOF_FAILED '+error.stack);finish(1);}});
+4 -1
View File
@@ -11,7 +11,10 @@ import {startTelemetry} from '../apps/server/telemetry.mjs';
import {openStore,digest} from '../apps/server/store.mjs';
const require=createRequire(import.meta.url),directory=mkdtempSync(join(tmpdir(),'safetynet-native-tabs-'));
let sink,management,collector,store,child,sdk;const previousEndpoint=process.env.OTEL_EXPORTER_OTLP_ENDPOINT;const requests={},posts=[],diagnosticRequests=[];let privateSentinels=[];
const diagnosticStatus=()=>{const spans=diagnosticRequests.filter(r=>r.path.endsWith('/traces')).flatMap(r=>r.payload.resourceSpans||[]).flatMap(r=>r.scopeSpans||[]).flatMap(r=>r.spans||[]);const logs=diagnosticRequests.filter(r=>r.path.endsWith('/logs')).flatMap(r=>r.payload.resourceLogs||[]).flatMap(r=>r.scopeLogs||[]).flatMap(r=>r.logRecords||[]);return {exports:diagnosticRequests.length,names:[...new Set(spans.map(span=>span.name))],spans:spans.length,logs:logs.length,timings:spans.every(span=>BigInt(span.endTimeUnixNano)>=BigInt(span.startTimeUnixNano)&&span.attributes.some(a=>a.key==='safetynet.duration.ms')),parentChild:spans.some(child=>child.parentSpanId&&spans.some(parent=>parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)),crossProcessTrace:spans.some(child=>child.name==='server.request'&&spans.some(parent=>parent.name==='browser.management'&&parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)),crossProcessTraceIds:[...new Set(spans.filter(child=>child.name==='server.request'&&spans.some(parent=>parent.name==='browser.management'&&parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)).map(span=>span.traceId))],correlatedLogs:logs.some(log=>log.traceId&&spans.some(span=>span.traceId===log.traceId&&span.spanId===log.spanId)),sensitivePresent:privateSentinels.some(secret=>JSON.stringify(diagnosticRequests).includes(secret))};};
// Backend and browser deliberately share this test receiver. Count requests per
// fixed OTLP resource identity so backend flushes cannot impersonate browser exports.
function exportCountsByService(records){const counts={browser:0,controlPlane:0,unknown:0};for(const record of records){const groups=[...(record.payload.resourceSpans||[]),...(record.payload.resourceLogs||[]),...(record.payload.resourceMetrics||[])],services=new Set(groups.map(group=>group.resource?.attributes?.find(a=>a.key==='service.name')?.value?.stringValue));if(services.has('safetynet-browser'))counts.browser++;if(services.has('safetynet-control-plane'))counts.controlPlane++;if(!services.size||[...services].some(v=>v!=='safetynet-browser'&&v!=='safetynet-control-plane'))counts.unknown++;}return counts;}
const diagnosticStatus=()=>{const spans=diagnosticRequests.filter(r=>r.path.endsWith('/traces')).flatMap(r=>r.payload.resourceSpans||[]).flatMap(r=>r.scopeSpans||[]).flatMap(r=>r.spans||[]);const logs=diagnosticRequests.filter(r=>r.path.endsWith('/logs')).flatMap(r=>r.payload.resourceLogs||[]).flatMap(r=>r.scopeLogs||[]).flatMap(r=>r.logRecords||[]);return {exports:diagnosticRequests.length,exportsByService:exportCountsByService(diagnosticRequests),names:[...new Set(spans.map(span=>span.name))],spans:spans.length,logs:logs.length,timings:spans.every(span=>BigInt(span.endTimeUnixNano)>=BigInt(span.startTimeUnixNano)&&span.attributes.some(a=>a.key==='safetynet.duration.ms')),parentChild:spans.some(child=>child.parentSpanId&&spans.some(parent=>parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)),crossProcessTrace:spans.some(child=>child.name==='server.request'&&spans.some(parent=>parent.name==='browser.management'&&parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)),crossProcessTraceIds:[...new Set(spans.filter(child=>child.name==='server.request'&&spans.some(parent=>parent.name==='browser.management'&&parent.spanId===child.parentSpanId&&parent.traceId===child.traceId)).map(span=>span.traceId))],correlatedLogs:logs.some(log=>log.traceId&&spans.some(span=>span.traceId===log.traceId&&span.spanId===log.spanId)),sensitivePresent:privateSentinels.some(secret=>JSON.stringify(diagnosticRequests).includes(secret))};};
try{
collector=createServer((req,res)=>{if(req.method==='GET'&&req.url==='/status'){res.setHeader('content-type','application/json');res.end(JSON.stringify(diagnosticStatus()));return;}const chunks=[];req.on('data',chunk=>chunks.push(chunk));req.on('end',()=>{diagnosticRequests.push({path:req.url,payload:JSON.parse(Buffer.concat(chunks).toString())});res.setHeader('content-type','application/json');res.end('{}');});}).listen(0,'127.0.0.1');await new Promise(r=>collector.once('listening',r));const collectorUrl=`http://127.0.0.1:${collector.address().port}`;process.env.OTEL_EXPORTER_OTLP_ENDPOINT=collectorUrl;sdk=startTelemetry();
sink=createServer((req,res)=>{const path=new URL(req.url,'http://localhost').pathname;requests[path]=(requests[path]||0)+1;
+1 -1
View File
@@ -14,7 +14,7 @@ assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize
for(const file of ['apps/browser/upload-lease.cjs','apps/browser/file-upload-gate.cjs','apps/browser/request-source.cjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged upload binding source is stale: ${file}`);
assert.deepEqual(await fs.readFile(path.join(root,'resources/inspector/upload_lease.py')),await fs.readFile('apps/inspector/upload_lease.py'),'Packaged Windows upload lease helper is stale');
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize('apps/browser/header-inspection.cjs')),await fs.readFile('apps/browser/header-inspection.cjs'),'Packaged outgoing header DLP source is stale');
for(const file of ['apps/browser/evidence-capture.cjs','apps/browser/evidence-delivery.cjs','packages/core/request-transfer-facts.cjs','packages/core/transfer-facts.cjs','packages/core/inspection-stages.cjs','apps/browser/history-navigation.cjs','apps/browser/agent-transport-client.cjs','apps/browser/agent-client.cjs','apps/browser/library-organization.cjs','packages/core/library.mjs','packages/core/branding.mjs','packages/core/file-transfer.cjs','packages/core/file-transfer.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged new security/branding source is stale: ${file}`);
for(const file of ['packages/core/siem.mjs','packages/core/evidence.mjs','packages/core/evidence-payload.cjs','packages/core/interaction-facts.cjs','apps/browser/evidence-capture.cjs','apps/browser/evidence-delivery.cjs','packages/core/request-transfer-facts.cjs','packages/core/transfer-facts.cjs','packages/core/inspection-stages.cjs','apps/browser/history-navigation.cjs','apps/browser/agent-transport-client.cjs','apps/browser/agent-client.cjs','apps/browser/library-organization.cjs','packages/core/library.mjs','packages/core/branding.mjs','packages/core/file-transfer.cjs','packages/core/file-transfer.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged new security/branding source is stale: ${file}`);
for(const file of ['apps/browser/agent-grants.cjs','apps/browser/policy-assignment.cjs','packages/core/policy-groups.mjs','apps/browser/tab-registry.cjs','apps/browser/agent-adapters.cjs','apps/browser/agent-control.cjs','apps/browser/password-fill.cjs','apps/browser/diagnostics.cjs','packages/core/diagnostics.mjs','packages/core/security.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged device/session policy source is stale: ${file}`);
for(const file of ['apps/browser/main.cjs','apps/browser/preload.cjs','apps/browser/site-preload.cjs','apps/browser/clipboard-deny.cjs','apps/browser/clipboard-guard.cjs','apps/browser/clipboard-dom.cjs','apps/browser/page-clipboard-guard.cjs','apps/browser/startup-security.cjs','apps/browser/visual-capture.cjs','apps/browser/renderer.js','apps/browser/index.html','apps/browser/style.css','packages/core/clipboard.mjs']){
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged source is stale: ${file}`);