Audit native transports and retain secure export diagnostics

This commit is contained in:
Karl committed 2026-10-14 00:37:32 +02:00
1 parent 8a253eda89
commit 0a5df7398e
28 files changed
+636 -53

No files matched your search

+10
View File
@@ -31,3 +31,13 @@ for(const change of ['revoke','same-version-policy','output-revoke','client-abor
await assert.rejects(runApprovedAI({store,config,deviceId,policy,prompt:'Public fixture prompt',signal:controller.signal}),error=>error.status===(change==='same-version-policy'?409:403)&&error.ruleId==='ai-authorization-changed');
assert.equal(deliveries,change==='output-revoke'?1:0);
});
test('caller cancellation retains fixed authorization status while native fetch is pending',async t=>{
const directory=mkdtempSync(join(tmpdir(),'safetynet-ai-abort-')),store=openStore(directory),deviceId=randomUUID(),controller=new AbortController();
store.db.prepare('INSERT INTO devices(id,name,platform,user,token_hash) VALUES(?,?,?,?,?)').run(deviceId,'Fixture','win32','Fixture',randomUUID());
let received;const incoming=new Promise(resolve=>{received=resolve;});const provider=createServer((req,_res)=>{req.resume();received();}).listen(0,'127.0.0.1');await new Promise(resolve=>provider.once('listening',resolve));
t.after(async()=>{provider.closeAllConnections();await new Promise(resolve=>provider.close(resolve));store.db.close();rmSync(directory,{recursive:true,force:true});});
const p=store.get('policy');p.ai={...p.ai,enabled:true,trustMode:'local',model:'fixture'};store.put('policy',p);
const pending=runApprovedAI({store,config:{aiBaseUrl:`http://127.0.0.1:${provider.address().port}`,aiModel:'fixture'},deviceId,policy:store.effectivePolicy(deviceId),prompt:'Public fixture',signal:controller.signal});
const rejected=assert.rejects(pending,error=>error.status===403&&error.ruleId==='ai-authorization-changed');await incoming;controller.abort();await rejected;
});
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createServer} from 'node:http';
import {createRequire} from 'node:module';
const {createDiagnostics}=createRequire(import.meta.url)('../apps/browser/diagnostics.cjs');
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function receiver(t,handler){const server=createServer(handler).listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));t.after(()=>new Promise(resolve=>{server.closeAllConnections();server.close(resolve);}));return `http://127.0.0.1:${server.address().port}`;}
const config=endpoint=>({enabled:true,mode:'debug',endpoint,debugSampleRate:1,sampleRate:1,flushIntervalMs:10000,exportTimeoutMs:250});
async function produce(d){await d.span('browser.request',{},async()=>{});await d.flush();}
test('real timeout, refusal and HTTP503 keep fixed reasons without error text',{timeout:15000},async t=>{
const secret='SYNTHETIC_EXPORT_ERROR_MUST_NOT_RETAIN';
for(const kind of ['timeout','refused','503']){let endpoint;if(kind==='refused'){const server=createServer().listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));endpoint=`http://127.0.0.1:${server.address().port}`;await new Promise(resolve=>server.close(resolve));}else endpoint=await receiver(t,(req,res)=>{req.resume();if(kind==='503'){res.writeHead(503,secret,{'retry-after':'100'});res.end(secret);}});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure(config(endpoint));await produce(d);for(let i=0;i<100&&d.status().lifetimeFailedExports<2;i++)await delay(10);const status=d.status();assert.ok(status.lifetimeFailedExports>=1,kind);const reason=kind==='timeout'?'timeout':kind==='refused'?'connection-refused':'http-server';assert.ok(status.failureReasons[reason]>=1,kind);assert.ok(status.failureFacts.every(f=>f.reason===reason));assert.ok(!JSON.stringify(status).includes(secret));assert.ok(!JSON.stringify(status).includes(endpoint));assert.ok(status.failureFacts.every(f=>['traces','logs'].includes(f.signal)&&f.activeAtCompletion&&!f.retirementOverlapped));const before=status.lifetimeFailedExports;d.configure({enabled:false});assert.equal(d.status().lifetimeFailedExports,before);assert.equal(d.status().failedExports,0);}
});
test('inflight retirement cancellation remains a failure with bounded facts and generation context',{timeout:15000},async t=>{
let received=0;const endpoint=await receiver(t,(req,_res)=>{req.resume();received++;});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure({...config(endpoint),exportTimeoutMs:5000},{policyVersion:1});await d.span('browser.request',{},async()=>{});const flush=d.flush();for(let i=0;i<100&&received<2;i++)await delay(10);assert.equal(received,2);d.configure(config(endpoint),{policyVersion:2});await flush;await delay(50);const status=d.status();assert.equal(status.failedExports,0);assert.equal(status.lifetimeFailedExports,2);assert.equal(status.failureReasons.cancelled,2);assert.ok(status.failureFacts.every(f=>f.generation===1&&!f.activeAtCompletion&&f.retirementOverlapped&&f.cancelSignalled));assert.deepEqual(new Set(status.failureFacts.map(f=>f.signal)),new Set(['traces','logs']));const copy=d.status();copy.failureFacts[0].reason='forged';copy.failureReasons.cancelled=0;assert.equal(d.status().failureReasons.cancelled,2);assert.equal(d.status().failureFacts[0].reason,'cancelled');d.configure({enabled:false});assert.equal(d.status().lifetimeFailedExports,2);assert.equal(received,2,'Retirement repeated a request');
});
test('failure history is bounded while lifetime reason totals retain every failed export',{timeout:15000},async t=>{
const endpoint=await receiver(t,(req,res)=>{req.resume();res.writeHead(400,'Synthetic error body');res.end('PRIVATE_BODY_NOT_RETAINED');});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure(config(endpoint));for(let i=0;i<18;i++)await produce(d);for(let i=0;i<100&&d.status().lifetimeFailedExports<2;i++)await delay(10);const status=d.status();assert.equal(status.lifetimeFailedExports,36);assert.equal(status.failureReasons['http-client'],36);assert.equal(status.failureFacts.length,32);assert.ok(!JSON.stringify(status).includes('PRIVATE_BODY_NOT_RETAINED'));
});
test('concurrent rejected flush cannot erase held native export attribution',{timeout:15000},async t=>{
let received=0;const endpoint=await receiver(t,req=>{req.resume();received++;}),d=createDiagnostics();t.after(()=>d.shutdown());d.configure({...config(endpoint),exportTimeoutMs:5000},{policyVersion:1});await d.span('browser.request',{},async()=>{});const first=d.flush();for(let i=0;i<100&&received<2;i++)await delay(10);assert.equal(received,2);await d.span('browser.request',{},async()=>{});const second=d.flush();for(let i=0;i<100&&!d.status().failureReasons['concurrency-limit'];i++)await delay(10);assert.ok(d.status().failureReasons['concurrency-limit']>=1);d.configure({enabled:false});await Promise.all([first,second]);const status=d.status();assert.equal(received,2);assert.equal(status.failureReasons.cancelled,2);assert.equal(status.lifetimeFailedExports,status.failureReasons.cancelled+status.failureReasons['concurrency-limit']);assert.ok(status.failureFacts.filter(f=>f.reason==='cancelled').every(f=>f.cancelSignalled&&f.retirementOverlapped&&f.generation===1));
});
+6
View File
@@ -15,6 +15,12 @@ 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('transport diagnostic phases and fixed failures contain no destination or protocol payload',()=>{
for(const reason of require('../packages/core/transport-facts.cjs').TRANSPORT_MONITOR_REASONS){
assert.deepEqual(attributes({operation:'transport.monitor-unavailable',reason,host:'private.invalid',url:'https://private.invalid/?token=secret',nativeId:'private-cdp-id'}),{'safetynet.operation':'transport.monitor-unavailable','safetynet.reason':reason});
}
assert.deepEqual(attributes({operation:'transport.created',reason:'arbitrary company secret'}),{'safetynet.operation':'transport.created'});
});
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));
+9 -1
View File
@@ -69,5 +69,13 @@ test('concurrent proposal requests are bounded and policy changes during provide
const f=await fixture(t);let release;f.setGate(new Promise(r=>{release=r;}));const pending=f.propose();
for(let n=0;n<100&&f.calls()===0;n++)await new Promise(r=>setTimeout(r,10));assert.equal(f.calls(),1);
assert.equal((await f.propose()).status,409);assert.equal(f.calls(),1);
const p=f.store.get('policy');p.version++;f.store.put('policy',p);release();assert.equal((await pending).status,409);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
const p=f.store.get('policy');p.version++;f.store.put('policy',p);const rejected=await pending;release();assert.equal(rejected.status,409);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
});
test('provider deadline returns fixed 504 before held provider output is released',async t=>{
const f=await fixture(t);let release,deadline;f.setGate(new Promise(resolve=>{release=resolve;}));
const timeout=globalThis.setTimeout;
t.mock.method(globalThis,'setTimeout',(callback,milliseconds,...args)=>{if(milliseconds===60000){deadline=callback;return timeout(()=>{},60000);}return timeout(callback,milliseconds,...args);});
const pending=f.propose();for(let n=0;n<100&&f.calls()===0;n++)await new Promise(resolve=>timeout(resolve,10));assert.equal(f.calls(),1);assert.equal(typeof deadline,'function');
deadline();const result=await pending;release();assert.equal(result.status,504);assert.equal(result.body.error,'Request failed');assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
});
+28
View File
@@ -0,0 +1,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
const source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
const start=source.indexOf('function installTransportObserver(tab){'),end=source.indexOf('function createNativeTab(input={})',start);
assert.ok(start>=0&&end>start);
const id='00000000-0000-4000-8000-000000000001';
function fixture(auditImplementation=async()=>id){
let callbacks;const actions=[],contents={id:1,isDestroyed:()=>false},tab={id,contents,security:{interactionDocumentGeneration:1}};
const context={device:{id},policy:{version:1,expiresAt:new Date(Date.now()+60000).toISOString()},localRelay:{},ready:true,clipboardFatal:false,tabs:{fromContents:()=>tab},Object,Date,
require:specifier=>specifier.endsWith('page-clipboard-guard.cjs')?{createNativeTransportChannel:()=>({private:true})}:{createNativeTransportObserver:value=>{callbacks=value;return {install:async()=>true};}},quarantine:()=>actions.push('quarantine'),
diagnosticSpan:async(_name,_attributes,fn)=>fn(),audit:async(...args)=>{actions.push(args[0]);return auditImplementation(...args);}};
vm.runInNewContext(source.slice(start,end)+'\nthis.install=installTransportObserver;',context);
return {context,actions,install:()=>context.install(tab),callbacks:()=>callbacks,tab};
}
test('actual main quarantines unexpected establishment before asynchronous persistence, without repeating the effect',async()=>{
let release;const gate=new Promise(resolve=>{release=resolve;});const f=fixture(async(kind)=>{if(kind==='transport.established')await gate;return id;});await f.install();
const callback=f.callbacks(),owner=callback.getContext();callback.onEstablished(owner);const pending=callback.audit({transportId:id,protocol:'webtransport',phase:'established',realm:'worker',sessionId:id,connectionEstablished:true,host:'receiver.example'},owner);
assert.equal(f.context.clipboardFatal,true);assert.deepEqual(f.actions,['quarantine','transport.established']);release();await pending;
assert.deepEqual(f.actions,['quarantine','transport.established','quarantine','transport.monitor-unavailable']);
});
test('actual main retains creation policy in lifecycle metadata while current owner remains bound',async()=>{
const records=[],f=fixture(async(...args)=>{records.push(args);return id;});await f.install();const callback=f.callbacks(),owner=callback.getContext();f.context.policy={...f.context.policy,version:2};
await callback.audit({transportId:id,protocol:'webtransport',phase:'closed',realm:'window',sessionId:id,connectionEstablished:false,host:'receiver.example'},owner);
assert.equal(records.length,1);assert.equal(records[0][8].policyVersion,1);assert.equal(records[0][9].phase,'closed');assert.equal(f.context.clipboardFatal,false);
f.context.device={id:'00000000-0000-4000-8000-000000000002'};await callback.audit({phase:'created'},owner);assert.equal(records.length,1,'Old device owner gained a new audit record');
});
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test';import assert from 'node:assert/strict';import {EventEmitter} from 'node:events';import {createRequire} from 'node:module';
const {createNativeTransportObserver}=createRequire(import.meta.url)('../apps/browser/native-transport-observer.cjs');
const id='00000000-0000-4000-8000-000000000001';
function fixture(options={}){const debug=new EventEmitter(),commands=[],events=[],failures=[];let attached=true,owner={deviceId:id,policyVersion:1,sessionId:id,documentGeneration:1};debug.isAttached=()=>attached;debug.attach=()=>{attached=true;};debug.detach=()=>{attached=false;debug.emit('detach',{},'target_closed');};debug.sendCommand=async(method,params,sessionId)=>{commands.push({method,params,sessionId});return {};};const channel={isHealthy:()=>attached,enableNetwork:sessionId=>debug.sendCommand('Network.enable',{maxPostDataSize:0},sessionId),autoAttachTargets:sessionId=>debug.sendCommand('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},sessionId),resumeTarget:sessionId=>debug.sendCommand('Runtime.runIfWaitingForDebugger',{},sessionId),subscribeMessage:fn=>{debug.on('message',fn);return ()=>debug.removeListener('message',fn);},subscribeDetach:fn=>{debug.on('detach',fn);return ()=>debug.removeListener('detach',fn);},release:()=>{}};const observer=createNativeTransportObserver({channel,getContext:()=>owner,isCurrent:c=>c.deviceId===owner.deviceId&&c.sessionId===owner.sessionId,audit:async(facts,context)=>events.push({facts,context}),onFailure:async reason=>failures.push(reason),onEstablished:()=>{},...options});return {debug,commands,events,failures,observer,setOwner:value=>owner=value,owner:()=>owner,emit:(method,params,sessionId)=>debug.emit('message',{},method,params,sessionId)};}
test('fixed private commands emit only bounded own-ID lifecycle metadata and retain creation policy across navigation',async()=>{
const f=fixture();await f.observer.install();assert.deepEqual(f.commands.map(x=>x.method),['Network.enable','Target.setAutoAttach']);f.emit('Network.requestWillBeSent',{request:{url:'https://private.example/?secret=private',postData:'private body'}});
f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/path?token=private',initiator:{stack:'private'}});f.setOwner({...f.owner(),policyVersion:2,documentGeneration:2});f.emit('Network.webTransportClosed',{transportId:'1.1'});await f.observer.flush();assert.equal(f.events.length,2);assert.equal(f.events[0].facts.transportId,f.events[1].facts.transportId);assert.match(f.events[0].facts.transportId,/^[a-f0-9-]{36}$/);assert.equal(f.events[1].context.policyVersion,1);assert.equal(f.events[1].context.documentGeneration,1);assert.equal(f.events[1].facts.connectionEstablished,false);assert.equal(f.events[0].facts.host,'receiver.example');assert.equal(JSON.stringify(f.events).includes('private'),false);assert.equal(JSON.stringify(f.events).includes('1.1'),false);f.observer.dispose();assert.equal(f.failures.length,0);assert.equal(f.debug.listenerCount('message'),0);assert.equal(f.debug.isAttached(),true,'Observer must preserve clipboard-owned debugger');
});
test('fresh workers queue observation and recursive attachment before startup resume',async()=>{
const f=fixture();await f.observer.install();f.emit('Target.attachedToTarget',{sessionId:'worker-1',targetInfo:{type:'worker',url:'blob:https://private.example/private'}});await f.observer.flush();assert.deepEqual(f.commands.slice(2).map(x=>x.method),['Network.enable','Target.setAutoAttach','Runtime.runIfWaitingForDebugger']);f.emit('Network.webTransportCreated',{transportId:'2.1',url:'https://receiver.example/'},'worker-1');f.emit('Network.webTransportConnectionEstablished',{transportId:'2.1'},'worker-1');f.emit('Network.webTransportClosed',{transportId:'2.1'},'worker-1');await f.observer.flush();assert.deepEqual(f.events.map(x=>x.facts.phase),['created','established','closed']);assert.ok(f.events.every(x=>x.facts.realm==='worker'));assert.equal(f.events[1].facts.connectionEstablished,true);assert.equal(f.events[2].facts.connectionEstablished,true);f.observer.dispose();
});
test('unknown session, overflow, unexpected detach and audit outage fail once without releasing protocol access',async()=>{
for(const type of ['unknown','overflow','detach','audit']){const f=fixture(type==='overflow'?{maxAttempts:1}:type==='audit'?{audit:async()=>{throw Error('private payload');}}:{});await f.observer.install();if(type==='unknown')f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/'},'unknown-session');else if(type==='detach')f.debug.detach();else{f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/'});if(type==='overflow')f.emit('Network.webTransportCreated',{transportId:'1.2',url:'https://receiver.example/'});}await f.observer.flush();assert.equal(f.observer.status().failed,true,type);const expected={unknown:'transport-observer-unavailable',overflow:'transport-observer-overflow',detach:'transport-observer-detached',audit:'transport-observer-audit-failed'};assert.deepEqual(f.failures,[expected[type]]);if(type==='overflow')assert.equal(f.events.length,1,'Previously captured attempt was erased by overflow');assert.equal(JSON.stringify(f.failures).includes('private payload'),false);f.observer.dispose();}
});
test('command timeout and unhealthy private channel never silently replaces clipboard ownership',async()=>{
const f=fixture({commandTimeoutMs:50});f.debug.sendCommand=()=>new Promise(()=>{});await assert.rejects(f.observer.install());assert.equal(f.observer.status().failed,true);f.observer.dispose();const g=fixture();g.debug.detach();await assert.rejects(g.observer.install());assert.equal(g.commands.length,0);g.observer.dispose();assert.equal(g.debug.isAttached(),false);
});
test('invalid URL metadata and changed device cannot enqueue a successful observed attempt',async()=>{
const f=fixture();await f.observer.install();f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://user:[email protected]/'});await f.observer.flush();assert.equal(f.events.length,0);assert.equal(f.observer.status().failed,true);f.observer.dispose();const g=fixture({isCurrent:()=>false});await g.observer.install();g.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/'});await g.observer.flush();assert.equal(g.events.length,0);g.observer.dispose();
});
test('IPv6 remains canonical and worker late close preserves captured lifecycle after target detach',async()=>{
const f=fixture();await f.observer.install();f.emit('Target.attachedToTarget',{sessionId:'worker-1',targetInfo:{type:'worker'}});await f.observer.flush();f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://[::1]:443/private?secret=never-log'},'worker-1');f.emit('Target.detachedFromTarget',{sessionId:'worker-1'});f.emit('Network.webTransportClosed',{transportId:'1.1'},'worker-1');await f.observer.flush();assert.equal(f.events.length,2);assert.equal(f.events[0].facts.host,'[::1]');assert.equal(f.events[1].facts.transportId,f.events[0].facts.transportId);assert.equal(f.failures.length,0);f.observer.dispose();
});
test('captured trusted audit drains after intentional teardown instead of erasing authorized event history',async()=>{
const f=fixture();await f.observer.install();f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/'});f.observer.dispose();await f.observer.flush();assert.equal(f.events.length,1);assert.equal(f.events[0].facts.phase,'created');assert.equal(f.failures.length,0);
});
test('observer requires a private fixed channel and cannot consume arbitrary debugger authority',()=>{assert.throws(()=>createNativeTransportObserver({contents:{debugger:new EventEmitter()},getContext:()=>null,isCurrent:()=>true,audit:async()=>{},onFailure:()=>{}}),TypeError);});
test('establishment containment is synchronous even while earlier Created persistence is held',async()=>{
let release;const gate=new Promise(resolve=>{release=resolve;});const phases=[],alarms=[];
const f=fixture({audit:async facts=>{phases.push(facts.phase);if(facts.phase==='created')await gate;},onEstablished:context=>alarms.push(context)});await f.observer.install();
f.emit('Network.webTransportCreated',{transportId:'1.1',url:'https://receiver.example/'});await Promise.resolve();
f.emit('Network.webTransportConnectionEstablished',{transportId:'1.1'});assert.equal(alarms.length,1);assert.deepEqual(phases,['created']);
f.emit('Network.webTransportClosed',{transportId:'1.1'});release();await f.observer.flush();assert.deepEqual(phases,['created','established','closed']);assert.equal(alarms.length,1);f.observer.dispose();
});
+17 -1
View File
@@ -4,7 +4,7 @@ import {EventEmitter} from 'node:events';
import {createRequire} from 'node:module';
import vm from 'node:vm';
const require=createRequire(import.meta.url);
const {attachPageClipboardGuard,guardedScreenshot,disposePageClipboardGuard}=require('../apps/browser/page-clipboard-guard.cjs');
const {attachPageClipboardGuard,guardedScreenshot,disposePageClipboardGuard,createNativeTransportChannel}=require('../apps/browser/page-clipboard-guard.cjs');
const {capturePNG}=require('../apps/browser/visual-capture.cjs');
function fixture(valid=true){
const api=new EventEmitter(),commands=[];let attached=false,detaches=0,failures=0;
@@ -15,6 +15,22 @@ function fixture(valid=true){
const contents={debugger:api,isDestroyed:()=>false};
return {api,contents,commands,png,detachCount:()=>detaches,failureCount:()=>failures,initialize:()=>attachPageClipboardGuard(contents,()=>failures++)};
}
test('private transport lease exposes only fixed commands and releases waiting targets without detaching clipboard protection',async()=>{
const f=fixture();await f.initialize();const channel=createNativeTransportChannel(f.contents),messages=[];
assert.equal(channel.attach,undefined);assert.equal(channel.detach,undefined);assert.equal(channel.sendCommand,undefined);
assert.throws(()=>createNativeTransportChannel(f.contents));channel.subscribeMessage((_event,method)=>messages.push(method));
f.api.emit('message',{},'Network.requestWillBeSent',{request:{url:'https://private.invalid/?token=secret',postData:'secret'}});
f.api.emit('message',{},'Target.attachedToTarget',{sessionId:'worker-1',targetInfo:{type:'worker'}});
assert.deepEqual(messages,['Target.attachedToTarget']);await channel.enableNetwork('worker-1');await channel.autoAttachTargets('worker-1');await channel.resumeTarget('worker-1');
assert.deepEqual(f.commands.slice(2).map(c=>c.method),['Network.enable','Target.setAutoAttach','Runtime.runIfWaitingForDebugger']);
const before=f.commands.length;await assert.rejects(channel.resumeTarget('private;command'));assert.equal(f.commands.length,before);
channel.release();await new Promise(resolve=>setImmediate(resolve));assert.equal(channel.isHealthy(),false);assert.equal(f.detachCount(),0);assert.equal(f.api.isAttached(),true);
assert.equal(f.commands.at(-1).parameters.autoAttach,false);assert.ok(f.commands.slice(before).some(c=>c.method==='Runtime.runIfWaitingForDebugger'));
assert.deepEqual(await capturePNG(f.contents),f.png);await assert.rejects(channel.enableNetwork());disposePageClipboardGuard(f.contents);assert.equal(f.failureCount(),0);
});
test('transport cannot borrow an unguarded or revoked native debugger',async()=>{
const f=fixture();assert.throws(()=>createNativeTransportChannel(f.contents));await f.initialize();const channel=createNativeTransportChannel(f.contents);let detach=0;channel.subscribeDetach(()=>detach++);f.api.detach();assert.equal(detach,1);assert.equal(channel.isHealthy(),false);await assert.rejects(channel.enableNetwork());channel.release();assert.equal(f.failureCount(),1);
});
test('static page guard registers only fixed initialization and shares screenshot without detaching',async()=>{
const f=fixture();await f.initialize();assert.deepEqual(f.commands.map(c=>c.method),['Page.enable','Page.addScriptToEvaluateOnNewDocument']);
assert.equal(/require\(|ipcRenderer|process\./.test(f.commands[1].parameters.source),false);
+31
View File
@@ -0,0 +1,31 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {randomUUID} from 'node:crypto';
import {openStore,digest} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {toECS,toCEF} from '../packages/core/siem.mjs';
import transportAudit from '../packages/core/transport-facts.cjs';
const facts=phase=>({transportId:randomUUID(),protocol:'webtransport',phase,realm:'worker',sessionId:randomUUID(),connectionEstablished:phase==='established'});
test('native transport ingestion retains only strict matching lifecycle facts',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-transport-audit-')),store=openStore(dir),credential='synthetic-device-transport-audit-token';
store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('device','Synthetic',digest(credential));
const server=createApp(store,{publicUrl:'http://127.0.0.1',adminToken:'synthetic-administrator-token-123456789',secure:false}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();assert.ok(dir.startsWith(join(tmpdir(),'safetynet-transport-audit-')));rmSync(dir,{recursive:true,force:true});});
const send=body=>fetch(`http://127.0.0.1:${server.address().port}/api/device/events`,{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify({eventId:randomUUID(),policyVersion:1,...body})});
for(const phase of ['created','established','closed']){const transport=facts(phase);assert.equal((await send({kind:`transport.${phase}`,host:'synthetic.example',transport})).status,200);const row=store.db.prepare('SELECT * FROM events WHERE kind=?').get(`transport.${phase}`);assert.deepEqual(JSON.parse(row.metadata).transport,transport);const ecs=toECS({...row,metadata:JSON.parse(row.metadata)});assert.deepEqual(ecs.event.type,['info']);assert.equal(ecs.event.outcome,'unknown');assert.equal(ecs.destination.domain,'synthetic.example');}
for(const body of [{kind:'transport.created'},{kind:'transport.created',transport:facts('established')},{kind:'transport.created',transport:{...facts('created'),connectionEstablished:true}},{kind:'transport.closed',transport:{...facts('closed'),url:'PRIVATE_BODY'}},{kind:'transport.created',transport:{...facts('created'),transportId:'raw-cdp-id'}},{kind:'clipboard.copy',transport:facts('created')},{kind:'transport.closed',transport:facts('closed'),request:{bytes:1,detectedType:'text'}},{kind:'transport.monitor-unavailable',ruleId:'PRIVATE_BODY'},{kind:'transport.monitor-unavailable',ruleId:'transport-observer-unavailable',transport:facts('closed')}])assert.equal((await send(body)).status,400);
for(const host of ['synthetic.example','127.0.0.1','[2001:db8::1]'])assert.equal((await send({kind:'transport.created',host,transport:facts('created')})).status,200);
for(const host of [undefined,'https://synthetic.example/private?secret=1','synthetic.example\r\nPRIVATE_BODY','user:[email protected]','synthetic.example:443','synthetic.example/private','synthetic.example?secret=1','Synthetic.Example','127.1','[2001:0db8::1]'])assert.equal((await send({kind:'transport.created',host,transport:facts('created')})).status,400);
assert.equal((await send({kind:'transport.monitor-unavailable',host:'synthetic.example',ruleId:'transport-observer-unavailable'})).status,400);
for(const ruleId of transportAudit.TRANSPORT_MONITOR_REASONS)assert.equal((await send({kind:'transport.monitor-unavailable',ruleId})).status,200);
assert.equal(store.auditIntegrity.verify().valid,true);
});
test('SIEM native lifecycle is informational and rejects injected or cross-kind facts',()=>{
const transport=facts('established'),event={id:1,time:new Date().toISOString(),kind:'transport.established',metadata:{host:'synthetic.example',transport,decision:'block',traceId:'1'.repeat(32)},audit:{hash:'2'.repeat(64),sequence:1}};
const ecs=toECS(event),cef=toCEF(event);assert.equal(ecs.event.outcome,'unknown');assert.deepEqual(ecs.event.type,['info']);assert.equal(ecs.safetynet.decision,null);assert.equal(ecs.safetynet.transport.connection_established,true);assert.equal(ecs.trace.id,'1'.repeat(32));assert.match(cef,/TransportPhase cs3=established/);assert.match(cef,/\|3\|/);assert.match(cef,/AuditRecordHash/);assert.ok(cef.includes('cs1Label=TransportId cs1='+transport.transportId));assert.equal(cef.includes('flexString3'),false);
for(const bad of [{...transport,url:'PRIVATE_BODY'},{...transport,phase:'closed'},{...transport,sessionId:'PRIVATE_BODY'}]){event.metadata.transport=bad;assert.equal(toECS(event).safetynet.transport,undefined);assert.equal(toCEF(event).includes('PRIVATE_BODY'),false);}
event.metadata.transport=transport;event.metadata.host='https://user:[email protected]/private';assert.equal(toECS(event).destination,undefined);assert.equal(toCEF(event).includes('PRIVATE_BODY'),false);event.metadata.host='synthetic.example';event.kind='upload.authorized';assert.equal(toECS(event).safetynet.transport,undefined);assert.equal(toCEF(event).includes('TransportPhase'),false);
});