feat: BGP hijack alerts, PDF export (3 formats), ASPA + IPv6 adoption tracker
- BGP Hijack Alerting: HMAC-SHA256 signed webhooks, exponential backoff, 6h dedup, /api/webhooks + /api/hijacks endpoints - PDF Export (3 formats): Executive 3p / Technical 8p (full tables, check scores) / Full Report 10p (ASPA analysis + data provenance); 5min cache - Comparison PDF: side-by-side ASN diff with shared IXPs + facilities - ASPA Adoption Tracker: daily snapshots, trend history, linear regression forecast, /api/aspa-adoption-stats - IPv6 Adoption per RIR: all 5 RIR delegation files, 24h cache, /api/ipv6-adoption-stats - UI: two-tab adoption overlay (ASPA + IPv6 charts), PDF download buttons in action bar and comparison panel - Bump version v0.6.1 → v0.6.5, add runtime cache files to .gitignore
This commit is contained in:
parent
46681ad6f2
commit
b31a911719
2
.gitignore
vendored
2
.gitignore
vendored
@ -44,6 +44,8 @@ ecosystem.config.js
|
||||
visitors.json
|
||||
feedback.json
|
||||
hijack-subs.json
|
||||
webhook-subs.json
|
||||
aspa-adoption-history.json
|
||||
audit/reports/
|
||||
audit/__pycache__/
|
||||
audit/asn_registry.json
|
||||
|
||||
@ -26,3 +26,10 @@
|
||||
{"d":"2026-04-09","t":"FIX","m":"renderResilienceScore + renderRouteLeak: functions were called but never defined — caused JS crash 'is not defined' breaking entire doLookup render"}
|
||||
{"d":"2026-04-09","t":"INFRA","m":"Production git synced to GitHub main (11 commits ahead fixed via git pull); deploy.sh script added for future deployments"}
|
||||
{"d":"2026-04-09","t":"INFRA","m":"PeeringDB SQLite daily cron: peeringdb sync at 03:00 UTC, refresh-peeringdb.sh installed on Erik; DB refreshed 34302→34387 networks"}
|
||||
{"d":"2026-04-30","t":"FEAT","m":"BGP Hijack Alerting + Webhooks: real-time hijack detection with HMAC-SHA256 signed webhooks, exponential backoff retries, 6h dedup, /api/webhooks + /api/hijacks endpoints"}
|
||||
{"d":"2026-04-30","t":"FEAT","m":"PDF Export: server-side Puppeteer PDF generation with 3 formats — Executive (3p), Technical (8p, full prefix/upstream tables + check scores), Full Report (10p, ASPA analysis + data provenance); 5min cache, /api/export/pdf"}
|
||||
{"d":"2026-04-30","t":"FEAT","m":"Comparison PDF: side-by-side ASN comparison as downloadable PDF with shared IXPs, common facilities, peering opportunities, /api/export/pdf/compare"}
|
||||
{"d":"2026-04-30","t":"FEAT","m":"ASPA Adoption Tracker: daily snapshots of global ASPA deployment rate (Atlas-denominated coverage %), trend history, linear regression forecast, /api/aspa-adoption-stats"}
|
||||
{"d":"2026-04-30","t":"FEAT","m":"IPv6 Adoption per RIR: fetches all 5 RIR delegation files daily, computes IPv6 record percentage per ARIN/RIPE/APNIC/AFRINIC/LACNIC + global, /api/ipv6-adoption-stats"}
|
||||
{"d":"2026-04-30","t":"UI","m":"Adoption Tracker overlay panel: two-tab overlay (ASPA Adoption + IPv6 per RIR) matching editorial design, Canvas trend charts, stat cards, data tables — replaces /aspa-adoption nav link"}
|
||||
{"d":"2026-04-30","t":"UI","m":"PDF download buttons in UI: action-bar PDF dropdown (Full Report / Executive / Technical Deep-Dive) after ASN lookup; Comparison PDF button at bottom of Network Comparison panel"}
|
||||
|
||||
280
server.js
280
server.js
@ -838,6 +838,97 @@ new Chart(document.getElementById('asnChart'), {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FEATURE 4: IPv6 Adoption per RIR
|
||||
// ============================================================
|
||||
|
||||
// In-memory cache: { data, ts }
|
||||
let ipv6StatsCache = null;
|
||||
const IPV6_STATS_TTL = 24 * 60 * 60 * 1000; // 24h
|
||||
|
||||
const RIR_DELEGATION_URLS = {
|
||||
RIPE: 'https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest',
|
||||
ARIN: 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
|
||||
APNIC: 'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest',
|
||||
AFRINIC: 'https://ftp.afrinic.net/stats/afrinic/delegated-afrinic-extended-latest',
|
||||
LACNIC: 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest',
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch and parse one RIR delegation file.
|
||||
* Returns { ipv4, ipv6, asns } count objects.
|
||||
*/
|
||||
function fetchRirDelegation(rir, url) {
|
||||
return new Promise((resolve) => {
|
||||
const result = { rir, ipv4: 0, ipv6: 0, asns: 0, ipv4_countries: new Set(), ipv6_countries: new Set() };
|
||||
const mod = url.startsWith('https') ? require('https') : require('http');
|
||||
const req = mod.get(url, { headers: { 'User-Agent': 'PeerCortex/1.0 (+https://peercortex.org)' }, timeout: 20000 }, res => {
|
||||
if (res.statusCode !== 200) { resolve(result); return; }
|
||||
let buf = '';
|
||||
res.on('data', d => { buf += d; });
|
||||
res.on('end', () => {
|
||||
for (const line of buf.split('\n')) {
|
||||
if (line.startsWith('#') || line.startsWith(rir.toLowerCase() + '|*') || !line.includes('|')) continue;
|
||||
const parts = line.split('|');
|
||||
if (parts.length < 7) continue;
|
||||
const type = parts[2];
|
||||
const status = parts[6].trim();
|
||||
const cc = parts[1];
|
||||
if (status !== 'allocated' && status !== 'assigned') continue;
|
||||
if (type === 'ipv4') { result.ipv4++; result.ipv4_countries.add(cc); }
|
||||
else if (type === 'ipv6') { result.ipv6++; result.ipv6_countries.add(cc); }
|
||||
else if (type === 'asn') { result.asns++; }
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
req.on('error', () => resolve(result));
|
||||
req.on('timeout', () => { req.destroy(); resolve(result); });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all 5 RIR delegation files and aggregate IPv6 stats.
|
||||
* Cached for 24 hours.
|
||||
*/
|
||||
async function fetchIpv6AdoptionStats(force) {
|
||||
if (!force && ipv6StatsCache && (Date.now() - ipv6StatsCache.ts) < IPV6_STATS_TTL) {
|
||||
return ipv6StatsCache.data;
|
||||
}
|
||||
console.log('[IPv6] Fetching RIR delegation stats...');
|
||||
const results = await Promise.all(
|
||||
Object.entries(RIR_DELEGATION_URLS).map(([rir, url]) => fetchRirDelegation(rir, url))
|
||||
);
|
||||
const rirs = results.map(r => ({
|
||||
rir: r.rir,
|
||||
ipv4_records: r.ipv4,
|
||||
ipv6_records: r.ipv6,
|
||||
asn_records: r.asns,
|
||||
ipv6_pct: (r.ipv4 + r.ipv6) > 0
|
||||
? Math.round((r.ipv6 / (r.ipv4 + r.ipv6)) * 10000) / 100
|
||||
: 0,
|
||||
countries_with_ipv6: r.ipv6_countries.size,
|
||||
countries_with_ipv4: r.ipv4_countries.size,
|
||||
}));
|
||||
const total_ipv4 = rirs.reduce((s, r) => s + r.ipv4_records, 0);
|
||||
const total_ipv6 = rirs.reduce((s, r) => s + r.ipv6_records, 0);
|
||||
const data = {
|
||||
fetched_at: new Date().toISOString(),
|
||||
global_ipv6_pct: total_ipv4 + total_ipv6 > 0
|
||||
? Math.round((total_ipv6 / (total_ipv4 + total_ipv6)) * 10000) / 100
|
||||
: 0,
|
||||
total_ipv4_records: total_ipv4,
|
||||
total_ipv6_records: total_ipv6,
|
||||
by_rir: rirs,
|
||||
};
|
||||
ipv6StatsCache = { data, ts: Date.now() };
|
||||
console.log(`[IPv6] Done. Global IPv6%: ${data.global_ipv6_pct}% across ${rirs.length} RIRs`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Pre-fetch on startup (non-blocking)
|
||||
fetchIpv6AdoptionStats().catch(() => {});
|
||||
|
||||
const UA = "PeerCortex/0.5.0 (+https://peercortex.org)";
|
||||
|
||||
// Static geocode cache for major networking cities (fallback when PDB facility coords missing)
|
||||
@ -1132,15 +1223,180 @@ function buildPdfHtml(data, aspa, format) {
|
||||
<span>Generated ${ts} · peercortex.org</span>
|
||||
</footer>`;
|
||||
|
||||
// ── Extra pages for technical / report formats ─────────────────────────────
|
||||
|
||||
// Technical: full prefix table (no row cap) + all upstream rows + check scores
|
||||
const allPfxRows = rpkiDetails.map(v => {
|
||||
const badge = v.status === 'valid' ? 'badge-green' : v.status === 'invalid' ? 'badge-red' : 'badge-gray';
|
||||
return `<tr><td>${escHtml(v.prefix)}</td>
|
||||
<td><span class="badge ${badge}">${v.status}</span></td>
|
||||
<td>${v.validating_roas ?? '-'}</td>
|
||||
<td>${v.max_length ?? '-'}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
const allUpstreamRows = (rel.upstreams || []).map(u =>
|
||||
`<tr><td>AS${u.asn}</td><td>${escHtml(u.name || '')}</td><td>${u.power ?? '-'}</td></tr>`
|
||||
).join('');
|
||||
|
||||
const checkScoreRows = checks.map(c =>
|
||||
`<tr>
|
||||
<td>${escHtml(c.check || '')}</td>
|
||||
<td><span class="badge ${c.status === 'pass' ? 'badge-green' : c.status === 'info' ? 'badge-indigo' : 'badge-red'}">${c.status?.toUpperCase()}</span></td>
|
||||
<td>${c.earned ?? '-'}</td><td>${c.weight ?? '-'}</td>
|
||||
<td style="font-size:8pt;color:#6b7280">${escHtml(c.detail || '')}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
|
||||
const technicalHeaderPage = `
|
||||
<div class="page" style="padding-top:2cm">
|
||||
<div style="border-left:4px solid #6366f1;padding-left:16px;margin-bottom:24px">
|
||||
<div style="font-size:8pt;text-transform:uppercase;letter-spacing:1px;color:#6b7280">PeerCortex · Technical Analysis Report</div>
|
||||
<div style="font-size:22pt;font-weight:700;margin:4px 0">AS${asn} — ${escHtml(name)}</div>
|
||||
<div style="font-size:9pt;color:#6b7280">Generated ${ts} · peercortex.org · ${totalPfx} prefixes · RPKI ${rpkiCoverage}%</div>
|
||||
</div>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card"><div class="label">Health Score</div><div class="value" style="color:${scoreColor}">${score}/100</div><div class="unit">${passedChecks} pass · ${failedChecks} fail</div></div>
|
||||
<div class="metric-card"><div class="label">RPKI Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">${rpkiValid} valid · ${rpkiInvalid} invalid · ${rpkiNotFound} missing</div></div>
|
||||
<div class="metric-card"><div class="label">Upstreams</div><div class="value">${counts.upstreams || 0}</div><div class="unit">transit providers</div></div>
|
||||
<div class="metric-card"><div class="label">Peers / Downstreams</div><div class="value">${counts.peers || 0} / ${counts.downstreams || 0}</div><div class="unit">BGP relationships</div></div>
|
||||
<div class="metric-card"><div class="label">ASPA</div>
|
||||
<div class="value" style="font-size:11pt;margin-top:6px"><span class="badge ${aspaDirect === true ? 'badge-green' : 'badge-red'}">${aspaDirect === true ? 'Deployed' : 'Not deployed'}</span></div>
|
||||
<div class="unit">${aspaProviders} providers registered</div>
|
||||
</div>
|
||||
<div class="metric-card"><div class="label">Prefixes Analysed</div><div class="value">${totalPfx}</div><div class="unit">${rpkiDetails.length} with RPKI data</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const rpkiFullPage = `
|
||||
<div class="page">
|
||||
<h2>RPKI Compliance — Full Prefix Table</h2>
|
||||
<p class="section-intro">${rpkiDetails.length} prefixes with RPKI validation data. Coverage: ${rpkiCoverage}% · Valid: ${rpkiValid} · Invalid: ${rpkiInvalid} · Not Found: ${rpkiNotFound}</p>
|
||||
<h3>ASPA Object</h3>
|
||||
<table>
|
||||
<tr><th>Attribute</th><th>Value</th></tr>
|
||||
<tr><td>ASPA Present</td><td><span class="badge ${aspaDirect ? 'badge-green' : 'badge-red'}">${aspaDirect ? 'Yes' : 'No'}</span></td></tr>
|
||||
<tr><td>Provider Count</td><td>${aspaProviders}</td></tr>
|
||||
<tr><td>Registered Providers</td><td style="font-size:8pt">${(aspa?.direct?.provider_asns || []).map(a => `AS${a}`).join(', ') || '—'}</td></tr>
|
||||
</table>
|
||||
${allPfxRows ? `
|
||||
<h3 style="margin-top:1.5em">All Prefixes</h3>
|
||||
<table>
|
||||
<tr><th>Prefix</th><th>RPKI Status</th><th>Validating ROAs</th><th>Max Length</th></tr>
|
||||
${allPfxRows}
|
||||
</table>` : '<p style="color:#9ca3af">No RPKI prefix data available.</p>'}
|
||||
</div>`;
|
||||
|
||||
const bgpFullPage = `
|
||||
<div class="page">
|
||||
<h2>BGP Topology — Full Relationship Table</h2>
|
||||
<p class="section-intro">Complete AS relationship data. Upstreams: ${counts.upstreams || 0} · Peers: ${counts.peers || 0} · Downstreams: ${counts.downstreams || 0}</p>
|
||||
${allUpstreamRows ? `
|
||||
<h3>All Upstream Providers</h3>
|
||||
<table>
|
||||
<tr><th>ASN</th><th>Name</th><th>Power Score</th></tr>
|
||||
${allUpstreamRows}
|
||||
</table>` : '<p style="color:#9ca3af">No upstream data available.</p>'}
|
||||
${(rel.peers || []).length > 0 ? `
|
||||
<h3 style="margin-top:1.5em">Peer Sample (top 20)</h3>
|
||||
<table>
|
||||
<tr><th>ASN</th><th>Name</th></tr>
|
||||
${(rel.peers || []).slice(0, 20).map(p => `<tr><td>AS${p.asn}</td><td>${escHtml(p.name || '')}</td></tr>`).join('')}
|
||||
</table>` : ''}
|
||||
</div>`;
|
||||
|
||||
const checkDetailPage = `
|
||||
<div class="page">
|
||||
<h2>Check Score Detail</h2>
|
||||
<p class="section-intro">All ${checks.length} health checks with individual scores and diagnostic notes.</p>
|
||||
<table>
|
||||
<tr><th>Check</th><th>Status</th><th>Earned</th><th>Weight</th><th>Detail</th></tr>
|
||||
${checkScoreRows}
|
||||
</table>
|
||||
<div style="margin-top:1.5em;padding:12px;background:#f9fafb;border:1px solid #e5e7eb;font-size:8pt;color:#6b7280">
|
||||
<strong>Scoring:</strong> Total = sum of earned points across all checks. Maximum possible: ${checks.reduce((s, c) => s + (c.weight || 0), 0)} points.
|
||||
Current score: ${checks.reduce((s, c) => s + (c.earned || 0), 0)} / ${checks.reduce((s, c) => s + (c.weight || 0), 0)} = ${score}/100
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Report: add data provenance and ASPA deep-dive
|
||||
const aspaPage = `
|
||||
<div class="page">
|
||||
<h2>ASPA — Provider Authorization Analysis</h2>
|
||||
<p class="section-intro">Autonomous System Provider Authorization (ASPA) is a BGP security mechanism that cryptographically links an ASN to its upstream providers in the RPKI.</p>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card"><div class="label">ASPA Deployed</div>
|
||||
<div class="value" style="font-size:12pt;margin-top:6px"><span class="badge ${aspaDirect === true ? 'badge-green' : 'badge-red'}">${aspaDirect === true ? 'Yes' : 'No'}</span></div>
|
||||
</div>
|
||||
<div class="metric-card"><div class="label">Providers Listed</div><div class="value">${aspaProviders}</div><div class="unit">in ASPA object</div></div>
|
||||
<div class="metric-card"><div class="label">RPKI Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">of announced prefixes</div></div>
|
||||
</div>
|
||||
<h3>What ASPA Protects Against</h3>
|
||||
<table>
|
||||
<tr><th>Threat</th><th>ASPA Mitigates?</th><th>Notes</th></tr>
|
||||
<tr><td>Route leaks (valley-free violations)</td><td><span class="badge badge-green">Yes</span></td><td>Providers verify upstream path</td></tr>
|
||||
<tr><td>BGP hijacks (origin AS spoofing)</td><td><span class="badge badge-indigo">Partial</span></td><td>Combined with ROV</td></tr>
|
||||
<tr><td>AS-path prepending attacks</td><td><span class="badge badge-green">Yes</span></td><td>Invalid paths rejected</td></tr>
|
||||
<tr><td>Forged-origin hijacks</td><td><span class="badge badge-red">No</span></td><td>ROA + ROV required</td></tr>
|
||||
</table>
|
||||
${aspaDirect === false ? `
|
||||
<div style="margin-top:1.5em;padding:14px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:4px">
|
||||
<div style="font-weight:600;color:#991b1b;margin-bottom:6px">Recommendation: Deploy ASPA</div>
|
||||
<div style="font-size:9pt;color:#6b7280">
|
||||
1. Identify all upstream transit providers (${counts.upstreams || 0} detected)<br>
|
||||
2. Create an ASPA object in your RIR portal listing their ASNs<br>
|
||||
3. Sign with your RPKI key — takes effect within hours<br>
|
||||
4. Verify with: <code>rpki-client -v</code> or RIPE RPKI Validator
|
||||
</div>
|
||||
</div>` : `
|
||||
<div style="margin-top:1.5em;padding:14px;background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px">
|
||||
<div style="font-weight:600;color:#15803d">ASPA is deployed — listed providers:</div>
|
||||
<div style="font-size:9pt;color:#374151;margin-top:6px">${(aspa?.direct?.provider_asns || []).map(a => `AS${a}`).join(' · ') || '—'}</div>
|
||||
</div>`}
|
||||
</div>`;
|
||||
|
||||
const provenancePage = `
|
||||
<div class="page">
|
||||
<h2>Data Provenance</h2>
|
||||
<p class="section-intro">All data sources, cache ages, and methodology used to generate this report.</p>
|
||||
<table>
|
||||
<tr><th>Data Source</th><th>What it provides</th><th>Update Frequency</th></tr>
|
||||
<tr><td>Cloudflare RPKI Feed</td><td>ROA + ASPA objects</td><td>Every 4 hours</td></tr>
|
||||
<tr><td>PeeringDB</td><td>IXP presence, peering policy, facilities</td><td>Daily (03:00 UTC)</td></tr>
|
||||
<tr><td>RIPE Stat</td><td>AS relationships, prefix visibility, BGP data</td><td>Real-time / cached 15 min</td></tr>
|
||||
<tr><td>RIPE Atlas</td><td>Active measurement probes by ASN</td><td>Cached 12 hours</td></tr>
|
||||
<tr><td>bgproutes.io</td><td>BGP route visibility fallback</td><td>Real-time</td></tr>
|
||||
<tr><td>bgp.he.net</td><td>AS relationship graph</td><td>Cached 24 hours</td></tr>
|
||||
</table>
|
||||
<h3 style="margin-top:1.5em">Report Metadata</h3>
|
||||
<table>
|
||||
<tr><th>Field</th><th>Value</th></tr>
|
||||
<tr><td>ASN</td><td>AS${asn}</td></tr>
|
||||
<tr><td>Network Name</td><td>${escHtml(name)}</td></tr>
|
||||
<tr><td>Report Format</td><td>${formatLabel(format)}</td></tr>
|
||||
<tr><td>Generated</td><td>${ts}</td></tr>
|
||||
<tr><td>Platform</td><td>PeerCortex · peercortex.org</td></tr>
|
||||
<tr><td>Prefixes Analysed</td><td>${totalPfx}</td></tr>
|
||||
<tr><td>RPKI Objects Checked</td><td>${rpkiDetails.length}</td></tr>
|
||||
<tr><td>Health Checks Run</td><td>${checks.length}</td></tr>
|
||||
</table>
|
||||
<div style="margin-top:1.5em;font-size:8pt;color:#9ca3af;border-top:1px solid #e5e7eb;padding-top:12px">
|
||||
This report is generated automatically based on publicly available BGP and RPKI data.
|
||||
Data may be delayed by cache TTLs. Not a substitute for real-time monitoring.
|
||||
MIT License · github.com/renefichtmueller/PeerCortex
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// ── Compose pages by format ────────────────────────────────────────────────
|
||||
let bodyContent;
|
||||
if (format === 'executive') {
|
||||
// 3 pages: clean stakeholder summary — no deep technical data
|
||||
bodyContent = coverPage + summaryPage + recsPage;
|
||||
} else if (format === 'technical') {
|
||||
bodyContent = coverPage + summaryPage + rpkiPage + bgpPage + recsPage;
|
||||
// ~8 pages: engineer-focused, dense, no decorative cover, full data tables
|
||||
bodyContent = technicalHeaderPage + rpkiFullPage + bgpFullPage + checkDetailPage + recsPage;
|
||||
} else {
|
||||
// 'report' — full
|
||||
bodyContent = coverPage + summaryPage + rpkiPage + bgpPage + recsPage;
|
||||
// 'report' — full stakeholder + technical, ~10 pages
|
||||
bodyContent = coverPage + summaryPage + rpkiPage + bgpPage + aspaPage + recsPage + provenancePage;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
@ -6497,11 +6753,25 @@ ${html}
|
||||
return res.end(JSON.stringify({ period, count: data.length, data }, null, 2));
|
||||
}
|
||||
|
||||
// GET /api/ipv6-adoption-stats?refresh=1
|
||||
if (reqPath === '/api/ipv6-adoption-stats' && req.method === 'GET') {
|
||||
const force = url.searchParams.get('refresh') === '1';
|
||||
try {
|
||||
const data = await fetchIpv6AdoptionStats(force);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify(data, null, 2));
|
||||
} catch (err) {
|
||||
res.writeHead(503);
|
||||
return res.end(JSON.stringify({ error: 'IPv6 stats unavailable', message: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Not found. Endpoints: /api/health, /api/validate?asn=X, /api/lookup?asn=X, /api/aspa?asn=X, /api/aspa/verify?asn=X, /api/bgproutes?asn=X, /api/compare?asn1=X&asn2=Y, /api/peers/find?ix=NAME, /api/prefix/detail?prefix=X, /api/ix/detail?ix_id=X, /api/export/pdf?asn=X&format=report|executive|technical, /api/export/pdf/compare?asn1=X&asn2=Y, /api/aspa-adoption-stats?period=30d",
|
||||
error: "Not found. Endpoints: /api/health, /api/validate?asn=X, /api/lookup?asn=X, /api/aspa?asn=X, /api/aspa/verify?asn=X, /api/bgproutes?asn=X, /api/compare?asn1=X&asn2=Y, /api/peers/find?ix=NAME, /api/prefix/detail?prefix=X, /api/ix/detail?ix_id=X, /api/export/pdf?asn=X&format=report|executive|technical, /api/export/pdf/compare?asn1=X&asn2=Y, /api/aspa-adoption-stats?period=30d, /api/ipv6-adoption-stats",
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -6667,7 +6937,7 @@ Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes(), fetchPdbOrgCountries()]
|
||||
recordAspaAdoptionSnapshot(rpkiAspaMap.size, roaStore.count);
|
||||
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log("PeerCortex v0.6.1 running on http://0.0.0.0:" + PORT);
|
||||
console.log("PeerCortex v0.6.5 running on http://0.0.0.0:" + PORT);
|
||||
console.log("bgproutes.io API key: " + (BGPROUTES_API_KEY ? "configured" : "NOT configured"));
|
||||
console.log("PeeringDB API key: " + (PEERINGDB_API_KEY ? "configured" : "NOT configured"));
|
||||
console.log("RPKI ASPA objects: " + rpkiAspaMap.size);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user