Files
safetynet/scripts/debug-collector.mjs
T

42 lines
5.3 KiB
JavaScript

// 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);