diff --git a/public/index.html b/public/index.html index ffe36a4..f3fe104 100644 --- a/public/index.html +++ b/public/index.html @@ -2553,6 +2553,58 @@ function renderHealthReport(d) { h += ''; + // === DATA ACCURACY SECTION === + h += '
'; + h += '
'; + h += 'Score Breakdown — Why ' + score + '/100?
'; + + // Score calculation table + h += ''; + h += ''; + + var weightMap = { bogon: 15, irr: 10, rpki_completeness: 15, abuse_contact: 5, blocklist: 15, manrs: 5, rdns: 5, visibility: 10, rpsl: 5, ix_route_server: 5, resource_cert: 10 }; + var totalW = 0, totalE = 0; + checkOrder.forEach(function(key) { + var v = validations[key]; + if (!v) return; + var w = weightMap[key] || 0; + if (w === 0) return; // communities, geolocation not scored + var info = checkLabels[key] || { label: key }; + var earned = 0; + var reason = ''; + if (v.status === 'info') { + reason = 'Excluded — unable to verify (API requires authentication)'; + // info checks excluded from scoring + } else { + if (v.status === 'pass') { earned = w; reason = 'Full points'; } + else if (v.status === 'warning') { earned = Math.round(w * 0.5); reason = '' + escHtml(v.note || v.message || 'Partial compliance') + ''; } + else { earned = 0; reason = '' + escHtml(v.note || v.message || 'Check failed') + ''; } + totalW += w; + totalE += earned; + } + var statusIcon = v.status === 'pass' ? '✅' : v.status === 'warning' ? '⚠️' : v.status === 'fail' ? '❌' : 'ℹ️'; + h += ''; + h += ''; + h += ''; + h += ''; + }); + + var calcScore = totalW > 0 ? Math.round((totalE / totalW) * 100) : 0; + h += ''; + h += ''; + h += ''; + h += ''; + h += '
CheckWeightEarnedReason
' + statusIcon + ' ' + info.label + '' + (v.status === 'info' ? '—' : w) + '' + (v.status === 'info' ? '—' : earned) + '' + reason + '
Total' + totalW + '' + totalE + '' + calcScore + '/100 = ' + totalE + '/' + totalW + ' weighted points
'; + + // Data source note + h += '
'; + h += 'Data Sources: PeeringDB (profile, IX, facilities), RIPE Stat (prefixes, neighbours, visibility, RPKI), '; + h += 'RIPE Atlas (probes), Cloudflare RPKI (ROA + ASPA), MANRS Observatory, RIPE DB (IRR objects).
'; + h += 'Scoring: Each check has a weight reflecting its importance to routing security. '; + h += '"Pass" = full weight, "Warning" = 50%, "Fail" = 0%, "Info" = excluded (unable to verify). '; + h += 'Score = earned / total_weight × 100. Checks marked "info" (e.g., MANRS when API is unavailable) are excluded from the denominator to avoid unfair penalties.'; + h += '
'; + // Expandable details h += '
Show detailed validation results
'; h += '
'; diff --git a/server.js b/server.js index b654d29..4bb60ca 100644 --- a/server.js +++ b/server.js @@ -940,8 +940,14 @@ const server = http.createServer(async (req, res) => { return res.end(); } - const url = new URL(req.url, "http://localhost"); - const reqPath = url.pathname; + let url, reqPath; + try { + url = new URL(req.url, "http://localhost"); + reqPath = url.pathname; + } catch (_) { + res.writeHead(400); + return res.end("Bad Request"); + } // Serve static files if (reqPath === "/" || reqPath === "/index.html") {