Files
safetynet/tests/diagnostics.test.mjs
T

18 lines
8.5 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {createServer} from 'node:http';import {DiagnosticsSchema,diagnosticsEndpointAllowed} from '../packages/core/diagnostics.mjs';
const require=createRequire(import.meta.url),{createDiagnostics,attributes}=require('../apps/browser/diagnostics.cjs');
const delay=ms=>new Promise(r=>setTimeout(r,ms));
async function collector(t){const requests=[];const server=createServer((req,res)=>{const chunks=[];req.on('data',c=>chunks.push(c));req.on('end',()=>{requests.push({path:req.url,headers:req.headers,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=>server.once('listening',r));t.after(()=>new Promise(r=>{server.closeAllConnections();server.close(r);}));return {requests,endpoint:`http://127.0.0.1:${server.address().port}`};}
const spans=requests=>requests.filter(r=>r.path.endsWith('/traces')).flatMap(r=>r.payload.resourceSpans||[]).flatMap(r=>r.scopeSpans||[]).flatMap(r=>r.spans||[]);
const logs=requests=>requests.filter(r=>r.path.endsWith('/logs')).flatMap(r=>r.payload.resourceLogs||[]).flatMap(r=>r.scopeLogs||[]).flatMap(r=>r.logRecords||[]);
const config=endpoint=>DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000});
test('diagnostics schema defaults disabled and rejects insecure or credentialed collectors',()=>{assert.equal(DiagnosticsSchema.parse({}).enabled,false);for(const endpoint of ['http://localhost:4318','http://10.0.0.1:4318','https://user:[email protected]','https://example.com/?token=secret','https://example.com/#secret','file:///private','ftp://example.com'])assert.equal(diagnosticsEndpointAllowed(endpoint),false,endpoint);for(const endpoint of ['https://collector.company.test/otel','http://127.0.0.1:4318','http://127.0.0.2:4318','http://[::1]:4318'])assert.equal(diagnosticsEndpointAllowed(endpoint),true);assert.equal(DiagnosticsSchema.safeParse({enabled:true}).success,false);assert.equal(DiagnosticsSchema.safeParse({enabled:false,rawContent:true}).success,false);assert.equal(DiagnosticsSchema.safeParse({maxQueueSize:999999}).success,false);});
test('browser diagnostics exports real OTLP parent/child traces and correlated safe logs',{timeout:15000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());const secret='PASSWORD_TOKEN_SECRET_MUST_NEVER_EXPORT';const oldHeaders=process.env.OTEL_EXPORTER_OTLP_HEADERS,oldResource=process.env.OTEL_RESOURCE_ATTRIBUTES;process.env.OTEL_EXPORTER_OTLP_HEADERS=`authorization=Bearer ${secret},x-private=${secret}`;process.env.OTEL_RESOURCE_ATTRIBUTES=`secret=${secret}`;t.after(()=>{if(oldHeaders===undefined)delete process.env.OTEL_EXPORTER_OTLP_HEADERS;else process.env.OTEL_EXPORTER_OTLP_HEADERS=oldHeaders;if(oldResource===undefined)delete process.env.OTEL_RESOURCE_ATTRIBUTES;else process.env.OTEL_RESOURCE_ATTRIBUTES=oldResource;});
assert.equal(diagnostics.configure(config(endpoint),{policyVersion:4,deviceId:'00000000-0000-4000-8000-000000000001'}),true);let calls=0;
const result=await diagnostics.span('browser.ipc',{operation:'inspect-file',prompt:secret,url:`https://example.com/?token=${secret}`,sessionId:'00000000-0000-4000-8000-000000000002'},async()=>{calls++;const ids=diagnostics.correlation();assert.match(ids.traceId,/^[a-f0-9]{32}$/);assert.match(ids.spanId,/^[a-f0-9]{16}$/);diagnostics.event('browser.security',{decision:'allow',password:secret});return diagnostics.span('browser.inspect.local',{bytes:24,policyVersion:4},async()=>{await delay(10);return 7;});});assert.equal(result,7);assert.equal(calls,1);
const failure=new Error(secret);await assert.rejects(diagnostics.span('browser.agent',{operation:'read',token:secret},async()=>{throw failure;}),error=>error===failure);await diagnostics.span(secret,{operation:secret},async()=>{});await diagnostics.flush();
const traceSpans=spans(requests),logRecords=logs(requests);assert.equal(traceSpans.length,3);const parent=traceSpans.find(s=>s.name==='browser.ipc'),child=traceSpans.find(s=>s.name==='browser.inspect.local');assert.ok(parent&&child);assert.equal(child.traceId,parent.traceId);assert.equal(child.parentSpanId,parent.spanId);assert.ok(BigInt(child.endTimeUnixNano)>BigInt(child.startTimeUnixNano));assert.ok(child.attributes.some(a=>a.key==='safetynet.duration.ms'&&(a.value.doubleValue??a.value.intValue)>=1));assert.ok(logRecords.length>=3);assert.ok(logRecords.some(log=>log.traceId===parent.traceId&&log.spanId===parent.spanId));assert.equal(JSON.stringify(requests).includes(secret),false);assert.equal(requests.some(r=>r.headers.authorization||r.headers['x-private']),false);assert.ok(traceSpans.find(s=>s.name==='browser.agent').status.code===2);assert.equal(JSON.stringify(requests).includes('process.command'),false);
});
test('disable and reconfiguration discard queued generation before export',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));await diagnostics.span('browser.navigation',{operation:'navigate'},async()=>{});diagnostics.event('browser.security',{decision:'block'});diagnostics.configure({enabled:false});await diagnostics.flush();await delay(100);assert.equal(requests.length,0);let calls=0;await diagnostics.span('browser.navigation',{},async()=>{calls++;});assert.equal(calls,1);assert.equal(diagnostics.status().enabled,false);diagnostics.configure(config(endpoint),{policyVersion:9});await diagnostics.span('browser.navigation',{operation:'navigate'},async()=>{});await diagnostics.flush();assert.equal(spans(requests).length,1);assert.equal(spans(requests)[0].attributes.find(a=>a.key==='safetynet.policy.version').value.intValue,9);diagnostics.configure({...config(endpoint),endpoint:'http://10.0.0.1:4318'});assert.equal(diagnostics.status().enabled,false);});
test('diagnostics bounded queues expire old records and never repeat failed security operations',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure({...config(endpoint),maxQueueSize:16,maxRecordAgeMs:1000});await diagnostics.span('browser.request',{requestType:'xhr'},async()=>{});await delay(1100);await diagnostics.flush();assert.equal(requests.length,0,'Expired diagnostic record exported');diagnostics.configure({...config(endpoint),maxQueueSize:16});for(let i=0;i<50;i++)diagnostics.event('browser.request',{requestType:'xhr'});await diagnostics.flush();assert.ok(logs(requests).length<=50);for(const request of requests)if(request.path.endsWith('/logs'))assert.ok(logs([request]).length<=16,'Export batch exceeded bounded queue');let calls=0;diagnostics.configure({...config(endpoint),endpoint:'http://127.0.0.1:1',exportTimeoutMs:250});assert.equal(await diagnostics.span('browser.request',{},async()=>{calls++;return 'security decision';}),'security decision');await diagnostics.flush();assert.equal(calls,1);assert.ok(diagnostics.status().failedExports>=1);});
test('diagnostic attributes are typed and never invoke hostile getters',()=>{const input={operation:'read',sessionId:'not-a-uuid',status:'ok',secret:'never',bytes:32};Object.defineProperty(input,'prompt',{enumerable:true,get(){throw Error('getter called');}});assert.deepEqual(attributes(input),{'safetynet.operation':'read','safetynet.status':'ok','safetynet.bytes':32});});
test('OTLP redirects do not forward diagnostics and zero sampling suppresses records',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t);const redirect=createServer((_req,res)=>{res.statusCode=307;res.setHeader('location',endpoint+'/v1/traces');res.end('redirect');}).listen(0,'127.0.0.1');await new Promise(r=>redirect.once('listening',r));t.after(()=>new Promise(r=>{redirect.closeAllConnections();redirect.close(r);}));const diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(`http://127.0.0.1:${redirect.address().port}`));await diagnostics.span('browser.navigation',{},async()=>{});await diagnostics.flush();assert.equal(requests.length,0,'Collector redirect received diagnostic payload');assert.ok(diagnostics.status().failedExports>=1);diagnostics.configure({...config(endpoint),sampleRate:0,debugSampleRate:0});await diagnostics.span('browser.ipc',{operation:'state'},async()=>{assert.deepEqual(diagnostics.correlation(),{});diagnostics.event('browser.security',{decision:'block'});});await diagnostics.flush();assert.equal(requests.length,0);});