/** * @module aspa/generator * Detect upstream providers from BGP data and produce an ASPA submission guide. * * Analyzes BGP path data to detect upstream provider relationships, then * produces a plain-language provider list plus per-RIR instructions for * creating the actual ASPA object. Real ASPA objects are signed RPKI/CMS * objects created through your RIR's hosted RPKI platform (or a delegated * RPKI CA); they are not RPSL text and cannot be submitted through the * RIPE Database (whois) the way an aut-num or route object can. There is * no `aspa:` or `upstream:` attribute in the RIPE DB schema. * * @see https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-profile/ (object format, still an IETF draft, not yet an RFC) * * @example * ```typescript * const providers = detectProviders(64501, bgpPaths); * console.log(providers); * // [{ asn: 174, name: "Cogent", confidence: 0.95, pathCount: 42 }] * * const guide = generateASPASubmissionGuide(64501, providers); * console.log(guide); * // Provider list + a link to the RIR's actual RPKI dashboard * ``` */ // ── Types ─────────────────────────────────────────────── /** A BGP path observation used for provider inference */ export interface BGPPath { /** The AS path from collector perspective (leftmost = collector peer) */ readonly asPath: ReadonlyArray; /** IP prefix associated with this path */ readonly prefix: string; /** The collector or vantage point that observed this path */ readonly collector: string; /** When this path was observed */ readonly timestamp: string; } /** A detected upstream provider with confidence scoring */ export interface Provider { /** The provider ASN */ readonly asn: number; /** Human-readable name (if available) */ readonly name: string; /** Confidence that this is a true provider (0.0 to 1.0) */ readonly confidence: number; /** Number of BGP paths supporting this inference */ readonly pathCount: number; /** Address families observed */ readonly afi: ReadonlyArray<"ipv4" | "ipv6">; } // ── Provider Detection ────────────────────────────────── /** * Detect upstream providers for an ASN by analyzing BGP path data. * * Uses the valley-free routing model: in a typical BGP path, a customer AS * appears to the right of its provider. By counting how often each AS appears * immediately to the left of the target ASN across many paths, we can infer * provider relationships with high confidence. * * Heuristics applied: * 1. An AS appearing left of the target in many paths is likely a provider. * 2. Higher path counts yield higher confidence. * 3. ASNs that only appear in a single path are treated as low-confidence. * * @param asn - The ASN to detect providers for * @param bgpPaths - Collection of observed BGP paths * @returns Sorted array of detected providers (highest confidence first) * * @example * ```typescript * const paths: BGPPath[] = [ * { asPath: [3356, 174, 64501], prefix: "192.0.2.0/24", collector: "rrc00", timestamp: "2026-03-26T00:00:00Z" }, * { asPath: [6939, 174, 64501], prefix: "192.0.2.0/24", collector: "rrc01", timestamp: "2026-03-26T00:00:00Z" }, * { asPath: [13335, 64501], prefix: "192.0.2.0/24", collector: "rrc03", timestamp: "2026-03-26T00:00:00Z" }, * ]; * * const providers = detectProviders(64501, paths); * // [ * // { asn: 174, name: "Unknown", confidence: 0.9, pathCount: 2, afi: ["ipv4"] }, * // { asn: 13335, name: "Unknown", confidence: 0.7, pathCount: 1, afi: ["ipv4"] }, * // ] * ``` */ export function detectProviders( asn: number, bgpPaths: ReadonlyArray ): ReadonlyArray { // Count occurrences of each ASN appearing immediately left of the target const providerCounts = new Map }>(); for (const path of bgpPaths) { const { asPath, prefix } = path; const afi = prefix.includes(":") ? "ipv6" : "ipv4"; for (let i = 1; i < asPath.length; i++) { if (asPath[i] === asn && asPath[i - 1] !== asn) { const providerAsn = asPath[i - 1]; const existing = providerCounts.get(providerAsn); if (existing) { existing.count++; existing.afiSet.add(afi); } else { providerCounts.set(providerAsn, { count: 1, afiSet: new Set([afi]), }); } } } } if (providerCounts.size === 0) { return []; } // Calculate confidence based on path count relative to max const maxCount = Math.max(...Array.from(providerCounts.values()).map((v) => v.count)); const providers: Provider[] = []; for (const [providerAsn, data] of providerCounts) { // Confidence formula: normalized count with a floor of 0.3 for single observations const rawConfidence = data.count / maxCount; const confidence = Math.max(0.3, Math.min(1.0, rawConfidence * 0.9 + 0.1)); providers.push({ asn: providerAsn, name: "Unknown", // Name resolution requires external lookup confidence: Math.round(confidence * 100) / 100, pathCount: data.count, afi: Array.from(data.afiSet).sort() as Array<"ipv4" | "ipv6">, }); } // Sort by confidence descending, then by path count descending return providers.sort((a, b) => { if (b.confidence !== a.confidence) return b.confidence - a.confidence; return b.pathCount - a.pathCount; }); } // ── ASPA Provider Summary ──────────────────────────────── /** * Summarize detected providers in plain language, in the ASPA object's * logical field order (customerASID, then providers ascending by ASN, * per draft-ietf-sidrops-aspa-profile). * * This is a human-readable summary for review, not a submittable object. * There is no text format you can paste into a registry to create an * ASPA object; it must be created through your RIR's RPKI platform. * * @param asn - The customer ASN * @param providers - Detected upstream providers * @returns Plain-language summary of the ASPA object's intended content * * @example * ```typescript * const text = summarizeASPAObject(64501, [ * { asn: 174, name: "Cogent", confidence: 0.95, pathCount: 42, afi: ["ipv4", "ipv6"] }, * ]); * console.log(text); * // Customer AS64501 would authorize: AS174 (Cogent, confidence 95%, seen in 42 paths) * ``` */ export function summarizeASPAObject( asn: number, providers: ReadonlyArray ): string { const lines: string[] = [ `# ASPA summary for AS${asn}`, `# Generated by PeerCortex on ${new Date().toISOString()}`, `# Based on BGP path analysis. Review before creating the real object.`, `#`, `# ASPA (Autonomous System Provider Authorization) declares which ASNs`, `# are authorized upstream providers of this AS. This helps prevent`, `# route leaks by allowing RPKI validators to verify AS path legitimacy.`, `#`, `# Reference: IETF draft-ietf-sidrops-aspa-profile (not yet an RFC).`, `# This is a summary for review only. It is NOT a submittable object.`, `# ASPA objects are signed RPKI objects created through your RIR's`, `# hosted RPKI platform, not RPSL text you paste into a registry.`, ``, `Customer: AS${asn}`, ]; const sortedProviders = [...providers].sort((a, b) => a.asn - b.asn); for (const provider of sortedProviders) { const afiStr = provider.afi.join(", "); const comment = `${provider.name} (confidence: ${Math.round(provider.confidence * 100)}%, seen in ${provider.pathCount} paths, ${afiStr})`; lines.push(` Provider: AS${provider.asn} # ${comment}`); } lines.push(``); return lines.join("\n"); } /** Which Regional Internet Registry an ASN belongs to, for RIR-specific guidance */ export type RIR = "ripe" | "arin" | "apnic" | "lacnic" | "afrinic"; /** Per-RIR ASPA production status and the correct place to create the object */ const RIR_ASPA_INFO: Record< RIR, { readonly status: string; readonly howTo: string; readonly infoUrl: string } > = { ripe: { status: "Production since 15 Dec 2025", howTo: "RIPE NCC LIR Portal, RPKI Dashboard, ASPA section", infoUrl: "https://www.ripe.net/manage-ips-and-asns/resource-management/rpki/aspa/", }, arin: { status: "Production since 20 Jan 2026", howTo: "ARIN Online, Routing Security menu, ASPA section", infoUrl: "https://www.arin.net/resources/manage/rpki/aspa/", }, apnic: { status: "Production (exact launch date not published by APNIC; blog post from 9 Jul 2026 confirms support is live)", howTo: "MyAPNIC or the APNIC Registry API", infoUrl: "https://help.apnic.net/s/article/ASPA", }, lacnic: { status: "Not yet in production. Committed for end of 2026", howTo: "Not yet available", infoUrl: "https://blog.lacnic.net/en/programa-rpki-nro-resumen/", }, afrinic: { status: "Not yet in production, no committed date", howTo: "Not yet available", infoUrl: "https://blog.afrinic.net/nro-rpki-program-2025-in-review", }, }; /** * Produce a submission guide for an ASN's detected providers. * * This does NOT produce a pasteable object, because ASPA objects are * signed RPKI objects, not RPSL text. It produces the provider list to * review plus the correct place to actually create the object for the * given RIR. * * @param asn - The customer ASN * @param providers - Detected upstream providers * @param rir - Which RIR this ASN is registered with * @returns Human-readable guide: provider list plus RIR-specific instructions * * @example * ```typescript * const guide = generateASPASubmissionGuide( * 13335, * [{ asn: 174, name: "Cogent", confidence: 0.95, pathCount: 100, afi: ["ipv4", "ipv6"] }], * "ripe" * ); * ``` */ export function generateASPASubmissionGuide( asn: number, providers: ReadonlyArray, rir: RIR ): string { const info = RIR_ASPA_INFO[rir]; const lines: string[] = [ `# ============================================================`, `# ASPA Submission Guide for AS${asn}`, `# Generated by PeerCortex, ${new Date().toISOString()}`, `# ============================================================`, `#`, `# ASPA support at your RIR (${rir.toUpperCase()}): ${info.status}`, `# Where to create the object: ${info.howTo}`, `# More info: ${info.infoUrl}`, `#`, `# ASPA objects are signed RPKI objects. There is no text format`, `# to paste into a registry database. Use the RIR platform above`, `# and enter the provider ASNs listed below.`, `#`, ``, `Customer: AS${asn}`, ]; const highConfidence = providers.filter((p) => p.confidence >= 0.5); const sortedHigh = [...highConfidence].sort((a, b) => a.asn - b.asn); for (const provider of sortedHigh) { const afiComment = provider.afi.length === 2 ? "" : ` # ${provider.afi[0]} only`; lines.push(` Provider to add: AS${provider.asn}${afiComment}`); } lines.push(``); const lowConfidence = providers.filter((p) => p.confidence < 0.5); if (lowConfidence.length > 0) { lines.push(`# The following providers were detected with low confidence.`); lines.push(`# Review before adding them:`); const sortedLow = [...lowConfidence].sort((a, b) => a.asn - b.asn); for (const provider of sortedLow) { lines.push( `# AS${provider.asn} (${provider.name}, confidence: ${Math.round(provider.confidence * 100)}%)` ); } lines.push(``); } return lines.join("\n"); }