43 lines
13 KiB
JavaScript
43 lines
13 KiB
JavaScript
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('fixed diagnostic reasons export in real OTLP fault spans and correlated logs while injected reasons are discarded',{timeout:15000},async t=>{
|
|
const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));
|
|
const reasons=['request-source-context-changed','header-source-context-changed','headers-inspection-timeout','request-inspection-timeout','upload-lease-lost','upload-lease-release-failed','invalid-source-owner','policy-changed'];
|
|
const secret='PASSWORD_REASON_SECRET_MUST_NEVER_EXPORT';
|
|
for(const reason of reasons)await diagnostics.span('browser.upload.fault',{reason,status:'error'},async()=>diagnostics.event('browser.security',{reason,decision:'block'}));
|
|
await diagnostics.span('browser.upload.fault',{reason:secret},async()=>diagnostics.event('browser.security',{reason:secret}));
|
|
await diagnostics.flush();const traceSpans=spans(requests),records=logs(requests);
|
|
assert.equal(traceSpans.length,reasons.length+1);
|
|
for(const reason of reasons){
|
|
const span=traceSpans.find(s=>s.attributes.some(a=>a.key==='safetynet.reason'&&a.value.stringValue===reason));assert.ok(span,reason);assert.equal(span.name,'browser.upload.fault');
|
|
assert.ok(records.some(log=>log.traceId===span.traceId&&log.spanId===span.spanId&&log.attributes.some(a=>a.key==='safetynet.reason'&&a.value.stringValue===reason)),reason);
|
|
}
|
|
assert.equal(traceSpans.filter(s=>s.attributes.some(a=>a.key==='safetynet.reason')).length,reasons.length);
|
|
assert.equal(JSON.stringify(requests).includes(secret),false);
|
|
for(const reason of [null,42,{},'upload-lease-lost '+secret,'__proto__'])assert.deepEqual(attributes({reason}),{});
|
|
const hostile={};Object.defineProperty(hostile,'reason',{enumerable:true,get(){throw Error('getter must not run');}});assert.deepEqual(attributes(hostile),{});
|
|
});
|
|
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);});
|
|
|
|
test('inspector timing batches export exact nested spans only inside live debug operations',{timeout:15000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));const base=Date.now()-100,batch={baseUnixMs:base,secret:'STAGE_SECRET',records:[{id:1,parentId:null,stage:'inspect',startOffsetMs:0,durationMs:80,status:'ok',secret:'STAGE_SECRET'},{id:2,parentId:1,stage:'parse',startOffsetMs:10,durationMs:20,status:'ok'},{id:3,parentId:1,stage:'semantic',startOffsetMs:35,durationMs:40,status:'error'}]};diagnostics.importStages(batch);await diagnostics.span('browser.inspect.local',{},async()=>{diagnostics.importStages(batch);});await diagnostics.flush();const output=spans(requests),outer=output.find(s=>s.name==='browser.inspect.local'),inspect=output.find(s=>s.name==='inspector.inspect'),parse=output.find(s=>s.name==='inspector.parse'),semantic=output.find(s=>s.name==='inspector.semantic');assert.equal(output.length,4);assert.equal(inspect.parentSpanId,outer.spanId);assert.equal(parse.parentSpanId,inspect.spanId);assert.equal(semantic.traceId,outer.traceId);assert.equal(semantic.status.code,2);assert.equal(BigInt(parse.startTimeUnixNano),BigInt(base+10)*1000000n);assert.equal(BigInt(parse.endTimeUnixNano)-BigInt(parse.startTimeUnixNano),20000000n);assert.equal(JSON.stringify(requests).includes('STAGE_SECRET'),false);
|
|
diagnostics.configure({...config(endpoint),mode:'operational'});await diagnostics.span('browser.inspect.local',{},async()=>diagnostics.importStages(batch));await diagnostics.flush();assert.equal(spans(requests).filter(s=>s.name.startsWith('inspector.')).length,3);
|
|
});
|
|
test('inspector timing validation rejects entire malformed batches and hostile accessors',{timeout:15000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));const record={id:1,parentId:null,stage:'parse',startOffsetMs:0,durationMs:5,status:'ok'},good=()=>({baseUnixMs:Date.now()-20,records:[record]});const bad=[{...good(),baseUnixMs:Date.now()-61000},{...good(),baseUnixMs:Date.now()+3000},{...good(),records:[record,record]},{...good(),records:[{...record,parentId:2}]},{...good(),records:[{...record,stage:'PASSWORD_SECRET'}]},{...good(),records:[{...record,durationMs:NaN}]},{...good(),records:[{...record,startOffsetMs:29999,durationMs:2}]},{...good(),records:Array(65).fill(record)}];const hostile={};Object.defineProperty(hostile,'baseUnixMs',{get(){throw Error('getter must not run');}});bad.push(hostile);await diagnostics.span('browser.inspect.local',{},async()=>{for(const batch of bad)assert.doesNotThrow(()=>diagnostics.importStages(batch));});await diagnostics.flush();assert.equal(spans(requests).length,1);diagnostics.configure({...config(endpoint),debugSampleRate:0});await diagnostics.span('browser.inspect.local',{},async()=>diagnostics.importStages(good()));await diagnostics.flush();assert.equal(spans(requests).length,1);
|
|
});
|