Files
safetynet/scripts/native-tabs-smoke.mjs

34 lines
7.7 KiB
JavaScript

import assert from 'node:assert/strict';
import {createServer} from 'node:http';
import {createRequire} from 'node:module';
import {mkdtempSync,rmSync,mkdirSync,writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join,resolve,sep,basename} from 'node:path';
import {spawn} 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),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=[];
// 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;
if(path==='/counts'){res.setHeader('content-type','application/json');res.end(JSON.stringify({requests,posts}));return;}
if(req.method==='POST'){posts.push(path);req.resume();res.end('received');return;}
res.setHeader('content-type','text/html');res.end(`<!doctype html><title>${path}</title><h1>Native tab ${path}</h1><input id="remember"><a id="next" href="${path==='/'?'/a-two':'/b-two'}">Next page</a><div style="height:4000px">Unstyled fixture content</div>`);
}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));const url=`http://127.0.0.1:${sink.address().port}`;
store=openStore(join(directory,'management'));const policy=store.get('policy');policy.diagnostics={...policy.diagnostics,enabled:true,mode:'debug',endpoint:collectorUrl,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};policy.browser.homeUrl=url;policy.browser.allowedHosts=['127.0.0.1'];policy.dlp.rules=[{id:'fixture-secret',label:'Fixture secret',kind:'literal',value:'TAB_FIXTURE_SECRET',action:'block'}];store.put('policy',policy);
const code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');privateSentinels=[code,adminToken,'TAB_FIXTURE_SECRET'];store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'synthetic-native-tabs-user');
const config={adminToken,publicUrl:'http://127.0.0.1',secure:false};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}`;
const env={...process.env,SAFETYNET_TABS_COLLECTOR:collectorUrl,SAFETYNET_TABS_SERVER:config.publicUrl,SAFETYNET_TABS_CODE:code,SAFETYNET_TABS_ADMIN:adminToken,SAFETYNET_TABS_SINK:url,SAFETYNET_TABS_USERDATA:join(directory,'browser'),SAFETYNET_SMOKE_USERDATA:join(directory,'browser')};delete env.ELECTRON_RUN_AS_NODE;
child=spawn(require('electron'),['scripts/native-tabs-smoke.cjs'],{env,windowsHide:true,stdio:['ignore','pipe','pipe']});child.stdout.on('data',b=>process.stdout.write(b));child.stderr.on('data',b=>{const message=String(b).replaceAll(code,'[fixture enrollment]').replaceAll(adminToken,'[fixture admin]');process.stderr.write(message);});
const timer=setTimeout(()=>child.kill(),120000);const codeExit=await new Promise(r=>{child.once('exit',c=>r(c??1));child.once('error',()=>r(1));});clearTimeout(timer);assert.equal(codeExit,0,'Actual native tabs proof failed');
assert.equal(posts.length,0,'Blocked fixture data reached receiver');await sdk.shutdown();sdk=null;const diagnostics=diagnosticStatus();assert.ok(diagnostics.spans>0&&diagnostics.logs>0&&diagnostics.timings&&diagnostics.parentChild&&diagnostics.correlatedLogs);assert.equal(diagnostics.sensitivePresent,false);assert.equal(diagnostics.crossProcessTrace,true,'Browser management span must parent actual backend HTTP request');for(const name of ['browser.ipc','browser.navigation','browser.dns','browser.request'])assert.ok(diagnostics.names.includes(name),`Missing native timing ${name}`);writeFileSync('artifacts/native-browser-diagnostics-proof.json',JSON.stringify(diagnostics,null,2));mkdirSync('artifacts',{recursive:true});writeFileSync('artifacts/native-tabs-receiver-proof.json',JSON.stringify({noBlockedPosts:true,requests,posts},null,2));
// eslint-disable-next-line no-unsafe-finally -- Cleanup failure must fail closed rather than report successful protected cleanup.
}finally{child?.kill();await sdk?.shutdown();if(previousEndpoint===undefined)delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;else process.env.OTEL_EXPORTER_OTLP_ENDPOINT=previousEndpoint;for(const server of [management,sink,collector])if(server)await new Promise(r=>{server.closeAllConnections?.();server.close(r);});store?.db.close();const target=resolve(directory);if(!target.startsWith(resolve(tmpdir())+sep)||!basename(target).startsWith('safetynet-native-tabs-'))throw Error('Unsafe fixture cleanup');rmSync(target,{recursive:true,force:true,maxRetries:10,retryDelay:100});}