68 lines
5.1 KiB
JavaScript
68 lines
5.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {toECS,toCEF} from '../packages/core/siem.mjs';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import {openStore} from '../apps/server/store.mjs';
|
|
import {startSIEM} from '../apps/server/siem.mjs';
|
|
function event(phase='forward.fault'){
|
|
const authorizedEventId=randomUUID();
|
|
return {id:2,time:new Date().toISOString(),device_id:'synthetic',kind:phase==='request.authorized'?'upload.authorized':'upload.block',metadata:{eventId:randomUUID(),ruleId:phase==='request.authorized'?'broker-approved':'broker-upstream-failed',policyVersion:4,request:{bytes:128,detectedType:'pdf'},broker:{requestId:randomUUID(),connectionId:randomUUID(),phase,reason:phase==='request.authorized'?'broker-approved':'broker-upstream-failed',...(phase==='forward.fault'?{authorizedEventId}:{})}}};
|
|
}
|
|
test('ECS retains actual causal UUIDs and marks terminal forwarding failure as error',()=>{
|
|
const input=event(),output=toECS(input);
|
|
assert.equal(output.event.outcome,'failure');assert.deepEqual(output.event.type,['error']);
|
|
assert.equal(output.safetynet.broker.event_id,input.metadata.eventId);
|
|
assert.equal(output.safetynet.broker.authorized_event_id,input.metadata.broker.authorizedEventId);
|
|
assert.equal(output.safetynet.broker.connection_id,input.metadata.broker.connectionId);
|
|
assert.equal(output.trace,undefined);assert.equal(output.span,undefined);
|
|
});
|
|
test('a durable authorization is not reported as completed delivery',()=>{
|
|
const output=toECS(event('request.authorized'));assert.equal(output.event.outcome,'unknown');assert.deepEqual(output.event.type,['allowed']);
|
|
});
|
|
test('typed download records retain actual file evidence and causal authorization',()=>{
|
|
for(const blocked of [false,true]){
|
|
const input=event();input.kind=blocked?'download.block':'download.scan';delete input.metadata.request;
|
|
input.metadata.file={bytes:617,detectedType:'pdf',sha256:'a'.repeat(64)};
|
|
input.metadata.broker.phase=blocked?'response.block':'response.scanned';input.metadata.broker.reason=blocked?'broker-download-inspection-block':'broker-download-approved';
|
|
const output=toECS(input),cef=toCEF(input);
|
|
assert.equal(output.file.size,617);assert.equal(output.file.hash.sha256,'a'.repeat(64));assert.equal(output.event.outcome,blocked?'failure':'success');
|
|
assert.equal(output.safetynet.broker.authorized_event_id,input.metadata.broker.authorizedEventId);
|
|
assert.ok(cef.includes('fileHash='+'a'.repeat(64)));assert.ok(cef.includes('outcome='+(blocked?'failure':'success')));
|
|
}
|
|
});
|
|
test('CEF standard custom string preserves all causation without overwriting integrity or type',()=>{
|
|
const input=event();input.audit={hash:'synthetic-hash',sequence:3};
|
|
const output=toCEF(input);assert.ok(output.includes('cs2Label=BrokerLifecycle'));
|
|
for(const value of [input.metadata.eventId,input.metadata.broker.requestId,input.metadata.broker.connectionId,input.metadata.broker.authorizedEventId])assert.ok(output.includes(value));
|
|
assert.ok(output.includes('flexString1=synthetic-hash'));assert.ok(output.includes('cs6=pdf'));
|
|
assert.ok(output.includes('outcome=failure'));assert.ok(toCEF(event('request.authorized')).includes('outcome=unknown'));
|
|
});
|
|
test('malformed or content-bearing broker facts are never exported as lifecycle metadata',()=>{
|
|
for(const modify of [input=>{input.metadata.broker.body='private-body';},input=>{input.metadata.broker.authorizedEventId=input.metadata.eventId;},input=>{input.metadata.broker.reason='private-reason';}]){
|
|
const input=event();modify(input);assert.equal(toECS(input).safetynet.broker,undefined);
|
|
const cef=toCEF(input);assert.equal(cef.includes('BrokerLifecycle'),false);assert.equal(cef.includes('private-'),false);
|
|
}
|
|
});
|
|
test('actual durable exporter preserves the authorization-to-fault UUID join',async()=>{
|
|
const root=path.resolve(os.tmpdir()),directory=fs.mkdtempSync(path.join(root,'safetynet-broker-siem-'));
|
|
if(path.dirname(path.resolve(directory))!==root||!path.basename(directory).startsWith('safetynet-broker-siem-'))throw Error('Synthetic cleanup path rejected');
|
|
const store=openStore(directory);let exporter;
|
|
try{
|
|
const authorized=event('request.authorized'),fault=event();
|
|
fault.metadata.broker.authorizedEventId=authorized.metadata.eventId;
|
|
fault.metadata.broker.requestId=authorized.metadata.broker.requestId;
|
|
store.event('synthetic','upload.authorized',authorized.metadata);store.event('synthetic','upload.block',fault.metadata);
|
|
exporter=startSIEM(store,directory,{});await exporter.flush();assert.equal(exporter.status().cursor,2);
|
|
const rows=fs.readFileSync(path.join(directory,'security.ndjson'),'utf8').trim().split('\n').map(JSON.parse);
|
|
assert.equal(rows.length,2);assert.equal(rows[0].event.outcome,'unknown');assert.equal(rows[1].event.outcome,'failure');
|
|
assert.equal(rows[1].safetynet.broker.authorized_event_id,rows[0].safetynet.broker.event_id);
|
|
assert.equal(rows[1].safetynet.broker.request_id,rows[0].safetynet.broker.request_id);
|
|
}finally{
|
|
exporter?.stop();store.db.close();
|
|
fs.rmSync(directory,{recursive:true,force:true});
|
|
}
|
|
});
|