const localDb = require("../../local-db-client"); const { fetchRipeStatCached } = require("../services/ripe-stat"); const { cacheGet, cacheSet } = require("../caches/response-caches"); // AS Relationships endpoint: /api/relationships?asn=X // Returns upstream providers, downstream customers, and peers with resolved // names. Based on RIPE Stat asn-neighbours. async function handleRelationships(req, res, url) { const rawAsn = (url.searchParams.get("asn") || "").replace(/[^0-9]/g, ""); if (!rawAsn) { res.writeHead(400); return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" })); } const cacheKey = "relationships:" + rawAsn; const cached = cacheGet(cacheKey); if (cached) { res.writeHead(200, { "Content-Type": "application/json", "X-Cache": "HIT" }); return res.end(JSON.stringify(cached)); } const start = Date.now(); try { const neighbourData = await fetchRipeStatCached( "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn + "&lod=1", { timeout: 8000 } ); const neighbours = (neighbourData && neighbourData.data && neighbourData.data.neighbours) || []; const counts = (neighbourData && neighbourData.data && neighbourData.data.neighbour_counts) || {}; const upstreams = neighbours.filter(n => n.type === "left").sort((a,b) => (b.power||0)-(a.power||0)); const downstreams = neighbours.filter(n => n.type === "right").sort((a,b) => (b.power||0)-(a.power||0)); const peers = neighbours.filter(n => n.type === "uncertain").sort((a,b) => (b.power||0)-(a.power||0)); // Resolve AS names for top entries (upstreams + downstreams all, top 20 peers) const toResolve = [...upstreams, ...downstreams, ...peers.slice(0, 20)]; const resolvedNames = {}; await Promise.race([ Promise.all(toResolve.map(n => fetchRipeStatCached("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + n.asn, { timeout: 3000 }) .then(r => { if (r && r.data && r.data.holder) resolvedNames[n.asn] = r.data.holder; }) .catch(() => {}) )), new Promise(r => setTimeout(r, 5000)), ]); // ---- Threat Intelligence Enrichment ---- // Enrich neighbors with threat status from local threat_intel table const threatMap = {}; const allNeighborsForThreat = [...upstreams, ...downstreams, ...peers]; const threatPromises = allNeighborsForThreat.slice(0, 100).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, }; } } catch (e) { console.error(`[Relationships Threat Lookup] Error checking ASN ${n.asn}:`, e.message); } }); await Promise.race([ Promise.all(threatPromises), new Promise(r => setTimeout(r, 3000)), ]); const fmt = n => ({ asn: n.asn, name: resolvedNames[n.asn] || "", power: n.power || 0, v4_peers: n.v4_peers || 0, v6_peers: n.v6_peers || 0, threat_level: threatMap[n.asn]?.threat_level || null, threat_confidence: threatMap[n.asn]?.confidence_score || null, threat_source: threatMap[n.asn]?.source || null, }); const result = { asn: parseInt(rawAsn), query_time: new Date().toISOString(), duration_ms: Date.now() - start, counts: { upstreams: counts.left || upstreams.length, downstreams: counts.right || downstreams.length, peers_total: counts.unique || peers.length, uncertain: counts.uncertain || peers.length, }, upstreams: upstreams.map(fmt), downstreams: downstreams.map(fmt), peers: peers.slice(0, 50).map(fmt), methodology: "RIPE Stat asn-neighbours API. left=upstream providers (carry your traffic), right=downstream customers (you carry their traffic), uncertain=lateral peers. Sorted by power score (number of prefixes seen via this relationship).", source_url: "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, }; // A neighbourData fetch failure (not a genuine zero-neighbour ASN) would // otherwise get cached as "0 upstreams/downstreams/peers" for the full 10min -- // match the existing /api/validate precedent (90s TTL for suspicious zero counts). cacheSet(cacheKey, result, neighbourData ? 10 * 60 * 1000 : 90 * 1000); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(result, null, 2)); } catch (err) { res.writeHead(500); return res.end(JSON.stringify({ error: "Relationships lookup failed", message: err.message })); } } module.exports = { handleRelationships };