const { crossCheckRpki } = require("../../services/rpki"); // Multi-source cross-checks (RPKI vs RIPE Validator, prefix/neighbour counts // vs bgp.he.net) plus the overall data-quality summary derived from them. async function computeCrossChecks(asn, allPrefixes, rpkiStatuses, prefixes, neighbours, bgpHeData) { let rpkiCrossCheck = { cloudflare_valid: 0, ripe_valid: 0, agreement_pct: null, disagreements: [], sample_size: 0, comparable_size: 0 }; let prefixCrossCheck = { ripe_stat: prefixes.length, bgp_he_net: null, agreement: null, note: "" }; let neighbourCrossCheck = { ripe_stat_total: neighbours.length, bgp_he_net_total: null }; try { // RPKI cross-check: sample up to 5 prefixes against RIPE Validator (with 8s total timeout) const rpkiCrossPromise = crossCheckRpki(asn, allPrefixes, rpkiStatuses); const rpkiCrossResult = await Promise.race([ rpkiCrossPromise, new Promise((resolve) => setTimeout(() => resolve(null), 8000)), ]); if (rpkiCrossResult) rpkiCrossCheck = rpkiCrossResult; } catch (_e) { /* cross-check failed, keep defaults */ } // bgp.he.net fetching is deliberately disabled (see the hardcoded // Promise.resolve(null) in index.js's timedFetch("bgp.he.net", ...)) -- // bgpHeData is therefore always null and this whole branch, along with the // prefix/neighbour cross-checks below, never actually runs. Left in place // (not deleted) in case bgp.he.net fetching is re-enabled later, but // overall_confidence below must not claim a 2-source agreement that never // happened -- see the sources:1/2 handling further down. // Prefix count cross-check: compare RIPE Stat vs bgp.he.net if (bgpHeData) { const heV4 = bgpHeData.prefixes_v4 || 0; const heV6 = bgpHeData.prefixes_v6 || 0; const heTotal = heV4 + heV6; if (heTotal > 0) { prefixCrossCheck.bgp_he_net = heTotal; const ripeStat = prefixes.length; if (ripeStat > 0 && heTotal > 0) { const ratio = Math.min(ripeStat, heTotal) / Math.max(ripeStat, heTotal); prefixCrossCheck.agreement = ratio >= 0.9; const diff = Math.abs(ripeStat - heTotal); prefixCrossCheck.note = diff === 0 ? "Exact match" : "Difference of " + diff + " prefixes (" + Math.round((1 - ratio) * 100) + "% divergence)"; } } else { prefixCrossCheck.note = "bgp.he.net prefix count unavailable"; } // Neighbour cross-check: compare RIPE Stat vs bgp.he.net peer_count if (bgpHeData.peer_count != null) { neighbourCrossCheck.bgp_he_net_total = bgpHeData.peer_count; } } else { prefixCrossCheck.note = "bgp.he.net data unavailable"; } // Compute overall data quality. Only push a score for a cross-check that // actually ran (agreement_pct !== null) -- an unrun/inconclusive check must // not silently count as a perfect 100 in the average, and confidence must // reflect "nothing was actually cross-checked" rather than defaulting high. const crossCheckScores = []; if (rpkiCrossCheck.agreement_pct !== null) crossCheckScores.push(rpkiCrossCheck.agreement_pct); // Prefix agreement: convert to percentage if (prefixCrossCheck.bgp_he_net != null && prefixes.length > 0) { const pfxRatio = Math.min(prefixes.length, prefixCrossCheck.bgp_he_net) / Math.max(prefixes.length, prefixCrossCheck.bgp_he_net); crossCheckScores.push(Math.round(pfxRatio * 100)); } // Neighbour agreement if (neighbourCrossCheck.bgp_he_net_total != null && neighbours.length > 0) { const nbrRatio = Math.min(neighbours.length, neighbourCrossCheck.bgp_he_net_total) / Math.max(neighbours.length, neighbourCrossCheck.bgp_he_net_total); crossCheckScores.push(Math.round(nbrRatio * 100)); } const avgAgreement = crossCheckScores.length > 0 ? Math.round(crossCheckScores.reduce((a, b) => a + b, 0) / crossCheckScores.length) : null; const overallConfidence = avgAgreement === null ? "unknown" : avgAgreement > 90 ? "high" : avgAgreement >= 70 ? "medium" : "low"; const dataQuality = { sources_queried: ["PeeringDB", "RIPE Stat", "bgp.he.net", "Cloudflare RPKI", "RIPE RPKI Validator"], cross_checks: { rpki: { sources: rpkiCrossCheck.agreement_pct !== null ? 2 : 1, agreement_pct: rpkiCrossCheck.agreement_pct, sample_size: rpkiCrossCheck.sample_size, comparable_size: rpkiCrossCheck.comparable_size, disagreements: rpkiCrossCheck.disagreements }, prefixes: { sources: prefixCrossCheck.bgp_he_net != null ? 2 : 1, agreement_pct: prefixCrossCheck.bgp_he_net != null ? Math.round((Math.min(prefixes.length, prefixCrossCheck.bgp_he_net) / Math.max(prefixes.length, prefixCrossCheck.bgp_he_net || 1)) * 100) : null, ripe_stat: prefixCrossCheck.ripe_stat, bgp_he_net: prefixCrossCheck.bgp_he_net, note: prefixCrossCheck.note }, neighbours: { sources: neighbourCrossCheck.bgp_he_net_total != null ? 2 : 1, agreement_pct: neighbourCrossCheck.bgp_he_net_total != null && neighbours.length > 0 ? Math.round((Math.min(neighbours.length, neighbourCrossCheck.bgp_he_net_total) / Math.max(neighbours.length, neighbourCrossCheck.bgp_he_net_total)) * 100) : null, ripe_stat_total: neighbourCrossCheck.ripe_stat_total, bgp_he_net_total: neighbourCrossCheck.bgp_he_net_total }, }, overall_confidence: overallConfidence, overall_agreement_pct: avgAgreement, }; return { rpkiCrossCheck, prefixCrossCheck, neighbourCrossCheck, dataQuality }; } module.exports = { computeCrossChecks };