import { DatabaseSync } from 'node:sqlite'; import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { randomBytes, createHash, generateKeyPairSync } from 'node:crypto'; import { defaultPolicy, signPolicy, PolicySchema } from '../../packages/core/policy.mjs'; import { recordSecurityEvent } from './telemetry.mjs'; import { evaluateSigma } from '../../packages/core/security.mjs'; import { signBookmarkSnapshot } from '../../packages/core/bookmarks.mjs'; import {applyUserProfiles} from '../../packages/core/user-profiles.mjs'; import {applyPolicyGroups} from '../../packages/core/policy-groups.mjs'; export const token = () => randomBytes(32).toString('base64url'); export const digest = value => createHash('sha256').update(value).digest('hex'); export function openStore(directory) { mkdirSync(directory,{recursive:true,mode:0o700}); const db = new DatabaseSync(join(directory,'safetynet.sqlite')); db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY,value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS devices(id TEXT PRIMARY KEY,name TEXT,platform TEXT,user TEXT,token_hash TEXT UNIQUE,revoked INTEGER DEFAULT 0,last_seen TEXT,policy_version INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS enrollments(hash TEXT PRIMARY KEY,expires INTEGER,user TEXT); CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY AUTOINCREMENT,time TEXT,device_id TEXT,kind TEXT,metadata TEXT); CREATE TABLE IF NOT EXISTS sessions(hash TEXT PRIMARY KEY,expires INTEGER,principal TEXT); CREATE TABLE IF NOT EXISTS management_session_handles(handle TEXT PRIMARY KEY,session_hash TEXT UNIQUE REFERENCES sessions(hash) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS logins(state TEXT PRIMARY KEY,expires INTEGER,verifier TEXT,nonce TEXT,ticket TEXT); CREATE TABLE IF NOT EXISTS tickets(id TEXT PRIMARY KEY,secret_hash TEXT,expires INTEGER,credential TEXT); CREATE TABLE IF NOT EXISTS policies(version INTEGER PRIMARY KEY,time TEXT,actor TEXT,policy TEXT); CREATE TABLE IF NOT EXISTS policy_group_history(version INTEGER PRIMARY KEY REFERENCES policies(version),groups TEXT NOT NULL); CREATE TABLE IF NOT EXISTS device_events(device_id TEXT,event_id TEXT,PRIMARY KEY(device_id,event_id)); CREATE TABLE IF NOT EXISTS teaching(id TEXT PRIMARY KEY,time TEXT,actor TEXT,label TEXT,status TEXT,reference TEXT,reviewer TEXT,policy_version INTEGER); CREATE TABLE IF NOT EXISTS bookmarks(device_id TEXT PRIMARY KEY REFERENCES devices(id),revision INTEGER NOT NULL,items TEXT NOT NULL); CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT,event_id INTEGER,rule_id TEXT,level TEXT,time TEXT); `); if(!db.prepare('PRAGMA table_info(devices)').all().some(c=>c.name==='document_analysis_enabled'))db.exec('ALTER TABLE devices ADD COLUMN document_analysis_enabled INTEGER'); if(!db.prepare('PRAGMA table_info(devices)').all().some(c=>c.name==='verified_subject'))db.exec('ALTER TABLE devices ADD COLUMN verified_subject TEXT'); db.exec('CREATE TABLE IF NOT EXISTS user_profile_history(version INTEGER PRIMARY KEY REFERENCES policies(version),configuration TEXT NOT NULL)'); const getRaw=key=>{const r=db.prepare('SELECT value FROM settings WHERE key=?').get(key);return r?JSON.parse(r.value):null;}; const get = key => {const value=getRaw(key);return key==='policy'&&value?PolicySchema.parse(value):value;}; const put = (key,value) => db.prepare('INSERT INTO settings VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(key,JSON.stringify(value)); if(!get('keys')) {const k=generateKeyPairSync('ed25519'); put('keys',{privateKey:k.privateKey.export({type:'pkcs8',format:'pem'}),publicKey:k.publicKey.export({type:'spki',format:'pem'})});} if(!getRaw('policy')) {const p=defaultPolicy();put('policy',p);db.prepare('INSERT INTO policies VALUES(?,?,?,?)').run(p.version,new Date().toISOString(),'bootstrap',JSON.stringify(p));} db.prepare('INSERT OR IGNORE INTO policy_group_history(version,groups) VALUES(?,?)').run(getRaw('policy').version,JSON.stringify(getRaw('policyGroups')||[])); db.prepare('INSERT OR IGNORE INTO user_profile_history(version,configuration) VALUES(?,?)').run(getRaw('policy').version,JSON.stringify(getRaw('userProfiles')||{profiles:[],categories:[]})); if(!get('installationId'))put('installationId',token()); function event(device,kind,metadata) { const time=new Date().toISOString(),records=[];db.exec('SAVEPOINT security_event'); try { const insert=(eventKind,eventMetadata)=>{const result=db.prepare('INSERT INTO events(time,device_id,kind,metadata) VALUES(?,?,?,?)').run(time,device,eventKind,JSON.stringify(eventMetadata));records.push({kind:eventKind,metadata:eventMetadata,id:String(result.lastInsertRowid)});return Number(result.lastInsertRowid);}; const sourceEventId=insert(kind,metadata); for(const alert of evaluateSigma({kind,metadata},getRaw('policy')?.security?.sigma?.rules||[])) { db.prepare('INSERT INTO alerts(event_id,rule_id,level,time) VALUES(?,?,?,?)').run(sourceEventId,alert.ruleId,alert.level,time); insert('sigma.alert',{ruleId:alert.ruleId,level:alert.level,sourceEventId}); } db.exec('RELEASE security_event'); }catch(error){db.exec('ROLLBACK TO security_event');db.exec('RELEASE security_event');throw error;} for(const record of records)recordSecurityEvent(record.kind,record.metadata,record.id); } function personalBookmarks(deviceId){const row=db.prepare('SELECT revision,items FROM bookmarks WHERE device_id=?').get(deviceId);return row?{revision:row.revision,items:JSON.parse(row.items)}:{revision:0,items:[]};} function effectivePolicy(deviceId){let p=get('policy');if(deviceId){const d=db.prepare('SELECT document_analysis_enabled FROM devices WHERE id=?').get(deviceId);if(d?.document_analysis_enabled!==null&&d?.document_analysis_enabled!==undefined)p.inspection.documentAnalysisEnabled=!!d.document_analysis_enabled;p=PolicySchema.parse(applyPolicyGroups(applyUserProfiles(p,getRaw('userProfiles')||{},db.prepare('SELECT * FROM devices WHERE id=?').get(deviceId)||{id:deviceId}),getRaw('policyGroups')||[],deviceId));}return p;} return {db,get,getRaw,put,event,effectivePolicy,personalBookmarks,envelope:deviceId=>{const p=effectivePolicy(deviceId),keys=get('keys'),envelope=signPolicy(p,keys.privateKey);if(deviceId)envelope.bookmarks=signBookmarkSnapshot({deviceId,policyVersion:p.version,...personalBookmarks(deviceId),expiresAt:p.expiresAt},keys.privateKey);return envelope;},publicKey:()=>get('keys').publicKey}; }