import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {createServer,request as httpRequest} from 'node:http'; import {once} from 'node:events'; import {mkdtemp,open,readFile,rm} from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; const require=createRequire(import.meta.url),{createBrokerAudit,BrokerAuditEventSchema}=require('../apps/network-broker/audit-gate.cjs'); const {gateRequest}=require('../apps/network-broker/http-request-gate.cjs'); async function fixture(t,configure={}){ const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-broker-audit-')),journal=path.join(root,'synthetic-events.jsonl'); const received=[];let connections=0; const receiver=createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.end('received');});receiver.on('connection',()=>connections++);receiver.listen(0,'127.0.0.1');await once(receiver,'listening'); let valid=true,persistenceCalls=0; const durable=async event=>{ persistenceCalls++;BrokerAuditEventSchema.parse(event); await configure.beforePersist?.(event); // Synthetic metadata only. Production requires the existing protected, // encrypted durable journal, not this test's plain fsync demonstration. const file=await open(journal,'a');try{await file.writeFile(JSON.stringify(event)+'\n');await file.sync();}finally{await file.close();} return {persisted:true,eventId:event.eventId}; }; const audit=createBrokerAudit({current:()=>valid,policyVersion:3,persist:configure.persist||durable,correlation:()=>({traceId:'a'.repeat(32),spanId:'b'.repeat(16)})}); const destination=`http://127.0.0.1:${receiver.address().port}/upload`; const proxy=createServer(async(req,res)=>{ try{ await gateRequest(req,{authority:'policy-3',current:()=>valid,deadlineMs:1000,inspect:async({body})=>{ const decision=body.toString().includes('PRIVATE_TEST_SECRET')?'block':'allow'; await audit.record(decision,{bytes:body.length,detectedType:'text',host:'127.0.0.1'});return {decision}; },forward:({body})=>new Promise((resolve,reject)=>{const outgoing=httpRequest(destination,{method:'POST',headers:{'content-length':body.length}},response=>{response.resume();response.once('end',resolve);});outgoing.once('error',reject);outgoing.end(body);})}); res.end('allowed'); }catch{res.writeHead(403);res.end('blocked');} });proxy.listen(0,'127.0.0.1');await once(proxy,'listening'); t.after(async()=>{proxy.closeAllConnections();receiver.closeAllConnections();await Promise.all([new Promise(resolve=>proxy.close(resolve)),new Promise(resolve=>receiver.close(resolve))]);await rm(root,{force:true,recursive:true});}); const send=body=>new Promise((resolve,reject)=>{const outgoing=httpRequest({host:'127.0.0.1',port:proxy.address().port,path:destination,method:'POST',headers:{host:new URL(destination).host,'content-length':Buffer.byteLength(body)}},response=>{response.resume();response.once('end',()=>resolve(response.statusCode));});outgoing.once('error',reject);outgoing.end(body);}); return {send,audit,received,revoke:()=>{valid=false;},get connections(){return connections;},get persistenceCalls(){return persistenceCalls;},events:async()=>String(await readFile(journal)).trim().split('\n').map(line=>JSON.parse(line))}; } test('pending durable allow audit releases no TCP connection and fsync receipt precedes release',async t=>{ let entered,release;const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve); const f=await fixture(t,{beforePersist:async()=>{entered();await hold;}}); const pending=f.send('public payload');await started;assert.equal(f.connections,0);release();assert.equal(await pending,200); assert.deepEqual(f.received,['public payload']);const events=await f.events();assert.equal(events.length,1);assert.equal(events[0].kind,'upload.authorized');assert.equal(events[0].policyVersion,3);assert.equal(events[0].traceId,'a'.repeat(32)); }); test('durable block records safe facts without original secret or request fingerprint',async t=>{ const f=await fixture(t);assert.equal(await f.send('PRIVATE_TEST_SECRET'),403);assert.equal(f.connections,0); const events=await f.events();assert.equal(events[0].kind,'upload.block');assert.equal(events[0].request.bytes,19);assert.equal(events[0].request.detectedType,'text'); const text=JSON.stringify(events);assert.equal(text.includes('PRIVATE_TEST_SECRET'),false);assert.equal(text.includes('sha256'),false); }); test('audit failure and uncertain receipt block without dial or automatic retry',async t=>{ for(const persist of [async()=>{throw Error('private persistence details');},async()=>({persisted:true,eventId:'wrong-id'})]){ let calls=0;const f=await fixture(t,{persist:async event=>{calls++;return persist(event);}}); assert.equal(await f.send('public'),403);assert.equal(f.connections,0);assert.equal(calls,1); } }); test('revocation while audit waits prevents release even after durable write succeeds',async t=>{ let entered,release;const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve); const f=await fixture(t,{beforePersist:async()=>{entered();await hold;}}),pending=f.send('public');await started;f.revoke();release(); assert.equal(await pending,403);assert.equal(f.connections,0);assert.equal((await f.events()).length,1);assert.equal(f.persistenceCalls,1); }); test('missing mandatory persistence and malformed security facts cannot authorize transfer',async()=>{ const audit=createBrokerAudit({current:()=>true,policyVersion:1}); await assert.rejects(audit.record('allow',{bytes:2,detectedType:'text'}),{code:'audit-unavailable'}); for(const facts of [{bytes:-1,detectedType:'text'},{bytes:2,detectedType:'text',body:'private'},{bytes:2,detectedType:'text',sha256:'a'.repeat(64)},{bytes:2,detectedType:'text',host:'https://company.example/private?q=secret'},{bytes:2,detectedType:'text',reason:'broker-inspection-fault'}])await assert.rejects(audit.record('allow',facts),{code:'audit-facts-invalid'}); }); test('fault event uses existing schema fixed reason without error detail',async()=>{ const events=[];const audit=createBrokerAudit({current:()=>false,policyVersion:7,persist:async event=>{events.push(BrokerAuditEventSchema.parse(event));return {persisted:true,eventId:event.eventId};}}); await audit.record('fault',{bytes:0,detectedType:'unknown'});assert.equal(events[0].kind,'upload.block');assert.equal(events[0].ruleId,'broker-inspection-fault'); }); test('decision reason mismatch and accessors fail with fixed errors without reading secret getters',async()=>{ let evaluated=false;const audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>({persisted:true,eventId:event.eventId})}); for(const [decision,reason] of [['allow','broker-policy-block'],['block','broker-approved'],['block','broker-audit-failed'],['fault','broker-authority-expired']])await assert.rejects(audit.record(decision,{bytes:1,detectedType:'text',reason}),{code:'audit-facts-invalid'}); await assert.rejects(audit.record('allow',{get bytes(){evaluated=true;throw Error('private secret');},detectedType:'text'}),{message:'audit-facts-invalid'});assert.equal(evaluated,false); }); test('only trusted policy and fixed built-in matched IDs enter security events',async()=>{ const events=[],ids=['company-secret']; const audit=createBrokerAudit({current:()=>true,policyVersion:1,allowedRuleIds:ids,persist:async event=>{events.push(event);return {persisted:true,eventId:event.eventId};}}); ids.push('RAW PRIVATE RULE TEXT'); await audit.record('block',{bytes:4,detectedType:'text',matchedRuleId:'company-secret'}); await audit.record('block',{bytes:4,detectedType:'text',matchedRuleId:'api-secret'}); assert.deepEqual(events.map(event=>event.ruleId),['company-secret','api-secret']); for(const [decision,id] of [['allow','company-secret'],['block','RAW PRIVATE RULE TEXT'],['fault','unknown-rule']])await assert.rejects(audit.record(decision,{bytes:4,detectedType:'text',matchedRuleId:id}),{code:'audit-facts-invalid'}); assert.equal(events.length,2);assert.equal(JSON.stringify(events).includes('RAW PRIVATE RULE TEXT'),false); }); test('canonical bare IPv6 destination is valid security metadata without an URL or port',async()=>{ const events=[],audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>{events.push(event);return {persisted:true,eventId:event.eventId};}}); await audit.record('allow',{bytes:1,detectedType:'text',host:'::1'});assert.equal(events[0].host,'::1'); await assert.rejects(audit.record('allow',{bytes:1,detectedType:'text',host:'[::1]:443'}),{code:'audit-facts-invalid'}); });