import {z} from 'zod'; const Options=z.object({days:z.number().int().min(1).max(3650),limit:z.number().int().min(1).max(1000).default(500)}).strict(); // Only the active exporter's durable cursor permits deletion. Old cursor keys // from an earlier SIEM configuration never authorize the current destination. export function pruneAuditEvents(store,{days,limit=500,exportedThrough,now=Date.now(),dryRun=true}){ const options=Options.parse({days,limit}); if(!Number.isSafeInteger(exportedThrough)||exportedThrough<0)throw Error('Verified SIEM export cursor required'); if(!Number.isFinite(now))throw Error('Invalid retention clock'); const cutoff=new Date(now-options.days*86400000).toISOString(); const db=store.db;db.exec('SAVEPOINT audit_retention'); try{ const hasCases=!!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='incident_cases'").get(); const protectedCase=hasCases?'AND NOT EXISTS (SELECT 1 FROM incident_cases c WHERE c.event_id=e.id)':''; const hasEvidence=!!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='protected_evidence'").get(); const protectedEvidence=hasEvidence?'AND NOT EXISTS (SELECT 1 FROM protected_evidence p WHERE p.source_event_id=e.id)':''; const candidates=db.prepare(`SELECT e.id FROM events e WHERE e.kind!='audit.retention-applied' AND e.timer.id)); // A batch boundary must not leave a retained correlation pointing at an event // we removed. It is safe to remove children first and their source next run. const bound=candidates.map(()=>'?').join(','); const children=candidates.length?db.prepare(`SELECT id,json_extract(metadata,'$.sourceEventId') AS source FROM events WHERE kind='sigma.alert' AND id IN (${bound})`).all(...ids):[]; const retainedSources=candidates.length?db.prepare(`SELECT DISTINCT json_extract(metadata,'$.sourceEventId') AS source FROM events WHERE kind='sigma.alert' AND json_extract(metadata,'$.sourceEventId') IN (${bound}) AND id NOT IN (${bound})`).all(...ids,...ids):[]; for(const row of retainedSources)ids.delete(row.source); let changed;do{changed=false;for(const child of children){if(ids.has(child.source)&&!ids.has(child.id)){ids.delete(child.source);changed=true;}}}while(changed); const selected=[...ids]; if(!dryRun&&selected.length){ const placeholders=selected.map(()=>'?').join(','); store.event(null,'audit.retention-applied',{count:selected.length,retentionDays:days,exportedThrough}); const retentionEventId=db.prepare('SELECT max(id) AS id FROM events').get().id; store.auditIntegrity.authorizePrune(selected,retentionEventId); db.prepare(`DELETE FROM alerts WHERE event_id IN (${placeholders})`).run(...selected); db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...selected); // Device event UUIDs deliberately survive: removing them would let a replay // recreate expired events. No signing/history/enrollment state is pruned. } db.exec('RELEASE audit_retention'); return {dryRun,cutoff,eligible:selected.length,exportedThrough,incidentEvidencePreserved:true,deduplicationPreserved:true}; }catch(error){db.exec('ROLLBACK TO audit_retention');db.exec('RELEASE audit_retention');throw error;} } export function startAuditRetention(store,siem,{days=0,intervalMs=3600000}={}){ if(!Number.isInteger(days)||days<0||days>3650)throw Error('AUDIT_RETENTION_DAYS must be 0 or 1..3650'); if(days)store.db.exec("CREATE INDEX IF NOT EXISTS audit_retention_time ON events(time,id); CREATE INDEX IF NOT EXISTS audit_sigma_source ON events(json_extract(metadata,'$.sourceEventId')) WHERE kind='sigma.alert';"); let running=false,lastRun=null,lastFailure=null,timer; function run(){ if(!days||running)return;running=true; try{const status=siem.status();lastRun={...pruneAuditEvents(store,{days,exportedThrough:status.cursor,dryRun:false}),at:new Date().toISOString()};lastFailure=null;} catch{lastFailure=new Date().toISOString();} finally{running=false;} } if(days){timer=setInterval(run,intervalMs);timer.unref();} return {run,stop:()=>clearInterval(timer),status:()=>({enabled:!!days,days,lastRun,lastFailure,scope:'exported-unreferenced-database-events'})}; }