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).
74 lines
2.6 KiB
JavaScript
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 };
|