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; } }