UNPKG

ulert

Version:

Open source website guardian — audit uptime, broken links, and security headers from the command line.

185 lines (167 loc) 6.34 kB
// src/reporter.js const fs = require('fs').promises; const path = require('path'); const { version } = require('../package.json'); // Diccionario de traducciones const translations = { es: { reportTitle: "Informe de Seguridad y Salud del Sitio", site: "Sitio", date: "Fecha", httpStatus: "Estado HTTP", online: "✅ En línea (200)", offline: "❌ Fuera de línea", loadTime: "Tiempo de carga", linkAudit: "Auditoría de Enlaces", totalLinks: "Total de enlaces internos", brokenLinks: "Enlaces rotos", noBrokenLinks: "No se encontraron enlaces rotos.", viewBrokenLinks: "Ver enlaces rotos", securityHeaders: "Encabezados de Seguridad", present: "✅ Presente", missing: "❌ Ausente", generatedBy: "Informe generado por", }, en: { reportTitle: "Website Security & Health Report", site: "Site", date: "Date", httpStatus: "HTTP Status", online: "✅ Online (200)", offline: "❌ Offline", loadTime: "Load Time", linkAudit: "Link Audit", totalLinks: "Total internal links", brokenLinks: "Broken links", noBrokenLinks: "No broken links found.", viewBrokenLinks: "View broken links", securityHeaders: "Security Headers", present: "✅ Present", missing: "❌ Missing", generatedBy: "Report generated by", } }; // Función para obtener traducción function t(lang, key) { return translations[lang]?.[key] || translations.es[key] || key; } // CLI report en consola (sin traducción por ahora) function cli(result) { console.log("✅ HTTP:", result.http.ok ? "Online" : "Offline"); console.log("⚡ Tiempo de carga:", result.http.loadTime ? `${result.http.loadTime}ms` : "N/A"); console.log("🔗 Enlaces:", `${result.links.total} totales, ${result.links.broken} rotos`); console.log("🛡️ Seguridad:", result.security.score, "chequeos aprobados"); if (result.links.brokenList?.length > 0) { console.log("\n❌ Enlaces rotos encontrados:"); result.links.brokenList.forEach(link => console.log(" -", link)); } } async function html(result, filePath, lang = "es") { const reportDir = path.dirname(filePath); try { await fs.access(reportDir); } catch { await fs.mkdir(reportDir, { recursive: true }); } const report = ` <!DOCTYPE html> <html lang="${lang}"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>${t(lang, "reportTitle")} - ${result.url}</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', sans-serif; background: #f9fafb; padding: 20px; color: #1f2937; } h1 { text-align: center; color: #111827; margin-bottom: 5px; } h2 { color: #374151; margin-top: 30px; } a { color: #2563eb; text-decoration: none; } a:hover { text-decoration: underline; } .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-bottom: 20px; } .badge { padding: 4px 10px; border-radius: 9999px; font-size: 0.8em; font-weight: bold; display: inline-block; } .success { background: #d1fae5; color: #065f46; } .error { background: #fee2e2; color: #991b1b; } .warning { background: #fef3c7; color: #92400e; } details { margin-top: 10px; } summary { cursor: pointer; } ul { margin: 0; padding-left: 20px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 8px; border-bottom: 1px solid #e5e7eb; text-align: left; } th { background: #f3f4f6; } footer { text-align: center; font-size: 0.85em; margin-top: 30px; color: #6b7280; } </style> </head> <body> <h1>🔍 ${t(lang, "reportTitle")}</h1> <div class="card"> <p><strong>${t(lang, "site")}:</strong> <a href="${result.url}" target="_blank">${result.url}</a></p> <p><strong>${t(lang, "date")}:</strong> ${new Date(result.timestamp).toLocaleString(lang)}</p> </div> <div class="card"> <h2>🌐 ${t(lang, "httpStatus")}</h2> <p class="badge ${result.http.ok ? 'success' : 'error'}"> ${result.http.ok ? t(lang, "online") : t(lang, "offline")} </p> <p><strong>${t(lang, "loadTime")}:</strong> ${result.http.loadTime ? `${result.http.loadTime} ms` : 'N/A'}</p> </div> <div class="card"> <h2>🔗 ${t(lang, "linkAudit")}</h2> <p>${t(lang, "totalLinks")}: ${result.links.total}</p> <p>${t(lang, "brokenLinks")}: <span class="badge ${result.links.broken > 0 ? 'error' : 'success'}">${result.links.broken}</span></p> ${result.links.brokenList?.length > 0 ? ` <details> <summary>${t(lang, "viewBrokenLinks")}</summary> <table> <thead> <tr><th>URL</th><th>Estado</th></tr> </thead> <tbody> ${result.links.brokenList.map(link => ` <tr><td>${link}</td><td><span class="badge error">404</span></td></tr> `).join('')} </tbody> </table> </details> ` : `<p>${t(lang, "noBrokenLinks")}</p>`} </div> <div class="card"> <h2>🛡️ ${t(lang, "securityHeaders")}</h2> <ul> ${Object.entries(result.security.checks).map(([header, passed]) => ` <li> <strong>${header}</strong>: <span class="badge ${passed ? 'success' : 'error'}"> ${passed ? t(lang, "present") : t(lang, "missing")} </span> </li> `).join('')} </ul> </div> <footer> ${t(lang, "generatedBy")} <a href="https://ulert.u-site.app" target="_blank">🛡️ Ulert</a> de <a href="https://u-site.app" target="_blank">u-site.app</a> — v${version} </footer> </body> </html> `; await fs.writeFile(filePath, report, 'utf-8'); } // JSON report sin traducción (datos crudos) async function json(result, filePath) { const reportDir = path.dirname(filePath); try { await fs.access(reportDir); } catch { await fs.mkdir(reportDir, { recursive: true }); } const reportData = { tool: "Ulert", version, timestamp: result.timestamp, url: result.url, http: result.http, links: result.links, security: result.security }; await fs.writeFile(filePath, JSON.stringify(reportData, null, 2), 'utf-8'); } module.exports = { cli, html, json };