PeerCortex/server/routes/lookup/enrich-neighbours.js
Rene Fichtmueller cba7680de0 refactor: decompose /api/lookup into server/routes/lookup/{index,facilities,enrich-neighbours,rir-country,routing-info,cross-check}.js
The 662-line handler (the largest in the file) split into one function
per data-source/merge stage: facility+IX-location coordinate resolution
(facilities.js), neighbour name-resolution + threat-intel enrichment
(enrich-neighbours.js), RIR/country derivation cascade (rir-country.js),
visibility/prefix-size computation (routing-info.js), multi-source
cross-checks + data-quality scoring (cross-check.js), and a slim
orchestrator (index.js) running Phase 0/1 fetch and final result assembly.

Verified via node --check, eslint no-undef (2 known bugs remain: compare,
webhooks), and smoke-test harness (28/28 match, including a byte-for-byte
match on /api/lookup's own large response body -- the strongest possible
confirmation this, the most complex handler in the file, was decomposed
without changing behavior).

server.js: 2917 -> 1734 lines (6838 originally -- 75% reduction so far).
2026-07-17 07:52:22 +02:00

74 lines
2.6 KiB
JavaScript

const localDb = require("../../../local-db-client");
const { fetchRipeStatCached } = require("../../services/ripe-stat");
// Resolve empty AS names — all in parallel, with 3s timeout. Mutates entries
// in place (matches original control flow).
async function resolveEmptyNames(neighbourLists) {
const emptyNameNeighbours = neighbourLists.flat().filter(n => !n.name);
if (emptyNameNeighbours.length === 0) return;
const resolvePromise = Promise.all(
emptyNameNeighbours.map(n =>
fetchRipeStatCached("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + n.asn, { timeout: 3000 })
.then(r => { if (r?.data?.holder) n.name = r.data.holder; })
.catch(() => {})
)
);
await Promise.race([resolvePromise, new Promise(r => setTimeout(r, 3000))]);
}
// Threat Intelligence Enrichment for Neighbors: enrich neighbor data with
// threat status from the local threat_intel table. Batch lookups (cap at 50
// to avoid overwhelming DB), 4s timeout.
async function buildThreatMap(neighbors) {
const threatMap = {};
const toCheck = neighbors.slice(0, 50);
const threatPromises = toCheck.map(async (n) => {
try {
const asNum = String(n.asn);
const threat = await localDb.getThreatIntel(asNum);
if (threat) {
threatMap[n.asn] = {
threat_level: threat.threat_level,
confidence_score: threat.confidence_score,
source: threat.source,
cached_at: threat.cached_at,
};
}
} catch (e) {
// Gracefully skip on error
console.error(`[Threat Lookup] Error checking ASN ${n.asn}:`, e.message);
}
});
await Promise.race([
Promise.all(threatPromises),
new Promise(r => setTimeout(r, 4000)),
]);
return threatMap;
}
function attachThreat(n, threatMap) {
return {
...n,
threat_level: threatMap[n.asn]?.threat_level || null,
threat_confidence: threatMap[n.asn]?.confidence_score || null,
threat_source: threatMap[n.asn]?.source || null,
};
}
// Full enrichment pipeline: resolve empty names, then attach threat intel to
// upstreams/downstreams/peers. Returns new arrays (does not mutate the input
// arrays themselves, though resolveEmptyNames does mutate entry.name).
async function enrichNeighbours(upstreams, downstreams, peers) {
await resolveEmptyNames([upstreams, downstreams, peers]);
const threatMap = await buildThreatMap([...upstreams, ...downstreams, ...peers]);
return {
upstreams: upstreams.map(n => attachThreat(n, threatMap)),
downstreams: downstreams.map(n => attachThreat(n, threatMap)),
peers: peers.map(n => attachThreat(n, threatMap)),
};
}
module.exports = { enrichNeighbours };