Files
safetynet/scripts/debug-collector-storage.mjs
T

39 lines
3.2 KiB
JavaScript
Raw Normal View History

// Encrypted developer telemetry only; this is not durable security evidence.
import {lstatSync,realpathSync,readdirSync,existsSync,renameSync,unlinkSync,openSync,writeSync,fsyncSync,closeSync} from 'node:fs';
import {resolve,join,dirname} from 'node:path';
import {randomUUID} from 'node:crypto';
export function collectorStorage(directory,{segmentBytes=50*1024*1024,retiredSegments=2}={}) {
if(!Number.isSafeInteger(segmentBytes)||segmentBytes<1024||!Number.isSafeInteger(retiredSegments)||retiredSegments<0||retiredSegments>10)throw Error('Invalid collector storage limits');
directory=resolve(directory);
const active=join(directory,'signals.encrypted.ndjson'),legacy=join(directory,'signals.legacy.encrypted.ndjson');
let rotations=0,droppedSegments=0,droppedBytes=0,rejectedWrites=0;
function guard(){
for(let current=directory;;current=dirname(current)){const s=lstatSync(current);if(!s.isDirectory()||s.isSymbolicLink()||realpathSync(current).toLowerCase()!==resolve(current).toLowerCase())throw Error('Unsafe collector directory');if(dirname(current)===current)break;}
const names=readdirSync(directory);
for(const name of names){if(!['collector-key.dpapi','signals.encrypted.ndjson','signals.legacy.encrypted.ndjson'].includes(name)&&!/^signals\.rolling\.[0-9]{13}\.[a-f0-9-]{36}\.encrypted\.ndjson$/.test(name))throw Error('Unexpected collector storage entry');const path=join(directory,name),s=lstatSync(path);if(!s.isFile()||s.isSymbolicLink()||s.nlink!==1||realpathSync(path).toLowerCase()!==path.toLowerCase())throw Error('Unsafe collector storage entry');}
return names.filter(n=>n.startsWith('signals.rolling.')).sort();
}
guard();
// The first old archive is pinned and never retention-deleted, including after restart.
function prune(){let retired=guard();while(retired.length>retiredSegments){const name=retired.shift(),path=join(directory,name);guard();const size=lstatSync(path).size;unlinkSync(path);droppedSegments++;droppedBytes+=size;}}
function rotate(){
guard();
if(existsSync(active)){
if(!existsSync(legacy))renameSync(active,legacy);
else renameSync(active,join(directory,`signals.rolling.${Date.now()}.${randomUUID()}.encrypted.ndjson`));
rotations++;
}
prune();
}
function append(line){
try{
guard();const bytes=Buffer.byteLength(line);if(bytes>segmentBytes)throw Error('Collector batch exceeds segment budget');prune();
const size=existsSync(active)?lstatSync(active).size:0;if(size+bytes>segmentBytes)rotate();
guard();const fd=openSync(active,existsSync(active)?'a':'wx',0o600);
try{const buffer=Buffer.from(line);let offset=0;while(offset<buffer.length){const count=writeSync(fd,buffer,offset,buffer.length-offset);if(count<1)throw Error('Collector short write');offset+=count;}fsyncSync(fd);}finally{closeSync(fd);}
}catch(error){rejectedWrites++;throw error;}
}
function summary(){const retired=guard(),activeBytes=existsSync(active)?lstatSync(active).size:0,legacyBytes=existsSync(legacy)?lstatSync(legacy).size:0;return {storedBytes:activeBytes,activeBytes,legacyBytes,retiredBytes:retired.reduce((n,name)=>n+lstatSync(join(directory,name)).size,0),retiredSegments:retired.length,segmentBytes,maxRollingBytes:segmentBytes*(retiredSegments+1),rotations,droppedSegments,droppedBytes,rejectedWrites};}
return {append,summary};
}