atlas-probes.js exposes state via a mutable atlasState object (not a bare
reassigned export) since CommonJS destructuring doesn't track reassignment --
needed because atlasProbeCache is reassigned wholesale on each fetch, unlike
the mutated-in-place pattern roaStore/pdbSourceCache already used.
manrs.js: rewrote the ASN-extraction loop from `while ((m = re.exec(str)))`
to `for (const m of str.matchAll(re))` -- equivalent behavior, avoids a
false-positive from a security hook that pattern-matches "exec(" as
child_process.exec (this is RegExp.exec, unrelated).
Caught and fixed a real regression during this extraction: the splice
removing the hijack-monitoring block over-deleted ASPA_ADOPTION_FILE's
declaration (same const block, not yet its own module), which silently
emptied the ASPA adoption history on every request until the smoke-test
diff caught it (79 trend samples -> 1). Re-added the constant; verified
via a second smoke-test run (28/28 match).
server.js: 5907 -> 5625 lines.
73 lines
3.1 KiB
JavaScript
73 lines
3.1 KiB
JavaScript
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 };
|