Files
safetynet/apps/server/siem.mjs
T
2026-10-13 10:34:48 +02:00

36 lines
2.7 KiB
JavaScript

import { appendFileSync,readFileSync } from 'node:fs';
import { join } from 'node:path';
import { request } from 'node:https';
import { toECS,toCEF } from '../../packages/core/siem.mjs';
import { digest } from './store.mjs';
export function startSIEM(store,directory,env=process.env) {
let stopped=false,running=false,failures=0,nextAttempt=0;
const url=env.SIEM_HTTP_URL?new URL(env.SIEM_HTTP_URL):null;
if(url&&(url.protocol!=='https:'||url.username||url.password))throw Error('SIEM push requires an HTTPS URL without embedded credentials');
const tlsOptions={...(env.SIEM_CA_FILE?{ca:readFileSync(env.SIEM_CA_FILE)}:{}),...(env.SIEM_CERT_FILE?{cert:readFileSync(env.SIEM_CERT_FILE)}:{}),...(env.SIEM_KEY_FILE?{key:readFileSync(env.SIEM_KEY_FILE)}:{})};
const format=env.SIEM_FORMAT||'ecs';if(!['ecs','cef'].includes(format))throw Error('SIEM_FORMAT must be ecs or cef');
const cursorKey=url?`siem.remote.${digest(url.href+':'+format)}`:`siem.file.${format}`;
async function tick(){
if(stopped||running||Date.now()<nextAttempt)return;running=true;
try{
const after=store.get(cursorKey)||0;
const rows=store.db.prepare('SELECT * FROM events WHERE id>? ORDER BY id LIMIT 100').all(after).map(e=>({...e,metadata:JSON.parse(e.metadata)}));
if(!rows.length)return;
const body=rows.map(e=>format==='ecs'?JSON.stringify(toECS(e,store.get('installationId'))):toCEF(e,store.get('installationId'))).join('\n')+'\n';
if(url)await push(url,body,format,env.SIEM_HTTP_TOKEN,tlsOptions);
else appendFileSync(join(directory,`security.${format==='ecs'?'ndjson':'cef'}`),body,{mode:0o600,flush:true});
if(!stopped)store.put(cursorKey,rows.at(-1).id);failures=0;nextAttempt=0;
}catch{failures++;nextAttempt=Date.now()+Math.min(300000,1000*2**Math.min(failures,8));}
finally{running=false;}
}
const timer=setInterval(tick,2000);timer.unref();tick();
return {stop:()=>{stopped=true;clearInterval(timer);},flush:tick,status:()=>({configured:!!url,format,failures,cursor:store.get(cursorKey)||0,pending:store.db.prepare('SELECT count(*) AS n FROM events WHERE id>?').get(store.get(cursorKey)||0).n})};
}
function push(url,body,format,credential,tlsOptions) {
return new Promise((resolve,reject)=>{
const req=request(url,{method:'POST',...tlsOptions,headers:{'content-type':format==='ecs'?'application/x-ndjson':'text/plain','content-length':Buffer.byteLength(body),...(credential?{authorization:`Bearer ${credential}`}:{})}},res=>{
res.resume();res.on('error',reject);res.on('end',()=>res.statusCode>=200&&res.statusCode<300?resolve():reject(Error('SIEM rejected batch')));
});req.setTimeout(10000,()=>req.destroy(Error('SIEM timeout')));req.on('error',reject);req.end(body);
});
}