fix: bgp.he.net name+country fallback for unregistered ASNs

For ASNs with no PeeringDB entry and no RIPE Stat holder (e.g. reserved
or unannounced ASNs), extract name from bgp.he.net page title and
country code from the /country/XX href. Eliminates the last 2 CRITICAL
audit failures (AS34465 → 'RIPE NCC ASN block'/GB, AS59947 → 'LLHOST
INC. SRL'/RO). Audit result: 80/82 PERFECT, 0 CRITICAL. v0.6.8.
This commit is contained in:
Rene Fichtmueller 2026-04-03 01:42:56 +02:00
parent 9012d2931f
commit 9038e280fa
2 changed files with 28 additions and 3 deletions

View File

@ -118,3 +118,12 @@ All notable changes to PeerCortex are documented here.
### Added
- **Name search with autocomplete**: Type any network or organization name in the search bar to get live suggestions. Results are sourced from both RIPE Stat and PeeringDB — covering thousands of registered networks worldwide. Use arrow keys to navigate, Enter or click to select.
## [0.6.8] — 2026-04-03
### Fixed
- **Name fallback via bgp.he.net title**: ASNs without a PeeringDB entry and no RIPE Stat holder
now extract their name from bgp.he.net page title (e.g. LLHOST INC. SRL, RIPE NCC ASN block)
- **Country code fallback via bgp.he.net**: ASNs with no country in rir-stats-country
now derive their 2-letter country code from bgp.he.net href (e.g. /country/RO, /country/GB)
- Zero CRITICAL data errors across 82 of 103 audited ASNs (21 large Tier-1 ASNs exceed 15s cold-cache)

View File

@ -1546,6 +1546,18 @@ async function fetchBgpHeNet(asn) {
if (peerMatch) result.peer_count = parseInt(peerMatch[1].replace(/,/g, ''));
const countryMatch = html.match(/Country[^<]*<[^>]*>[^<]*<[^>]*>\s*<[^>]*>([^<]+)/i);
if (countryMatch) result.country = countryMatch[1].trim();
// Extract 2-letter country code from href="/country/XX"
const ccMatch = html.match(/href="\/country\/([A-Z]{2})"/i);
if (ccMatch) result.country_code = ccMatch[1].toUpperCase();
// Extract clean AS name from title: "AS12345 Some Name - bgp.he.net" → "Some Name"
if (titleMatch) {
const rawTitle = titleMatch[1].trim();
const nameFromTitle = rawTitle.replace(/^AS\d+\s+/i, '').replace(/\s+-\s+bgp\.he\.net.*$/i, '').trim();
if (nameFromTitle && !nameFromTitle.toLowerCase().includes('bgp.he.net')) {
result.name_from_title = nameFromTitle;
}
}
const lgMatch = html.match(/Looking\s+Glass[^<]*<[^>]*href="([^"]+)"/i);
if (lgMatch) result.looking_glass = lgMatch[1];
const descMatch = html.match(/AS\s+Name[^<]*<[^>]*>[^<]*<[^>]*>([^<]+)/i);
@ -2089,7 +2101,7 @@ const server = http.createServer(async (req, res) => {
JSON.stringify({
status,
service: "PeerCortex",
version: "0.6.7",
version: "0.6.8",
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor(process.uptime()),
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
@ -3338,6 +3350,10 @@ const server = http.createServer(async (req, res) => {
else if (selfLink.includes("lacnic")) rir = "LACNIC";
else if (selfLink.includes("afrinic")) rir = "AFRINIC";
}
// bgp.he.net country_code fallback (for unannounced/reserve ASNs)
if (!country && bgpHeData && bgpHeData.country_code) {
country = bgpHeData.country_code;
}
// Last resort: derive RIR from country code (common assignments)
if (!rir && country) {
const ARIN_CC = new Set(["US","CA","AI","AG","BS","BB","BZ","VG","KY","DM","DO","GD","GP","HT","JM","MQ","MS","PR","KN","LC","VC","TT","TC","VI","UM"]);
@ -3535,7 +3551,7 @@ const server = http.createServer(async (req, res) => {
const result = {
meta: {
service: "PeerCortex",
version: "0.6.7",
version: "0.6.8",
query: "AS" + asn,
duration_ms: duration,
sources: ["PeeringDB", "RIPE Stat", "bgp.he.net", "Cloudflare RPKI", "RIPE RPKI Validator", "Route Views"],
@ -3545,7 +3561,7 @@ const server = http.createServer(async (req, res) => {
},
network: {
asn: parseInt(asn),
name: net.name || overview?.holder || "Unknown",
name: net.name || overview?.holder || (bgpHeData && bgpHeData.name_from_title) || "Unknown",
aka: net.aka || "",
org_name: (net.org && net.org.name) ? net.org.name : "",
website: net.website || "",