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'}); s.db.prepare('UPDATE events SET time=?').run(old); 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'); const p=pruneAuditEvents(s,{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,{days:30,exportedThrough:3,dryRun:false});assert.equal(r.eligible,2); 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=>{ 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',{});s.db.prepare('UPDATE events SET time=?').run(old); assert.equal(pruneAuditEvents(s,{days:30,limit:1,exportedThrough:2,dryRun:false}).eligible,0); assert.equal(pruneAuditEvents(s,{days:30,exportedThrough:1,dryRun:false}).eligible,0); assert.equal(pruneAuditEvents(s,{days:30,exportedThrough:2,dryRun:false}).eligible,2); 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=>{ const s=fixture(t);s.event(null,'policy.published',{});s.db.prepare('UPDATE events SET time=?').run(old); 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=>{ const s=fixture(t);s.event(null,'policy.published',{});s.db.prepare('UPDATE events SET time=?').run(old); s.db.exec("CREATE TRIGGER retention_reject BEFORE INSERT ON events WHEN NEW.kind='audit.retention-applied' BEGIN SELECT RAISE(ABORT,'fixture'); END"); assert.throws(()=>pruneAuditEvents(s,{days:30,exportedThrough:1,dryRun:false}),/fixture/); assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,1); });