const crypto = require("crypto"); const localDb = require("../../local-db-client"); const { loadHijackSubs, loadHijackAlerts, saveHijackAlerts, saveHijackSubs } = require("./store"); const { notifyWebhooks } = require("./webhooks"); // Classify hijack severity function classifyHijackSeverity(unexpected, missing, rpkiInvalid) { if (rpkiInvalid > 0 || unexpected.length >= 5) return 'CRITICAL'; if (unexpected.length >= 2 || missing.length >= 3) return 'HIGH'; if (unexpected.length >= 1 || missing.length >= 1) return 'MEDIUM'; return 'LOW'; } // Returns null (not []) when the prefix lookup itself failed -- distinct from a // genuine zero-prefix result. runHijackCheck must not treat these the same way: // setting an empty baseline from a transient failure would permanently disable // hijack detection for that subscription (baseline.size stays 0 forever after, // so "unexpected" never fires again -- see the baseline.size > 0 guard below). async function checkHijacksForAsn(asn) { try { const data = await localDb.getRipeStatAnnouncedPrefixes(asn); if (data && data.status === 'error') return null; const prefixes = (data && data.data && data.data.prefixes || []).map(p => p.prefix); return prefixes; } catch (_) { return null; } } async function runHijackCheck() { const subs = loadHijackSubs(); if (!subs.length) return; const alerts = loadHijackAlerts(); let changed = false; for (const sub of subs) { const current = await checkHijacksForAsn(sub.asn); if (current === null) { // Lookup failed this cycle -- skip both anomaly detection and baseline // initialization; retry on the next 30-minute run rather than locking in // an empty baseline that would silently disable detection forever. continue; } const baseline = new Set(sub.prefixes || []); const unexpected = current.filter(p => baseline.size > 0 && !baseline.has(p)); const missing = [...baseline].filter(p => !current.includes(p)); if (unexpected.length || missing.length) { // Dedup: only alert if no alert in last 6 hours for this ASN const sixHoursAgo = Date.now() - 6 * 60 * 60 * 1000; const recentAlert = alerts.find(a => a.asn === sub.asn && new Date(a.ts).getTime() > sixHoursAgo); if (!recentAlert) { const severity = classifyHijackSeverity(unexpected, missing, 0); const alert = { id: crypto.randomBytes(8).toString('hex'), asn: sub.asn, ts: new Date().toISOString(), severity, unexpected, missing, resolved: false, msg: `BGP anomaly for AS${sub.asn}: ${unexpected.length} unexpected prefix(es), ${missing.length} missing` }; alerts.push(alert); changed = true; notifyWebhooks(sub.asn, alert); } } // Set baseline on first monitoring if (!sub.prefixes || !sub.prefixes.length) { sub.prefixes = current; changed = true; } } if (changed) { saveHijackAlerts(alerts); saveHijackSubs(subs); } } // Run hijack check every 30 minutes setInterval(runHijackCheck, 30 * 60 * 1000); module.exports = { classifyHijackSeverity, checkHijacksForAsn, runHijackCheck };