Implement managed browser security, individual agents and observability
This commit is contained in:
1 parent
ff482ebe23
commit
ea448de2d4
172 files changed
+5404
-347
No files matched your search
@@ -15,4 +15,28 @@ test('browser diagnostics exports real OTLP parent/child traces and correlated s
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user