53 lines
4.0 KiB
JavaScript
53 lines
4.0 KiB
JavaScript
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.time<? AND e.id<=? ${protectedCase} ${protectedEvidence} ORDER BY e.id LIMIT ?`).all(cutoff,exportedThrough,options.limit);
|
|
const ids=new Set(candidates.map(r=>r.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(',');
|
|
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.
|
|
store.event(null,'audit.retention-applied',{count:selected.length,retentionDays:days,exportedThrough});
|
|
}
|
|
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'})};
|
|
}
|