// Local development receiver. This is not a production SIEM or audit store. import {createServer} from 'node:http'; import {mkdirSync,existsSync,readFileSync,writeFileSync} from 'node:fs'; import {collectorStorage} from './debug-collector-storage.mjs'; import {normalizeCollectorBatch} from './debug-collector-batch.mjs'; import {collectorAdmission} from './debug-collector-admission.mjs'; import {protectCollectorDirectory} from './debug-collector-acl.mjs'; import {monitorEventLoopDelay} from 'node:perf_hooks'; import {resolve} from 'node:path'; import {spawnSync} from 'node:child_process'; import {randomBytes,createCipheriv} from 'node:crypto'; const port=Number(process.env.DEBUG_COLLECTOR_PORT||4318),directory=resolve(process.env.DEBUG_COLLECTOR_DIRECTORY||'data/observability'); if(process.platform!=='win32')throw Error('Local protected collector currently requires Windows DPAPI'); mkdirSync(directory,{recursive:true}); const storage=collectorStorage(directory,{segmentBytes:Number(process.env.DEBUG_COLLECTOR_SEGMENT_BYTES||50*1024*1024),retiredSegments:Number(process.env.DEBUG_COLLECTOR_RETIRED_SEGMENTS||2)}); protectCollectorDirectory(directory); function protect(operation,value){const result=spawnSync(resolve('.runtime/windows/python/python.exe'),[resolve('apps/inspector/dpapi.py'),operation],{input:value,encoding:'utf8',windowsHide:true,timeout:5000,env:{SystemRoot:process.env.SystemRoot,USERPROFILE:process.env.USERPROFILE,LOCALAPPDATA:process.env.LOCALAPPDATA,PYTHONDONTWRITEBYTECODE:'1'}});if(result.status!==0)throw Error('Collector DPAPI key protection failed');return result.stdout.trim();} const keyFile=resolve(directory,'collector-key.dpapi'); let key;if(existsSync(keyFile)){const value=protect('unprotect',readFileSync(keyFile,'utf8'));if(!value.startsWith('observability-v1:'))throw Error('Collector key binding rejected');key=Buffer.from(value.slice(17),'base64');}else{key=randomBytes(32);writeFileSync(keyFile,protect('protect','observability-v1:'+key.toString('base64')),{flag:'wx',mode:0o600});} if(key.length!==32)throw Error('Invalid collector key'); const admission=collectorAdmission({maxRequests:Number(process.env.DEBUG_COLLECTOR_MAX_REQUESTS||8),maxBufferedBytes:Number(process.env.DEBUG_COLLECTOR_MAX_BUFFERED_BYTES||8*1024*1024)}); const eventLoop=monitorEventLoopDelay({resolution:20});eventLoop.enable(); const lag=()=>({meanMs:Number.isFinite(eventLoop.mean)?eventLoop.mean/1e6:0,maxMs:eventLoop.max/1e6,p99Ms:eventLoop.percentile(99)/1e6}); const counts={traces:0,logs:0,metrics:0},recent=[];let requests=0; function summary(){return {service:'safetynet-local-debug',protectedStorage:'Windows DPAPI + AES-256-GCM',requests,counts,admission:admission.summary(),eventLoopLag:lag(),...storage.summary(),retainedSpans:recent.length,slowest:recent.slice().sort((a,b)=>b.durationMs-a.durationMs).slice(0,15)};} const server=createServer((req,res)=>{ res.setHeader('cache-control','no-store');res.setHeader('x-content-type-options','nosniff'); const origin=`http://127.0.0.1:${port}`; if(req.headers.host!==`127.0.0.1:${port}`||(req.headers.origin&&req.headers.origin!==origin)){res.writeHead(403);res.end();return;} if(req.method==='GET'&&['/','/health','/api/summary'].includes(req.url)){res.setHeader('content-type','application/json');try{res.end(JSON.stringify(summary(),null,2));}catch{res.writeHead(503);res.end('{"error":"Protected collector storage unavailable"}');}return;} if(req.method==='GET'&&/^\/api\/trace\/[a-f0-9]{32}$/.test(req.url)){res.setHeader('content-type','application/json');res.end(JSON.stringify({traceId:req.url.slice(11),spans:recent.filter(span=>span.traceId===req.url.slice(11))},null,2));return;} if(req.method!=='POST'||!['/v1/traces','/v1/logs','/v1/metrics'].includes(req.url)||req.headers['content-encoding']){res.writeHead(404);res.end();return;} const lease=admission.acquire(); const rejectBusy=()=>{res.setHeader('connection','close');res.setHeader('retry-after','1');res.writeHead(503);res.end('{"error":"Collector busy"}');}; if(!lease){rejectBusy();return;} const chunks=[];let finished=false; const release=()=>{lease.release();chunks.length=0;}; const fail=(status,body)=>{if(finished)return;finished=true;release();res.setHeader('connection','close');res.writeHead(status);res.end(body);}; req.once('aborted',()=>{finished=true;release();});req.once('error',()=>{finished=true;release();}); res.once('close',()=>{if(!finished){finished=true;release();}}); req.setTimeout(10000,()=>fail(408,'{"error":"Collector request timed out"}')); req.on('data',chunk=>{if(finished)return;const rejected=lease.add(chunk.length);if(rejected){if(rejected==='body-limit')fail(413,'{"error":"Telemetry batch too large"}');else{finished=true;release();rejectBusy();}}else chunks.push(chunk);}); req.on('end',()=>{if(finished)return;req.setTimeout(0); void lease.enqueue(()=>{if(finished)return; try{ const payload=JSON.parse(Buffer.concat(chunks).toString('utf8')),signal=req.url.slice(4);if(!payload||typeof payload!=='object'||Array.isArray(payload))throw Error('Invalid OTLP'); const normalized=normalizeCollectorBatch(signal,payload); const nonce=randomBytes(12),cipher=createCipheriv('aes-256-gcm',key,nonce);cipher.setAAD(Buffer.from('safetynet-debug-v1')); const plain=Buffer.from(JSON.stringify({signal,receivedAt:new Date().toISOString(),payload})),encrypted=Buffer.concat([cipher.update(plain),cipher.final()]);plain.fill(0); const line=JSON.stringify({v:1,nonce:nonce.toString('base64'),tag:cipher.getAuthTag().toString('base64'),ciphertext:encrypted.toString('base64')})+'\n'; // HTTP success remains after the guarded encrypted append and fsync. try{storage.append(line);}catch{fail(507,'{"error":"Protected collector storage unavailable"}');throw Error('Collector commit failed');} requests++;counts.traces+=normalized.spans.length;counts.logs+=normalized.logs;counts.metrics+=normalized.metrics;recent.push(...normalized.spans);while(recent.length>5000)recent.shift(); finished=true;res.setHeader('content-type','application/json');res.end('{}'); }catch{fail(400,'{"error":"Invalid telemetry batch"}');throw Error('Collector batch failed');} }).catch(()=>{if(!finished)fail(503,'{"error":"Collector busy"}');}).finally(release); }); }); server.listen(port,'127.0.0.1',()=>console.log(`Local debug collector: http://127.0.0.1:${port}/api/summary`)); function stop(){eventLoop.disable();server.closeAllConnections();server.close(()=>{key.fill(0);process.exit(0);});}process.on('SIGINT',stop);process.on('SIGTERM',stop);