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

+5
View File
@@ -16,6 +16,11 @@ still open: a reproducible Chromium WebRTC renderer crash in initial blank
iframes requires a verified native fix. Other platforms and broader features
remain outside this milestone.
[Native transport auditing](docs/native-transport-observability.md) records
tested WebTransport lifecycle attempts without exposing raw browser debugging
APIs. Its observation channel preserves clipboard protection; unsupported
transport payloads remain blocked by the native relay boundary.
S Browser uses translucent materials, rounded controls, light/dark themes, and a content-first layout inspired by Safari. Website content is left unchanged.
## A look inside
+15 -9
View File
@@ -27,6 +27,10 @@ for(const name of ['browser.upload.lease','browser.upload.part','browser.upload.
// Diagnostic reasons are fixed code identifiers, never exception messages,
// policy rule text, request content or arbitrary caller-supplied labels.
const reasons=new Set(['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']);
names.add('browser.transport');
for(const operation of ['transport.install','transport.target','transport.dispose','transport.created','transport.closed','transport.established','transport.monitor-unavailable'])operations.add(operation);
// Keep the diagnostics cold path free of schema/SDK initialization.
for(const reason of ['transport-guard-unavailable','transport-observer-detached','transport-observer-command-timeout','transport-observer-overflow','transport-observer-audit-failed','transport-observer-unavailable','unexpected-webtransport-established'])reasons.add(reason);
const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
function attributes(input){const out={};if(!input||typeof input!=='object')return out;for(const [key,descriptor]of Object.entries(Object.getOwnPropertyDescriptors(input))){if(!('value'in descriptor))continue;const value=descriptor.value;
if(['sessionId','deviceId'].includes(key)&&typeof value==='string'&&uuid.test(value))out[key==='sessionId'?'safetynet.session.id':'safetynet.device.id']=value;
@@ -42,25 +46,27 @@ function attributes(input){const out={};if(!input||typeof input!=='object')retur
function endpointAllowed(value){try{const u=new URL(value);if(u.username||u.password||u.search||u.hash)return false;const host=u.hostname.replace(/^\[|\]$/g,'');return u.protocol==='https:'||(u.protocol==='http:'&&(host==='::1'||/^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(host)&&host.split('.').every(p=>+p<=255)));}catch{return false;}}
function valid(config){return config&&config.enabled===true&&['operational','debug'].includes(config.mode)&&endpointAllowed(config.endpoint)&&['sampleRate','debugSampleRate'].every(k=>Number.isFinite(config[k])&&config[k]>=0&&config[k]<=1)&&Number.isSafeInteger(config.maxQueueSize)&&config.maxQueueSize>=16&&config.maxQueueSize<=4096&&Number.isSafeInteger(config.flushIntervalMs)&&config.flushIntervalMs>=250&&config.flushIntervalMs<=10000&&Number.isSafeInteger(config.maxRecordAgeMs)&&config.maxRecordAgeMs>=1000&&config.maxRecordAgeMs<=300000&&Number.isSafeInteger(config.exportTimeoutMs)&&config.exportTimeoutMs>=250&&config.exportTimeoutMs<=10000;}
const defaults={enabled:false,mode:'operational',sampleRate:0.1,debugSampleRate:1,maxQueueSize:512,flushIntervalMs:2000,maxRecordAgeMs:30000,exportTimeoutMs:2000};
function createDiagnostics(){const storage=new AsyncLocalStorage();let generation=null,configurationKey='',closed=false;const retiring=new Set();let activeSpans=0,lifetimeFailedExports=0;
function createDiagnostics(){const storage=new AsyncLocalStorage();let generation=null,configurationKey='',closed=false;const retiring=new Set();let activeSpans=0,lifetimeFailedExports=0,generationOrdinal=0;const failureReasons=Object.create(null),failureFacts=[];
// Parser timing is untrusted metadata: validate the entire batch before creating
// spans, and attach it only to a currently recording browser operation.
function importStages(envelope){try{const current=generation,parent=storage.getStore();if(!current?.active||current.config.mode!=='debug'||parent?.generation!==current||!parent.span.isRecording()||activeSpans>=128)return;
const batch=validateStages(envelope);if(!batch)return;const base=batch.baseUnixMs,validated=batch.records.map(item=>({...item,start:item.startOffsetMs,duration:item.durationMs}));
const imported=new Map();for(const item of validated){if(!current.active||generation!==current||!parent.span.isRecording())return;const ancestor=item.parentId===null?parent.span:imported.get(item.parentId);if(!ancestor)return;const child=current.tracer.startSpan(`inspector.${item.stage}`,{startTime:base+item.start,attributes:attributes({component:'inspector',status:item.status,durationMs:item.duration})},trace.setSpan(ROOT_CONTEXT,ancestor));imported.set(item.id,child);child.setStatus({code:item.status==='ok'?SpanStatusCode.OK:SpanStatusCode.ERROR});child.end(base+item.start+item.duration);}
}catch{/* Diagnostics must never change inspection results. */}}
function retire(current){if(!current)return;current.active=false;for(const agent of current.agents)agent.destroy();const pending=Promise.allSettled([current.tracerProvider?.shutdown(),current.loggerProvider?.shutdown()]);retiring.add(pending);pending.finally(()=>retiring.delete(pending));}
function gatedExporter(exporter,current){return {export(records,callback){if(!current.active){callback({code:0});return;}const now=Date.now(),fresh=records.filter(record=>{const timestamp=record.endTime||record.hrTime||record.hrTimeObserved;return timestamp&&now-(timestamp[0]*1000+timestamp[1]/1e6)<=current.config.maxRecordAgeMs;});if(!fresh.length){callback({code:0});return;}try{exporter.export(fresh,result=>{if(result.code!==0){current.failedExports++;lifetimeFailedExports++;}callback(result);});}catch{current.failedExports++;lifetimeFailedExports++;callback({code:1});}},shutdown:()=>exporter.shutdown(),forceFlush:()=>exporter.forceFlush?.()||Promise.resolve()};}
function retire(current){if(!current)return;current.active=false;current.retiredAt=Date.now();for(const [request,attempt]of current.requests){attempt.cancelSignalled=true;const error=Error('Diagnostics cancelled');error.code='SAFETYNET_DIAGNOSTICS_CANCELLED';request.destroy(error);}for(const agent of current.agents)agent.destroy();const pending=Promise.allSettled([current.tracerProvider?.shutdown(),current.loggerProvider?.shutdown()]);retiring.add(pending);pending.finally(()=>retiring.delete(pending));}
function failureReason(error,attempt){const code=error?.code;if(code==='SAFETYNET_DIAGNOSTICS_CANCELLED'||attempt.nativeCode==='SAFETYNET_DIAGNOSTICS_CANCELLED')return 'cancelled';if(code==='ETIMEDOUT'||error?.message==='Request timed out')return 'timeout';if(code==='ECONNREFUSED'||attempt.nativeCode==='ECONNREFUSED')return 'connection-refused';if(code==='ECONNRESET'||attempt.nativeCode==='ECONNRESET')return 'connection-reset';if(error?.message==='Concurrent export limit reached')return 'concurrency-limit';const status=Number.isInteger(code)?code:attempt.httpStatus;if(status>=300&&status<400)return 'http-redirect';if(status>=400&&status<500)return 'http-client';if(status>=500&&status<600)return 'http-server';return 'exporter-failure';}
function gatedExporter(exporter,current,signal){return {export(records,callback){if(!current.active){callback({code:0});return;}const now=Date.now(),fresh=records.filter(record=>{const timestamp=record.endTime||record.hrTime||record.hrTimeObserved;return timestamp&&now-(timestamp[0]*1000+timestamp[1]/1e6)<=current.config.maxRecordAgeMs;});if(!fresh.length){callback({code:0});return;}const attempt={startedAt:Date.now(),httpStatus:null,nativeCode:null,cancelSignalled:false};if(!current.attempts[signal])current.attempts[signal]=attempt;let finished=false;const done=result=>{if(finished)return;finished=true;if(current.attempts[signal]===attempt)delete current.attempts[signal];if(result.code!==0){current.failedExports++;lifetimeFailedExports++;const reason=failureReason(result.error,attempt);failureReasons[reason]=(failureReasons[reason]||0)+1;failureFacts.push({signal,generation:current.ordinal,reason,durationMs:Math.max(0,Date.now()-attempt.startedAt),activeAtCompletion:current.active,retirementOverlapped:!!current.retiredAt&&current.retiredAt>=attempt.startedAt,cancelSignalled:attempt.cancelSignalled,httpStatus:attempt.httpStatus});if(failureFacts.length>32)failureFacts.shift();}callback(result);};try{exporter.export(fresh,done);}catch(error){done({code:1,error});}},shutdown:()=>exporter.shutdown(),forceFlush:()=>exporter.forceFlush?.()||Promise.resolve()};}
function configure(input={},context={}){if(closed)return false;const config={...defaults,...input};const key=JSON.stringify([config,attributes(context)]);if(key===configurationKey)return !!generation?.active;configurationKey=key;const previous=generation;generation=null;retire(previous);if(!valid(config)||retiring.size>=4)return false;
let current;try{current={active:true,config,context:attributes(context),agents:new Set(),failedExports:0};
const agentFactory=async protocol=>{const Base=protocol==='https:'?https.Agent:http.Agent;class PrivateAgent extends Base{addRequest(request,options){if(!current.active){request.destroy(Error('Diagnostics disabled'));return;}for(const key of request.getHeaderNames())if(!['host','content-type','content-length','user-agent'].includes(key.toLowerCase()))request.removeHeader(key);super.addRequest(request,options);}}
let current;try{current={active:true,config,context:attributes(context),agents:new Set(),requests:new Map(),attempts:Object.create(null),ordinal:++generationOrdinal,failedExports:0};
const agentFactory=async(protocol,signal)=>{const Base=protocol==='https:'?https.Agent:http.Agent;class PrivateAgent extends Base{addRequest(request,options){if(!current.active){const error=Error('Diagnostics cancelled');error.code='SAFETYNET_DIAGNOSTICS_CANCELLED';request.destroy(error);return;}const attempt=current.attempts[signal];if(attempt){current.requests.set(request,attempt);request.once('close',()=>current.requests.delete(request));request.once('response',response=>{if(Number.isInteger(response.statusCode)&&response.statusCode>=100&&response.statusCode<=599)attempt.httpStatus=response.statusCode;});request.once('error',error=>{if(['SAFETYNET_DIAGNOSTICS_CANCELLED','ETIMEDOUT','ECONNREFUSED','ECONNRESET'].includes(error.code))attempt.nativeCode=error.code;});}for(const key of request.getHeaderNames())if(!['host','content-type','content-length','user-agent'].includes(key.toLowerCase()))request.removeHeader(key);super.addRequest(request,options);}}
const agent=new PrivateAgent({keepAlive:false,maxSockets:1,rejectUnauthorized:true});current.agents.add(agent);return agent;};
const base=config.endpoint.replace(/\/$/,'');const exporterConfig=path=>({url:base+path,headers:{},timeoutMillis:config.exportTimeoutMs,concurrencyLimit:1,compression:'none',httpAgentOptions:agentFactory});
const base=config.endpoint.replace(/\/$/,'');const exporterConfig=(path,signal)=>({url:base+path,headers:{},timeoutMillis:config.exportTimeoutMs,concurrencyLimit:1,compression:'none',httpAgentOptions:protocol=>agentFactory(protocol,signal)});
const {TracerProvider,BatchSpanProcessor,TraceIdRatioBasedSampler,ParentBasedSampler,LoggerProvider,BatchLogRecordProcessor,resourceFromAttributes,OTLPTraceExporter,OTLPLogExporter}=loadTelemetryModules();
const resource=resourceFromAttributes({'service.name':'safetynet-browser','service.version':'0.1.0','safetynet.diagnostics.mode':config.mode});
const limits={maxQueueSize:config.maxQueueSize,maxExportBatchSize:Math.min(config.maxQueueSize,128),scheduledDelayMillis:config.flushIntervalMs,exportTimeoutMillis:config.exportTimeoutMs};
current.tracerProvider=new TracerProvider({resource,sampler:new ParentBasedSampler({root:new TraceIdRatioBasedSampler(config.mode==='debug'?config.debugSampleRate:config.sampleRate)}),spanLimits:{attributeCountLimit:20,attributeValueLengthLimit:64,eventCountLimit:0,linkCountLimit:0},spanProcessors:[new BatchSpanProcessor({...limits,exporter:gatedExporter(new OTLPTraceExporter(exporterConfig('/v1/traces')),current)})]});
current.loggerProvider=new LoggerProvider({resource,logRecordLimits:{attributeCountLimit:20,attributeValueLengthLimit:64},processors:[new BatchLogRecordProcessor({...limits,exporter:gatedExporter(new OTLPLogExporter(exporterConfig('/v1/logs')),current)})]});current.tracer=current.tracerProvider.getTracer('safetynet.browser','0.1.0');current.logger=current.loggerProvider.getLogger('safetynet.browser','0.1.0');generation=current;return true;
current.tracerProvider=new TracerProvider({resource,sampler:new ParentBasedSampler({root:new TraceIdRatioBasedSampler(config.mode==='debug'?config.debugSampleRate:config.sampleRate)}),spanLimits:{attributeCountLimit:20,attributeValueLengthLimit:64,eventCountLimit:0,linkCountLimit:0},spanProcessors:[new BatchSpanProcessor({...limits,exporter:gatedExporter(new OTLPTraceExporter(exporterConfig('/v1/traces','traces')),current,'traces')})]});
current.loggerProvider=new LoggerProvider({resource,logRecordLimits:{attributeCountLimit:20,attributeValueLengthLimit:64},processors:[new BatchLogRecordProcessor({...limits,exporter:gatedExporter(new OTLPLogExporter(exporterConfig('/v1/logs','logs')),current,'logs')})]});current.tracer=current.tracerProvider.getTracer('safetynet.browser','0.1.0');current.logger=current.loggerProvider.getLogger('safetynet.browser','0.1.0');generation=current;return true;
}catch{generation=null;retire(current);return false;}
}
function event(name,input={}){const current=generation;if(!current?.active||!names.has(name))return;try{const parent=storage.getStore(),rate=current.config.mode==='debug'?current.config.debugSampleRate:current.config.sampleRate;if(parent?.generation===current){if(!parent.span.isRecording())return;}else if(randomInt(1000000)>=Math.floor(rate*1000000))return;
@@ -95,6 +101,6 @@ function createDiagnostics(){const storage=new AsyncLocalStorage();let generatio
}
async function flush(){const current=generation;if(!current?.active)return;await Promise.allSettled([current.tracerProvider.forceFlush({timeoutMillis:current.config.exportTimeoutMs}),current.loggerProvider.forceFlush({timeoutMillis:current.config.exportTimeoutMs})]);}
async function shutdown(){if(closed)return;await flush();closed=true;const current=generation;generation=null;retire(current);await Promise.allSettled([...retiring]);}
return {configure,span,spanSync,event,importStages,withCorrelation,flush,shutdown,correlation:()=>{const parent=storage.getStore();if(!parent||parent.generation!==generation||!generation?.active||!parent.span.isRecording())return {};const ids=parent.span.spanContext();return {traceId:ids.traceId,spanId:ids.spanId};},status:()=>({enabled:!!generation?.active,mode:generation?.config.mode||'operational',failedExports:generation?.failedExports||0,lifetimeFailedExports,activeSpans})};
return {configure,span,spanSync,event,importStages,withCorrelation,flush,shutdown,correlation:()=>{const parent=storage.getStore();if(!parent||parent.generation!==generation||!generation?.active||!parent.span.isRecording())return {};const ids=parent.span.spanContext();return {traceId:ids.traceId,spanId:ids.spanId};},status:()=>({enabled:!!generation?.active,mode:generation?.config.mode||'operational',failedExports:generation?.failedExports||0,lifetimeFailedExports,activeSpans,failureReasons:{...failureReasons},failureFacts:failureFacts.map(fact=>({...fact}))})};
}
module.exports={createDiagnostics,attributes,endpointAllowed};
+27 -3
View File
@@ -210,12 +210,12 @@ async function flushAudit(){
try{for(let n=0;n<100&&auditQueue.length;n++){const record=auditQueue[0];await diagnostics.withCorrelation(record,()=>diagnosticSpan('browser.management',{operation:'audit.deliver'},()=>require('./evidence-delivery.cjs').deliverAuditRecord({record,send:(route,body)=>request(device.server,route,body,device.token),getContext:()=>({ready,policy,deviceId:device.id})})));auditQueue.shift();persistAudit();}}
catch{}finally{flushing=false;}
}
async function audit(kind,ruleId,host,fileFacts,requestFacts,evidencePayload,agentFacts,interactionFacts,origin){
async function audit(kind,ruleId,host,fileFacts,requestFacts,evidencePayload,agentFacts,interactionFacts,origin,transportFacts){
if(!device||!policy)return;
// Operational correlation contains no rule text, hostname or forensic payload.
try{diagnostics.event('browser.security',diagnosticContext({decision:kind.endsWith('.block')?'block':kind.endsWith('.denied')?'deny':kind.endsWith('.authorized')||kind.endsWith('.released')?'allow':'unknown',reason:ruleId}));}catch{}
if(auditQueue.length>=10000){quarantine('Security event queue is full. Contact your administrator.');return;}
try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;const request=requestFacts?require('../../packages/core/request-transfer-facts.cjs').requestTransferFacts(requestFacts):undefined;const agent=agentFacts?require('../../packages/core/agent-audit.cjs').agentAuditFacts(agentFacts):undefined;const interaction=interactionFacts?require('../../packages/core/interaction-facts.cjs').InteractionFactsSchema.parse(interactionFacts):undefined;const evidenceType=evidencePayload?.action?'interaction':'download';auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...(request?{request}:{}),...(agent?{agent}:{}),...(interaction?{interaction}:{}),...(evidencePayload&&require('./evidence-delivery.cjs').collectionAllowed({ready,policy,type:evidenceType})?{protectedEvidence:{deviceId:device.id,type:evidenceType,payload:require('./evidence-delivery.cjs').validatedEvidence(evidencePayload,evidenceType)}}:{}),...diagnosticCorrelation(),policyVersion:Number.isSafeInteger(origin?.policyVersion)&&origin.policyVersion>0?origin.policyVersion:policy.version});persistAudit();const eventId=auditQueue.at(-1).eventId;flushAudit();return eventId;}
try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;const request=requestFacts?require('../../packages/core/request-transfer-facts.cjs').requestTransferFacts(requestFacts):undefined;const agent=agentFacts?require('../../packages/core/agent-audit.cjs').agentAuditFacts(agentFacts):undefined;const interaction=interactionFacts?require('../../packages/core/interaction-facts.cjs').InteractionFactsSchema.parse(interactionFacts):undefined;const transport=transportFacts?require('../../packages/core/transport-facts.cjs').transportFactsForEvent(kind,transportFacts):undefined;if(transportFacts&&!transport)throw Error('Transport audit metadata mismatch');const evidenceType=evidencePayload?.action?'interaction':'download';auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...(request?{request}:{}),...(agent?{agent}:{}),...(interaction?{interaction}:{}),...(transport?{transport}:{}),...(evidencePayload&&require('./evidence-delivery.cjs').collectionAllowed({ready,policy,type:evidenceType})?{protectedEvidence:{deviceId:device.id,type:evidenceType,payload:require('./evidence-delivery.cjs').validatedEvidence(evidencePayload,evidenceType)}}:{}),...diagnosticCorrelation(),policyVersion:Number.isSafeInteger(origin?.policyVersion)&&origin.policyVersion>0?origin.policyVersion:policy.version});persistAudit();const eventId=auditQueue.at(-1).eventId;flushAudit();return eventId;}
catch{quarantine('Security events cannot be persisted. Contact your administrator.');}
}
function auditAgent(event){
@@ -695,6 +695,28 @@ function installSiteHooks(tab){
audit:event=>audit(event.kind,event.ruleId,event.host),block:result=>{security.visualBlocked={...result,message:result.reason==='credential-brand-impersonation'?'Blocked: this login resembles a protected brand on an unapproved origin.':'Blocked: this login could not be inspected safely.'};security.visualApproval=null;quarantine(security.visualBlocked.message);}
});
}
function installTransportObserver(tab){
const contents=tab.contents;
const current=context=>!!context&&context.deviceId===device?.id&&context.sessionId===tab.id&&tabs?.fromContents(contents.id)===tab&&!contents.isDestroyed();
const unavailable=reason=>{clipboardFatal=true;quarantine('Managed transport monitoring failed. Restart this browser.');try{tab.security.transportObserver?.dispose();}catch{}return diagnosticSpan('browser.transport',{operation:'transport.monitor-unavailable',sessionId:tab.id,reason,status:'error'},()=>audit('transport.monitor-unavailable',reason));};
tab.security.transportObserver=require('./native-transport-observer.cjs').createNativeTransportObserver({
channel:require('./page-clipboard-guard.cjs').createNativeTransportChannel(contents),
getContext:()=>ready&&policy&&device&&localRelay&&Date.parse(policy.expiresAt)>Date.now()?Object.freeze({deviceId:device.id,policyVersion:policy.version,sessionId:tab.id,documentGeneration:tab.security.interactionDocumentGeneration}):null,
isCurrent:current,
traceOperation:(operation,callback,context)=>diagnosticSpan('browser.transport',{operation,sessionId:context?.sessionId||tab.id,policyVersion:context?.policyVersion||policy?.version},callback),
// Synchronous native alarm precedes the serialized durable audit queue.
onEstablished:context=>{if(current(context)){clipboardFatal=true;quarantine('An uninspectable transport connected. Restart this browser.');}},
audit:async(facts,context)=>{
if(!current(context))return;
const {host,...transport}=facts;
const eventId=await diagnosticSpan('browser.transport',{operation:`transport.${transport.phase}`,sessionId:tab.id,policyVersion:context.policyVersion},()=>audit(`transport.${transport.phase}`,undefined,host,undefined,undefined,undefined,undefined,undefined,{policyVersion:context.policyVersion},transport));
if(typeof eventId!=='string')throw Error('Transport audit persistence failed');
if(transport.phase==='established')await unavailable('unexpected-webtransport-established');
},
onFailure:unavailable
});
return tab.security.transportObserver.install();
}
function createNativeTab(input={}){return diagnosticSpan('browser.tab',{operation:'create'},()=>createNativeTabImpl(input));}
async function createNativeTabImpl(input={},controls={}){
if(!input||typeof input!=='object'||Object.keys(input).some(key=>!['group','url'].includes(key)))throw Error('Invalid native tab request');
@@ -705,6 +727,8 @@ async function createNativeTabImpl(input={},controls={}){
await tab.view.webContents.loadURL('about:blank');
controls.assertCurrent?.();
await diagnosticSpan('browser.clipboard',{operation:'create',sessionId:tab.id},()=>attachPageClipboardGuard(tab.view.webContents,()=>{clipboardFatal=true;quarantine('Managed clipboard protection failed; restart the browser');audit('clipboard.block','clipboard-primitive-unavailable');app.quit();}));
controls.assertCurrent?.();
await installTransportObserver(tab);
controls.assertCurrent?.();tab.guarded=true;selectTab(tab.id);controls.onActivated?.(tab);controls.assertCurrent?.();
if(input.url){if(tab.security.agentAuthority){await agentActorStorage.run(tab.security.agentAuthority,()=>navigate({url:input.url,tabId:tab.id},undefined,{signal:controls.signal}));}else await navigate({url:input.url,tabId:tab.id},undefined,{signal:controls.signal});}controls.assertCurrent?.();emit();return state();
}catch(error){if(tabs.all().includes(tab)){tabs.remove(tab.id);if(tabs.active()?.guarded)selectTab(tabs.active().id);}throw error;}
@@ -811,7 +835,7 @@ app.whenReady().then(async()=>{
Menu.setApplicationMenu(null);
win=new BrowserWindow({width:1320,height:900,minWidth:800,minHeight:600,frame:false,roundedCorners:true,hasShadow:true,backgroundMaterial:'acrylic',backgroundColor:'#eef1f5',autoHideMenuBar:true,webPreferences:{preload:path.join(__dirname,'preload.cjs'),nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false}});
win.removeMenu();
tabs=createTabRegistry({createView:()=>new WebContentsView({webPreferences:{session:browserSession,preload:path.join(__dirname,'site-preload.cjs'),nodeIntegration:false,nodeIntegrationInSubFrames:true,contextIsolation:true,sandbox:true,webSecurity:true,allowRunningInsecureContent:false}}),attach:v=>win.contentView.addChildView(v),detach:v=>win.contentView.removeChildView(v),dispose:tab=>{tab.security.interactionObserver?.dispose();disposePageClipboardGuard(tab.contents);}});
tabs=createTabRegistry({createView:()=>new WebContentsView({webPreferences:{session:browserSession,preload:path.join(__dirname,'site-preload.cjs'),nodeIntegration:false,nodeIntegrationInSubFrames:true,contextIsolation:true,sandbox:true,webSecurity:true,allowRunningInsecureContent:false}}),attach:v=>win.contentView.addChildView(v),detach:v=>win.contentView.removeChildView(v),dispose:tab=>{tab.security.interactionObserver?.dispose();Promise.resolve(tab.security.transportObserver?.dispose()).catch(()=>{});disposePageClipboardGuard(tab.contents);}});
const executeClipboard=(mode,args)=>args.webContents.executeJavaScriptInIsolatedWorld(999,[{code:clipboardScript(mode,args)}],false);
clipboardGuard=createClipboardGuard({
getContext:contents=>{const target=contents||view?.webContents,tab=target&&tabs.fromContents(target.id),site=!!tab,url=target?.getURL()||'',token=site?tabContext(tab.id):null;return {ready:ready&&target?.isFocused()&&(site?tab===activeTab()&&!tab.startPage&&!overviewPanel&&!toolsOpen&&core.destinationAllowed(url,policy):target===win.webContents&&url===uiUrl),policy,webContents:target,url,navigationId:site?tab.navigationId:0,tabId:tab?.id,activation:token?.activation};},
@@ -0,0 +1,58 @@
'use strict';
const {randomUUID}=require('node:crypto');
const phases=Object.freeze({'Network.webTransportCreated':'created','Network.webTransportClosed':'closed','Network.webTransportConnectionEstablished':'established'});
const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
const opaque=value=>typeof value==='string'&&value.length>0&&value.length<=128&&/^[a-zA-Z0-9._:-]+$/.test(value);
function transportHost(value){if(typeof value!=='string'||value.length>8192)throw Error('Transport metadata refused');const u=new URL(value);if(u.protocol!=='https:'||u.username||u.password||u.hostname.length>253)throw Error('Transport metadata refused');return u.hostname.toLowerCase();}
// Observability only: Chromium's mandatory private proxy remains the release
// boundary. No generic command runner, raw protocol events or page bridge escape.
function createNativeTransportObserver({channel,getContext,isCurrent,audit,onFailure,onEstablished,traceOperation=(_stage,fn)=>fn(),commandTimeoutMs=5000,maxTargets=64,maxAttempts=256,maxAuditQueue=256}={}){
if(!channel||['isHealthy','enableNetwork','autoAttachTargets','resumeTarget','subscribeMessage','subscribeDetach','release'].some(name=>typeof channel[name]!=='function')||[getContext,isCurrent,audit,onFailure,onEstablished,traceOperation].some(fn=>typeof fn!=='function')||!Number.isInteger(commandTimeoutMs)||commandTimeoutMs<50||commandTimeoutMs>10000||[maxTargets,maxAttempts,maxAuditQueue].some(n=>!Number.isInteger(n)||n<1||n>256))throw TypeError('Native transport observer callbacks required');
const targets=new Map(),attempts=new Map(),setups=new Set();let owned=false,installed=false,disposed=false,failed=false,installPromise,auditTail=Promise.resolve(),queued=0,unsubscribeMessage,unsubscribeDetach;
const active=()=>owned&&!disposed&&!failed;
function failure(reason='transport-observer-unavailable'){if(disposed||failed)return;failed=true;installed=false;attempts.clear();targets.clear();try{Promise.resolve(traceOperation('transport.monitor-unavailable',()=>onFailure(reason))).catch(()=>{});}catch{} }
async function command(method,_parameters={},sessionId){if(!active())throw Error('Native transport monitor unavailable');let timer;try{return await Promise.race([(method==='Network.enable'?channel.enableNetwork(sessionId):method==='Target.setAutoAttach'?channel.autoAttachTargets(sessionId):channel.resumeTarget(sessionId)),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Object.assign(Error('Native transport command timeout'),{reason:'transport-observer-command-timeout'})),commandTimeoutMs);})]);}finally{clearTimeout(timer);}}
function context(){try{const value=getContext();if(!value)return null;const copy=Object.freeze({...value});if(!uuid.test(copy.sessionId)||!Number.isSafeInteger(copy.policyVersion)||copy.policyVersion<1||!isCurrent(copy))return null;return copy;}catch{return null;}}
function enqueue(row,phase){if(queued>=maxAuditQueue){failure('transport-observer-overflow');return;}const facts=Object.freeze({transportId:row.id,protocol:'webtransport',phase,realm:row.realm,sessionId:row.context.sessionId,connectionEstablished:row.established,host:row.host});queued++;
// Metadata retains the creation policy/owner across later navigation/policy
// changes. Rechecking release authority here would erase legitimate audit.
auditTail=auditTail.then(async()=>{await traceOperation('transport.'+phase,()=>audit(facts,row.context),row.context);}).catch(()=>failure('transport-observer-audit-failed')).finally(()=>queued--);
}
function event(_event,method,params,sessionId){if(!active())return;
if(method==='Target.attachedToTarget'){
const id=params?.sessionId,type=params?.targetInfo?.type;
if(!opaque(id)||targets.has(id)){failure();return;}if(targets.size>=maxTargets){failure('transport-observer-overflow');return;}
const realm=['worker','shared_worker','service_worker'].includes(type)?'worker':type==='iframe'?'iframe':null;
if(!realm){failure();return;}targets.set(id,{realm});
const work=Promise.resolve().then(()=>traceOperation('transport.target',async()=>{
// Issue setup before resume, but do not await responses while the fresh
// target is waiting for startup. Chromium may defer them until resume.
const network=command('Network.enable',{maxPostDataSize:0},id),children=command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},id);
await Promise.all([network,children,command('Runtime.runIfWaitingForDebugger',{},id)]);
})).catch(error=>failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable')).finally(()=>setups.delete(work));setups.add(work);return;
}
// Target detachment can precede its final Network close event. Keep the
// already captured bounded attempt correlation until that event or disposal.
if(method==='Target.detachedFromTarget'){targets.delete(params?.sessionId);return;}
const phase=phases[method];if(!phase)return;
if(sessionId&&!targets.has(sessionId)&&phase==='created'){failure();return;}if(!opaque(params?.transportId)){failure();return;}
const key=(sessionId||'window')+':'+params.transportId;
if(phase==='created'){
if(attempts.has(key)){failure();return;}if(attempts.size>=maxAttempts){failure('transport-observer-overflow');return;}const owner=context();if(!owner)return;
try{const row={id:randomUUID(),context:owner,host:transportHost(params.url),realm:sessionId?targets.get(sessionId).realm:'window',target:sessionId,established:false};attempts.set(key,row);enqueue(row,phase);}catch{failure();}return;
}
const row=attempts.get(key);if(!row)return;
if(phase==='established'){if(row.established){failure();return;}row.established=true;try{onEstablished(row.context);}catch{failure();}enqueue(row,phase);return;}
attempts.delete(key);enqueue(row,phase);
}
function detach(){if(!disposed&&owned)failure('transport-observer-detached');owned=false;installed=false;}
async function install(){if(installPromise)return installPromise;if(disposed||failed||!channel.isHealthy())throw Error('Private transport observer unavailable');
// eslint-disable-next-line preserve-caught-error -- Expose only a fixed error; protocol causes may contain private URLs or target data.
installPromise=Promise.resolve(traceOperation('transport.install',async()=>{try{unsubscribeMessage=channel.subscribeMessage(event);unsubscribeDetach=channel.subscribeDetach(detach);owned=true;await command('Network.enable',{maxPostDataSize:0});await command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true});if(!active())throw Error('Native transport monitor unavailable');installed=true;}catch(error){failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable');throw Error('Native transport monitor installation failed');}}));return installPromise;
}
function dispose(){if(disposed)return;disposed=true;installed=false;attempts.clear();targets.clear();try{unsubscribeMessage?.();}catch{}try{unsubscribeDetach?.();}catch{}try{channel.release();}catch{}owned=false;}
async function flush(){await Promise.allSettled([...setups]);await auditTail;}
return {install,dispose,flush,status:()=>Object.freeze({installed,failed,disposed,attached:owned&&channel.isHealthy(),targets:targets.size,pending:attempts.size,queued})};
}
module.exports={createNativeTransportObserver};
+34 -2
View File
@@ -24,7 +24,7 @@ function deadline(operation){let timer;return Promise.race([operation,new Promis
async function attachPageClipboardGuard(contents,onFailure){
const api=contents.debugger;
if(!api||api.isAttached()||owners.has(contents))throw Error('Page security initialization unavailable');
const owner={api,healthy:false};
const owner={api,healthy:false,onFailure};
const detached=()=>{owners.delete(contents);owner.healthy=false;if(!contents.isDestroyed())onFailure();};
owner.detached=detached;
api.on('detach',detached);
@@ -49,9 +49,41 @@ function guardedScreenshot(contents,parameters){
}
function disposePageClipboardGuard(contents){
const owner=owners.get(contents);if(!owner)return;
owner.releaseTransport?.();
owners.delete(contents);owner.healthy=false;owner.api.removeListener('detach',owner.detached);
if(owner.api.isAttached())owner.api.detach();
}
function createNativeTransportChannel(contents){
const owner=owners.get(contents),api=owner?.api;
if(!owner?.healthy||api!==contents.debugger||!api.isAttached()||owner.transportLease)throw Error('Private transport channel unavailable');
const lease=Symbol('native-transport'),subscriptions=new Set(),targets=new Set();let released=false;
owner.transportLease=lease;
const healthy=()=>!released&&owners.get(contents)===owner&&owner.healthy&&owner.transportLease===lease&&!contents.isDestroyed()&&api.isAttached();
const session=value=>value===undefined||typeof value==='string'&&value.length>0&&value.length<=128&&/^[a-zA-Z0-9._:-]+$/.test(value);
const command=(method,parameters,sessionId)=>{if(!healthy()||!session(sessionId))return Promise.reject(Error('Private transport channel unavailable'));return api.sendCommand(method,parameters,sessionId);};
const subscribe=(name,listener)=>{if(!healthy()||typeof listener!=='function')throw Error('Private transport subscription unavailable');api.on(name,listener);let removed=false;const remove=()=>{if(removed)return;removed=true;api.removeListener(name,listener);subscriptions.delete(remove);};subscriptions.add(remove);return remove;};
const messages=new Set(['Target.attachedToTarget','Target.detachedFromTarget','Network.webTransportCreated','Network.webTransportConnectionEstablished','Network.webTransportClosed']);
function release(){
if(released)return;released=true;for(const remove of [...subscriptions])remove();
if(owner.transportLease===lease){owner.transportLease=null;owner.releaseTransport=null;}
// Release private observation without detaching the clipboard security owner.
// Fresh workers may still be waiting for startup; resume before dropping targets.
if(api.isAttached()){
const send=(method,parameters,id)=>{try{return Promise.resolve(api.sendCommand(method,parameters,id));}catch{return Promise.reject(Error('Private transport cleanup unavailable'));}};
const pending=[];for(const id of targets)pending.push(send('Runtime.runIfWaitingForDebugger',{},id));
pending.push(send('Target.setAutoAttach',{autoAttach:false,waitForDebuggerOnStart:false,flatten:true}));
void Promise.allSettled(pending).then(results=>{if(results.some(result=>result.status==='rejected')&&owners.get(contents)===owner&&owner.healthy&&!contents.isDestroyed())owner.onFailure();});
}
targets.clear();
}
owner.releaseTransport=release;
return Object.freeze({isHealthy:healthy,
enableNetwork:sessionId=>command('Network.enable',{maxPostDataSize:0},sessionId),
autoAttachTargets:sessionId=>command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},sessionId),
resumeTarget:sessionId=>sessionId===undefined?Promise.reject(Error('Private transport target required')):command('Runtime.runIfWaitingForDebugger',{},sessionId),
subscribeMessage:listener=>subscribe('message',(event,method,params,sessionId)=>{if(!messages.has(method))return;if(method==='Target.attachedToTarget'&&session(params?.sessionId)&&params.sessionId!==undefined)targets.add(params.sessionId);if(method==='Target.detachedFromTarget')targets.delete(params?.sessionId);listener(event,method,params,sessionId);}),
subscribeDetach:listener=>subscribe('detach',()=>listener()),release});
}
async function guardedVisualStructure(contents){
const owner=owners.get(contents);
if(!owner?.healthy||!owner.api.isAttached()||contents.isDestroyed())throw Error('Managed visual export refused');
@@ -71,4 +103,4 @@ async function guardedVisualStructure(contents){
}
return {safe:true,nodes};
}
module.exports={attachPageClipboardGuard,guardedScreenshot,guardedVisualStructure,disposePageClipboardGuard};
module.exports={attachPageClipboardGuard,guardedScreenshot,guardedVisualStructure,disposePageClipboardGuard,createNativeTransportChannel};
+6 -5
View File
@@ -16,18 +16,19 @@ export function approvedAIEndpoint(policy,config){
}
export async function runApprovedAI({store,config,deviceId,policy,prompt,signal}){
return traceOperation('ai.gateway',{policyVersion:policy.version,bytes:Buffer.byteLength(prompt)},async()=>{
const controller=new AbortController();
const cancel=()=>controller.abort();
const controller=new AbortController();let cancellationError;
const abortWith=error=>{cancellationError??=error;controller.abort();};
const cancel=()=>abortWith(rejected(403,'AI authorization changed during processing','ai-authorization-changed'));
signal?.addEventListener('abort',cancel,{once:true});
if(signal?.aborted)cancel();
function assertCurrent(){
const active=store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(deviceId);
const policyChanged=active&&!isDeepStrictEqual(store.effectivePolicy(deviceId),policy);
if(controller.signal.aborted||!active||Date.parse(policy.expiresAt)<=Date.now()||policyChanged){
controller.abort();throw rejected(policyChanged?409:403,'AI authorization changed during processing','ai-authorization-changed');
abortWith(rejected(policyChanged?409:403,'AI authorization changed during processing','ai-authorization-changed'));throw cancellationError;
}
}
const deadline=setTimeout(()=>controller.abort(),60000);
const deadline=setTimeout(()=>abortWith(rejected(504,'AI request timed out','ai-request-timeout')),60000);
const monitor=setInterval(()=>{try{assertCurrent();}catch{/* Abort pending provider work; release checks report the fixed failure. */}},100);
monitor.unref();deadline.unref();
try{
@@ -62,7 +63,7 @@ export async function runApprovedAI({store,config,deviceId,policy,prompt,signal}
const output=await traceOperation('ai.inspect.output',{bytes:Buffer.byteLength(answer),policyVersion:policy.version},()=>scan(answer));store.event(deviceId,'ai.output-decision',{...output,policyVersion:policy.version});
if(output.decision==='block'){store.event(deviceId,'ai.output-block',{ruleId:output.ruleId,policyVersion:policy.version});throw rejected(403,'AI response blocked by company policy',output.ruleId);}
assertCurrent();return answer;
}catch(e){store.event(deviceId,'ai.failure',{policyVersion:policy.version});throw e;}
}catch(e){store.event(deviceId,'ai.failure',{policyVersion:policy.version});throw cancellationError||e;}
finally{clearTimeout(deadline);clearInterval(monitor);signal?.removeEventListener('abort',cancel);controller.abort();}
});
}
+7 -2
View File
@@ -4,6 +4,7 @@ import transferFacts from '../../packages/core/transfer-facts.cjs';
import requestTransferFacts from '../../packages/core/request-transfer-facts.cjs';
import agentAudit from '../../packages/core/agent-audit.cjs';
import interactionAudit from '../../packages/core/interaction-facts.cjs';
import transportAudit from '../../packages/core/transport-facts.cjs';
import {installEvidence} from './evidence.mjs';
import {installIncidents} from './incidents.mjs';
import { installBookmarkRoutes,globalBookmarksValid } from './bookmarks.mjs';
@@ -107,7 +108,11 @@ export function createApp(store,config) {
store.db.prepare('UPDATE devices SET last_seen=?,policy_version=? WHERE id=?').run(new Date().toISOString(),b.policyVersion,req.device.id);res.json({ok:true});
});
app.post('/api/device/events',device,(req,res)=>{
const b=z.object({eventId:z.string().uuid(),kind:z.enum(['browser.interaction','browser.interaction.dropped','dlp.block','navigation.block','download.block','policy.applied','browser.started','agent.action','agent.denied','library.ai.proposed','library.ai.applied','library.ai.reconcile','devtools.open-requested','devtools.opened','devtools.closed','devtools.denied','download.scan','download.released','upload.authorized','upload.block','dns.block','content.block','vault.submission-completed','vault.submission.failure','vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.created','vault.unlocked','vault.locked','vault.saved','vault.deleted','vault.filled','vault.fill-requested','phishing.block','phishing.error','phishing.scanned','clipboard.block','clipboard.copy','clipboard.cut','clipboard.paste']),ruleId:z.string().max(100).optional(),host:z.string().max(253).optional(),policyVersion:z.number().int().positive(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional(),file:transferFacts.FileFactsSchema.optional(),request:requestTransferFacts.RequestFactsSchema.optional(),agent:agentAudit.AgentAuditSchema.optional(),interaction:interactionAudit.InteractionFactsSchema.optional()}).strict().parse(req.body);
const b=z.object({eventId:z.string().uuid(),kind:z.enum([...transportAudit.TRANSPORT_KINDS,'browser.interaction','browser.interaction.dropped','dlp.block','navigation.block','download.block','policy.applied','browser.started','agent.action','agent.denied','library.ai.proposed','library.ai.applied','library.ai.reconcile','devtools.open-requested','devtools.opened','devtools.closed','devtools.denied','download.scan','download.released','upload.authorized','upload.block','dns.block','content.block','vault.submission-completed','vault.submission.failure','vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.created','vault.unlocked','vault.locked','vault.saved','vault.deleted','vault.filled','vault.fill-requested','phishing.block','phishing.error','phishing.scanned','clipboard.block','clipboard.copy','clipboard.cut','clipboard.paste']),ruleId:z.string().max(100).optional(),host:z.string().max(253).optional(),policyVersion:z.number().int().positive(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional(),file:transferFacts.FileFactsSchema.optional(),request:requestTransferFacts.RequestFactsSchema.optional(),agent:agentAudit.AgentAuditSchema.optional(),interaction:interactionAudit.InteractionFactsSchema.optional(),transport:transportAudit.TransportFactsSchema.optional()}).strict().parse(req.body);
if(b.kind.startsWith('transport.')){
const unavailable=b.kind==='transport.monitor-unavailable';
if(b.file||b.request||b.agent||b.interaction|| (unavailable?(b.host!==undefined||b.transport!==undefined||!transportAudit.TRANSPORT_MONITOR_REASONS.includes(b.ruleId)):(b.ruleId!==undefined||!transportAudit.canonicalTransportHostname(b.host)||!transportAudit.transportFactsForEvent(b.kind,b.transport))))return res.status(400).json({error:'Transport audit events require matching lifecycle metadata'});
}else if(b.transport!==undefined)return res.status(400).json({error:'Transport facts require a matching transport event'});
if(b.interaction&&!interactionAudit.interactionFactsForEvent(b.kind,b.interaction))return res.status(400).json({error:'Interaction facts require a matching lifecycle event'});
if(b.agent&&!agentAudit.agentFactsForEvent(b.kind,b.agent))return res.status(400).json({error:'Agent facts require a matching agent lifecycle event'});
if(b.file&&!b.kind.startsWith('download.')&&!b.kind.startsWith('upload.'))return res.status(400).json({error:'File facts require a transfer event'});
@@ -269,7 +274,7 @@ export function createApp(store,config) {
app.use((error,_req,res,_next)=>{
config.onError?.(error);
const invalid=error instanceof z.ZodError || error.type==='entity.parse.failed';
res.status(invalid?400:error.type==='entity.too.large'?413:[400,403,404,409].includes(error.status)?error.status:500).json({error:invalid?'Request validation failed':'Request failed',...(error instanceof z.ZodError?{issues:error.issues.map(v=>({path:v.path.join('.'),message:v.message}))}:{})});
res.status(invalid?400:error.type==='entity.too.large'?413:[400,403,404,409,504].includes(error.status)?error.status:500).json({error:invalid?'Request validation failed':'Request failed',...(error instanceof z.ZodError?{issues:error.issues.map(v=>({path:v.path.join('.'),message:v.message}))}:{})});
});
return app;
}
+63 -7
View File
@@ -84,9 +84,55 @@ that the whole application is clean, fully optimized or independently audited.
## Latest reproducible checks
- Full source suite: 749 tests passed, no failures or skips; see the ignored
`artifacts/mvp-final-startup-suite.log`. `npm run lint` passed again with
zero warnings on 2026-10-13.
A subsequent full run after the Established-alarm regression added a test
passed 764 of765 and exposed a real gateway cancellation race: the policy
monitor aborted the provider but discarded its fixed409 error, producing500.
The gateway now preserves the first fixed cancellation reason (policy409,
caller403, deadline504). Fourteen focused tests and scoped lint passed. Actual
collector trace `3b6b5e374da506f7ae5ffbb118933bf6` shows the provider aborted at
101.6ms under the gateway and library parents. The earlier failed full run is
retained in `artifacts/transport-owner-containment-final-suite.log`.
The subsequent native clipboard/upload scenario passed all17 enforcement
checks but failed diagnostics acceptance because lifetime exports recorded two
errors. Collector rejected writes remained0; their exact causes cannot be
reconstructed from the original counters. Failure instrumentation and a fresh
acceptance run must resolve this separately from DLP transmission proof.
The browser now retains at most32 cloned export-failure facts and lifetime
totals by fixed reason. Facts include traces/logs, generation, duration, fixed
HTTP status, retirement overlap and explicitly signalled cancellation. Raw
error text, response bodies, credentials and collector URLs are excluded.
Actual held HTTP exports prove that retirement cancellation remains counted
as failure; the instrumentation does not silently reset or retry operations.
Seventeen focused diagnostics tests passed, including timeout, refusal,503,
bounded history and concurrent flush attribution.
A fresh public-only native diagnostic run completed with four independently
reviewed policy publications and four public HTTPS submissions. Every snapshot
had zero current/lifetime failures and an empty failure history; actual exported
trace `b5eefaf6bb8407f6917bd5cada6e8587` and collector rejectedWrites0 were
verified. Reproduction: `node scripts/native-diagnostics-public.mjs` with the
protected local collector already running. This does not retrospectively prove
the cause of the earlier two failures or reset their historical evidence.
A further quiet public-only native measurement delivered80 benign HTTPS
responses across four reviewed publications: median59.53ms, p95 83.19ms,
max88.17ms; observed main event-loop maximum drift328.29ms over637 samples.
Current/lifetime failures remained0, collector rejectedWrites0. Trace
`d4689e3d28ebe902b752500c0f79e30b` and
`artifacts/public-backpressure-native-proof.json` are local evidence. This does
not resolve or causally explain the semantic run's three timeouts and one
concurrency failure. A bounded export queue, single-flight flush and collector
commit-latency measurements are the next narrowly scoped diagnostics work;
mandatory inspection and audit durability must remain intact.
- Full source suite: 771 tests passed with `--test-concurrency=2`, no failures
or skips; see `artifacts/transport-owner-final-failure-ledger-suite.log`. An earlier
higher-concurrency run failed one evidence-key ACL check; its cause remains
unconfirmed and its log is retained. All 18 evidence tests passed separately.
No ACL protection or timeout was weakened. `npm run lint` passed with zero
warnings after the final source changes.
- A fresh `npm audit --omit=dev --json` reported zero advisories. Development
toolchain findings remain tracked separately in `dependency-audit.md`.
- In the paired single module-load profiles, diagnostics require time changed
@@ -95,7 +141,11 @@ that the whole application is clean, fully optimized or independently audited.
measurements do not establish full application startup or percentile gains.
- Native Windows DLP fixtures delivered public data and blocked confidential
literal and semantic payloads. Restart reused the protected device profile.
These are separate from final packaged-artifact acceptance.
These are separate from packaged-artifact acceptance. The final literal and
actual-restart fixtures pass17+5 checks with zero lifetime exports failed.
The latest semantic receiver/model checks pass, but diagnostics acceptance
fails on three measured2s timeouts and one concurrency rejection. These are
retained as real failures, not cancelled exports or successful delivery.
- The protected local OpenTelemetry receiver reported zero rejected writes.
Trace `33b46998b44c3179cccd2826d5790c00` correlates browser.request,
browser.inspect.local, inspector.inspect, semantic load/inference and durable
@@ -111,6 +161,12 @@ that the whole application is clean, fully optimized or independently audited.
- The full enrolled browser WebTransport receiver fixture now passes through
reviewed DIRECT and fetched HTTPS PAC policies: zero managed packets/bytes,
paired against 132 baseline bytes, with ordinary HTTPS submissions working.
It records source hashes and genuine operational traces. There is still no
per-attempt WebTransport security event because this path produces no
Electron webRequest callback; transport denial and audit coverage are separate.
It records source hashes and genuine operational traces. A fixed private
native channel now shares the existing clipboard debugger owner and records
six Created and six Closed security events across the tested window and
dedicated-worker attempts, with zero Established events. All twelve events
correlate with exported audit traces. No raw URL, payload or CDP identifier
enters these events. See [native transport observability](native-transport-observability.md)
for coverage limits, failure behavior and ECS/CEF mapping. The private relay
remains the transmission boundary; lifecycle observations alone do not prove
content inspection or universal realm coverage.
+15
View File
@@ -2,6 +2,12 @@
This is a read-only review of the current source and unpacked Windows executable. It is not a penetration test or a claim of complete Chromium DLP coverage.
Later entries record implementation and repeated native verification following
this initial review. For current WebTransport per-attempt auditing and its
coverage limits, use [native transport observability](native-transport-observability.md).
Historical missing-event findings below have been superseded by that work;
the separate native WebRTC engine crash remains an open acceptance gate.
## Official references
Electron recommends isolated, sandboxed remote renderers, constrained IPC and navigation, current dependencies, and consideration of custom protocols and packaging fuses. [Security](https://www.electronjs.org/docs/latest/tutorial/security).
@@ -81,6 +87,15 @@ Inspection/inference runs outside the main process, but synchronous audit encryp
### Final package follow-up
The subsequent shared clipboard/transport owner implementation also passed
actual Windows package/runtime and fuse negative verifiers. Its enrolled native
fixture now proves six Created and six Closed records, zero Established records,
and twelve correlated lifecycle/audit traces. Clipboard/restart and actual local
Gemma regressions passed with matching source hashes. This supersedes the
transport audit-gap statements below for the specifically tested Window and
dedicated Worker cases; it is not universal transport or payload coverage.
See [native transport observation](native-transport-observability.md).
The new frozen-source Windows build and both package verifiers passed after
the startup correction. Actual baseline, RUN_AS_NODE, NODE_OPTIONS, CLI
inspector, eleven certificate/feature override launches and copied-ASAR tampering
+6 -3
View File
@@ -28,9 +28,12 @@ Native evidence: `artifacts/webtransport-native-proof.json`. The receiver got
zero packets/bytes with the private relay while ordinary HTTP loaded. The
Windows core DLP and real restart fixtures also passed with relay integration.
Chromium upgrades must repeat this receiver proof; it is version-specific.
WebTransport-specific denial audit events are not yet observed because the
native rejection occurs before existing HTTP/relay callbacks. Do not claim
complete transport-attempt SIEM coverage from the ordinary DLP event pipeline.
The native rejection still occurs before HTTP/relay callbacks. A private native
observer now supplies real lifecycle records for the tested Window/dedicated
Worker paths through enrollment and policy changes. See
[native transport observation](native-transport-observability.md). These are
Created/Closed observations, not inspected-payload decisions or universal
transport coverage. The relay remains the release boundary.
Inspected WebSocket/WebRTC support remains the intended compatibility goal.
Uninspected transports remain restricted until their content gates have their
+77
View File
@@ -105,3 +105,80 @@ attempts; its tagged binder source lacks the no-op implementation. A later
binary is acceptable only after source inclusion checks and all native gates
above. Current ignored artifacts retain the original crashes and positive
controls; an upgrade must not overwrite that evidence.
## Local setup started, 2026-10-13
An isolated checkout now exists at
`C:/Users/karl/Safetynet/artifacts/native-build/electron44`.
Electron tag44.7.0 resolved to commit
`61f55c4ce540416b3e43f75f6c0f125d22e5e908`; its DEPS pins152.0.7977.130.
The adjacent depot_tools checkout is pinned at
`52ae103994896b9ce7742a605de8f43b410e5515`.
The ignored `run-sync.mjs` wrapper supplies process-only PATH/toolchain/Git
configuration and runs `gclient.bat sync --nohooks --shallow --jobs=2
--revision=src@2c592105bbcd9490a9894df48d0fe59b2c512651`.
It records `source-sync.log`, stops its own process tree if free disk falls below
150GiB, and bounds each run to30minutes. A live bootstrap is not source completion.
Current depot_tools expects `git.bat`; the first sync failed because only
`git.exe` was installed. An isolated `artifacts/native-build/bin/git.bat` now
forwards to the existing signed `C:/Program Files/Git/cmd/git.exe`; only the
wrapper's PATH includes that shim. The initial Chromium clone observed after
retry did not include a depth argument, so the requested shallow option must
not be treated as proof of a shallow fetch. Disk/deadline bounds still apply.
Hooks, patch application against Electron's full source, compilation and native
acceptance remain pending.
The controlled tagged bootstrap completed on2026-10-14 with exit0. Chromium
`152.0.7977.130` resolved to
`2c592105bbcd9490a9894df48d0fe59b2c512651`; Git reports a shallow repository.
Electron's pinned nested checkout was preserved. The backport's `git apply
--check` also passed against this **complete pristine Chromium checkout**.
Electron's dependency synchronization and native patch hooks have not yet
completed, so this does not replace the required check after Electron patches.
The bootstrap wrapper is `artifacts/native-build/run-bootstrap.mjs`; its log is
`bootstrap.log`. Initial fetch transferred a1.43GB pack; free disk after checkout
was259.3GiB. No compiler or native test was executed at this milestone.
The first dependency-sync attempt after bootstrap attempted a broad Chromium
remote fetch because the DEPS revision was a textual tag. That owned process
tree was explicitly stopped; its terminal exit was1. The matching local tag
was installed and the wrapper now overrides `src` with the exact existing
commit above, without modifying DEPS. The next run advanced into dependencies:
Node's `v24.21.0` checkout completed at
`955266bfdd854cd280dffd47548673914484e4c0`, followed by other Chromium
dependencies. At this documentation snapshot the bounded dependency run is
still active: dependency completion, patch hooks and compilation are **not
verified**. Gclient initial dependency clones may still fetch full histories
despite the shallow option; the disk floor and run deadline remain mandatory.
The same dependency sync was subsequently stopped deliberately to free resources
for failed application-test diagnosis. Its terminal exit was1; the owned process
tree was verified and stopped, without starting another sync. Chromium, Electron
and Node pins remain unchanged. Dawn, DevTools, ICU and Ink checkouts are present;
V8 and WebRTC dependency checkouts are still absent. Free disk at this checkpoint
was246.2GiB. This is a resumable preparation checkpoint, not a completed build.
### Missing ATL/MFC: explicit administrator step
Inspection found the existing installer at
`C:/Program Files (x86)/Microsoft Visual Studio/Installer/setup.exe`, version
4.10.30.62513. Its Authenticode status was **Valid**, signer Microsoft Corporation.
Existing installation:
`C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools`.
The official required component ID is
`Microsoft.VisualStudio.Component.VC.ATLMFC`.
The current agent process is not elevated. No UAC bypass or installer launch was
attempted. To install only this missing component, a user can run the following
in an **administrator PowerShell** and allow the normal installer flow:
```powershell
& 'C:/Program Files (x86)/Microsoft Visual Studio/Installer/setup.exe' modify `
--installPath 'C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools' `
--add Microsoft.VisualStudio.Component.VC.ATLMFC --passive --norestart
```
Verify afterward using `vswhere.exe -products '*' -requires
Microsoft.VisualStudio.Component.VC.ATLMFC -property installationPath`.
Treat installer exit3010 as restart required; do not claim build readiness until
the component is detected and toolchain checks pass.
+76
View File
@@ -0,0 +1,76 @@
# Native WebTransport security observation
The private HTTP relay is the release boundary. Observation does not inspect
WebTransport payloads and cannot replace native transport containment.
Electron webRequest does not observe the tested Chromium WebTransport path.
The browser now subscribes privately to native Created, Established and Closed
events through the existing clipboard security owner's CDP connection.
The [Electron debugger API](https://www.electronjs.org/docs/latest/api/debugger)
and [Chromium Network protocol](https://chromedevtools.github.io/devtools-protocol/tot/Network/)
describe this instrumentation. No debugger port or protocol dispatcher is
exposed to websites, UI JavaScript or local agents.
## Ownership and lifecycle
The clipboard guard owns the only debugger attachment. Its fixed transport
lease can enable network observation, attach related targets and resume startup
targets; it cannot submit arbitrary commands or detach clipboard protection.
Initial about:blank navigation completes before setup commands, which Chromium
can otherwise defer indefinitely. Setup finishes before real page navigation.
Worker observation is queued before startup resume, covering the tested worker's
first statement. Disposal unsubscribes observation, resumes waiting targets and
releases auto-attachment while preserving clipboard protection.
Commands, targets, pending attempts and audit queues are bounded. Unexpected
detach, command timeout, overflow or audit failure makes the browser remain
quarantined until restart. Unexpected native establishment also quarantines
synchronously in the native event handler, before entering the serialized audit
queue. A held Created-audit regression verifies this ordering. This is a containment alarm;
it is not proof that an observer could prevent already transmitted bytes.
## Security records
`transport.created`, `transport.closed` and `transport.established` carry an
application-generated UUID, protocol, phase, realm, associated native tab UUID,
connection-established flag and canonical destination hostname. Creation policy
and owner context survive subsequent navigation or policy changes. Native CDP
identifiers, URLs, query strings, initiators, certificates, headers and bodies
are absent. Backend validation rejects contradictory phases, extra fields and
noncanonical host data. Monitor failures accept only fixed reason identifiers.
Records use the protected durable event pipeline. ECS and CEF preserve the
lifecycle facts as observations with unknown outcome: a Closed event is not
automatically claimed as an inspected or blocked payload. Operational traces
use fixed browser.transport stages and correlate audit encryption, persistence
and delivery without destination or content attributes. The signed diagnostics
switch still controls operational exports independently of security auditing.
## Actual Windows evidence
The current shared-owner native fixture proves Window and first-statement
dedicated Worker behavior through enrollment, reviewed DIRECT and fetched HTTPS
PAC→DIRECT policies. The unguarded control delivered 132 bytes. Managed cases
delivered zero packets or bytes, while three public HTTPS submissions worked.
Six Created and six Closed records reached the central backend; none Established.
All twelve exported phase traces have matching audit-stage parents. Ordinary
webRequest callbacks remain zero, so these are actual native observations.
Evidence: ignored `artifacts/fullmain-webtransport-native-proof.json`, source
hashes and `artifacts/cdp-webtransport-native-proof.json`. The latter additionally
checks real DevTools coexistence and verifies that observer disposal preserves
clipboard ownership. Earlier literal/restart and real local Gemma regressions
passed on the same source hashes. The later final-source literal/restart test
also passes; the later semantic DLP checks pass but its debug acceptance records
three export timeouts and one concurrency rejection. Those failures remain
open and are separate from receiver containment. These scenarios do not prove every worker, iframe,
shared-worker attribution or future Chromium transport implementation.
## Measured limits
During concurrent native source checkout/build work, semantic paste took
2.9–3.7 seconds and public-file inspection 14.3 seconds. Protected audit fsync
reached 414 ms. These traces identify work to profile; they do not isolate an
observer regression or justify weakening scanning, isolation or persistence.
The separate initial-about:blank WebRTC engine crash remains unresolved until a
fixed native runtime is compiled and passes its paired receiver tests.
+12
View File
@@ -7,6 +7,18 @@ Die breiteren Anforderungen und Erweiterungen unten bleiben Roadmap.
## Aktuelle nächste Abnahmen
**Aktualisierung des Windows-MVP:** 771 Tests bestehen mit begrenzter
Parallelität. Der aktuelle Windows-Build besteht Paket-, Fuse- und
Manipulationsprüfungen, ist aber nicht signiert. Native Clipboard-/Upload-Tests,
Prozessneustart bestehen auf dem finalen Quellstand. Die lokalen semantischen
Gemma-DLP-Prüfungen bestehen, ihre Debug-Abnahme scheitert noch an drei
Export-Timeouts und einer Concurrency-Ablehnung. WebTransport-Versuche in Fenster und Dedicated Worker sind nun
als korrelierte Security-Events und OpenTelemetry-Traces nachgewiesen; siehe
[Transport-Nachweise](native-transport-observability.md). Der bekannte native
WebRTC-Absturz bleibt offen, bis ein korrigiertes Binary geprüft ist.
Die folgenden älteren Integrationszahlen beschreiben historische Zwischenstände;
die aktuelle verbindliche Abnahme steht im [Windows-MVP-Ziel](windows-dlp-mvp.md).
Aktueller Integrationsstand: 713 Tests bestehen
(`artifacts/worker-integration-suite.log`). Der echte Admin-Workflow für typisierte
Login-Regeln, unabhängige Veröffentlichung und Auditor-Leserechte ist geprüft:
+40 -5
View File
@@ -48,8 +48,34 @@ erlaubte Ziele gelangen. Erst nach der folgenden Abnahme wird das MVP abgeschlos
## Aktueller Abnahmestand
749 Tests bestehen im letzten vollständigen Lauf; ESLint verlangt null Fehler
und null Warnungen. Nachweis: `artifacts/mvp-final-startup-suite.log`.
**Letzter Quellstand:** Windows-Paket und beide Paketverifikationen bestehen;
Authenticode bleibt `NotSigned`. Native Clipboard-/Upload- und echte Neustarttests
bestehen erneut mit17+5 Prüfungen,13 öffentlichen Requests, null vertraulichen
Requests und zwei exakt übertragenen Dateien. Aktuelle und lebenslange
Exportfehler bleiben in diesen Prozessen0. Der frische öffentliche Diagnosetest
mit vier geprüften Policy-Veröffentlichungen besteht ebenfalls.
**Noch offen:** Die letzte echte lokale Gemma-Abnahme besteht alle vier
DLP-/Empfängerprüfungen, scheitert aber an drei gemessenen2s-Export-Timeouts und
einem `concurrency-limit`. Die neuen begrenzten Fehlerdaten belegen aktive
Generation1 ohne Policy-Abbruch; sie beweisen noch nicht die Ursache der
Antwortverzögerung. Kein Zähler wurde gelöscht oder eine Erwartung gelockert.
Für die Debug-Abnahme sind begrenzte Export-Warteschlangen, ein gemeinsamer
Flush und Latenz-/Eventloop-Nachweise zu prüfen. Ein weiterer geschützter
Transporttest wurde nach diesem Fehler nicht einfach wiederholt.
Der native WebRTC-Absturz bleibt ein separater offener Stabilitäts-Gate.
Das MVP ist deshalb weiterhin **nicht abgeschlossen**.
771 Tests bestehen im letzten vollständigen Lauf mit zwei parallelen Testdateien;
ESLint verlangt null Fehler und null Warnungen. Nachweis:
`artifacts/transport-owner-final-failure-ledger-suite.log`. Ein späterer
Policy-Abbruch verlor ursprünglich seinen festen409-Fehler und lieferte500;
dieser echte Gateway-Fehler ist mit deterministischen Tests und exportierten
Traces behoben. Der stärker parallelisierte
Lauf scheiterte an einer Evidence-Schlüssel-Rechteprüfung. Die kontrollierte
ACL-Nachprüfung war geschützt; alle 18 Evidence-Tests bestanden anschließend
separat. Die ursprüngliche Ausnahme enthält keine ausreichende Ursache, um
einen Timeout rückwirkend als bewiesen zu behaupten. Kein Schutz wurde gelockert.
Die neue native Windows-Abnahme besteht mit 17 Checks, einschließlich echtem
Maus-Klick im contenteditable-Editor, Klartext/HTML-Clipboard, Policy-Race,
YARA-/ICAP-Block und Scanner-Ausfall. Danach startete ein anderer Browserprozess
@@ -61,7 +87,11 @@ mit null Exportfehlern. Nachweise: `artifacts/windows-dlp-mvp-process-restart-su
`windows-dlp-mvp-first-process-native-proof.json` und
`windows-dlp-mvp-restart-native-proof.json`.
Der aktuelle integrierte Relay-/Proxy-/Preload-Stand bestand diese native
Abnahme erneut: `artifacts/windows-dlp-mvp-current-frozen-native.log`.
Abnahme erneut. Der gemeinsame Clipboard-/Transport-Owner bestand danach
wieder alle 17 plus fünf echten Neustart-Checks:
`artifacts/transport-owner-mvp-native.log`. Auch die echte lokale Gemma-Abnahme
bestand erneut: `artifacts/transport-owner-semantic-native.log`; Quellhashes
blieben unverändert.
Das Gateway prüft normalisierte Textdarstellungen einzeln und revalidiert
Policy/Widerruf vor Versand und Ausgabe. Tatsächliche ICAP-/Provider-Tests
@@ -90,8 +120,13 @@ enrollte Browser übertrug null Pakete und Bytes, auch nach reviewed DIRECT und
HTTPS-PAC→DIRECT-Policywechseln. Drei gewöhnliche HTTPS-POSTs funktionierten.
Nachweis: `artifacts/fullmain-webtransport-native-proof.json`, mit genauen
Quellhashes; spätere Änderungen betreffen ausschließlich Startparameterprüfung.
Die vor HTTP blockierten Versuche liefern keine webRequest-Callbacks und
damit weiterhin keine einzelnen DLP-Audit-Ereignisse. Diese SIEM-Grenze bleibt offen.
Die vor HTTP blockierten Versuche liefern weiterhin keine webRequest-Callbacks.
Der neue private native Observer erfasst jedoch tatsächlich sechs Created- und
sechs Closed-Lebenszyklen mit korrelierten Traces und ECS-/CEF-Metadaten.
Er teilt den begrenzten Kanal des Clipboard-Owners und trennt dessen Schutz
nicht. Die Reichweite ist Window und getesteter Dedicated Worker, keine
universelle Realm- oder Nutzlastprüfung. Details:
[Native Transportbeobachtung](native-transport-observability.md).
Ältere fehlgeschlagene Testläufe bleiben als solche erhalten.
## Verbleibende Abnahmen
+9 -8
View File
@@ -1,26 +1,27 @@
import interactionAudit from './interaction-facts.cjs';
import transportAudit from './transport-facts.cjs';
import agentAudit from './agent-audit.cjs';
import transferFacts from './transfer-facts.cjs';
import requestTransferFacts from './request-transfer-facts.cjs';
const requestFacts=(metadata,kind)=>{if(!kind?.startsWith('upload.'))return null;const old=metadata.file;const parsed=requestTransferFacts.RequestFactsSchema.safeParse(metadata.request||(old?{bytes:old.bytes,detectedType:old.detectedType}:undefined));return parsed.success?parsed.data:null;};
const facts=metadata=>{const parsed=transferFacts.FileFactsSchema.safeParse(metadata.file);return parsed.success?parsed.data:null;};
export function toECS(event,installationId='safetynet') {
const m=event.metadata,interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),block=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',failure=event.kind.endsWith('.failure')||agent?.outcome==='failed',alert=event.kind==='sigma.alert';
const m=event.metadata,transport=transportAudit.canonicalTransportHostname(m.host)?transportAudit.transportFactsForEvent(event.kind,event.metadata.transport):null,transportEvent=transportAudit.TRANSPORT_KINDS.includes(event.kind),interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),block=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',failure=event.kind.endsWith('.failure')||agent?.outcome==='failed',alert=event.kind==='sigma.alert';
const config=event.kind.startsWith('policy.')||event.kind.startsWith('enrollment.')||event.kind==='device.revoked';
const auth=event.kind.startsWith('sso.'),clipboard=event.kind.startsWith('clipboard.');
return {
'@timestamp':event.time,ecs:{version:'8.17.0'},
event:{id:`${installationId}:${event.id}`,kind:alert?'alert':'event',module:'safetynet',dataset:'safetynet.security',
action:event.kind,category:[alert?'intrusion_detection':auth?'authentication':config?'configuration':clipboard?'process':'web'],
type:[alert||interaction?.outcome==='observed'?'info':failure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:interaction?.outcome==='observed'?'unknown':failure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
type:[transportEvent||alert||interaction?.outcome==='observed'?'info':failure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:transportEvent||interaction?.outcome==='observed'?'unknown':failure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
observer:{vendor:'SafetyNet',product:'Managed Enterprise Browser',type:'dlp',version:'0.1.0'},
...(event.device_id?{host:{id:event.device_id}}:{}),
...(m.actor?{user:{id:m.actor}}:{}),...(m.host?{destination:{domain:m.host}}:{}),
...(m.actor?{user:{id:m.actor}}:{}),...(m.host&&(!transportEvent||transport)?{destination:{domain:m.host}}:{}),
...(m.ruleId?{rule:{id:m.ruleId}}:{}),
...(event.kind.startsWith('download.')&&facts(m)?{file:{size:facts(m).bytes,type:facts(m).detectedType,hash:{sha256:facts(m).sha256}}}:{}),
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{trace:{id:m.traceId}}:{}),
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{span:{id:m.spanId}}:{}),
safetynet:{...(interaction?{interaction:{action:interaction.action,outcome:interaction.outcome,session_id:interaction.sessionId}}:{}),...(agent?{agent:{action:agent.action,scope:agent.scope,outcome:agent.outcome,...(agent.sessionId?{session_id:agent.sessionId}:{}),...(agent.principalId?{principal_id:agent.principalId,credential_generation:agent.credentialGeneration}:{})}}:{}),...(requestFacts(m,event.kind)?{request:{bytes:requestFacts(m,event.kind).bytes,detected_type:requestFacts(m,event.kind).detectedType}}:{}),...(event.audit?{audit:event.audit}:{}),policy_version:m.policyVersion??m.version??null,decision:m.decision??(block?'block':null),
safetynet:{...(transport?{transport:{transport_id:transport.transportId,protocol:transport.protocol,phase:transport.phase,realm:transport.realm,session_id:transport.sessionId,connection_established:transport.connectionEstablished}}:{}),...(event.kind==='transport.monitor-unavailable'&&transportAudit.TRANSPORT_MONITOR_REASONS.includes(m.ruleId)?{reason:m.ruleId}:{}),...(interaction?{interaction:{action:interaction.action,outcome:interaction.outcome,session_id:interaction.sessionId}}:{}),...(agent?{agent:{action:agent.action,scope:agent.scope,outcome:agent.outcome,...(agent.sessionId?{session_id:agent.sessionId}:{}),...(agent.principalId?{principal_id:agent.principalId,credential_generation:agent.credentialGeneration}:{})}}:{}),...(requestFacts(m,event.kind)?{request:{bytes:requestFacts(m,event.kind).bytes,detected_type:requestFacts(m,event.kind).detectedType}}:{}),...(event.audit?{audit:event.audit}:{}),policy_version:m.policyVersion??m.version??null,decision:transportEvent?null:m.decision??(block?'block':null),
content_type:m.contentType??null,model:m.model??null,coverage:m.coverage??null,classification:m.classification==='local-only'?'local-only':null,
...(alert?{source_event_id:`${installationId}:${m.sourceEventId}`,severity:m.level}:{})}
};
@@ -29,16 +30,16 @@ const header=v=>String(v).replace(/\\/g,'\\\\').replace(/\|/g,'\\|').replace(/[\
const ext=v=>String(v).replace(/\\/g,'\\\\').replace(/=/g,'\\=').replace(/\r/g,'\\r').replace(/\n/g,'\\n');
const severity=level=>({low:3,medium:5,high:7,critical:10}[level]??3);
export function toCEF(event,installationId='safetynet') {
const m=event.metadata,interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),blocked=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',alert=event.kind==='sigma.alert';
const m=event.metadata,transport=transportAudit.canonicalTransportHostname(m.host)?transportAudit.transportFactsForEvent(event.kind,event.metadata.transport):null,transportEvent=transportAudit.TRANSPORT_KINDS.includes(event.kind),interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),blocked=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',alert=event.kind==='sigma.alert';
// suid identifies the verified agent capability principal, not an employee account.
// suser, when present, remains the independently supplied human actor.
const fields={...(interaction?{cs2Label:'InteractionAction',cs2:interaction.action,cs3Label:'InteractionOutcome',cs3:interaction.outcome,requestContext:interaction.sessionId}:{}),...(agent?{cs2Label:'AgentAction',cs2:agent.action,cs3Label:'AgentScope',cs3:agent.scope,cs6Label:'AgentOutcome',cs6:agent.outcome,...(agent.sessionId?{requestContext:agent.sessionId}:{}),...(agent.principalId?{suid:agent.principalId,flexNumber1Label:'AgentCredentialGeneration',flexNumber1:agent.credentialGeneration}:{})}:{}),...(requestFacts(m,event.kind)?{cn3Label:'RequestBytes',cn3:requestFacts(m,event.kind).bytes,cs6Label:'DetectedRequestType',cs6:requestFacts(m,event.kind).detectedType}:{}),...(event.audit?{flexString1Label:'AuditRecordHash',flexString1:event.audit.hash,flexString2Label:'AuditSequence',flexString2:event.audit.sequence}:{}),externalId:`${installationId}:${event.id}`,rt:Date.parse(event.time),act:event.kind,
const fields={...(transport?{cs2Label:'TransportProtocol',cs2:transport.protocol,cs3Label:'TransportPhase',cs3:transport.phase,cs6Label:'TransportRealm',cs6:transport.realm,requestContext:transport.sessionId,cs1Label:'TransportId',cs1:transport.transportId,flexNumber2Label:'ConnectionEstablished',flexNumber2:transport.connectionEstablished?1:0}:{}),...(event.kind==='transport.monitor-unavailable'&&transportAudit.TRANSPORT_MONITOR_REASONS.includes(m.ruleId)?{reason:m.ruleId}:{}),...(interaction?{cs2Label:'InteractionAction',cs2:interaction.action,cs3Label:'InteractionOutcome',cs3:interaction.outcome,requestContext:interaction.sessionId}:{}),...(agent?{cs2Label:'AgentAction',cs2:agent.action,cs3Label:'AgentScope',cs3:agent.scope,cs6Label:'AgentOutcome',cs6:agent.outcome,...(agent.sessionId?{requestContext:agent.sessionId}:{}),...(agent.principalId?{suid:agent.principalId,flexNumber1Label:'AgentCredentialGeneration',flexNumber1:agent.credentialGeneration}:{})}:{}),...(requestFacts(m,event.kind)?{cn3Label:'RequestBytes',cn3:requestFacts(m,event.kind).bytes,cs6Label:'DetectedRequestType',cs6:requestFacts(m,event.kind).detectedType}:{}),...(event.audit?{flexString1Label:'AuditRecordHash',flexString1:event.audit.hash,flexString2Label:'AuditSequence',flexString2:event.audit.sequence}:{}),externalId:`${installationId}:${event.id}`,rt:Date.parse(event.time),act:event.kind,
...(event.device_id?{deviceExternalId:event.device_id}:{}),...(m.actor?{suser:m.actor}:{}),
...(m.ruleId?{cs1Label:'RuleId',cs1:m.ruleId}:{}),...(m.host?{dhost:m.host}:{}),
...(m.ruleId?{cs1Label:'RuleId',cs1:m.ruleId}:{}),...(m.host&&(!transportEvent||transport)?{dhost:m.host}:{}),
...(m.policyVersion||m.version?{cn1Label:'PolicyVersion',cn1:m.policyVersion??m.version}:{}),
...(event.kind.startsWith('download.')&&facts(m)?{cn2Label:'FileBytes',cn2:facts(m).bytes,cs6Label:'DetectedFileType',cs6:facts(m).detectedType,fileHash:facts(m).sha256}:{}),
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{cs4Label:'TraceId',cs4:m.traceId}:{}),
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{cs5Label:'SpanId',cs5:m.spanId}:{}),
...(alert?{cs2Label:'SourceEventId',cs2:`${installationId}:${m.sourceEventId}`,cs3Label:'AlertLevel',cs3:m.level}:{})};
return `CEF:0|SafetyNet|Enterprise Browser|0.1.0|${header(event.kind)}|${header(event.kind)}|${alert?severity(m.level):blocked?7:3}|`+Object.entries(fields).map(([k,v])=>`${k}=${ext(v)}`).join(' ');
return `CEF:0|SafetyNet|Enterprise Browser|0.1.0|${header(event.kind)}|${header(event.kind)}|${alert?severity(m.level):!transportEvent&&blocked?7:3}|`+Object.entries(fields).map(([k,v])=>`${k}=${ext(v)}`).join(' ');
}
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const {z}=require('zod');
const TransportFactsSchema=z.object({transportId:z.string().uuid(),protocol:z.literal('webtransport'),phase:z.enum(['created','closed','established']),realm:z.enum(['window','worker','iframe']),sessionId:z.string().uuid(),connectionEstablished:z.boolean()}).strict().refine(f=>f.phase==='closed'||f.connectionEstablished===(f.phase==='established'));
const TRANSPORT_MONITOR_REASONS=Object.freeze(['transport-observer-detached','transport-observer-command-timeout','transport-observer-overflow','transport-observer-audit-failed','transport-observer-unavailable','unexpected-webtransport-established']);
const TRANSPORT_KINDS=Object.freeze(['transport.created','transport.closed','transport.established','transport.monitor-unavailable']);
function canonicalTransportHostname(host){
if(typeof host!=='string'||!host.length||host.length>253)return false;
try{const url=new URL(`https://${host}`);return url.hostname===host&&url.host===host&&url.username===''&&url.password===''&&url.port===''&&url.pathname==='/'&&url.search===''&&url.hash==='';}catch{return false;}
}
function transportFacts(value){return TransportFactsSchema.parse(value);}
function transportFactsForEvent(kind,value){const parsed=TransportFactsSchema.safeParse(value);return parsed.success&&kind===`transport.${parsed.data.phase}`?parsed.data:null;}
module.exports={canonicalTransportHostname,TransportFactsSchema,transportFacts,transportFactsForEvent,TRANSPORT_MONITOR_REASONS,TRANSPORT_KINDS};
+3 -3
View File
@@ -6,9 +6,9 @@ if(mode==='managed'){const partition=session.fromPartition.bind(session),seenSes
/* global WebTransport */
async function attemptWT(url,hash){const result={api:typeof WebTransport==='function',connected:false,datagramWritten:false,streamWritten:false,errorClass:null};let transport;try{transport=new WebTransport(url,{serverCertificateHashes:[{algorithm:'sha-256',value:new Uint8Array(hash)}]});await Promise.race([transport.ready,new Promise((_,reject)=>setTimeout(()=>reject(Error('timeout')),5000))]);result.connected=true;const bytes=new TextEncoder().encode('SYNTHETIC_WEBTRANSPORT_DLP_MARKER'),datagram=transport.datagrams.writable.getWriter();await datagram.write(bytes);datagram.releaseLock();result.datagramWritten=true;const stream=await transport.createUnidirectionalStream(),writer=stream.getWriter();await writer.write(bytes);await writer.close();result.streamWritten=true;await new Promise(r=>setTimeout(r,350));}catch(error){result.errorClass=['Error','TypeError','WebTransportError','NetworkError'].includes(error.name)?error.name:'OtherError';}finally{transport?.close();}return result;}
const wait=ms=>new Promise(r=>setTimeout(r,ms));async function until(fn){for(let i=0;i<300;i++){const value=await fn();if(value)return value;await wait(100);}throw Error('Full main WebTransport fixture timeout');}
async function attemptRealms(page){const window=await page.executeJavaScript(`(${attemptWT.toString()})(${JSON.stringify(target)},${JSON.stringify(hashBytes)})`);const source=`const attempt=${attemptWT.toString()};onmessage=async e=>{postMessage(await attempt(e.data.url,e.data.hash));close();};`,worker=await page.executeJavaScript(`new Promise((resolve,reject)=>{const url=URL.createObjectURL(new Blob([${JSON.stringify(source)}],{type:'text/javascript'})),worker=new Worker(url),timer=setTimeout(()=>{worker.terminate();URL.revokeObjectURL(url);reject(Error('Worker timeout'));},8000);worker.onmessage=e=>{clearTimeout(timer);worker.terminate();URL.revokeObjectURL(url);resolve(e.data)};worker.onerror=()=>{clearTimeout(timer);worker.terminate();URL.revokeObjectURL(url);reject(Error('Worker fault'));};worker.postMessage({url:${JSON.stringify(target+'/worker')},hash:${JSON.stringify(hashBytes)}});})`);return {window,worker};}
async function attemptRealms(page){const window=await page.executeJavaScript(`(${attemptWT.toString()})(${JSON.stringify(target)},${JSON.stringify(hashBytes)})`);const source=`const attempt=${attemptWT.toString()};attempt(${JSON.stringify(target+'/worker')},${JSON.stringify(hashBytes)}).then(async result=>{await new Promise(r=>setTimeout(r,200));postMessage(result);close();});`,worker=await page.executeJavaScript(`new Promise((resolve,reject)=>{const url=URL.createObjectURL(new Blob([${JSON.stringify(source)}],{type:'text/javascript'})),worker=new Worker(url),timer=setTimeout(()=>{worker.terminate();URL.revokeObjectURL(url);reject(Error('Worker timeout'));},8000);worker.onmessage=e=>{clearTimeout(timer);worker.terminate();URL.revokeObjectURL(url);resolve(e.data)};worker.onerror=()=>{clearTimeout(timer);worker.terminate();URL.revokeObjectURL(url);reject(Error('Worker fault'));};})`);return {window,worker,workerExecution:'first-statement'};}
app.whenReady().then(async()=>{let page,win,stage='startup';try{const fingerprint=new X509Certificate(fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem'))).fingerprint256;if(mode==='baseline'){session.defaultSession.setCertificateVerifyProc((req,cb)=>{let valid=false;try{valid=req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint;}catch{}cb(valid?0:-2);});let callbacks=0;session.defaultSession.webRequest.onBeforeRequest({urls:[target+'*']},(_details,callback)=>{callbacks++;callback({cancel:true});});win=new BrowserWindow({show:false,webPreferences:{sandbox:true,nodeIntegration:false,contextIsolation:true}});page=win.webContents;await win.loadURL(origin+'/workspace');const realms=await attemptRealms(page);assert.equal(callbacks,0);for(const realm of [realms.window,realms.worker])assert.ok(realm.connected&&realm.datagramWritten&&realm.streamWritten);fs.writeFileSync(process.env.SAFETYNET_WT_OUTPUT,JSON.stringify({actualElectron:true,mode,realms,webRequestCallbacks:callbacks,electron:process.versions.electron,chromium:process.versions.chrome},null,2));win.destroy();app.exit(0);return;}
win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});for(const partition of ['persist:safetynet-managed','persist:safetynet-management'])session.fromPartition(partition).setCertificateVerifyProc((req,cb)=>{let valid=false;try{valid=req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint;}catch{}cb(valid?0:-2);});const base=process.env.SAFETYNET_SMOKE_SERVER,headers={authorization:'Bearer '+process.env.SAFETYNET_SMOKE_ADMIN,'content-type':'application/json'},ui=async(method,value)=>{const response=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);assert.equal(response.ok,true);return response.data;};stage='actual enrollment';await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading()&&page.getURL()===origin+'/workspace');const phases=[];const test=async name=>{stage=name;const before=await(await fetch(process.env.SAFETYNET_WT_STATS)).json(),resolvedProxy=await page.session.resolveProxy(target);assert.notEqual(resolvedProxy,'DIRECT');const realms=await attemptRealms(page);for(const realm of [realms.window,realms.worker])assert.ok(!realm.connected&&!realm.datagramWritten&&!realm.streamWritten);const after=await(await fetch(process.env.SAFETYNET_WT_STATS)).json();for(const field of ['packets','connections','datagrams','streams','bytes','workerBytes'])assert.equal(after[field],before[field],'WebTransport reached receiver');assert.equal(await page.executeJavaScript("fetch('/receiver',{method:'POST',body:'Public full main ordinary prompt'}).then(r=>r.ok).catch(()=>false)"),true);phases.push({name,policyVersion:(await ui('state')).policyVersion,proxyIsNonDirect:true,realms,receiverDelta:Object.fromEntries(['packets','connections','datagrams','streams','bytes','workerBytes'].map(key=>[key,after[key]-before[key]])),benignHTTPSPostAllowed:true});};await test('enrolled private relay');
win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});for(const partition of ['persist:safetynet-managed','persist:safetynet-management'])session.fromPartition(partition).setCertificateVerifyProc((req,cb)=>{let valid=false;try{valid=req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint;}catch{}cb(valid?0:-2);});const base=process.env.SAFETYNET_SMOKE_SERVER,headers={authorization:'Bearer '+process.env.SAFETYNET_SMOKE_ADMIN,'content-type':'application/json'},ui=async(method,value)=>{const response=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);assert.equal(response.ok,true);return response.data;};stage='actual enrollment';await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading()&&page.getURL()===origin+'/workspace');const phases=[];const test=async name=>{stage=name;const before=await(await fetch(process.env.SAFETYNET_WT_STATS)).json(),resolvedProxy=await page.session.resolveProxy(target);assert.notEqual(resolvedProxy,'DIRECT');const realms=await attemptRealms(page);for(const realm of [realms.window,realms.worker])assert.ok(!realm.connected&&!realm.datagramWritten&&!realm.streamWritten);const after=await(await fetch(process.env.SAFETYNET_WT_STATS)).json();for(const field of ['packets','connections','datagrams','streams','bytes','workerBytes'])assert.equal(after[field],before[field],'WebTransport reached receiver');assert.equal(await page.executeJavaScript("fetch('/receiver',{method:'POST',body:'Public full main ordinary prompt'}).then(r=>r.ok).catch(()=>false)"),true);phases.push({name,sessionId:(await ui('state')).activeTabId,policyVersion:(await ui('state')).policyVersion,proxyIsNonDirect:true,realms,receiverDelta:Object.fromEntries(['packets','connections','datagrams','streams','bytes','workerBytes'].map(key=>[key,after[key]-before[key]])),benignHTTPSPostAllowed:true});};await test('enrolled private relay');
const change=async proxy=>{const p=(await(await fetch(base+'/api/admin/overview',{headers})).json()).policy;p.version++;p.proxy=proxy;const proposal=await fetch(base+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(p)});assert.equal(proposal.status,202);const review=await proposal.json();const approved=await fetch(base+`/api/admin/policy-reviews/${review.reviewId}/approve`,{method:'POST',headers:{cookie:'sn_session='+process.env.SAFETYNET_SMOKE_REVIEWER,origin:base,'content-type':'application/json'},body:JSON.stringify({payloadHash:review.payloadHash,expectedVersion:review.expectedVersion,approve:true})});assert.equal(approved.status,200);await until(async()=>{const state=await ui('state');return state.ready&&state.policyVersion===p.version;});await until(()=>!page.isLoading()&&page.getURL()===origin+'/workspace');};stage='reviewed direct upstream policy';await change({mode:'direct'});await test('reviewed DIRECT upstream retains relay');stage='reviewed PAC direct upstream policy';await change({mode:'pac_script',pacUrl:origin+'/direct.pac'});await test('reviewed PAC DIRECT upstream retains relay');
stage='actual ordinary request observability';assert.equal(nativeWTCallbacks,0);await actualDiagnostics.flush();await wait(1500);const events=[];let cursor=0;for(let batch=0;batch<20;batch++){const response=await fetch(base+`/api/admin/events/export?after=${cursor}&limit=1000&format=ecs`,{headers});assert.equal(response.status,200);const rows=(await response.text()).trim().split('\n').filter(Boolean).map(JSON.parse);events.push(...rows);if(rows.length<1000)break;cursor=Number(response.headers.get('x-safetynet-next-cursor'));}const event=events.filter(e=>e.event.action==='upload.authorized'&&e.trace?.id).at(-1);assert.ok(event);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+event.trace.id)).json();assert.ok(trace.spans.some(s=>s.name==='browser.request'));const diagnostics=actualDiagnostics.status();assert.equal(diagnostics.failedExports,0);assert.equal(diagnostics.lifetimeFailedExports,0);const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);fs.writeFileSync(process.env.SAFETYNET_WT_OUTPUT,JSON.stringify({actualFullMain:true,mode,phases,nativeWebRequestCallbacks:nativeWTCallbacks,ordinaryTraceId:event.trace.id,ordinaryStages:trace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs})),unsupportedTransportAuditGap:'Chromium WebTransport bypasses Electron webRequest; receiver absence is native containment evidence, not an attributed per-attempt DLP audit event.',diagnostics,health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites}},null,2));console.log('Full enrolled main WebTransport protection passed');require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(0);
stage='actual ordinary request observability';assert.equal(nativeWTCallbacks,0);await until(async()=>{const response=await fetch(base+'/api/admin/overview',{headers});assert.equal(response.status,200);const overview=await response.json();const rows=overview.events.filter(e=>e.kind.startsWith('transport.'));assert.ok(!rows.some(e=>e.kind==='transport.established'||e.kind==='transport.monitor-unavailable'));return rows.filter(e=>e.kind==='transport.created').length===6&&rows.filter(e=>e.kind==='transport.closed').length===6;});await actualDiagnostics.flush();await wait(1500);const events=[];let cursor=0;for(let batch=0;batch<20;batch++){const response=await fetch(base+`/api/admin/events/export?after=${cursor}&limit=1000&format=ecs`,{headers});assert.equal(response.status,200);const rows=(await response.text()).trim().split('\n').filter(Boolean).map(JSON.parse);events.push(...rows);if(rows.length<1000)break;cursor=Number(response.headers.get('x-safetynet-next-cursor'));}const transportEvents=events.filter(e=>e.event.action.startsWith('transport.'));assert.equal(transportEvents.filter(e=>e.event.action==='transport.created').length,6);assert.equal(transportEvents.filter(e=>e.event.action==='transport.closed').length,6);assert.equal(transportEvents.length,12);const created=new Map(),transportTraces=[];for(const e of transportEvents){const t=e.safetynet.transport;assert.ok(t);assert.deepEqual(Object.keys(t).sort(),['connection_established','phase','protocol','realm','session_id','transport_id']);assert.equal(t.protocol,'webtransport');assert.equal(t.connection_established,false);assert.deepEqual(e.event.type,['info']);assert.equal(e.event.outcome,'unknown');assert.equal(e.destination.domain,'127.0.0.1');assert.ok(['window','worker'].includes(t.realm));assert.equal(t.session_id,phases[0].sessionId);assert.ok(!JSON.stringify(e).includes(target));assert.ok(!JSON.stringify(e).includes(process.env.SAFETYNET_WT_HASH));assert.ok(!JSON.stringify(e).includes('SYNTHETIC_WEBTRANSPORT_DLP_MARKER'));assert.ok(!/requestId|certificate|serverCertificateHashes|stackTrace|query/.test(JSON.stringify(e)));if(t.phase==='created'){assert.ok(!created.has(t.transport_id));created.set(t.transport_id,e);}else{const start=created.get(t.transport_id);assert.ok(start);assert.equal(start.safetynet.transport.realm,t.realm);assert.equal(start.safetynet.transport.session_id,t.session_id);assert.equal(start.safetynet.policy_version,e.safetynet.policy_version);}assert.ok(e.trace?.id);const exported=await(await fetch('http://127.0.0.1:4318/api/trace/'+e.trace.id)).json();assert.ok(!JSON.stringify(exported).includes(target));assert.ok(!JSON.stringify(exported).includes(process.env.SAFETYNET_WT_HASH));assert.ok(!JSON.stringify(exported).includes('SYNTHETIC_WEBTRANSPORT_DLP_MARKER'));assert.ok(exported.spans.some(s=>s.name==='browser.transport'&&s.operation==='transport.'+t.phase));const byId=new Map(exported.spans.map(s=>[s.spanId,s]));for(const stage of exported.spans.filter(s=>s.name==='browser.security'&&s.operation?.startsWith('audit.'))){let current=stage,linked=false;const seen=new Set();while(current&&!seen.has(current.spanId)){if(current.name==='browser.transport'){linked=true;break;}seen.add(current.spanId);current=byId.get(current.parentSpanId);}assert.ok(linked,'Transport audit stage parent missing');}transportTraces.push({phase:t.phase,realm:t.realm,transportId:t.transport_id,traceId:e.trace.id,stages:exported.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs}))});}for(const phase of phases){for(const realm of ['window','worker'])for(const eventPhase of ['created','closed'])assert.equal(transportEvents.filter(e=>e.safetynet.policy_version===phase.policyVersion&&e.safetynet.transport.realm===realm&&e.safetynet.transport.phase===eventPhase).length,1);}const event=events.filter(e=>e.event.action==='upload.authorized'&&e.trace?.id).at(-1);assert.ok(event);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+event.trace.id)).json();assert.ok(trace.spans.some(s=>s.name==='browser.request'));const diagnostics=actualDiagnostics.status();assert.equal(diagnostics.failedExports,0);assert.equal(diagnostics.lifetimeFailedExports,0);const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);fs.writeFileSync(process.env.SAFETYNET_WT_OUTPUT,JSON.stringify({actualFullMain:true,mode,phases,nativeWebRequestCallbacks:nativeWTCallbacks,ordinaryTraceId:event.trace.id,ordinaryStages:trace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs})),nativeTransportAudit:{created:6,closed:6,established:0,firstStatementWorker:true,events:transportEvents,transportTraces,coverage:'Actual native Window and dedicated Worker lifecycles in these three policy phases; no claim for every realm or future transport implementation.'},diagnostics,health:{storedBytes:health.storedBytes,rejectedWrites:healthLine truncated
}catch(error){fs.writeFileSync(path.resolve('artifacts/fullmain-wt-native-error.json'),JSON.stringify({mode,stage,name:error.name},null,2));console.error('Full main WebTransport failed at '+stage+': '+error.name);if(mode==='managed'&&page&&!page.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(1);}});
+4 -4
View File
@@ -11,11 +11,11 @@ import {createApp} from '../apps/server/app.mjs';
import {openStore,digest} from '../apps/server/store.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
const collector='http://127.0.0.1:4318';assert.equal((await fetch(collector+'/health')).ok,true);process.env.OTEL_EXPORTER_OTLP_ENDPOINT=collector;const telemetry=startTelemetry(),root=mkdtempSync(join(tmpdir(),'safetynet-fullmain-wt-'));mkdirSync('artifacts',{recursive:true});assert.ok(resolve(root).startsWith(resolve(tmpdir())+sep)&&root.split(/[\\/]/).at(-1).startsWith('safetynet-fullmain-wt-'));
const files=['apps/browser/main.cjs','apps/browser/startup-security.cjs','apps/browser/local-dlp-relay.cjs','apps/browser/diagnostics.cjs','packages/core/policy.mjs','package.json'],sourceHashFacts=Object.fromEntries(files.map(file=>[file,createHash('sha256').update(readFileSync(file)).digest('hex')]));let python,page,management,store,child,stage='receiver startup';const ordinary={posts:0,pacRequests:0};
const files=['apps/browser/main.cjs','apps/browser/native-transport-observer.cjs','apps/browser/page-clipboard-guard.cjs','packages/core/transport-facts.cjs','packages/core/siem.mjs','apps/browser/startup-security.cjs','apps/browser/local-dlp-relay.cjs','apps/browser/diagnostics.cjs','packages/core/policy.mjs','package.json'],sourceHashFacts=Object.fromEntries(files.map(file=>[file,createHash('sha256').update(readFileSync(file)).digest('hex')]));let python,page,management,store,child,stage='receiver startup';const ordinary={posts:0,pacRequests:0};
try{python=spawn('python',['scripts/webtransport-receiver.py',root],{env:{...process.env,PYTHONPATH:resolve('artifacts/webtransport-python')},windowsHide:true,stdio:['ignore','pipe','pipe']});python.stderr.on('data',()=>{});const metadata=await new Promise((accept,reject)=>{let text='';const timer=setTimeout(()=>reject(Error('Pinned QUIC receiver timeout')),15000);python.stdout.on('data',chunk=>{text+=chunk;try{const value=JSON.parse(text.split('\n')[0]);clearTimeout(timer);accept(value);}catch{}});python.once('error',()=>reject(Error('Pinned QUIC receiver startup refused')));python.once('exit',()=>{clearTimeout(timer);reject(Error('Pinned QUIC receiver exited'));});});const stats=`http://127.0.0.1:${metadata.statsPort}/facts`,target=`https://127.0.0.1:${metadata.quicPort}/webtransport`;
page=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.url==='/direct.pac'){ordinary.pacRequests++;res.setHeader('content-type','application/x-ns-proxy-autoconfig');res.end('function FindProxyForURL(url,host) { return "DIRECT"; }');return;}if(req.method==='POST'){ordinary.posts++;req.resume();res.end('accepted');return;}res.setHeader('content-type','text/html');res.end('<!doctype html><title>Native transport fixture</title><h1>Public native transport workspace</h1>');}).listen(0,'127.0.0.1');await new Promise(r=>page.once('listening',r));const origin=`https://127.0.0.1:${page.address().port}`,env={...process.env,SAFETYNET_WT_TARGET:target,SAFETYNET_WT_HASH:metadata.hash,SAFETYNET_WT_STATS:stats,SAFETYNET_SMOKE_ORIGIN:origin};delete env.ELECTRON_RUN_AS_NODE;
const run=async(mode,extra={})=>{const output=join(root,mode+'.json');child=spawn(createRequire(import.meta.url)('electron'),['scripts/fullmain-wt-native.cjs'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...env,SAFETYNET_WT_MODE:mode,SAFETYNET_SMOKE_USERDATA:join(root,mode+'-profile'),SAFETYNET_WT_OUTPUT:output,...extra}});child.stdout.on('data',chunk=>process.stdout.write(chunk));child.stderr.on('data',chunk=>process.stderr.write(chunk));const timer=setTimeout(()=>child.kill(),120000),exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(timer);assert.equal(exit,0,'Full main native transport phase failed');return JSON.parse(readFileSync(output,'utf8'));};
stage='real baseline delivery';const before=await(await fetch(stats)).json(),baseline=await run('baseline'),after=await(await fetch(stats)).json();assert.ok(after.bytes-before.bytes>=132);assert.ok(after.workerBytes-before.workerBytes>=66);assert.ok(after.datagrams>before.datagrams&&after.streams>before.streams&&after.marker);const baselineReceiverDelta=Object.fromEntries(['packets','connections','datagrams','streams','bytes','workerBytes'].map(key=>[key,after[key]-before[key]]));
stage='real baseline delivery';const before=await(await fetch(stats)).json(),baseline=await run('baseline'),after=await(await fetch(stats)).json();assert.ok(after.bytes-before.bytes>=132);assert.ok(after.workerBytes-before.workerBytes>=66);assert.ok(after.datagrams>before.datagrams&&after.streams>before.streams&&after.marker);const baselineReceiverDelta=Object.fromEntries(['packets','connections','datagrams','streams','bytes','workerBytes'].map(key=>[key,after[key]-before[key]]));writeFileSync('artifacts/fullmain-wt-baseline-native-proof.json',JSON.stringify({baseline,baselineReceiverDelta,sourceHashFacts},null,2));
store=openStore(join(root,'db'));const code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url'),reviewer=randomBytes(32).toString('base64url'),policy=store.get('policy');policy.browser.homeUrl=origin+'/workspace';policy.browser.allowedHosts=['127.0.0.1'];policy.agents.enabled=false;policy.diagnostics={...policy.diagnostics,enabled:true,mode:'debug',endpoint:collector,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',policy);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'fullmain-wt-fixture');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewer),Date.now()+600000,JSON.stringify({sub:'independent-wt-reviewer',role:'admin'}));const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement:true,requireIndependentPolicyApproval:true};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}`;
stage='real enrolled main and reviewed policies';const managed=await run('managed',{SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer});assert.equal(managed.phases.length,3);assert.equal(ordinary.posts,3);assert.ok(ordinary.pacRequests>=1,'PAC was not actually retrieved');for(const file of files)assert.equal(createHash('sha256').update(readFileSync(file)).digest('hex'),sourceHashFacts[file],'Production source changed during native proof');const report={actualFullMain:true,actualPinnedHTTP3Receiver:true,aioquicVersion:metadata.aioquic,sourceHashFacts,baseline,baselineReceiverDelta,managed,ordinary,unsupportedTransportAuditGap:managed.unsupportedTransportAuditGap};writeFileSync('artifacts/fullmain-webtransport-native-proof.json',JSON.stringify(report,null,2));console.log('Full main pinned Window/Worker WebTransport proof passed');
}catch(error){writeFileSync('artifacts/fullmain-wt-harness-error.json',JSON.stringify({stage,name:error.name},null,2));console.error('Full main WT harness failed at '+stage+': '+error.name);process.exitCode=1;}finally{child?.kill();python?.kill();for(const server of [page,management])if(server){server.closeAllConnections();await new Promise(r=>server.close(r));}store?.db.close();await telemetry?.shutdown();rmSync(root,{recursive:true,force:true});}
stage='real enrolled main and reviewed policies';const managed=await run('managed',{SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer});assert.equal(managed.phases.length,3);assert.equal(ordinary.posts,3);assert.ok(ordinary.pacRequests>=1,'PAC was not actually retrieved');for(const file of files)assert.equal(createHash('sha256').update(readFileSync(file)).digest('hex'),sourceHashFacts[file],'Production source changed during native proof');const report={actualFullMain:true,actualPinnedHTTP3Receiver:true,aioquicVersion:metadata.aioquic,sourceHashFacts,baseline,baselineReceiverDelta,managed,ordinary,nativeLifecycleAudit:managed.nativeTransportAudit};writeFileSync('artifacts/fullmain-webtransport-native-proof.json',JSON.stringify(report,null,2));console.log('Full main pinned Window/Worker WebTransport proof passed');
}catch(error){writeFileSync('artifacts/fullmain-wt-harness-error.json',JSON.stringify({stage,name:error.name,sourceHashFacts},null,2));console.error('Full main WT harness failed at '+stage+': '+error.name);process.exitCode=1;}finally{child?.kill();python?.kill();for(const server of [page,management])if(server){server.closeAllConnections();await new Promise(r=>server.close(r));}store?.db.close();await telemetry?.shutdown();rmSync(root,{recursive:true,force:true});}
+6
View File
@@ -0,0 +1,6 @@
'use strict';
const {app,BrowserWindow,session}=require('electron'),fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{X509Certificate}=require('node:crypto');app.setPath('userData',process.env.SAFETYNET_SMOKE_USERDATA);app.disableHardwareAcceleration();let diagnostics;const moduleDiagnostics=require('../apps/browser/diagnostics.cjs'),create=moduleDiagnostics.createDiagnostics;moduleDiagnostics.createDiagnostics=(...args)=>{const value=create(...args);diagnostics=value;return value;};require('../apps/browser/main.cjs');const delay=ms=>new Promise(r=>setTimeout(r,ms));async function until(fn){for(let i=0;i<300;i++){const v=await fn();if(v)return v;await delay(100);}throw Error('Public diagnostics timeout');}
app.whenReady().then(async()=>{let page;try{const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;}),base=process.env.SAFETYNET_SMOKE_SERVER,origin=process.env.SAFETYNET_SMOKE_ORIGIN,fingerprint=new X509Certificate(fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem'))).fingerprint256;session.fromPartition('persist:safetynet-managed').setCertificateVerifyProc((req,cb)=>{let allowed=false;try{allowed=req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint;}catch{}cb(allowed?0:-2);});const ui=async(method,value)=>{const r=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);assert.equal(r.ok,true);return r.data;},headers={authorization:'Bearer '+process.env.SAFETYNET_SMOKE_ADMIN,'content-type':'application/json'};await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading()&&page.getURL()===origin+'/public');const snapshots=[];
for(let i=0;i<4;i++){assert.equal(await page.executeJavaScript("fetch('/public-post',{method:'POST',body:'Public diagnostic-only activity'}).then(r=>r.ok)"),true);snapshots.push({phase:'before-publication',index:i,status:diagnostics.status()});const p=(await(await fetch(base+'/api/admin/overview',{headers})).json()).policy;p.version++;p.brand.color=i%2?'#137c66':'#245fbc';const response=await fetch(base+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(p)});assert.equal(response.status,202);const review=await response.json(),approved=await fetch(base+`/api/admin/policy-reviews/${review.reviewId}/approve`,{method:'POST',headers:{cookie:'sn_session='+process.env.SAFETYNET_SMOKE_REVIEWER,origin:base,'content-type':'application/json'},body:JSON.stringify({payloadHash:review.payloadHash,expectedVersion:review.expectedVersion,approve:true})});assert.equal(approved.status,200);await until(async()=>{const state=await ui('state');return state.ready&&state.policyVersion===p.version;});await until(()=>!page.isLoading());await delay(300);snapshots.push({phase:'after-publication',index:i,status:diagnostics.status()});}
await diagnostics.flush();await delay(1000);const status=diagnostics.status(),overview=await(await fetch(base+'/api/admin/overview',{headers})).json(),event=overview.events.find(e=>e.kind==='upload.authorized'&&e.metadata.traceId);assert.ok(event);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+event.metadata.traceId)).json();assert.ok(trace.spans.some(s=>s.name==='browser.request'));const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);fs.writeFileSync('artifacts/native-diagnostics-public-proof.json',JSON.stringify({freshProfile:true,publicOnly:true,protectedOperationsNotRepeated:true,reviewedPolicyPublications:4,snapshots,status,traceId:event.metadata.traceId,stages:trace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs})),health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites},zeroLifetimeFailures:status.lifetimeFailedExports===0},null,2));console.log('Fresh public native diagnostic measurement completed');require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(status.lifetimeFailedExports===0?0:2);
}catch(error){fs.writeFileSync('artifacts/native-diagnostics-public-error.json',JSON.stringify({name:error.name,status:diagnostics?.status()},null,2));if(page&&!page.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(1);}});
+2
View File
@@ -0,0 +1,2 @@
import assert from 'node:assert/strict';import {createServer} from 'node:https';import {createRequire} from 'node:module';import {mkdtempSync,readFileSync,rmSync} from 'node:fs';import {tmpdir} from 'node:os';import {join,resolve,sep} from 'node:path';import {randomBytes} from 'node:crypto';import {spawn} from 'node:child_process';import {createApp} from '../apps/server/app.mjs';import {openStore,digest} from '../apps/server/store.mjs';import {startTelemetry} from '../apps/server/telemetry.mjs';
const endpoint='http://127.0.0.1:4318';assert.equal((await fetch(endpoint+'/health')).ok,true);process.env.OTEL_EXPORTER_OTLP_ENDPOINT=endpoint;const telemetry=startTelemetry(),temporary=mkdtempSync(join(tmpdir(),'safetynet-public-diagnostics-'));assert.ok(resolve(temporary).startsWith(resolve(tmpdir())+sep)&&temporary.split(/[\\/]/).at(-1).startsWith('safetynet-public-diagnostics-'));const store=openStore(join(temporary,'db')),adminToken=randomBytes(32).toString('base64url'),code=randomBytes(32).toString('base64url'),reviewer=randomBytes(32).toString('base64url');let posts=0;const site=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.method==='POST'){posts++;req.resume();res.end('accepted');}else{res.setHeader('content-type','text/html');res.end('<!doctype html><h1>Public diagnostic fixture</h1>');}}).listen(0,'127.0.0.1');await new Promise(r=>site.once('listening',r));const origin=`https://127.0.0.1:${site.address().port}`,p=store.get('policy');p.browser.homeUrl=origin+'/public';p.agents.enabled=false;p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'public-diagnostics');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewer),Date.now()+600000,JSON.stringify({sub:'independent-public-reviewer',role:'admin'}));const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement:true,requireIndependentPolicyApproval:true},server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));config.publicUrl=`http://127.0.0.1:${server.address().port}`;const child=spawn(createRequire(import.meta.url)('electron'),['scripts/native-diagnostics-public.cjs'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...process.env,SAFETYNET_SMOKE_USERDATA:join(temporary,'profile'),SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_ORIGIN:origin,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer}});child.stdout.on('data',v=>process.stdout.write(v));child.stderr.on('data',v=>process.stderr.write(v));const deadline=setTimeout(()=>child.kill(),120000),exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);for(const s of [site,server])s.closeAllConnections();await Promise.all([site,server].map(s=>new Promise(r=>s.close(r))));store.db.close();await telemetry?.shutdown();rmSync(temporary,{recursive:true,force:true});assert.equal(posts,4);process.exit(exit);
+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);
});