Files
safetynet/tests/request-audit.test.mjs

53 lines
7.1 KiB
JavaScript

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';
test('central ingestion strips legacy upload body hashes and retains safe request facts',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-request-audit-')),store=openStore(dir),credential='synthetic-device-request-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-request-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})});
const hash=digest('synthetic-low-entropy-password');let response=await send({kind:'upload.authorized',file:{bytes:32,detectedType:'text',sha256:hash}});assert.equal(response.status,200);
let row=store.db.prepare('SELECT * FROM events').get(),metadata=JSON.parse(row.metadata);assert.equal(metadata.file,undefined);assert.deepEqual(metadata.request,{bytes:32,detectedType:'text'});assert.equal(row.metadata.includes(hash),false);
response=await send({kind:'upload.block',request:{bytes:50,detectedType:'unknown'}});assert.equal(response.status,200);
response=await send({kind:'clipboard.copy',request:{bytes:50,detectedType:'text'}});assert.equal(response.status,400);
response=await send({kind:'upload.authorized',request:{bytes:50,detectedType:'text',sha256:hash}});assert.equal(response.status,400);
response=await send({kind:'download.scan',file:{bytes:32,detectedType:'text',sha256:hash}});assert.equal(response.status,200);row=store.db.prepare("SELECT * FROM events WHERE kind='download.scan'").get();assert.equal(JSON.parse(row.metadata).file.sha256,hash);assert.equal(store.auditIntegrity.verify().valid,true);
});
test('SIEM suppresses historical upload hashes while preserving transfer amounts and download hashes',()=>{
const hash=digest('synthetic-password'),legacy={id:1,time:new Date().toISOString(),kind:'upload.authorized',metadata:{file:{bytes:18,detectedType:'text',sha256:hash}}};
assert.equal(toECS(legacy).file,undefined);assert.deepEqual(toECS(legacy).safetynet.request,{bytes:18,detected_type:'text'});assert.equal(toCEF(legacy).includes(hash),false);
const current={...legacy,metadata:{request:{bytes:18,detectedType:'text'}}};assert.deepEqual(toECS(current).safetynet.request,{bytes:18,detected_type:'text'});assert.match(toCEF(current),/cn3=18/);assert.equal(JSON.stringify(toECS(current)).includes(hash),false);
legacy.kind='download.scan';assert.equal(toECS(legacy).file.hash.sha256,hash);
});
test('central API accepts typed broker causation and rejects invalid lifecycle semantics',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-request-audit-')),store=openStore(dir),credential='synthetic-device-broker-token';
store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('broker-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();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(body)});
const requestId=randomUUID(),authorizedEventId=randomUUID();
const allow={eventId:authorizedEventId,policyVersion:1,kind:'upload.authorized',ruleId:'broker-approved',request:{bytes:7,detectedType:'text'},broker:{requestId,phase:'request.authorized',reason:'broker-approved'}};
assert.equal((await send(allow)).status,200);
const fault={...allow,eventId:randomUUID(),kind:'upload.block',ruleId:'broker-upstream-failed',broker:{requestId,phase:'forward.fault',reason:'broker-upstream-failed',authorizedEventId}};
assert.equal((await send(fault)).status,200);
for(const broker of [{...fault.broker,authorizedEventId:undefined},{...fault.broker,authorizedEventId:fault.eventId},{...allow.broker,phase:'connect.block'},{...allow.broker,reason:'arbitrary secret details'},{...allow.broker,body:'private'}])assert.equal((await send({...fault,eventId:broker.authorizedEventId===fault.eventId?fault.eventId:randomUUID(),broker})).status,400);
const rows=store.db.prepare('SELECT metadata FROM events ORDER BY id').all();assert.equal(rows.length,2);assert.deepEqual(JSON.parse(rows[1].metadata).broker,fault.broker);assert.equal(store.auditIntegrity.verify().valid,true);
});
test('central typed download audit retains genuine file hash and requires causal request authorization',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-request-audit-')),store=openStore(dir),credential='synthetic-device-download-broker-token';store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('download-broker','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();rmSync(dir,{recursive:true,force:true});});
const send=event=>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(event)});
const event={eventId:randomUUID(),policyVersion:1,kind:'download.block',ruleId:'registered-document',file:{bytes:613,detectedType:'pdf',sha256:digest('synthetic real PDF bytes')},broker:{requestId:randomUUID(),authorizedEventId:randomUUID(),phase:'response.block',reason:'broker-download-inspection-block'}};assert.equal((await send(event)).status,200);
for(const value of [{...event,eventId:randomUUID(),request:{bytes:613,detectedType:'pdf'}},{...event,eventId:randomUUID(),file:undefined},{...event,eventId:randomUUID(),broker:{...event.broker,authorizedEventId:undefined}},{...event,eventId:randomUUID(),broker:{...event.broker,phase:'request.block'}}])assert.equal((await send(value)).status,400);
const stored=JSON.parse(store.db.prepare('SELECT metadata FROM events').get().metadata);assert.deepEqual(stored.file,event.file);assert.deepEqual(stored.broker,event.broker);assert.equal(store.auditIntegrity.verify().valid,true);
});