2026-10-13 10:33:31 +02:00
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 { openStore } from '../apps/server/store.mjs' ;
import { pruneAuditEvents , startAuditRetention } from '../apps/server/audit-retention.mjs' ;
function fixture ( t ){ const directory = mkdtempSync ( join ( tmpdir (), 'safetynet-retention-' )), store = openStore ( directory ); t . after (()=>{ store . db . close (); rmSync ( directory ,{ recursive : true , force : true });}); return store ;}
const old = '2020-01-01T00:00:00.000Z' ;
test ( 'retention preserves pending export, incident evidence and replay deduplication' , t =>{
const s = fixture ( t ); s . db . exec ( 'CREATE TABLE IF NOT EXISTS incident_cases(id TEXT PRIMARY KEY,event_id INTEGER REFERENCES events(id))' );
for ( let i = 0 ; i < 4 ; i ++ ) s . event ( 'device' , 'dlp.block' ,{ ruleId : 'rule' });
2026-10-13 16:58:15 +02:00
2026-10-13 10:33:31 +02:00
s . db . prepare ( 'INSERT INTO incident_cases(id,event_id) VALUES(?,?)' ). run ( 'case' , 2 );
s . db . prepare ( 'INSERT INTO device_events VALUES(?,?)' ). run ( 'device' , 'replay-id' );
2026-10-13 16:58:15 +02:00
const p = pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , exportedThrough : 3 }); assert . equal ( p . eligible , 2 ); assert . equal ( s . db . prepare ( 'SELECT count(*) n FROM events' ). get (). n , 4 );
const r = pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , exportedThrough : 3 , dryRun : false }); assert . equal ( r . eligible , 2 );
2026-10-13 10:33:31 +02:00
assert . deepEqual ( s . db . prepare ( 'SELECT id FROM events ORDER BY id' ). all (). map ( x => x . id ),[ 2 , 4 , 5 ]);
assert . equal ( s . db . prepare ( 'SELECT count(*) n FROM device_events' ). get (). n , 1 );
assert . equal ( s . db . prepare ( 'SELECT kind FROM events WHERE id=5' ). get (). kind , 'audit.retention-applied' );
});
test ( 'batch and SIEM boundaries never orphan a Sigma correlation' , t =>{
2026-10-13 16:58:15 +02:00
const s = fixture ( t ), p = s . get ( 'policy' ); p . security . sigma . rules = [{ id : 'r' , title : 'Rule' , level : 'high' , detection : { selection : { kind : 'dlp.block' }, condition : 'selection' }}]; s . put ( 'policy' , p ); s . event ( 'd' , 'dlp.block' ,{});
assert . equal ( pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , limit : 1 , exportedThrough : 2 , dryRun : false }). eligible , 0 );
assert . equal ( pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , exportedThrough : 1 , dryRun : false }). eligible , 0 );
assert . equal ( pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , exportedThrough : 2 , dryRun : false }). eligible , 2 );
2026-10-13 10:33:31 +02:00
assert . equal ( s . db . prepare ( 'SELECT count(*) n FROM alerts' ). get (). n , 0 );
});
test ( 'missing cursor fails closed and retention requires explicit deployment opt-in' , t =>{
2026-10-13 16:58:15 +02:00
const s = fixture ( t ); s . event ( null , 'policy.published' ,{});
2026-10-13 10:33:31 +02:00
assert . throws (()=> pruneAuditEvents ( s ,{ days : 30 }), /cursor/ );
const off = startAuditRetention ( s ,{ status : ()=>({ cursor : 1 })}); off . run (); assert . equal ( off . status (). enabled , false ); assert . equal ( s . db . prepare ( 'SELECT count(*) n FROM events' ). get (). n , 1 ); off . stop ();
});
test ( 'failed deletion rolls back the whole retention operation' , t =>{
2026-10-13 16:58:15 +02:00
const s = fixture ( t ); s . event ( null , 'policy.published' ,{});
2026-10-13 10:33:31 +02:00
s . db . exec ( "CREATE TRIGGER retention_reject BEFORE INSERT ON events WHEN NEW.kind='audit.retention-applied' BEGIN SELECT RAISE(ABORT,'fixture'); END" );
2026-10-13 16:58:15 +02:00
assert . throws (()=> pruneAuditEvents ( s ,{ now : Date . now () + 60 * 86400000 , days : 30 , exportedThrough : 1 , dryRun : false }), /fixture/ );
2026-10-13 10:33:31 +02:00
assert . equal ( s . db . prepare ( 'SELECT count(*) n FROM events' ). get (). n , 1 );
});