// 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 {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)}); const acl=spawnSync('powershell.exe',['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$a=New-Object System.Security.AccessControl.DirectorySecurity;$a.SetOwner($sid);$a.SetAccessRuleProtection($true,$false);$a.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','ContainerInherit,ObjectInherit','None','Allow')));Set-Acl -LiteralPath $env:SAFETYNET_COLLECTOR_DIRECTORY -AclObject $a"],{windowsHide:true,timeout:10000,env:{SystemRoot:process.env.SystemRoot,SAFETYNET_COLLECTOR_DIRECTORY:directory},stdio:'pipe'}); if(acl.status!==0)throw Error('Collector directory protection failed'); 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 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,...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 chunks=[];let size=0;req.on('data',chunk=>{size+=chunk.length;if(size>2*1024*1024)req.destroy();else chunks.push(chunk);}); req.on('end',()=>{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'; try{storage.append(line);}catch{res.writeHead(507);res.end('{"error":"Protected collector storage unavailable"}');return;}requests++; counts.traces+=normalized.spans.length;counts.logs+=normalized.logs;counts.metrics+=normalized.metrics;recent.push(...normalized.spans);while(recent.length>5000)recent.shift(); res.setHeader('content-type','application/json');res.end('{}'); }catch{res.writeHead(400);res.end('{"error":"Invalid telemetry batch"}');}}); }); server.listen(port,'127.0.0.1',()=>console.log(`Local debug collector: http://127.0.0.1:${port}/api/summary`)); function stop(){server.closeAllConnections();server.close(()=>{key.fill(0);process.exit(0);});}process.on('SIGINT',stop);process.on('SIGTERM',stop);