Files
speedboard/checker.js

109 lines
4.1 KiB
JavaScript
Raw Permalink Normal View History

import { URL } from 'url';
const SECURITY_HEADERS = [
{ key: 'strict-transport-security', label: 'Strict-Transport-Security', important: true },
{ key: 'content-security-policy', label: 'Content-Security-Policy', important: true },
{ key: 'x-frame-options', label: 'X-Frame-Options', important: true },
{ key: 'x-content-type-options', label: 'X-Content-Type-Options', important: true },
{ key: 'referrer-policy', label: 'Referrer-Policy', important: true },
{ key: 'permissions-policy', label: 'Permissions-Policy', important: false },
{ key: 'cross-origin-embedder-policy', label: 'Cross-Origin-Embedder-Policy', important: false },
{ key: 'cross-origin-opener-policy', label: 'Cross-Origin-Opener-Policy', important: false },
{ key: 'cross-origin-resource-policy', label: 'Cross-Origin-Resource-Policy', important: false },
];
function headerGrade(score) {
if (score >= 90) return 'A+';
if (score >= 75) return 'A';
if (score >= 60) return 'B';
if (score >= 45) return 'C';
if (score >= 30) return 'D';
return 'F';
}
export async function checkSecurityHeaders(urlStr) {
try {
const res = await fetch(urlStr, {
method: 'GET',
redirect: 'follow',
signal: AbortSignal.timeout(12000),
});
// Cancel body — we only need headers
await res.body?.cancel();
const headers = SECURITY_HEADERS.map(h => ({
key: h.key,
label: h.label,
important: h.important,
present: res.headers.has(h.key),
value: res.headers.get(h.key),
}));
const important = headers.filter(h => h.important);
const optional = headers.filter(h => !h.important);
const impScore = (important.filter(h => h.present).length / important.length) * 70;
const optScore = optional.length
? (optional.filter(h => h.present).length / optional.length) * 30
: 30;
const score = Math.round(impScore + optScore);
return { score, grade: headerGrade(score), headers };
} catch (err) {
console.warn('[checker] headers check failed:', err.message);
return null;
}
}
// ─── SSL Labs ─────────────────────────────────────────────────────────────────
const SSL_API = 'https://api.ssllabs.com/api/v3/analyze';
function parseSSLData(data) {
const ep = data.endpoints?.[0];
if (!ep) return null;
const notAfter = ep.details?.cert?.notAfter;
return {
grade: ep.grade || 'T',
ipAddress: ep.ipAddress || null,
certExpiry: notAfter ? new Date(notAfter).toISOString().split('T')[0] : null,
daysUntilExpiry: notAfter ? Math.round((notAfter - Date.now()) / 86_400_000) : null,
protocols: ep.details?.protocols?.map(p => `${p.name} ${p.version}`) ?? [],
statusMessage: ep.statusMessage || null,
};
}
export async function checkSSL(urlStr) {
try {
const host = new URL(urlStr).hostname;
if (!urlStr.startsWith('https')) return null;
// Kick off assessment (or retrieve existing)
const init = await fetch(
`${SSL_API}?host=${encodeURIComponent(host)}&publish=off&startNew=on&all=done`,
{ signal: AbortSignal.timeout(15000) }
);
if (!init.ok) return null;
const initData = await init.json();
if (initData.status === 'READY') return parseSSLData(initData);
if (initData.status === 'ERROR') return null;
// Poll until READY (up to 5 min)
const deadline = Date.now() + 5 * 60_000;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 10_000));
const poll = await fetch(
`${SSL_API}?host=${encodeURIComponent(host)}&publish=off&all=done`,
{ signal: AbortSignal.timeout(15000) }
);
if (!poll.ok) continue;
const data = await poll.json();
if (data.status === 'READY') return parseSSLData(data);
if (data.status === 'ERROR') return null;
}
return null; // timed out
} catch (err) {
console.warn('[checker] SSL check failed:', err.message);
return null;
}
}