← Volver al inicio
Novedades
Lo último que se añade y mejora en CyberEscudo: herramientas y funciones nuevas, artículos del blog y cambios de la plataforma. El detalle técnico completo (tests, rendimiento, accesibilidad…) está más abajo y en GitHub.
✨ Novedad
Fase 16: rutas con certificado verificable
- Un examen por ruta de «Aprende» (phishing y correo, detection engineering, vulnerabilidades y threat intel, fundamentos): 8 preguntas sacadas al azar de un banco de 12, con las opciones barajadas; se aprueba con 6. En
/certificados/examen/<ruta> (ES/EN).
- Corregido en el servidor: el navegador recibe los enunciados y las opciones, nunca cuál es la buena, y solo ve la nota (decir qué falló permitiría sacar el banco en pocos intentos). La tirada vive en la sesión con un nonce que cada envío consume, así que no se puede reenviar la misma cambiando respuestas, y recargar no cambia las preguntas. Como mucho 5 exámenes por hora y cliente, contados como en la API: HMAC con la clave del día, sin guardar la IP (sin esa clave, no se corrige).
- El certificado, en
/certificado/<id>: el nombre que escribe quien aprobó, la ruta, la nota, la fecha y el código (128 bits al azar). Página pública sin indexar, con el aviso de que CyberEscudo no comprueba la identidad, y botón «Añadir a LinkedIn» (el formulario de licencias y certificaciones del perfil, con la URL y el código).
- Borrado sin cuenta ni correo: al emitirlo se enseña una sola vez (sin caché) un enlace de borrado; en la BD solo está su SHA-256. El enlace pasa el token a la sesión, limpia la URL y el borrado es un botón con CSRF.
- Solo se guarda el nombre, la ruta, la nota, el idioma y la fecha (tabla
ce_certs, que la librería crea si falta). Sección nueva en la política de privacidad y la cookie de sesión descrita como es (idioma y estado de los formularios).
/certificados: el hub con las cuatro rutas y cómo funciona; entra en «Aprender» y en el sitemap. Cada tema de /aprende enlaza el examen de su ruta y el examen enlaza de vuelta al tema.
- Verificación: +67 tests PHP (2349 → 2416, con los de BD en local), JS sin cambios (748); guards falsificados 19/19 (dos pruebas se reforzaron antes: respuestas «casi buenas» como
03 o 3.0, y una hora cerca de medianoche para pillar un corrimiento de zona); smoke 276/276, enlaces internos sin rotos (1192) y auditoría delta sin bloqueantes nuevos (el escáner pidió for explícito en los radios); probado en el navegador: aprobar justo con 6, nombre inválido y válido, emisión, LinkedIn, el enlace de borrado una sola vez, borrado (y 404 después) y reenvío de un examen ya corregido.
✨ Novedad
Fase 15: El Turno diario
- El incidente del día pasa a ser «El Turno #N»: el mismo para todo el mundo, numerado desde hoy (#1, 24 de septiembre de 2026). Solo cuenta como turno oficial si la plantilla es la de ese día, así que un enlace retocado no se hace pasar por el de hoy.
- Compartir el resultado sin desvelar las respuestas: al emitir el veredicto, un botón copia un cuadro por pregunta (🟩 acierto, 🟥 fallo), la línea de tiempo, la nota y el enlace al turno de hoy, estilo Wordle.
- Racha de días seguidos: jugar el turno de hoy suma; rejugarlo o jugar uno antiguo no. Se guarda solo en el navegador (sin cuenta y sin enviarnos nada), y lo guardado se valida para que un valor roto no rompa nada.
/turno (ES/EN): el turno de hoy, cómo funciona y el archivo de los turnos anteriores con su familia de incidente. La portada enseña el número del turno y enlaza el archivo.
- La lógica (racha, día anterior, texto para compartir) está en un módulo puro,
assets/js/range-daily.js, probado con node --test; el número de turno y el archivo, en lib/range-engine.php.
- Verificación: +17 tests PHP (2332 → 2349) y +9 JS (739 → 748); guards falsificados 14/14 (dos salieron vacuos por huecos en los datos de prueba, como una fecha imposible que rodaba a antes del inicio, y se reforzaron); smoke 269/269, enlaces internos sin rotos (1184) y auditoría delta sin bloqueantes nuevos; probado en el navegador: cinta e insignia del turno, veredicto, racha guardada y botón de compartir.
✨ Novedad
Fase 14: API pública del auditor de correo
GET /api/v1/email?domain=… (también /api/v1/email/<dominio>/, con barra final por el cortafuegos): la nota de SPF, DMARC y DKIM de cualquier dominio en un JSON estable (v1), con la nota y la puntuación, la metodología, los registros, la política DMARC, los extras, los hallazgos en español e inglés y los enlaces al informe y a la insignia. Mismo motor y caché de 6 h que el auditor. Gratis y sin clave.
- Límite sin guardar la IP: 60 peticiones por hora y cliente (su IPv4, o el prefijo /64 de su IPv6), de las que como mucho 10 pueden ser consultas nuevas, que además comparten el presupuesto global del auditor. El cliente se cuenta con un HMAC y una sal aleatoria que se sustituye cada día y vive fuera del sitio web; si no se puede guardar o leer el estado, el límite se cierra en vez de desaparecer. Sin sesión ni cookies, y errores JSON con un código estable (400, 404, 405, 429 con
Retry-After, 503).
- Documentación en
/api/ (ES/EN): un ejemplo de respuesta que calcula el propio motor (así no se desvía de lo que devuelve la API), los campos, los errores, un workflow de GitHub Actions para copiar que falla si la nota baja de un umbral y la insignia en Markdown y HTML. Enlaces desde el auditor y la página de cada resultado, y una sección nueva en la política de privacidad.
- Sin CORS: la API es para scripts, CI y servidores; para enseñar la nota en otra web está la insignia.
- Revisión separada (Fable, sin el contexto de la implementación). Corregido lo que encontró: rotar direcciones dentro de un /64 de IPv6 multiplicaba el cupo, y llenar la tabla de clientes dejaba fuera a todos los nuevos durante una hora (ahora IPv6 cuenta por su /64, los contadores van en 16 ficheros pequeños y quien no cabe entra por un cupo compartido);
dkim.record era el texto de pantalla del motor y no un registro (ahora dkim.keys, con lo encontrado por selector, y los campos de /api/ salen del código con un test que los compara con la respuesta); cambiar los selectores en cada petición gastaba el presupuesto global de todos (ahora hay un límite propio de consultas nuevas); un fichero de contadores ilegible reiniciaba la ventana (ahora se escribe de golpe y, si no se puede leer, se cierra); la carpeta temporal se usaba aunque fuera un enlace o de otro usuario; un Cache-Control: public que el servidor nunca dejaba salir; y display_errors sin fijar en el endpoint.
- Verificación: +46 tests PHP (2286 → 2332); guards falsificados 35/35; smoke 268/268, enlaces internos sin rotos (1180) y auditoría delta sin bloqueantes nuevos (el escáner marcó el CORS abierto, que se quitó, y un enlace sin nombre accesible en el fragmento de la insignia, que ahora lleva
aria-label); pa11y pidió que los bloques de código se puedan recorrer con el teclado. Probado por HTTP: pasan 60 peticiones y la siguiente da 429 con Retry-After; de 11 consultas nuevas pasan 10; en los ficheros de estado no aparece la IP.
✨ Novedad
Fase 13: guías de SPF, DKIM y DMARC por proveedor
/guias y una guía por proveedor (ES/EN): Microsoft 365, Google Workspace, Cloudflare, Plesk, cPanel e IONOS. Cada una dice qué publicar (registro, nombre, tipo y valor), dónde se hace en ese panel, los errores que más vemos, cómo llegar a p=reject y lleva un formulario al auditor.
- Solo valores oficiales, con sus fuentes y la fecha de consulta en la página: la documentación de cada proveedor, y los
include: de SPF comprobados también en DNS público. Lo que es distinto en cada cliente (los CNAME de DKIM de Microsoft, la clave de Google) no se inventa: la guía dice de dónde copiarlo. Un test pasa cada SPF recomendado por nuestro propio auditor: ninguno sale con un error grave.
- La página de cada resultado (
/email/<dominio>/) enlaza la guía de su proveedor cuando el SPF lo delata (Microsoft 365, Google, Cloudflare o IONOS); si no, el índice. El auditor también enlaza las guías. Detalles que no son obvios: en Cloudflare, los CNAME de DKIM van sin proxy y sin aplanar, y Email Routing necesita un único SPF compartido; en Plesk, la plantilla ya publica p=quarantine con alineación estricta; con IONOS, hay un include europeo (_spf-eu) y otro de EE. UU.
- El contenido es texto, no HTML: se escapa entero y solo los tramos de código pasan a
<code>. Sitemap, smoke, enlaces y pa11y cubren las páginas nuevas.
- Verificación: +30 tests PHP (2256 → 2286); guards falsificados 13/13 (uno salió vacuo, porque el test usaba un TXT que la expresión ya descartaba, y se reforzó); smoke 267/267, enlaces internos sin rotos (1179) y auditoría delta sin bloqueantes nuevos; revisado en el navegador, también a 375 px.
✨ Novedad
Fase 11: boletín semanal
/boletin (ES/EN): cada lunes por correo, las vulnerabilidades que CISA añadió al catálogo KEV esa semana (todas, con severidad, CVSS y EPSS cuando están en el Threat Radar), lo más grave del radar si no llegan a cinco y lo nuevo del sitio (artículos y ediciones del observatorio). Texto plano, sin píxeles de seguimiento y con baja en un clic (RFC 8058). La portada enseña el archivo de números y lo que lleva la semana en curso.
- Cada número queda publicado en
/boletin/<AAAA>-s<SS> (indexable y en el sitemap) y no cambia: se genera una vez y la web y el correo salen del mismo fichero. Ninguna cifra se escribe a mano.
- Solo con datos al día:
scripts/send-boletin.php (solo CLI, los lunes) no genera un número si el catálogo KEV no se ha sincronizado después de que acabara la semana o si el radar tiene más de dos días; con datos parados diría «esta semana CISA no añadió nada». Envía a cada suscripción confirmada y anota cada envío en cuanto sale: si se corta, sigue donde lo dejó y nadie lo recibe dos veces.
- Altas con el mismo doble opt-in que «Vigila tu dominio» (revisado en la fase 10) y su propia tabla,
ce_news: enlace de un solo uso válido 48 h (en la BD, solo su SHA-256), el token pasa a la sesión y la URL se limpia, confirmar exige pulsar un botón, «¿No has sido tú?» cancela y bloquea la dirección 7 días, tope global por hora y la misma respuesta exista o no la suscripción.
- El formulario del blog lleva al boletín. Se retiran la integración con Brevo y el registro local del formulario antiguo, que además guardaba la IP (la política decía «solo tu correo»). Quien se apuntó a los avisos de artículos no recibe el boletín, porque consintió otra cosa. Política de privacidad actualizada (ES/EN), recuentos en el panel y entradas desde el Threat Radar, la portada y el blog.
- Verificación: +123 tests PHP (2133 → 2256, 29 de integración con la BD local dentro de una transacción que se deshace; JS sin cambios, 739); guards falsificados 27/27; smoke 259/259, enlaces internos sin rotos y auditoría delta sin bloqueantes nuevos. En local, el cron generó el número de la semana 38 con las 7 vulnerabilidades que CISA añadió del 14 al 20 de septiembre y no lo regenera al repetirse; flujos probados contra el servidor local (confirmación, baja, «no he sido yo», falta de cookie, baja en un clic y formulario del blog).
✨ Novedad
Fase 12: auditor de correo v2 (extras que no cuentan en la nota)
- El auditor (
/tool-email-check) y la página de cada resultado (/email/<dominio>/) muestran ahora MTA-STS, TLS-RPT, BIMI, DNSSEC y CAA: lo que protege el correo en tránsito (cifrado obligatorio e informes de fallos), la marca (logotipo verificado) y los propios registros (firma DNSSEC, qué autoridades pueden emitir certificados). No cambian la nota: la metodología sigue en 2026.2, y la insignia, el radar y el observatorio no se tocan.
- Solo DNS, como el resto del auditor: de MTA-STS se comprueba el registro
_mta-sts (una sola versión y un id válido), no la política que se publica por HTTPS, y la página lo dice. Las consultas van en el mismo lote que las demás, así que no alargan la auditoría.
- Cada extra dice qué hace y en qué estado está (publicado, no publicado, mal formado o varios registros), sin marcar en rojo lo que falta. Detalles que cuentan: BIMI avisa si el DMARC no está en quarantine al 100 % o en reject (los buzones no lo mostrarían) y si falta el certificado VMC; CAA sin etiqueta
issue no limita los certificados normales (RFC 8659).
- CAA se hereda del dominio superior más cercano que lo tenga (RFC 8659):
mail.google.com no publica CAA propio y sale «Heredado de google.com: pueden emitir pki.goog», no «cualquier autoridad puede emitir». Y si el resolvedor falla en una consulta de un extra, sale «No se pudo comprobar», nunca «No publicado». Los dos fallos se vieron al repasar la fase contra DNS real, antes de publicarla.
- Misma lógica y mismos textos en el navegador (referencia) y en el servidor, probados con las respuestas DNS crudas: 31 casos de extras en el fixture de paridad (fallos del resolvedor y herencia de CAA incluidos), que compara también lo que se muestra.
- Verificación: +73 tests PHP (2060 → 2133) y +4 JS (735 → 739); guards falsificados 23/23; smoke 256/256, enlaces internos sin rotos y auditoría delta sin bloqueantes nuevos; probado en el navegador (el auditor, que consulta a Cloudflare) y en
/email/<dominio>/ (el servidor, que consulta a Google), también a 375 px.
✨ Novedad
Fase 10: «Vigila tu dominio» y analizador de informes DMARC
/tool-dmarc-report (ES/EN): sube los informes DMARC agregados que llegan al buzón rua (XML, .gz o .zip, varios a la vez) y ve quién envía correo con tu dominio, cuánto pasa DMARC, qué servicio es cada fuente (por su firma DKIM o su SPF) y qué falta para p=reject. Todo ocurre en el navegador: el fichero no se sube y no se hace ninguna consulta de red, ni siquiera de las IP. Parser XML propio que rechaza DOCTYPE y no expande entidades externas; descompresión con límites que cortan a mitad de flujo (anti-bomba) y sin fiarse del tamaño declarado.
/vigila (ES/EN): un aviso por correo si cambia la protección de un dominio (la nota del auditor, la política DMARC o el all del SPF), con una comprobación semanal (scripts/sync-vigilancia.php, solo CLI; necesita una tarea semanal en Plesk). La primera pasada solo fija la referencia, y si una consulta falla no se avisa ni se toca.
- Doble opt-in y baja: el enlace de confirmación es de un solo uso, caduca a las 48 h y en la BD solo queda su SHA-256. Al abrir cualquier enlace del correo, el token pasa a la sesión y la URL se limpia: nunca queda en una página que la analítica o el
Referer puedan ver. Confirmar o darse de baja exige pulsar un botón (los escáneres de enlaces de los gestores de correo no confirman solos), y cada aviso trae baja en un clic desde el cliente de correo (RFC 8058). Hasta 3 dominios por dirección, tope global de confirmaciones por hora y la misma respuesta exista o no la suscripción.
- RGPD: solo correo, dominio, idioma, fechas y tokens; sin IP. La baja borra la fila (y la foto del dominio si nadie más lo vigila); sin confirmar, se borra a los 7 días del último correo de confirmación, y una solicitud cancelada con «¿No has sido tú?», a los 7 días de cancelarla. Política de privacidad actualizada en los dos idiomas. Tablas nuevas en
db/schema.sql, que la librería también crea si faltan.
- Entradas desde el auditor, la página de cada resultado (
/email/<dominio>/, con el dominio ya puesto) y el analizador. El panel enseña solo recuentos; los eventos de alta y confirmación se registran desde el servidor y el endpoint público no los acepta.
- Revisión separada (un revisor Fable sin el contexto de la implementación). Corregido lo que encontró: cualquiera podía hacer que el sitio mandara confirmaciones repetidas a una dirección ajena; ahora no se reenvía mientras el enlace vale (48 h), el correo de confirmación trae «¿No has sido tú?», que cancela la solicitud e impide volver a pedirla para esa dirección, y las canceladas cuentan para el límite de 3 dominios. La limpieza de pendientes cuenta desde el último correo, no desde el alta; el cron guarda la foto nueva solo si se han enviado todos los avisos (si falla un envío, vuelve a avisar la semana siguiente); las tablas se comprueban con una consulta y solo se crean si faltan (así funciona también con un usuario de BD sin permiso CREATE); los asuntos largos se parten en palabras codificadas de 75 caracteres como mucho (RFC 2047); el correo sale con el remitente del sitio en el sobre (
-f), para que el SPF alinee; si se llega al tope por hora, la página lo dice en vez de responder «revisa tu correo» sin mandar nada, y si el navegador no guardó la cookie de sesión lo explica en vez de dar el enlace por inválido. En el analizador, el límite total se comparte entre todos los ficheros y todas las entradas de un ZIP (20 entradas que apuntan a los mismos datos comprimidos ya no lo multiplican) y una fecha absurda en un informe ya no lo rompe.
- Verificación: +103 tests PHP (1957 → 2060, 34 de integración contra la BD local dentro de una transacción que se deshace) y +19 JS (716 → 735); guards falsificados 25/25 y, tras la revisión, 21/21 más (uno salió vacuo y destapó que ningún test comprobaba que el hash se borra al confirmar: añadido; otros tres se reforzaron antes de falsificar); smoke 256/256, enlaces internos sin rotos y auditoría delta sin bloqueantes nuevos; probado en navegador el flujo completo (alta, confirmación, baja), contra el servidor local «no he sido yo», la falta de cookie y el tope por hora, y el cron contra DNS real.
✨ Novedad
Fase 9: Observatorio de suplantación por correo
/observatorio (ES/EN, indexable): la protección contra la suplantación por correo de las grandes cotizadas europeas, índice a índice. En la primera edición (DNS del 23 y el 24 de septiembre de 2026), 47 de 226 empresas de los índices IBEX 35, Euro Stoxx 50, FTSE 100, DAX 40 y CAC 40 no tienen una política DMARC que impida usar el dominio de su web corporativa para enviar correo falso: 11 de 35 en el IBEX 35, 3 de 50 en el Euro Stoxx 50, 23 de 100 en el FTSE 100, 7 de 40 en el DAX 40 y 7 de 40 en el CAC 40. Una empresa que está en varios índices cuenta una vez en el total, con su medición más reciente.
- Un informe por índice (
/observatorio/<índice>/; el del IBEX 35 conserva /informe-ibex35) con la estructura del informe del IBEX 35: agregado, buscador por empresa (sin ranking), cifras, metodología, límites, bloque para medios con su cita y su tarjeta, y descargas (JSON y CSV). Sección nueva de evolución, que se llena edición a edición. La portada compara porcentajes entre índices, nunca empresas.
- Serie mensual:
scripts/sync-observatorio.php (solo CLI; sustituye a build-ibex35-report.php) genera cada mes la edición de cada índice en assets/data/informes-vivo/ (fuera de git) y nunca reescribe una publicada. Salta un índice entero, sin publicar nada, si un dominio no responde tras reintentar, si no se puede resolver el destino de un redirect= o si la composición de su muestra se comprobó hace más de 100 días (los índices se revisan cada trimestre). Una ejecución a la vez, la instantánea se escribe la última y, si faltan descargas de una edición, se reponen desde ella. Necesita una tarea de cron mensual en Plesk (día 1, a partir de las 02:00).
- Qué empresas y qué dominio: la composición de cada índice parte de su tabla en Wikipedia (revisión citada), corregida con los anuncios del proveedor que la tabla aún no recogía: en el Euro Stoxx 50 entran Engie y Nokia y salen Volkswagen y Wolters Kluwer (21-09-2026); en el DAX, Hochtief por Porsche SE (22-06-2026); en el CAC 40, Euronext por Teleperformance y Eiffage por Edenred (septiembre y diciembre de 2025). La metodología de cada informe lo explica con su fuente. El dominio es el de la web corporativa (la del grupo y sus inversores, no la comercial): el sitio oficial en Wikidata contrastado con la ficha de Wikipedia y, donde discrepan o el grupo tiene web propia, su web de inversores, citada en el CSV; el contraste corrigió 12 dominios (webs comerciales como ig.com o adidas.com y webs antiguas tras un cambio de nombre, como Teleperformance → tp.com).
- Revisión separada (un revisor Opus sin el contexto de la implementación): C2, C3, C5, C7, C8, C9 y C10 resisten (las cinco semillas se reproducen, la deduplicación es correcta, las rutas y el escapado también). Corregido lo que encontró: las composiciones desfasadas de arriba (el total pasa de 49 a 47), la tarjeta decía «sin política DMARC» cuando la mayoría publica
p=none (ahora «sin una política DMARC que lo impida»), las tarjetas no se regeneraban con las ediciones del cron (ahora llevan la edición en el nombre y solo se enseña la de la edición que muestra la página), el buscador decía «IBEX 35» en todos los índices, «grandes empresas europeas» incluía cotizadas que operan fuera (ahora «grandes cotizadas europeas»), meses mezclados en la cita, tres tickers y la robustez del cron.
lib/observatorio.php generaliza la lógica del informe del IBEX 35 (las funciones ce_ibex_* quedan como capa fina) y lib/observatorio-render.php, la presentación. La portada del sitio y el auditor enlazan al observatorio; JSON-LD CollectionPage con un Dataset por índice; sitemap, humo, enlaces y pa11y cubren las páginas nuevas. La consulta del auditor cuenta ahora los selectores DKIM sin respuesta (dkim_errors), sin cambiar la nota.
- Verificación: +168 tests PHP (1789 → 1957) y +1 JS (716); guards falsificados 27/27; smoke 254/254; enlaces internos sin rotos; auditoría delta sin bloqueantes nuevos; revisado en navegador en ES, EN y a 375 px.
✨ Novedad
Fase 8: bloque «Para medios» en el informe del IBEX 35
- Nueva sección en
/informe-ibex35 (ES/EN) para quien vaya a citarlo: una cita fiel con la cifra, la fuente y la fecha de los datos, la tarjeta del informe para descargar, las cifras (JSON) y los dominios con sus fuentes (CSV), y el contacto para entrevistas. Es la pieza pública del kit de lanzamiento; el resto (nota de prensa, textos para LinkedIn, Hacker News y Reddit, lista de medios) es material local para la difusión.
- La cita sale de la instantánea (
ce_ibex_citation()) con la misma frase que el titular: si un medio la copia tal cual, no exagera. Un test comprueba que la frase de la cita y la del titular coinciden. +5 tests PHP (1784 → 1789), guards falsificados 2/2.
✨ Novedad
Fase 7: informe «¿Cuántas empresas del IBEX 35 se pueden suplantar por correo?»
/informe-ibex35 (ES/EN, indexable): la protección contra la suplantación por correo de las 35 empresas del IBEX 35, con el motor del auditor (metodología 2026.2) y solo DNS público. En la primera edición (DNS del 23 de septiembre de 2026), 11 de 35 no tienen una política DMARC que impida usar el dominio de su web corporativa para enviar correo falso; del resto, 20 lo rechazan y 4 lo mandan a spam. Ninguna cifra de la página, la portada, la tarjeta para redes o el JSON-LD se escribe a mano: todas salen de la instantánea.
- Agregado + buscador, sin ranking: barras por protección, política DMARC y nota, e indicadores (SPF estricto, informes
rua, DKIM detectado, dominios sin MX). El buscador (nombre, ticker o dominio) enseña los registros de cada empresa tal cual y enlaza a su auditoría en vivo. Ninguna tabla ordena empresas por nota.
- Reproducible:
scripts/build-ibex35-report.php (solo CLI) guarda una instantánea fechada con las entradas crudas del motor, y un test comprueba en las 35 que el motor, aplicado a esos registros, da la nota guardada. Descargas: dominios con su fuente (CSV, sin notas) y cifras (JSON). Una edición nueva será otro fichero: el informe no cambia de números en silencio.
- Qué dominio: el de la web corporativa, la de accionistas (en las cotizadas españolas, la que exige el art. 11 bis de la Ley de Sociedades de Capital; ArcelorMittal y Ferrovial tienen sede fuera). La regla se fijó antes de consultar el DNS, y nunca nos conectamos a los servidores de las empresas: las consultas van a Google Public DNS. La CNMV no publica la web en su registro, así que la fuente es Wikidata contrastada con un buscador; donde discrepaban (Logista, CaixaBank, Sabadell, Unicaja; en Indra, Wikidata daba dos webs), manda la web de accionistas. Los límites de la página lo dicen: en banca, la web corporativa no es la comercial. Sabadell y Unicaja entran en las 11 por un dominio corporativo que no usan para correo (sin MX ni SPF); sus fichas lo explican.
- Revisión separada (un revisor Opus sin el contexto de la implementación): la afirmación central resiste —el reparto 11/0/4/20 se reproduce, los cubos siguen RFC 7489, los datos coinciden hoy en las 35 y ningún flujo se rompe—, y sus hallazgos están corregidos abajo: un falso positivo del motor con
redirect=, el minificador de CSS y cinco matices de texto.
- Tarjeta para redes (
og:image, ES/EN) generada desde el dataset, JSON-LD Article + Dataset (hook $pageGraph nuevo en el header), enlaces desde el auditor y la portada, y la página entra en pa11y en los dos idiomas.
ce_email_lookup devuelve ahora sus entradas (inputs: SPF, DMARC con su procedencia, DKIM y el destino de redirect=); es lo que hace reproducible la instantánea.
- Verificación: +185 tests PHP y +17 JS (1599 → 1784, 698 → 715); guards falsificados 21/21 (una mutación salió vacua y destapó una comprobación redundante, retirada); smoke 247/247, enlaces internos sin rotos, auditoría delta sin bloqueantes nuevos; revisado en navegador en ES, EN y móvil.
✨ Novedad
Fase 6: el auditor de correo como herramienta estrella
- Resultado compartible en
/email/<dominio>: la misma auditoría que el auditor, renderizada en el servidor y con URL propia — lo que se reenvía al equipo de IT («mira, nos sale una F»). Botón «Compartir este resultado» en el auditor, enlace para copiar e insignia de correo /badge/email/<dominio>.svg con su código Markdown y HTML. Se comparte por enlace pero no se indexa (noindex en meta y cabecera HTTP): así el resultado de una empresa no aparece en Google al buscar su marca, y miles de URLs generadas por usuarios no pasan por páginas puerta. Si se indican selectores DKIM (?s=), la página, la insignia y el código para copiar usan los mismos: no pueden dar notas distintas. Ni la página ni la insignia entran en el sitemap (/badge, que es un SVG, ya se colaba en el de producción).
- Una sola metodología en todo el sitio (versión
2026.1, guardada en cada resultado). El auditor y el Radar de Postura puntuaban el correo con fórmulas y escalas distintas: medido con el código real, el mismo dominio sacaba A en el radar y C en el auditor. Ahora hay un motor único (lib/email-audit.php, port de assets/js/email-check.js) que usan la página, la insignia y el radar, y unos fixtures de paridad (tests/fixtures/email-parity.json, generados desde el JS) obligan a que JS y PHP den exactamente lo mismo, textos incluidos. Comprobado con un dominio real: el navegador y el servidor dan la misma nota.
- Tope por protección real: la nota no puede ser mejor que la protección contra suplantación de la política DMARC que de verdad se aplica. Sin DMARC, o con dos registros (los receptores no aplican ninguno), F;
p=none —o quarantine con pct=0, que no pone nada en cuarentena—, como mucho D; quarantine, B. Antes un dominio sin DMARC —suplantable— sacaba una C.
- DKIM no detectado ya no puntúa como inexistente: sin conocer el selector, DKIM no se descubre por DNS. google.com, con
p=reject, salía con C solo porque usa selectores propios; ahora la nota sale de SPF y DMARC y queda en A. Si DKIM se detecta y está mal, sí resta.
- Sección pública «Cómo puntuamos» en el auditor, generada desde el propio motor (pesos, escala y tabla de topes): no puede quedarse desfasada del código.
- Caché en fichero (6 h, versionada por metodología, sin tablas nuevas: no hay paso de base de datos en producción). El límite de uso solo se gasta en consultas nuevas. Las peticiones salientes van al resolvedor DoH, nunca al dominio auditado. El contador agrega
/email/* en una ruta: no guarda qué dominios audita la gente. La caché se limpia sola (resultados de más de 8 días y temporales huérfanos).
✨ Novedad
Sección «Productos propios» en la portada y en el menú
- Los dos productos (Secure AI Kit y HandMetric) ya estaban en la línea temporal de
/sobre-mi, pero merecían un sitio propio: nueva sección en la portada, entre el blog y «Quién lo construye», con dos tarjetas que enlazan directamente a cada web (pestaña nueva, noopener). Reutiliza el patrón featured-card de los laboratorios.
- Cada tarjeta lleva su estado honesto: Secure AI Kit «Lista de espera» (la landing capta inscripciones; el módulo y los ataques están publicados), HandMetric «Próximamente» en ámbar. Las descripciones salen de las landings de cada producto, no inventadas.
- Menú «Sobre Mí»: las dos webs al final del desplegable, separadas por una línea de las páginas del sitio y marcadas con ↗. Fuera del catálogo a propósito: no son páginas de aquí y no deben entrar en sitemap ni buscador.
- Verificado en local: tarjetas con
target/rel correctos, orden de secciones, separador del menú aplicado. style.min.css regenerado.
✨ Novedad
Medición del CTA «Hablemos»: la conversión que el tráfico no dice
- Los datos decían que la gente llega, usa las herramientas y se va. Lo que no decían es si alguien pulsa la oferta de ayuda que aparece tras una auditoría con hallazgos serios: el contador descarta la query, así que
/contacto?src=email-check contaba como una visita más a /contacto.
- Dos eventos:
cta_shown (una auditoría encontró algo serio y ofreció ayuda) y cta_click (alguien pulsó «Hablemos»). Juntos dan la conversión por herramienta. Cada apply() que muestra el bloque es una impresión: las herramientas lo llaman una vez por resultado, así que «impresión» = «auditoría que cualificó». Ocultarlo no cuenta nada.
- Tabla aparte
ce_events (día, evento, etiqueta, n), mismo modelo de privacidad que el contador: agregados, sin cookie ni nada por visitante. Aparte porque un clic no es una página vista, y mezclarlos habría exigido filtrar eventos en seis consultas de tráfico. La lista de eventos es cerrada (CE_EVENTS): el endpoint es público y sin sesión, y un nombre libre permitiría llenar la tabla de basura. La etiqueta se valida por forma; los bots no cuentan.
- El envío va con
sendBeacon, que sobrevive a la navegación: el clic en «Hablemos» se va de la página y aun así se registra. hit.js gana CeHit.track(); help-cta.js lo llama al mostrar y al pulsar, comprobando que esté cargado en el momento de usarlo (va en el footer, después).
- Panel: bloque «CTA Hablemos» con mostrado, clics y conversión, total y por herramienta, con el aviso de que con pocas impresiones la tasa engaña. Si falta la tabla, lo dice y explica dónde crearla — la lección de
ce_hits.
- Verificado de punta a punta en local: endpoint (204 sin cookie para eventos válidos, 400 para inventados o con HTML, bots descartados) y DOM real (impresión al mostrar, clic al pulsar, nada al ocultar, impresión nueva con otra auditoría). +28 tests PHP y +6 JS (1131 → 1159, 631 → 637); guards falsificados 5/5.
- Requiere crear
ce_events en producción (bloque en db/schema.sql, misma base que ce_hits). Hasta entonces los eventos se descartan en silencio y el panel lo avisa.
✨ Novedad
HandMetric en «Proyectos Propios», marcado como próximamente
- Segundo proyecto propio en
/sobre-mi: HandMetric (handmetric.com), que mide lo que una empresa hace a mano y qué automatizar primero. La descripción es la de su propia landing, no inventada.
- Va después de Secure AI Kit y con «Próximamente» en ámbar (fecha y punto de la línea temporal) en vez de una fecha: primero lo que ya está vivo, después lo que viene, y que se distinga de un vistazo. ES y EN.
✨ Novedad
Enlace a Secure AI Kit, el otro proyecto
- Secure AI Kit (secureaikit.com), el kit de arranque para aplicaciones con IA, no estaba enlazado desde la web personal. Ahora sí, en dos sitios elegidos por intención del lector, no por visibilidad:
- Artículo de inyección de prompts: un callout justo tras el checklist final, en ES y EN. Quien acaba de leer «trata la entrada como datos, aísla las instrucciones» es exactamente a quien le sirve un kit que ya lo implementa. Sigue el patrón de callouts que el artículo ya usa.
/sobre-mi: nueva sección «Proyectos Propios» con el patrón timeline existente, entre certificaciones e historial laboral — lo que construye ahora, antes de dónde trabajó.
- Descartado
/proyectos a propósito: es la página de manuales y prácticas, sus tarjetas solo admiten enlaces internos, y meter un producto real entre tutoriales lo infravalora.
- Enlaces externos con
rel="noopener noreferrer", la convención del sitio. Verificado en local en ambos idiomas.
✨ Novedad
El panel compara periodos, y el gráfico deja de mentir
- La sección «Visitas» daba totales sueltos: útiles, pero sin responder a la pregunta de siempre, ¿voy a más o a menos? Ahora la tarjeta de visitas lleva la variación frente al periodo anterior (misma longitud de ventana, sin solaparse) y hay una tarjeta de visitas de hoy.
- Con el periodo anterior vacío no se inventa un porcentaje: escribe «sin comparación». No existe el aumento porcentual sobre cero, y pintar «+100%» sería una cifra falsa justo en las primeras semanas, que es cuando más se mira.
- El gráfico corregido: repartía el ancho entre los días que tenían filas, así que 2 días de datos ocupaban los 30 días enteros y parecía que hubo tráfico todo el mes. Ahora hay una barra por día del periodo, los días sin visitas se ven como el hueco que son, y el eje muestra las fechas de inicio y fin. El día de hoy va resaltado.
- Lo que no se puede añadir, y conviene tener claro: visitantes únicos, sesiones, rebote, países o navegador. Todo eso exige identificar a cada visitante (cookie, huella o IP) y tumbaría la base legal que permite medir el 100% sin consentimiento — que es justo lo que hace valioso este contador frente a Google Analytics.
- +18 tests PHP (1078 → 1096); las dos funciones nuevas son puras y se prueban sin BD. Guards falsificados 4/4 en rojo por el motivo correcto. Sin bloqueantes nuevos.
✨ Novedad
Enlace de vuelta al sitio desde el panel
- El panel es autónomo a propósito (sin la navegación del sitio), pero eso dejaba sin salida: para volver a la web había que editar la URL a mano. Ahora la barra superior lleva «← Ir al sitio» junto a «Cerrar sesión».
- El estilo pasa de la clase concreta
.logout al selector .topbar a, así el enlace nuevo lo hereda sin duplicar reglas y el siguiente que se añada también.
✨ Novedad
Las credenciales de la BD pueden ir en secrets.php, fuera del web root
- Hasta ahora las credenciales de base de datos solo podían vivir en
db-config.php, dentro de httpdocs/. Ahí lo único que impide servirlas como texto plano es que el servidor ejecute el PHP en vez de entregarlo; si esa configuración falla algún día, quedan expuestas. secrets.php vive fuera del web root (private/ en Plesk), donde eso no puede ocurrir.
- Ahora
ce_db() acepta las dos fuentes: primero db-config.php (sin cambios para nada que ya funcione, incluido el XAMPP local) y, si no está, las claves db_host / db_user / db_pass / db_name de secrets.php. Un db-config.php copiado a medias (sin name) no cuenta como configurado, para que no gane sobre unos secretos que sí están puestos.
- El detalle que lo hacía delicado:
api/hit.php no carga bootstrap.php a propósito, porque bootstrap llama a session_start() y una cookie ahí invalidaría la base legal de contar visitas sin consentimiento. Por eso el lector de secretos se ha extraído a lib/secret-reader.php, sin efectos secundarios, en vez de arrastrar bootstrap. Verificado: el endpoint sigue devolviendo 204 sin Set-Cookie y registrando.
- No puede llamarse
lib/secrets.php: el .gitignore ignora ese nombre en cualquier nivel, así que no se habría versionado y producción habría recibido un require de un fichero inexistente — error fatal en todas las páginas. Queda anotado en el propio fichero.
- El diagnóstico del panel ya menciona las dos opciones cuando no hay conexión. +10 tests PHP (1068 → 1078), los 4 guards nuevos falsificados 4/4 en rojo por el motivo correcto. Sin bloqueantes nuevos.
✨ Novedad
Oferta de ayuda tras una auditoría (solo cuando hay algo que arreglar)
- Las herramientas que auditan algo real —correo, configuración, WordPress— daban un veredicto y ahí se acababa: quien descubría que su dominio es suplantable no tenía ningún camino hacia «puedo arreglártelo». Ahora, al final del resultado, aparece un bloque sobrio ofreciendo revisarlo.
- Regla número uno: solo aparece si se ha encontrado algo serio (un problema grave, o dos moderados). Con un resultado limpio no se ofrece nada — sería spam y, sobre todo, restaría credibilidad a un sitio cuya función principal es profesional. La decisión vive en un único sitio (
assets/js/help-cta.js), no repartida por cada herramienta, y está cubierta por tests que fijan justo eso: *resultado limpio → no se ofrece nada*.
- El bloque nace
hidden en el HTML, así que sin JavaScript o con la auditoría limpia no se ve. Enganchado en tool-email-check, tool-config-audit y tool-wp-check, cada una avisando con sus propios hallazgos (se aceptan los dos vocabularios de severidad del proyecto: high/med y crit/warn).
- El enlace lleva el contexto (
/contacto?src=…) para saber de qué auditoría viene quien escribe, y el formulario pre-rellena el asunto desde una lista blanca: nunca entra texto de la URL en el formulario, y no viaja el dominio auditado ni ningún dato de la herramienta — solo cuál era. Verificado: ?src=<script>… y rutas de recorrido dejan el asunto vacío.
- Verificado en navegador: config con fallos → CTA visible; config correcta → oculto, y se oculta solo al repetir la consulta. +7 tests JS y +17 PHP (PHP 1044 → 1068, JS 624 → 631); auditoría delta sin bloqueantes nuevos.
style.min.css regenerado.
✨ Novedad
Contador de visitas propio, sin cookies y sin datos personales
- Google Analytics solo mide a quien acepta el banner; con una audiencia técnica —la que más rechaza cookies y más bloqueadores usa— eso deja fuera a la mayoría y las decisiones se toman sobre una cifra falseada a la baja. Este contador mide el 100% del tráfico.
- Puede hacerlo sin consentimiento porque no registra eventos: la tabla
ce_hits son contadores agregados por (día, ruta, origen), con PRIMARY KEY en esos tres campos. No hay una fila por visita, ni IP, ni user-agent, ni huella, ni cookie — no hay nada reidentificable por construcción, no por promesa. La query y el fragmento de la URL se descartan a propósito (ahí viaja entrada del usuario), y del referrer solo se guarda el host, nunca su ruta.
- Piezas:
lib/stats.php (normalización pura y testeable + lectura agregada), api/hit.php (POST mismo origen, sin sesión: ce_rate_ok() habría puesto cookie y habría invalidado justo la base legal), assets/js/hit.js (aviso con sendBeacon, no cuenta en local ni con Do Not Track) y una sección de visitas en el panel privado ya existente (/panel), reutilizando su login en vez de añadir una segunda puerta con token en la URL.
- Verificado de extremo a extremo: el endpoint responde 204 sin
Set-Cookie, registra ruta y origen, incrementa en la siguiente visita, y rechaza bots (204 pero sin contar), rutas maliciosas (400) y origen cruzado (403). La lista blanca de columnas impide inyección SQL en los agrupados. Sin la tabla, el sitio no se rompe: deja de contar y ya. +38 tests PHP y +5 JS (PHP 1006 → 1044, JS 619 → 624); auditoría delta sin bloqueantes nuevos.
- Requiere un paso manual en producción: crear la tabla con el bloque
ce_hits de db/schema.sql. Hasta entonces el contador no registra (sin error).
✨ Novedad
Expedientes técnicos reproducibles (/expedientes) — Fase 5
- Nueva página
/expedientes: la investigación completa de un caso sintético, de la evidencia a la decisión y a la detección medida. Cierra el arco de las fases 1–4: donde /proof prueba qué se ha construido, los expedientes enseñan cómo se razona — evidencia citada, alternativas descartadas, decisión con su confianza, la detección con sus números y qué NO demuestra el expediente.
- Reproducible de verdad: cada cita apunta a
{conjunto de datos, versión, registro} de los datasets sintéticos de las fases 1–3, y cada ficha enlaza al laboratorio con esos mismos datos. Dos expedientes: *regla de reenvío tras un acceso sospechoso* (identidad M365 → expediente → banco) y *PowerShell codificado y tarea programada* (banco → ATT&CK).
- La honestidad está atornillada con tests, no prometida: un guard re-deriva las métricas con el motor de detección real y falla si un expediente declara números que el banco no produce; otro comprueba que cada cita resuelve contra su fixture y que cada enlace de reproducción existe en disco. Falsificación canónica 3/3 en rojo por la razón correcta, restauración byte-exacta.
- Cada ficha tiene ruta propia (
/expedientes/<slug>), como el resto de fichas indexables del sitio (/attack/Txxxx, /threat-radar/CVE-…): así el conmutador ES/EN conserva el expediente que estabas leyendo, el canonical/og:url apuntan a la ficha —y no al índice— cuando se comparte el enlace, y las fichas entran en el sitemap. Una ficha inexistente responde 404 y lo explica, en vez de mostrar el índice como si el enlace hubiera funcionado.
- SSR completo (la página es útil sin JavaScript; el JS solo filtra el índice), bilingüe ES/EN, integrado en el catálogo,
/proof (banda «Y así es como razono»), /recruiter, la tira «✨ Nuevas» y .pa11yci.js. Suites PHP 933 → 1004 y JS 593 → 619; auditoría delta contra la fase 4: cero bloqueantes nuevos.
✨ Novedad
Recruiter Challenge (/recruiter-challenge): «Test My Skills» interactivo
- Nueva página
/recruiter-challenge: un tour guiado de ~5 min para quien selecciona. Convierte «confía en mi CV» en «experimenta mi trabajo»: el recruiter —aunque no sea técnico— toma 3 decisiones SOC reales y accesibles (¿es phishing? · ¿ese powershell -enc es peligroso? · ¿qué CVE parcheas primero?), y en cada revelación ve el razonamiento correcto y descubre que Sergio construyó la herramienta/lab que lo resuelve (triage de correo, decodificador de PowerShell, Threat Radar). Termina en el resultado con métricas reales y CTAs al Passport, Proof, CV, LinkedIn y contacto.
- Sin cuentas ni backend: secuenciador en JS (
assets/js/recruiter-challenge.js, lógica pura dual-mode + delegación, sin handlers inline; progreso por pasos y reveal por decisión). Enlazado desde /recruiter (CTA «¿Prefieres vivirlo?»), en el catálogo (Perfil) y primero en «✨ Nuevas».
- Verificado: flujo completo (intro → 3 decisiones → resultado), marcado correcto/incorrecto por opción, dots de progreso, ES/EN, 0 errores de consola. +3 tests JS (
node --test) y +11 tests PHP (PHP 748 → 759).
✨ Novedad
Incidente de la semana: 3 casos nuevos + programación semanal
- +3 incidentes (dominios distintos, con su lección de triage): ransomware en curso (vssadmin borra shadows → *aquí SÍ se contiene primero*, contraste con los demás), exploit web tipo Log4Shell (
${jndi:...} → confirmar callback antes de remediar) y exfiltración de insider (preservar evidencia + Legal/RRHH antes de deshabilitar). Cada uno con feedback por opción, MITRE, evidencia y detección; bilingüe. Con esto el archivo cubre 6 casos: OAuth, AiTM, PowerShell -enc, ransomware, web-exploit e insider.
- Programación semanal por fecha: el «incidente de esta semana» pasa a ser el más reciente ya publicado (
ce_iotw_current() mira la fecha en Europe/Madrid), y los de fecha futura quedan en cola —accesibles por ?w= para previsualizar/compartir, pero fuera del archivo público hasta su día—. Así hay munición encolada para semanas y la serie avanza sola. El archivo de la página lista solo publicados. +8 tests (PHP 742 → 748). Verificado: actual = #3 hoy, futuros ocultos pero jugables por slug, mecánica OK.
✨ Novedad
CyberEscudo Passport (/u/sergio): la tarjeta pública compartible
- Nueva página
/u/sergio: el «pasaporte» de habilidades —identidad, métricas reales (65 herramientas · 14 labs · 22 técnicas ATT&CK · 26 artículos), skills y certificaciones (eCPPT, eJPT)— pensado para compartir: una URL para el «Featured» de LinkedIn, la firma o el CV, con imagen OG 1200×630 (assets/img/og-passport-sergio.png, misma marca que las cards de LinkedIn) para que la preview del enlace luzca. twitter:card = summary_large_image.
- Honesto por diseño: NO hay scores de habilidad autoemitidos (nada de «SOC 92/100»); cada número es real, derivado del catálogo y enlaza a su prueba. El badge «Verificado por evidencia» aclara que no es una certificación oficial y lleva a
/proof. Es la respuesta a la trampa de credibilidad de las plataformas de auto-badges: la prueba fuerte son los artefactos, no un número que te pones tú.
- Enrutado en
/u/ (funciona con el fallback de .htaccess y el router de dev; rutas con __DIR__ . '/../'). En el catálogo (Perfil), enlazado desde /recruiter, y cubierto por el smoke-test (carpeta u añadida). +11 tests (tests/php/passport.test.php, PHP 731 → 742). Verificado: métricas reales enlazadas, OG servida (200), ES/EN, 0 errores.
✨ Novedad
Incidente de la semana: motor de contenido «What would you do?»
- Nueva página
/incidente-de-la-semana: un escenario SOC realista con una pregunta de decisión («¿qué investigas primero?»); el visitante vota y se revela el razonamiento, la evidencia dónde mirar, MITRE ATT&CK y cómo detectarlo, además de las herramientas del sitio para practicarlo. Auto-evaluación honesta (marca tu opción y la correcta, con feedback por opción) y motor de contenido para LinkedIn: botón «copiar para LinkedIn» con el reto ya formateado y el enlace a la resolución.
- Arranca con 3 incidentes reales y razonados: persistencia por consentimiento OAuth (password spray → app OAuth; la trampa: resetear la contraseña no revoca el token), robo de sesión AiTM (MFA aprobado + viaje imposible), y
powershell -enc en tarea programada (decodificar primero para confirmar y sacar IOCs). El más reciente es el «actual»; el resto, archivo navegable con ?w=slug (URL propia para compartir cada semana).
- Sin cuentas ni backend: la mecánica es cliente (
assets/js/incident-week.js, lógica pura dual-mode + localStorage recuerda tu elección), la respuesta correcta vive en el DOM (es auto-evaluación, no examen), y la resolución va en un <details> (accesible sin JS; el JS lo auto-abre al votar). Sin handlers inline (delegación). En el catálogo (Aprender) y primero en «✨ Nuevas».
- Verificado: voto correcto/incorrecto marca opciones y abre la resolución, MITRE + práctica enlazan,
?w= y ES/EN OK, 0 errores de consola. +4 tests JS (node --test) y +16 tests PHP (tests/php/incidente-semana.test.php; integridad de datos, accesores y guards). *(Nota: los flagships TLS/OAuth rotaron fuera de la tira «Nuevas» al entrar piezas más recientes; sus guards temporales se ajustan.)*
✨ Novedad
Aprende por temas: hubs de contenido (Fase 4)
- Nueva página
/aprende: agrupa el contenido —hasta ahora plano— en clusters temáticos (Phishing y correo · Detection Engineering · Vulnerabilidades & Threat Intel · Fundamentos «cómo funciona»), y en cada uno conecta las tres formas de aprender: Usa (herramientas), Practica (labs/flagships) y Lee (artículos). Llena un hueco que ni el árbol de habilidades (progresión) ni /proof (evidencia por skill) ni /tools (lista) cubrían: el hilo *leer ↔ usar ↔ practicar* por tema. Bueno para SEO (autoridad temática) y como URL para compartir.
- DRY: se resuelve del catálogo (
ce_item/ce_url/ce_title) y del blog (ce_blog_posts, referencias post:slug); un item que no exista se omite (nada que mantener a mano). En el catálogo (Aprender) y con +14 tests (tests/php/aprende.test.php, PHP 704 → 718). Verificado: 4 clusters, 22 items, 0 enlaces rotos, bilingüe.
- Nota honesta: la otra mitad de la Fase 4 del plan («cadencia de LinkedIn») es un hábito, no código; el sitio ya genera la munición (Incidente del día, Security Pulse, este hub).
✨ Novedad
Security Pulse: cada CVE cruza a ATT&CK y al arsenal (Fase 3, parte 2)
- El análisis de cada CVE del Threat Radar cierra ahora el círculo Threat Intel ↔ ATT&CK ↔ arsenal: bajo «Cómo defenderte» aparece la técnica MITRE ATT&CK con la que se materializa el fallo y las herramientas del sitio para practicarla/detectarla. Ej.: una inyección de comandos (CWE-78) → *T1190 Exploit Public-Facing Application* + *T1059 Command and Scripting Interpreter* → WAF Bypass · Decodificador PowerShell · Reverse Shells. Aparece en el radar y en la ficha por CVE (una sola fuente de render).
- Puente determinista y conservador
ce_cve_attack_tids() (familia de debilidad CWE → técnica ATT&CK; solo mapeos claros que además cubre el arsenal — los huecos son honestos) + forward-lookup ce_covmap_tools_for_tid() en lib/attack-map.php (técnica → herramientas, empareja padre y sub-técnicas). El render reutiliza lib/attack-map.php (mismo mapa que el cross-link inverso del footer). Sin IA ni red.
- Verificado en vivo (CVE-2026-8037): técnicas y 3 herramientas correctas, 0 errores de consola, bilingüe. +19 tests (
tests/php/security-pulse.test.php, PHP 685 → 704).
✨ Novedad
Cross-link ATT&CK bidireccional: cada herramienta enseña su técnica (Fase 3)
- Antes,
/cobertura-attack mapeaba técnica → herramienta (un solo sentido). Ahora se cierra el bucle: cada página de herramienta mapeada muestra un badge «🗺️ Ejercita en ATT&CK: Txxxx» que enlaza al mapa de cobertura. Un analista (o quien selecciona) ve, herramienta por herramienta, a qué técnica MITRE corresponde — señal de fluidez en ATT&CK en todo el arsenal.
- Implementado DRY y sin tocar 65 ficheros: se extrae el mapa a
lib/attack-map.php como fuente única (ce_covmap_tactics()), con índice inverso ce_covmap_for_url() / ce_covmap_for_page() (deduplica por técnica). cobertura-attack.php pasa a consumir el lib (salida idéntica: 14 tácticas, 25 técnicas, 24 herramientas) y el footer pinta el badge solo cuando la página actual está mapeada (~18 tools). Nombres ce_covmap_* para no colisionar con lib/attack.php (la capa de datos MITRE completa).
- Verificado: badge correcto en
/tool-ps-decode (T1059.001 + T1027), ausente donde no toca (home, /attack, /cobertura-attack), y /attack (que carga lib/attack.php) sin colisión ni errores. +11 tests (tests/php/attack-map.test.php, PHP 674 → 685).
✨ Novedad
Incidente del día: un reto de SOC nuevo cada día (Fase 2)
- Nueva pieza «Incidente del día» que reencuadra *El Turno* como contenido vivo y da motivo para volver: cada día, un incidente distinto —el mismo para todo el mundo— generado por el motor procedural existente. Reutiliza el generador determinista por semilla del range (no se reconstruye nada): la semilla es la fecha (
Ymd) y la plantilla rota por CRC entre las 4 familias, en ce_range_iotd() (puro y testeable, fecha en Europe/Madrid para no depender de la TZ del servidor).
- Enlace estable y compartible
/incidente-del-dia: redirige (302) al range con el turno de hoy (?seed=…&t=…&iotd=1), así una URL fija apunta siempre al reto actual (útil para compartir a diario). El range muestra un ribbon «🚨 Incidente del día · fecha» al llegar por ese enlace, y la home estrena un teaser con la fecha de hoy y CTA. En el catálogo (Aprender) → nav + sitemap.
- Verificado: la redirect sigue a 200 (CI-safe con
FOLLOWLOCATION), el motor recibe la semilla/plantilla del día y genera el incidente, y la plantilla varía entre días. +13 tests (tests/php/incidente-del-dia.test.php, PHP 661 → 674).
✨ Novedad
Recruiter Mode (/recruiter) — la vista de 60 segundos
- Tercer paso de la Fase 1: nueva página
/recruiter, el one-pager «hire me» pensado para quien selecciona. Responde de un vistazo —sin ruido de producto— a lo que un técnico busca: quién soy (foto, rol, pitch), qué sé hacer (skills agrupadas y enlazadas a su prueba), certificaciones (eCPPT, eJPT), proyectos que lo demuestran (El Turno, Threat Radar, Threat Hunting Lab) y cómo contactar (CV, LinkedIn, contacto). Bilingüe ES/EN.
- Todo el contenido es veraz: rol y pitch salen de
/sobre-mi, cada skill está respaldada por una herramienta/lab real (enlaza a su prueba) y las métricas se derivan del catálogo (nada inventado). Punto de entrada visible: pill «👔 ¿Recruiter? Vista de 60 segundos» en el hero de la home. En el catálogo (Perfil) y con +8 tests (tests/php/recruiter.test.php, PHP 653 → 661).
✨ Novedad
Proof of Work (/proof) — la evidencia por habilidad
- Segundo paso de la Fase 1: nueva página
/proof que materializa el «demuestra, no digas». Arriba, métricas reales derivadas del catálogo y clicables (65 herramientas, 14 laboratorios, 22 técnicas ATT&CK, 26 artículos, 18 guías); debajo, 8 clusters por habilidad —Detection Engineering, SOC·Sentinel·KQL, investigación de incidentes, threat intel & vulnerabilidades, phishing/correo, MITRE ATT&CK, ofensiva/AD, fundamentos «cómo funciona»— cada uno con enlaces a la evidencia real (herramientas, labs y flagships que existen). Bilingüe ES/EN.
- La evidencia se resuelve del catálogo (
ce_item/ce_url, títulos y URLs siempre correctos; slugs inexistentes se omiten) + enlaces manuales para páginas con alias (Threat Radar, /attack, el lab de Threat Hunting). El CTA «Ver mi trabajo» del hero ahora apunta a /proof. En el catálogo (Perfil) y con +8 tests (tests/php/proof.test.php, PHP 645 → 653).
✨ Novedad
WordPress vuln checker · Fases 2-3: ETL + endpoint + UI
- La tool ya es usable (
/tool-wp-check): pegas la lista de plugins de tu WordPress —o la salida de wp plugin list— y te dice cuáles tienen vulnerabilidades conocidas, su gravedad (CVSS) y a qué versión actualizar, con enlace al detalle. Sin instalar nada ni dar acceso a tu web: solo se compara la lista, server-side (el índice no se expone). Datos de Wordfence Intelligence (atribución a Defiant en la UI).
- Fase 2 · ETL
scripts/sync-wp-vulns.php: descarga el feed V3 (autenticado con Bearer, la key en secrets.php → ce_secret('wordfence_key')), lo recorre en streaming (lib/wp-feed.php, sin cargar los ~117 MB en memoria) y escribe un índice adelgazado {slug:[vulns]} en assets/data/wp-vulns.json (gitignored; lo regenera un cron diario). Normalización del esquema V3 confirmada con muestra real. Rate-limit-aware (429) + guarda de frescura (no descarga si el índice tiene <12 h) para no agotar la cuota del feed.
- Fase 3 · endpoint + UI:
api/wp-check.php (rate-limited, entrada acotada, parsea wp plugin list y cruza con el índice vía el motor de Fase 1) + tool-wp-check.php/wp-check.js (render por plugin, orden por CVSS, distingue «no está en el feed» de «sin vulnerabilidades»). Parser ce_wp_parse_list puro y testeado.
- Cambio de premisa (verificado): desde 2025-03-09 el feed V3 requiere una API key gratuita (cuenta Wordfence → Integrations); ya no es «sin clave» como decía la nota de la Fase 1. La clave es un secreto y
ce_secret() ahora lee secrets.php fuera del web root (home del vhost). +37 tests (parser en streaming, normalización contra registro real, parser de lista; PHP 611 → 645).
✨ Novedad
WordPress vuln checker · Fase 1: motor de matching
- Primera fase de la tool «¿Qué plugin me va a reventar el WordPress?» (pegas tu lista de plugins y te dice cuáles tienen vulnerabilidades conocidas y a qué versión actualizar). Fuente verificada: el feed de Wordfence Intelligence es gratis para uso comercial, sin clave, con atribución (MITRE + Wordfence).
- Esta fase entrega el núcleo puro y testeable:
lib/wp-vuln.php cruza la lista [{slug, version}] con un índice normalizado {slug: [vulns]} y devuelve, por plugin, las vulns que le afectan (con CVE/CVSS/severidad) y la versión de parche a la que subir. Rangos de versión con version_compare (from/to con inclusividad), orden por severidad, y distingue «plugin no está en el feed» de «no vulnerable» (no lo da por seguro). Desacoplado del esquema crudo de Wordfence: el ETL (Fase 2) lo normalizará. +24 tests (tests/php/wp-vuln.test.php, PHP 587 → 611).
- Aún no es user-facing: Fase 2 = ETL por cron (descarga + parser en streaming del feed de ~117 MB → índice); Fase 3 = endpoint
/api/wp-check + UI + atribución + cruce EPSS/KEV. Plan completo en implementation_plan.md.
✨ Novedad
Auditor de configuración (sshd_config, .htaccess, wp-config.php)
- Nueva tool (
/tool-config-audit): pega un sshd_config, un .htaccess o un wp-config.php y obtén una nota A–F con los fallos de seguridad y la línea corregida al lado. 100% en el navegador — el fichero (con rutas internas, hosts y a veces credenciales) no se sube a ningún sitio. Detecta, entre otros: login de root por SSH, contraseñas/empty, SSHv1, cifradores/MACs/KEX débiles, MaxAuthTries, listado de directorios (Options Indexes), CORS comodín, display_errors, cabeceras de seguridad ausentes, salts por defecto de WordPress, WP_DEBUG, contraseña de BD débil y editor de ficheros activo.
- Cubre el hueco que dejaban las demás: Docker/K8s →
tool-iac-lint; TLS de nginx/Apache → tool-tls-config; aquí van sshd/apache-.htaccess/WordPress. Motor puro config-audit.js (dual-mode; detección de tipo + reglas deterministas que devuelven {code, sev, data}; nota A–F como la familia de linters). i18n y «corrección» en el DOM. En el catálogo (vuln) y en «✨ Nuevas». +14 tests (JS 7 · PHP 580 → 587).
✨ Novedad
Flagship «El viaje de una consulta DNS, paso a paso»
- Nueva pieza interactiva (
/viaje-de-una-consulta-dns, sección Aprender): recorre una resolución DNS recursiva de principio a fin —de tu navegador al resolver recursivo, y de la raíz al TLD y al servidor autoritativo, con la referencia (NS) en cada nivel, el registro A + TTL y el cacheo final. Explica en cada paso qué ocurre y por qué. Tres tarjetas: caché y TTL, jerarquía y delegación (recursivo vs autoritativo) y privacidad/autenticidad (DoH/DoT cifran, DNSSEC firma). Fuentes: RFC 1034/1035 y root-servers.org. Enlaza con el flagship de TLS (lo siguiente tras tener la IP).
- Mejora progresiva y accesibilidad como los flagships de TLS/OAuth: sin JS es artículo; con JS, stepper (anterior/siguiente/reiniciar + flechas).
<ol>/<li>, orden de encabezados h1→h2→h3, pasos futuros con [hidden] (sin opacidad sobre texto), animación solo transform, contador aria-live. Bilingüe ES/EN. Motor puro dnsClampStep(). Entra en el catálogo (learn) y en «✨ Nuevas». +9 tests (JS 1 · PHP 568 → 576).
- Con TLS + OAuth + DNS, la serie «Cómo funciona» ya cubre tres piezas del ciclo de una petición web.
✨ Novedad
Flagship «Login con Google (OAuth 2.0), paso a paso»
- Nueva pieza interactiva (
/login-con-google, sección Aprender): recorre el flujo OAuth 2.0 Authorization Code + PKCE —el de «Iniciar sesión con Google»— paso a paso: preparación de PKCE (code_verifier/code_challenge), redirección de autorización, login+consentimiento, vuelta con el code, verificación del state (anti-CSRF), canje del code por tokens y validación PKCE del servidor. Explica en cada paso qué se envía y por qué es seguro. Tres tarjetas de concepto: PKCE, state (CSRF) e id_token ≠ access_token (con el porqué de evitar el implicit flow). Fuentes: RFC 6749 y RFC 7636.
- Mejora progresiva y accesibilidad, igual que el flagship de TLS: sin JS es un artículo completo; con JS, un stepper (anterior/siguiente/reiniciar + flechas).
<ol>/<li>, orden de encabezados h1→h2→h3, pasos futuros con [hidden] (sin opacidad sobre texto), animación solo transform, contador aria-live. Bilingüe ES/EN. Motor puro oauthClampStep(). Entra en el catálogo (learn) y en «✨ Nuevas». +9 tests (JS 1 · PHP 560 → 568).
- Retocado de paso: la descripción de catálogo de
tool-jwt (lo vendía como simple «decodificador») ahora refleja que audita de verdad (alg=none, confusión HS/RS, jku/x5u, expiración, kid, datos sensibles, claims de authz) → mejor discoverability/SEO.
✨ Novedad
Flagship «El handshake TLS, paso a paso»
- Nueva pieza interactiva (
/handshake-tls, sección Aprender): recorre el handshake de TLS 1.3 mensaje a mensaje —ClientHello, intercambio de claves ECDHE, EncryptedExtensions, certificado, CertificateVerify (prueba de posesión), Finished y el canal cifrado— explicando en cada paso qué se envía y por qué importa (con la nota de seguridad de cada mensaje). Tres tarjetas de concepto: la clave que no viaja (forward secrecy), cifrar ≠ autenticar, y TLS 1.3 vs 1.2. Fuente: RFC 8446.
- Mejora progresiva: sin JS se lee como artículo completo; con JS es un stepper (anterior/siguiente/reiniciar + flechas ←→) que acumula mensajes y despliega el detalle del paso activo. Accesible:
<ol> con <li> directos, orden de encabezados correcto (h1→h2→h3), pasos futuros con [hidden] (sin opacidad sobre texto), animación solo de transform, contador aria-live. Bilingüe ES/EN.
- Motor puro
tlsClampStep() en assets/js/handshake-tls.js (dual-mode). Entra en el catálogo (learn) y en la tira «✨ Nuevas». +9 tests (JS 1 · PHP 552 → 560, guard de página/JS/catálogo).
✨ Novedad
Threat Radar: tipo de fallo (CWE) en el análisis
- El análisis de cada CVE (radar y ficha) muestra ahora el tipo de fallo en lenguaje claro: «Tipo de fallo: Inyección SQL (CWE-89). Manipula las consultas a la base de datos…», con enlace a la definición oficial en MITRE CWE. Catálogo curado de ~30 familias (SQLi, XSS, path traversal, deserialización, SSRF, XXE, OOB write/read, UAF, command injection, auth bypass…) con nombre y descripción ES/EN.
- Datos reales, sin inventar: el ETL extrae el CWE de la fuente (
problemTypes de CIRCL / weaknesses de NVD) y, como fallback conservador, lo infiere del texto oficial de NVD solo cuando la frase es inequívoca (ce_tr_guess_cwe: "SQL Injection"→CWE-89, "Path Traversal"→CWE-22…; ambiguo → nada). La semilla versionada se enriqueció así (20/40). CVE sin CWE → la línea simplemente no aparece; CWE no catalogado → se muestra el id enlazado.
- Motor:
ce_cwe_key() (id→familia) y cwe/cweKey en ce_cve_analyze. +24 tests (PHP 528 → 552): extracción real, inferencia conservadora, clasificación y render ES/EN.
✨ Novedad
Threat Radar: ficha por CVE con URL propia (SEO)
- Cada CVE del radar tiene ahora su página propia, compartible e indexable:
/threat-radar/CVE-XXXX-YYYY (projects/cve-detail.php), con el análisis determinista completo (peligro, cómo se explota, impacto, estado, cómo defenderse), el vector CVSS en crudo, métricas (CVSS/EPSS/KEV), resumen y fechas de NVD, migas de pan y CTAs (NVD, CISA KEV, CVE→detección, ¿me afecta?, priorizar). Mismo patrón que /attack/T1234 y /ports/443.
- SEO programático: ruta en
.htaccess + scripts/router.php; el id de cada CVE en el radar enlaza a su ficha; las 40 fichas del snapshot entran en el sitemap (y el hub pasa a listarse como /threat-radar canónico, no /projects/cve-tracker). Título/description propios por CVE, canónica autorreferente y JSON-LD BreadcrumbList. CVE fuera del snapshot → 404 (evita indexar vacío); minúsculas → 301 a mayúsculas (sin duplicados). El churn diario es inherente al radar.
- Refactor DRY: el render del panel se extrae a
lib/cve-analysis-render.php (ce_cve_analysis_html, una sola fuente de verdad) y los estilos a assets/css/cve-analysis.css (los comparten radar y ficha). +32 tests (tests/php/cve-detail.test.php, PHP 496 → 528): render ES/EN, degradación sin vector, y guards de routing/sitemap/enlaces.
✨ Novedad
Threat Radar: análisis determinista por CVE (sin IA)
- Cada CVE del radar (
/threat-radar) trae ahora un panel «Análisis» desplegable (<details> nativo, accesible, sin JS) que explica qué tan peligrosa es, cómo se explota y cómo defenderse — en lenguaje claro y sin alucinaciones: todo se deriva de datos reales (el propio vector CVSS ya codifica AV/AC/PR/UI/S + impactos), más KEV/EPSS/flag de ransomware. Motor puro lib/cve-analysis.php (ce_cve_analyze): nivel de peligro (crítica-ransomware/crítica-activa/alta-inminente/…), «cómo se explota» a partir del vector (remota vs adyacente/local, con/sin auth, con/sin interacción, complejidad, *scope changed*, y alerta de combinación trivial de automatizar cuando es red+sin-auth+sin-interacción+baja-complejidad), impacto C/I/A (soporta también CVSS 4.0 vía VC/VI/VA), estado de explotación y defensa priorizada (parchea-ya si KEV, reduce-exposición si es por red, concienciación si requiere interacción, y CVE→detección). Enlaza a tool-cve-detect y tool-exposure.
- Devuelve códigos neutros; el texto i18n vive en la plantilla (ES/EN). Sin clave, sin coste, sin red → robusto y offline. +36 tests (
tests/php/cve-analysis.test.php, PHP 460 → 496): parseo de vector 3.x/4.0, niveles de peligro, traits derivados, combinación trivial, impacto 4.0 y degradación elegante sin vector.
✨ Novedad
SOC Range: dos casos reales más (ProxyLogon, RDP-ransomware)
- ProxyLogon / HAFNIUM (Exchange, CVE-2021-26855, 2021): SSRF → webshell
.aspx en /aspnet_client/ → PowerShell desde w3wp.exe → C2 → tarea. Y ransomware por RDP expuesto (patrón Phobos/Dharma): miles de 4625 → 4624 type 10 → PowerShell → C2 (Cobalt Strike) → servicio. Ya son 4 casos reales en el selector.
- Se eligieron casos que encajan con fidelidad en el modelo (web/RDP → PowerShell → C2). Nota honesta: WannaCry (EternalBlue/SMB, sin
-enc, killswitch domain) y SolarWinds (supply-chain, beacon a dominio) NO se añadieron porque distorsionarían la realidad; requerirían extender el motor con otro vector y una cadena sin -enc. Coherencia -enc→C2 de ambos casos cubierta por el test "todos los escenarios fijos" (33 JS · 460 PHP).
✨ Novedad
SOC Range «El Turno»: Fase 4 — «Revive una brecha famosa»
- Dos casos reales curados, jugables como turnos especiales: Log4Shell (CVE-2021-44228 — exploit web con
${jndi:ldap://…} → PowerShell → C2 → tarea) y Emotet → ransomware (phishing de secuestro de hilo → cargador PowerShell → C2 → servicio). Con contexto real en el informe (qué fue, impacto, contención) — educativo y muy compartible.
- Encajan en la misma forma de escenario (reutilizan UI, scoring, línea de tiempo); su
-enc decodifica de verdad al C2 (blobs generados con el codificador del motor). Selector «Casos reales» en la página (?case=log4shell|emotet). Al ser hand-authored (el C2 está en el código), dan XP pero no puntúan en el ranking global — como el intro, para no ensuciar el board anti-trampa. +2 tests (→ 33): recorrido de integridad y coherencia -enc→C2 por todos los escenarios fijos.
✨ Novedad
SOC Range «El Turno»: Fase 3 — leaderboard global (validado en servidor)
- Completar un turno con el veredicto correcto ya puntúa en el ranking oficial (
/leaderboard), con validación server-side: el endpoint /api/validate-range regenera la verdad del turno desde (t, seed) y comprueba las respuestas. El C2 no se puede acertar sin decodificar el -enc, así que el ranking no se puede falsear desde el navegador (misma garantía que el CTF). No se filtra la solución en la respuesta.
- Anti-farming (las semillas son infinitas): no se puntúa por turno, sino logros finitos en
ce_solves (range:solved 100 pts con veredicto 4/4; range:hard 150 en difícil), una vez cada uno → aporte acotado. Reutiliza el sistema de alias y ce_solves (sin migración de BD).
- Lo delicado:
lib/range-engine.php reimplementa el PRNG (mulberry32, 32-bit) idéntico al JS para derivar el mismo C2 en el servidor. Cubierto por un cross-test (tests/php/range-truth.test.php, C2 fijados) anclado también en el test JS → si un motor cambia su secuencia de draws, CI falla y avisa. PHP 460 · JS 31 tests. El intro (sin semilla) no puntúa; el cliente muestra la posición o invita a ponerse alias.
✨ Novedad
SOC Range «El Turno»: Fase 3 — línea de tiempo interactiva
- Además de la kill chain, el jugador ordena cronológicamente los eventos del incidente (acceso → ejecución → descarga → C2 → persistencia) con botones subir/bajar (accesible, sin drag). Las horas no se muestran en la lista: hay que razonar la secuencia, no leer timestamps. Al emitir el veredicto, el informe puntúa cuántos eventos quedaron en su sitio y revela el orden correcto con sus horas (payoff didáctico).
- Se puntúa como bonus de XP (+3 por evento en su posición), aparte del /100 de pins+veredicto → no cambia el modelo existente. Motor:
ce_range_score_timeline(order, scen) puro; cada escenario (incluido el intro y los procedurales) trae su timeline. +3 tests (→ 30).
✨ Novedad
SOC Range «El Turno»: Fase 3 — niveles de dificultad
- El turno acepta dificultad (
?d=facil|normal|dificil, selector en la página): fácil (pistas visibles, C2 con 3 opciones), normal (actual), difícil (pistas ocultas, +2 artefactos señuelo benignos como ruido — Windows Update, backup, DNS, login rutinario — que hay que descartar, y C2 con 4 opciones). XP ×0.75 / ×1 / ×1.5 según nivel.
- El incidente base no cambia entre niveles con la misma semilla (puedes rejugar el mismo caso más difícil); la dificultad solo añade ruido/quita ayudas. Señuelos sin
pin → no afectan al scoring. HUD de artefactos ahora dinámico (/N según el turno). +4 tests (→ 27): señuelos en difícil, 3 opciones en fácil, incidente base invariante entre niveles, y fallback a normal.
✨ Novedad
SOC Range «El Turno»: Fase 3 — dos familias más (4 en total)
- El motor procedural genera ya de cuatro familias de incidente:
phishing-ps, web-exploit, y las nuevas rdp-brute (fuerza bruta RDP → PowerShell → clave Run) y usb-lnk (USB con LNK malicioso → PowerShell → servicio). «Nuevo turno» rota entre las cuatro.
- Refactor: el generador despacha el acceso inicial por
vector (correo / log web / log de autenticación 4625→4624 / medio extraíble) y la persistencia por persist (servicio 7045 / tarea 4698 / clave Run T1547.001) con switch. Añadir una familia que reutilice esos vectores/persistencias ya no requiere código nuevo de artefactos, solo una entrada en CE_RANGE_TEMPLATES. Se mantiene la coherencia (el -enc de cada familia decodifica a su C2). +2 tests (→ 23), incluido un recorrido por todas las familias verificando forma, integridad y coherencia -enc→C2.
✨ Novedad
SOC Range «El Turno»: Fase 3 (segunda familia de incidente)
- El motor procedural ya genera de más de una familia: además de
phishing-ps (phishing → PowerShell → servicio), la nueva web-exploit (explotación de app web → webshell → PowerShell → tarea programada). Cada familia cambia el acceso inicial (correo .eml vs log de acceso web con webshell) y la persistencia (servicio 7045 vs tarea 4698), compartiendo el tramo ejecución→C2 y toda la maquinaria de coherencia (el -enc decodificado sigue dando el C2 real).
ce_range_generate despacha por plantilla (CE_RANGE_TEMPLATES + ce_range_templates()); «Nuevo turno» elige familia y semilla al azar (ambas en la URL → reproducible/compartible). +6 tests (→ 21 en la suite del Range): familia web (vector=web, persist=task), coherencia -enc→C2 en la nueva familia, que la familia cambia acceso/persistencia, fallback de plantilla desconocida y scoring del turno web.
✨ Novedad
SOC Range «El Turno»: Fase 2 (motor procedural)
- El turno de SOC (
/range) ya es rejugable e infinito: ?seed=<código> genera un incidente determinista (misma semilla = mismo turno) y compartible. Sin semilla se juega el turno de introducción curado (wks-042).
- Motor
ce_range_generate(plantilla, seed) en range-engine.js (puro, testeable): PRNG mulberry32 sembrado + *pools* de datos (host, usuario, dominio *lookalike*, C2, servicio, documento, hora, severidad). Coherencia total desde una única verdad: el -enc se genera con un codificador Base64 UTF-16LE puro y al decodificarlo con la tool real (ce_ps_decode) sale exactamente el C2 que aparece en los logs y que es la respuesta correcta (con un señuelo *near-miss* que obliga a decodificar).
- UI (
range-ui.js): controles de nuevo turno (semilla aleatoria → URL), compartir (copia el enlace) y volver al intro; botón «jugar otro turno» en el informe. +8 tests (determinismo, variación, integridad, coherencia -enc→C2, round-trip, scoring sobre turno generado) → 16 en la suite del Range. Gate a11y (+1 URL con semilla).
✨ Novedad
«El Turno» (SOC Range) · Fase 1 MVP
- Nueva página (
/range, bilingüe): un turno de analista de SOC jugable. Investigas un incidente real (WKS-042) con sus artefactos —correo .eml, log 4688, un powershell -enc, tráfico C2 en Sysmon 3, servicio 7045—, decodificas el -enc con la tool real del sitio (ce_ps_decode, que además te extrae el IOC), anclas los hallazgos para reconstruir la kill chain, y emites un veredicto que se puntúa (nota A–F) con un informe que reconstruye la historia. Otorga XP una vez por escenario (AgentCore.addXP).
- Arquitectura pensada para crecer: motor puro y testeable (
range-engine.js: datos del escenario + ce_range_score, +9 tests) separado de la UI (range-ui.js, workspace scoped a #range-root); el -enc reutiliza ps-decode.js (sinergia real con el arsenal). Coherencia desde una única *verdad* del incidente. a11y: artefactos y opciones como controles enfocables, <dialog> para el veredicto, animaciones con transform. Progressive enhancement con <noscript>. Menú Aprender (primero, type=content), sitemap, gate a11y (+1).
- Es la Fase 1 (1 incidente hand-authored); la Fase 2 añadirá el motor procedural (semilla → rejugable) manteniendo esta forma de datos y scoring.
✨ Novedad
Página insignia «Cobertura ATT&CK» (mapa del arsenal)
- Nueva página (
/cobertura-attack, bilingüe): la matriz MITRE ATT&CK Enterprise (14 tácticas, de *reconnaissance* a *impact*) donde cada técnica cubierta se ilumina con la herramienta gratuita de CyberEscudo que la analiza o detecta, enlazada en vivo. Muestra el arsenal como un sistema coherente sobre el marco estándar; honesta con los huecos (Collection y Exfiltration sin cobertura directa). Resumen: 12/14 tácticas · 25 técnicas · 24 herramientas.
- Técnica: renderizada en el servidor (indexable, funciona sin JS); interacción *hover-aislar* en CSS puro (sin fichero JS). Matriz con scroll horizontal estilo ATT&CK Navigator, región desplazable accesible (
tabindex/aria-label) con celdas-enlace enfocables; todo el texto claro-sobre-oscuro (contraste AA), CSS namespaced (ac-*). Menú Aprender (type=content), sitemap, gate a11y (+1) y cross-links con /attack, /anatomia-de-un-ataque (recíproco) y /tools.
✨ Novedad
Página insignia «Disección de un phishing» (interactiva)
- Nueva página (
/diseccion-de-un-phishing, bilingüe): un correo de phishing realista (tarjeta clara, como un cliente de correo) con 7 banderas rojas que descubres haciendo clic en las partes sospechosas; cada acierto revela por qué es una señal (dominio parecido/typosquatting, Reply-To distinto, urgencia, saludo genérico, enlace engañoso, doble extensión .pdf.exe, errores). Contador X/7 + «revelar todas»; al completarlo enlaza al Triage de Email como la versión automática. Interacción por *hotspots* — distinta del resto — y muy didáctica.
- Técnica: banderas como
<button> (enfocables, aria-expanded), contador aria-live, sin onclick inline (respeta la CSP estricta del sitio), estados «sin descubrir» sin opacity para no romper el contraste AA, badges con texto oscuro sobre color. phishing-anatomy.js (UI pura, scoped a #ph-root). Menú Aprender (type=content), sitemap, gate a11y (+1) y cross-link recíproco con /tool-eml.
✨ Novedad
Página insignia «Anatomía de un ataque» (scrollytelling)
- Nueva página (
/anatomia-de-un-ataque, bilingüe): una intrusión real contada fase a fase (entrega → ejecución → C2 → persistencia → escalada → lateral → impacto) con *scrollytelling*. Cada fase muestra qué hace el atacante, el rastro que deja (Event IDs) y la herramienta que lo caza enlazada en vivo (/tool-eml, /tool-ps-decode, /event-id/*, /tool-defang, Sigma Forge, BloodHound…). Mapeada a MITRE ATT&CK y con un rail de progreso cuyo color escala de azul a rojo (mensaje: *córtala cuanto antes*). Teje todo el arsenal en una sola narrativa.
- Técnica: rail de fases con
IntersectionObserver (attack-anatomy.js), progressive enhancement (sin JS, todo el contenido es visible; el ocultar-y-revelar solo aplica con .aa-js), respeta prefers-reduced-motion, rail como <nav> con anclas accesibles por teclado, CSS namespaced (aa-*). Entra en el menú Aprender (catálogo, type=content), en el sitemap, y con cross-link recíproco desde el post de Event IDs. Gate de a11y (+1 URL).
✨ Novedad
Decodificador de PowerShell (-EncodedCommand)
- Nueva herramienta (
/tool-ps-decode): pega un powershell -enc <base64> (o el Base64 a secas) y recupera el script original. La clave es que -EncodedCommand codifica en Base64 de UTF-16LE (no UTF-8), por lo que los decodificadores genéricos lo dejan lleno de basura; este entiende el formato real (con detección automática UTF-16LE/UTF-8).
- Además marca anti-patrones de ofuscación/comportamiento (IEX, *download cradles*, reverse shell por socket, carga reflexiva, AMSI bypass, Base64 anidado, payload comprimido, flags de sigilo, trucos de cadena) y extrae IOCs neutralizados (URLs/IPs, con enlace al *defang*). 100% en el navegador, nada sale del equipo.
- Motor puro y testeable (
ps-decode.js, dual-mode: ce_ps_b64_bytes / ce_ps_utf16le / ce_ps_utf8 / ce_ps_extract / ce_ps_findings / ce_ps_iocs / ce_ps_decode). +18 tests JS; guard de motor PHP → 441. Catálogo (🔩 *blue*, destacada) y primera en la tira ✨ Nuevas. Gate de a11y (+1, con <pre tabindex=0>). Enlaza a /event-id/4104 y /event-id/4688; cross-link desde el post de Event IDs (sección PowerShell) al decodificador.
✨ Novedad
Explicador «Cómo cae tu contraseña»
- Nueva página (
/como-cae-tu-contrasena, bilingüe): escribes una contraseña y ves, en vivo, cuánto aguanta bajo tres modelos de atacante (formulario online con límite, hash lento *bcrypt*, hash rápido *MD5/NTLM* en GPU) — con el veredicto, la entropía, las clases de carácter y el porqué de cada consejo en lenguaje claro. El golpe didáctico: el mismo password aguanta *segundos o siglos* según cómo lo guarden (por qué importa el *hashing lento*). 100% en el navegador.
- Craft: tope honesto en «más que la edad del universo» (nada de notación científica), cálculo con logaritmos (no desborda), lista de contraseñas comunes (caen por diccionario, no fuerza bruta). Motor puro y testeable (
password-crack.js, dual-mode con separación i18n: ce_pw_entropy / ce_pw_log10_seconds / ce_pw_findings / ce_pw_time_parts / ce_pw_verdict devuelven códigos neutros; el DOM localiza). +13 tests JS. Menú Aprender (catálogo type=content) + sitemap; gate de a11y (+1).
✨ Novedad
Arsenal: tira «✨ Nuevas» en /tools
- El catálogo ya tenía buscador en vivo, filtros por categoría con conteos, favoritos, recientes (por usuario) y badges DESTACADA. Faltaba una sola cosa: un visitante nuevo no tenía forma de ver qué se ha añadido últimamente (los «Recientes» son por-usuario en
localStorage; las DESTACADA están dispersas por categoría).
- Nueva tira server-rendered «✨ Nuevas» al principio de
/tools con las 6 herramientas más recientes (De CVE a detección, Linter de IaC, Evaluador de CSP, Generador de config TLS, Explicador de Regex, Linter de detecciones). Aditiva y sin JS: no toca catalog.js. La lista se mantiene en $ceNewSlugs (tools.php) — al publicar una tool, se pone la primera.
✨ Novedad
Blog: «Los Windows Event IDs que todo SOC debería vigilar»
- Nuevo artículo técnico bilingüe (
/blog/event-ids-windows-soc): los IDs de evento de Windows que cuentan la historia de un ataque, agrupados por fase (autenticación, privilegios/ejecución, escalada/lateral, persistencia, anti-forense, PowerShell/Sysmon), con qué cazar en cada uno y el recordatorio de activar lo que no sale por defecto (4688 con línea de comandos, 4104, Sysmon). 19 enlaces a la familia /event-id/* + al hub, con callouts al Log Analyzer, Sigma Forge y el linter de detecciones. Registrado en lib/posts.php.
✨ Novedad
Blog: «De una CVE a una detección»
- Nuevo artículo técnico bilingüe (
/blog/de-cve-a-deteccion): el método para pasar de un advisory de CVE a una detección — extraer servicio/puerto/vector/resultado, traducirlo a telemetría (event 4688, Sysmon 1/3), situarlo en ATT&CK y escribir un esqueleto Sigma, detectando comportamiento (padre-hijo anómalo, egress) y no solo la firma. Es el post que teje el ecosistema: enlaza /ports, /event-id, /attack, el puente /tool-cve-detect, Sigma Forge y el linter de detecciones. Registrado en lib/posts.php.
✨ Novedad
Blog: «Entender una regex (y ReDoS)»
- Nuevo artículo técnico bilingüe (
/blog/regex-y-redos): cómo leer una expresión regular token a token y la trampa del catastrophic backtracking (ReDoS) — la firma del cuantificador anidado, por qué es un DoS de un solo request y cómo evitarlo (incl. que JS no tiene grupos atómicos). Con callout al explicador de Regex (/tool-regex-explain). Registrado en lib/posts.php (listado, feed, sitemap, relacionados); OG de marca por defecto.
✨ Novedad
Blog: «Endurece tu Dockerfile»
- Nuevo artículo técnico bilingüe (
/blog/endurecer-dockerfile) sobre los anti-patrones de seguridad en Dockerfile / docker-compose / Kubernetes (corre como root, :latest, secretos en ENV, curl|sh, privileged, hostPath, sin limits) y cómo arreglarlos, con callout al linter de IaC (/tool-iac-lint). Registrado en lib/posts.php → aparece en el listado, el feed RSS, el sitemap y los relacionados. Usa la tarjeta OG de marca por defecto (fallback correcto).
✨ Novedad
Puente CVE → detección
- Nueva herramienta (
/tool-cve-detect): pega el id + descripción/advisory de una CVE y obtén un punto de partida para detectarla — puertos afectados, IDs de evento de Windows, tácticas ATT&CK y un esqueleto de regla Sigma. Enlaza el ecosistema: cada sugerencia abre su ficha (/ports, /event-id, /attack) y el esqueleto se refina en Sigma Forge.
- Síntesis heurística por palabras clave, 100% en el navegador (sin red). Framing honesto: son sugerencias / esqueleto, no una detección lista para producción. Motor puro y testeable (
cve-detect.js, dual-mode: ce_cd_cves / ce_cd_suggest / ce_cd_logsource / ce_cd_sigma). +7 tests JS → 370; guard de motor PHP → 438. Catálogo (🌉 *blue*, destacada), gate de a11y (+1) y cross-link desde el Threat Radar.
✨ Novedad
Insignia de seguridad embebible (badge SVG)
- Nueva ruta (
/badge/<dominio>.svg): una insignia SVG estilo *shields* con la nota A–F del Radar de Postura, para lucirla en una web o README. Enfoque solo-caché por seguridad: el badge NO escanea (un asset embebido en webs de terceros nunca debe disparar escaneos → sin amplificación ni abuso); sirve la nota del último escaneo del dominio y muestra «?» si no se ha escaneado.
api/scan.php escribe la nota por dominio en una caché gitignored (assets/data/badge-cache/); badge.php la lee y pinta el SVG. Validación anti-traversal del dominio, Content-Type: image/svg+xml, caché HTTP 1 h. Snippet para incrustar en /tool-posture.
- SVG puro y testeable (
lib/badge.php: ce_badge_svg / ce_badge_color / ce_badge_valid_domain + caché). +20 aserciones PHP → 435 (SVG bien formado, colores por nota, anti-traversal, roundtrip de caché, escapado XML). Verificado el flujo real: escaneo → badge con la nota.
✨ Novedad
Generador de configuración TLS (estilo Mozilla)
- Nueva herramienta (
/tool-tls-config): elige servidor (nginx / Apache / HAProxy) y perfil (moderno / intermedio) y llévate la config TLS endurecida lista para pegar — protocolos, cifradores, HSTS y OCSP stapling — según los perfiles de Mozilla. Complementa el Analizador TLS: generas ↔ verificas.
- Motor puro y testeable (
tls-config.js, dual-mode: ce_tls_config; plantillas versionadas por servidor×perfil; notas por códigos i18n con avisos de compatibilidad/dhparam/OCSP/HSTS). +6 tests JS → 363 (incl. que no queden placeholders sin sustituir); guard de motor PHP → 415. Catálogo (🔒 *vuln*, destacada), gate de a11y (+1) y cross-link con el Analizador TLS.
✨ Novedad
Linter de Dockerfile / IaC
- Nueva herramienta (
/tool-iac-lint): pega un Dockerfile, docker-compose o manifiesto de Kubernetes y recibe una nota A–F con sus anti-patrones de seguridad, antes de desplegar. 100% en el navegador. Hermano del linter de detecciones, ahora para DevSecOps.
- Dockerfile: base sin fijar (
:latest/sin etiqueta, respetando etapas AS), corre como root, ADD remoto, curl|sh, secretos en ENV/ARG, sin HEALTHCHECK, sudo, apt sin --no-install-recommends. Compose/K8s: privileged, hostPath, red del host, allowPrivilegeEscalation, capabilities peligrosas, imagen sin fijar, secretos en claro (excluye secretKeyRef/valueFrom); en K8s además falta de resources.limits, runAsNonRoot y securityContext.
- Motor puro y testeable (
iac-lint.js, dual-mode: ce_iac_detect + linters por formato + ce_iac_grade; separación de i18n por códigos). +10 tests JS → 357; guard de motor PHP → 412. Catálogo (🐳 *vuln*, destacada), gate de a11y (+1) y cross-link con el linter de detecciones.
✨ Novedad
Explicador de Regex (desglose + probador + ReDoS)
- Nueva herramienta (
/tool-regex-explain): pega una expresión regular y obtén su desglose token a token en lenguaje claro, un aviso de catastrophic backtracking (ReDoS) y un probador en vivo que resalta las coincidencias. 100% en el navegador. Complementa el generador de políticas (tool-regex): crear ↔ entender.
- Motor puro y testeable (
regex-explain.js, dual-mode): ce_rx_tokens tokeniza clases abreviadas, clases [...], cuantificadores (incl. {n,m} y perezosos), grupos (captura/no-captura/lookarounds/nombrados), anclas, escapes, alternancia y backrefs; ce_rx_redos detecta el cuantificador anidado clásico ((a+)+). El probador usa el RegExp del navegador con guarda anti-bucle (matches vacíos) y tope de 1000 chars. +9 tests JS → 347; guard de motor PHP → 409. Catálogo (🧩 *data*, destacada), gate de a11y (+1) y cross-link con el generador.
✨ Novedad
Evaluador de CSP
- Nueva herramienta (
/tool-csp-eval): pega una Content-Security-Policy real (de una cabecera o <meta>) y recibe una nota A–F con sus bypasses, antes de confiar en ella. 100% en el navegador. Complementa el Constructor de CSP (crear ↔ auditar), el mismo salto decoder→auditor que hicimos en JWT.
- Detecta:
unsafe-inline efectivo (vs ignorado por nonce/hash/strict-dynamic), data:/comodín/esquema suelto en script-src, unsafe-eval, strict-dynamic sin nonce, ausencia de script-src/default-src, y falta de object-src 'none', base-uri, frame-ancestors (o su comodín), form-action y endpoint de reporte.
- Motor puro y testeable (
csp-eval.js, dual-mode: ce_csp_parse + ce_csp_eval + ce_csp_grade; solo crit/warn penalizan la nota, los info son advisory; separación de i18n por códigos). +12 tests JS → 338; guard de motor PHP → 406. Catálogo (🧯 *vuln*, destacada), gate de a11y (+1) y cross-link con el Constructor de CSP.
✨ Novedad
Reto del día (retención)
- Nueva página (
/daily-challenge): un reto de ciberseguridad de opción múltiple que rota de forma determinista por fecha (el mismo para todos ese día), con racha de días acertados y explicación del porqué. Da una razón para volver a diario. Banco bilingüe ampliable (assets/data/daily-challenge.json, 24 preguntas).
- Respeta la arquitectura de gamificación: NO toca el leaderboard server-authoritative ni las flags CTF. La racha vive en su propia clave (
cyberescudo_daily) y el XP de perfil se otorga con AgentCore.addXP (contador único de XP), idempotente por día.
- Motor puro y testeable (
daily-challenge.js, dual-mode: ce_daily_index, ce_daily_days_between, ce_daily_apply, ce_daily_streak_display). +8 tests JS → 326. Catálogo (🎯 *learn*, destacada), gate de a11y (+1) y enlaces a los retos CTF y al Asistente de Seguridad.
✨ Novedad
Explicador IA (Modelo B) del comando Nmap
- El constructor de Nmap suma una lectura holística offline del escaneo, además de la explicación mecánica por flag: un perfil de ruido/sigilo (ruidoso / equilibrado / sigiloso) y el porqué, con las cautelas de cada decisión (tipo de escaneo, alcance de puertos, detección, scripts NSE, evasión y timing). Se compone de las señales que ya elige el usuario a partir de una base de conocimiento pre-generada (
assets/data/ai-nmap.json): sin clave, sin coste, funciona offline. "Entrenar" = ampliar el JSON.
- Segundo caso del patrón Modelo B (tras el explicador de phishing de
tool-eml). Motor puro y testeable (ai-nmap.js, dual-mode: ce_nmap_profile + ce_nmap_explain, con separación de i18n); se autoengancha al builder sin acoplarse (también re-renderiza al aplicar un preset). scripts/ai-nmap-gaps.php reporta huecos de cobertura y tests/ai-nmap.test.js hace de guard: +6 tests JS → 318, guard de motor PHP → 403.
✨ Novedad
Linter de detecciones (Sigma / YARA)
- Nueva herramienta (
/tool-detection-lint): pega una regla Sigma o YARA y obtén una nota A–F con sus anti-patrones antes de subirla al SIEM/EDR. 100% en el navegador (la regla no se envía). Autodetecta el formato (o lo fuerzas).
- Sigma: campos ausentes (title/id/description/logsource/level/falsepositives/tags),
logsource incompleto, level inválido, ausencia de detection/condition/selecciones y análisis de la condición — selecciones no definidas referenciadas y selecciones sin usar, con soporte de comodines sel* y them.
- YARA: ausencia de
rule/condition/meta, condition: true, strings demasiado cortas (falsos positivos), condición amplia (any/all of them con pocas strings), comodines hex excesivos (escaneo lento) y falta de filesize con all of them.
- Motor puro y testeable (
detection-lint.js, dual-mode: ce_dl_detect/ce_dl_lint_sigma/ce_dl_lint_yara/ce_dl_grade/ce_dl_lint; parseo YAML mínimo sin dependencias; separación de i18n por códigos). +17 tests JS → 312; guard de motor PHP → 402. Catálogo (🔎 *blue*, destacada), gate de a11y (+1) y cross-links recíprocos con Sigma Forge y el generador YARA.
✨ Novedad
SEO programático: familia de IDs de eventos de Windows
- Nueva familia de referencia servida por el motor genérico (
lib/seo-ref.php + reference.php): 55 IDs de eventos de Windows relevantes para seguridad en /event-id/<n> + hub /event-id, bilingües e indexables. Agrupados en 9 categorías (inicio de sesión, gestión de cuentas/grupos, privilegios/procesos, persistencia, movimiento lateral, Kerberos/NTLM, auditoría, PowerShell y Sysmon), cada ficha con log source, nivel de riesgo, qué cazar y datos estructurados TechArticle + BreadcrumbList.
- Contenido curado (
assets/data/seo-event-id.json): 4624/4625/4672/4688, altas en grupos privilegiados (4728/4732/4756), persistencia (7045/4697/4698), Kerberoasting (4769), borrado de logs (1102/104), PowerShell (4104/4103) y Sysmon (1/3/7/8/10/11/13/22), entre otros.
- Integración: alta en
CE_SEO_FAMILIES, $FAM_ROUTE, rutas (.htaccess + router de dev), sitemap (+55 URLs + hub), tarjeta en el índice /reference, y cross-links recíprocos con el Analizador de Logs y Sigma Forge (cada ficha enlaza a ambos; ambas tools enlazan al hub). Gate de a11y +2 páginas. Tests actualizados: guard PHP (+4 → 399) y JS (validez del JSON + conteo ≥50 → 283).
✨ Novedad
Threat Radar: distribución (feed RSS + widget "CVE del día")
- Feed RSS 2.0 del radar en
/threat-radar/feed.xml: el top diario como feed suscribible (título con ranking + CVSS/EPSS/KEV, enlace a la ficha NVD de cada CVE, guid estable, categorías por severidad y KEV). La generación del XML es pura y testeable (ce_tr_feed_xml en lib/threat-radar.php, escapado ENT_XML1); el fichero threat-radar-feed.php es un wrapper fino. Lee el snapshot del cron con reserva a la semilla. +12 tests PHP (SimpleXML) → 390.
- Widget embebible
/embed/cve-today: pinta la CVE #1 del radar (server-side desde el snapshot), chrome-less, auto-resize y backlink, reusando el marco de embeds. Añadido a la galería /embed. El regex de slug de embeds pasa a [a-z0-9-]+ para admitir el guion.
- Descubrimiento: hook
$pageFeeds en header.php (aditivo, escapado) para declarar feeds por página; el radar declara el suyo como rel="alternate" en <head> y añade enlaces visibles (RSS + «incrusta el CVE del día»). Gate de a11y +1 página (/embed/cve-today). Coherencia: los /embed/* y los feeds siguen fuera del sitemap (política existente noindex).
✨ Novedad
Tests JS de herramientas (dual-mode)
- 19 herramientas refactorizadas a dual-mode en 6 lotes: la suite JS pasa de 147 a 282 tests (
node --test, 43 ficheros, 0 fallos). Se extrae la lógica pura a nivel de módulo con module.exports y el cableado del DOM queda bajo if (typeof document…) e intacto.
- Lote 1 (4 tools): Base64 (
ce_b64_encode/decode), Chmod (permisos ↔ octal ↔ simbólico), Constructor de peticiones HTTP (ce_http_curl) y JWT (ce_jwt_b64url/decode).
- Lote 2 (5 tools): Cabeceras HTTP (
headers.js → parseo + comprobaciones + scoring/nota A–F + análisis fino de CSP), CSP builder (csp.js → ensamblado de la política + avisos), Reverse shell (revshell.js → validación IP, relleno de plantillas, msfvenom, PowerShell-Base64 y codificación url/durl/b64), Wordlist (wordlist.js → motor de mutaciones: caso, leet, afijos, combinación y filtro por política) y Fuerza de contraseña (passcheck.js → entropía real con castigo por patrones, tiempo de crackeo y veredicto).
- Lote 3 (5 tools): MAC (
mac.js → normalización/formateo y validación), Regex (regex.js → patrón de política de contraseña), Motor de priorización (vuln-score.js → CEVuln: score/bucket/pct + CVSS-desde-vector, compartido por «¿Qué parcheo primero?» y «¿Me afecta a mí?»), Claves SSH (ssh.js → clasificación tipo/algoritmo/bits/seguridad) y Defang/Refang (defang.js → defang/refang y extracción/clasificación de IOCs).
- Lote 4 (2 tools grandes): Evasión de WAF (
waf-tools.js → detección de contexto XSS/SQLi/genérico + motor de mutación: codificadores y transformaciones de evasión) y Analizador de logs (log-analyzer.js → parser de log combinado Apache/Nginx, detección por firmas, doble-decode de evasión y análisis completo con patrón dominante).
- Lote 5 (
sigma-forge.js, el mayor motor): normalización de campos al estándar Sigma, parsers (JSON, texto del Visor de eventos, key=value, access log; el XML se queda en el DOM porque necesita DOMParser), inferencia de logsource, detección de campos «ruido», sugerencia de técnicas ATT&CK, generación de la regla Sigma y los 5 transpiladores (KQL, SPL, Lucene, EQL, Wazuh) — todo puro y testeable.
- Lote 6 (cierre, los 2 solo-DOM más grandes): Email Triage (
eml-triage.js → decodificación RFC 2045/2047/2231, parseo MIME/cabeceras, identidad del remitente, cadena Received, autenticación SPF/DKIM/DMARC y desenvoltura de URLs Safelinks/Proofpoint; el análisis global con DOMParser e i18n queda en el DOM) y Exposición de dependencias (exposure.js → parsers de inventario de 8 formatos: npm, composer, pip, go.mod, Gemfile.lock, Cargo.lock, Maven, dpkg + detección de formato).
- Todas verificadas en el navegador tras el refactor. Con esto queda cubierta la lógica pura de toda herramienta con cómputo en el cliente; el resto (dns, tls, posture, cve, ip, recon, takeover, mac-OUI…) tiene su lógica en servidor y se cubre en
tests/php/ (378 tests). Nota: el test de wordlist (lote 2) cazó un bug real ([].slice.call(new Set()) devolvía vacío) que se corrigió a Array.from.
✨ Novedad
SEO programático: 4 familias de referencia (puertos, HTTP, DNS, firmas)
- 298 páginas nuevas indexables y bilingües servidas por un motor genérico (
lib/seo-ref.php + reference.php), no por cuatro plantillas: puertos de red (/ports/<n>, 120), códigos de estado HTTP (/http/<code>, 63 — el set estándar completo), tipos de registro DNS (/dns-records/<tipo>, 40) y firmas de fichero / magic bytes (/filetype/<ext>, 70), más un hub por familia y un índice global /reference que también enlaza MITRE ATT&CK.
- Datos curados y commiteados (
assets/data/seo-*.json), sin cron ni dependencia externa (conjuntos finitos y estables). Cada página trae descripción, facetas (protocolo/servicio/riesgo, clase HTTP, nº de registro, firma hex…), herramientas relacionadas, items relacionados, migas y datos estructurados TechArticle + BreadcrumbList.
- Linking interno: enlace en el pie a
/reference en todo el sitio y cross-links recíprocos desde tool-ports→/ports, tool-headers→/http, tool-dns→/dns-records y tool-magic→/filetype; cada ficha enlaza a la herramienta que la trabaja.
- Motor puro y testeado en
lib/seo-ref.php (tests/php/seo-ref.test.php, +41 aserciones → 378 PHP; validez de los 4 JSON: campos es+en, slugs únicos y válidos, links bien formados). Rutas en .htaccess + router de dev; 298 URLs nuevas en el sitemap; 6 páginas de muestra añadidas al gate de a11y. Framework reutilizable para futuras familias.
- Filtro cliente en los hubs (progressive enhancement,
assets/js/seo-ref.js): filtra las fichas en vivo por nombre, número o etiqueta —oculta también las categorías vacías— sin recargar ni salir de la página. Lógica pura dual-mode y testeada en tests/seo-ref.test.js (+9 tests → 156 JS) que además valida los 4 JSON (campos es+en, slugs únicos y válidos, cat declarada y links bien formados). El test cazó una categoría mal asignada (554 RTSP) que se corrigió.
✨ Novedad
Widgets embebibles ("CyberEscudo Embed")
- Otras webs pueden incrustar herramientas de CyberEscudo con un
<iframe>: CVSS 3.1, CIDR, Base64, fuerza de contraseña, hash SHA (256/1/512) y defang/refang de IOCs en /embed/<slug> (chrome-less, client-side, con auto-resize por postMessage y backlink de atribución). Galería en /embed con preview en vivo y snippet para copiar. Reparte marca y backlinks por la web (visibilidad/SEO).
- Seguridad del framing: el anti-clickjacking sigue estricto en todo el sitio (
X-Frame-Options: DENY + frame-ancestors 'none') y se relaja solo en /embed/* (flag CE_EMBED en bootstrap.php + <If> en .htaccess); la galería usa frame-src 'self' (flag CE_FRAME_SELF) para previsualizar sus propios widgets. Whitelist estricta de slugs (404 si no). Verificado: páginas normales siguen con DENY.
- Lógica pura y testeada en
embed-widgets.js (CVSS v3.1 con roundup oficial, entropía de contraseña + tiempo de crackeo, defang/refang de IOCs y hex de hash; 14 tests → 147 JS). CIDR reutiliza cidr-tools.js; Base64 UTF-8; el hash usa Web Crypto del navegador (SHA-256/1/512, sin dependencias); defang espeja defang.js. embed.css autónomo (+ .min). Catálogo (🧩), sitemap (/embed) y gate de a11y (5 páginas de embed, con nombres accesibles en todos los controles).
✨ Novedad
Asistente de Seguridad offline (Modelo B)
- Nueva herramienta (
/tool-assistant): un asistente tipo chat que responde preguntas de ciberseguridad desde una base de conocimiento curada (50 tópicos bilingües: CVSS, SPF/DMARC, reverse shell, Sigma/YARA, MITRE ATT&CK, magic bytes, JWT, Nmap, CVE/EPSS/KEV, phishing, hardening SSH, XSS/SQLi/CSRF, OWASP Top 10, hashing, X.509, chmod, regex, epoch, C2, Active Directory/BloodHound, análisis de URL, geolocalización de IP, DNS tunneling…), 100% en el navegador: sin clave, sin coste, sin enviar nada a la nube. Enruta a la tool/artículo/página ATT&CK adecuados y, si no conoce algo, lo dice y sugiere temas (nunca inventa).
- Motor de recuperación puro y testeado (
ai-assistant.js, dual-mode: tokenización sin tildes/stopwords + scoring frase>tokens + umbral; +9 tests, 133 JS en total). KB ai-assistant.json versionada y ampliable; extiende el patrón Modelo B existente. Framing honesto (no es un LLM en vivo, es recuperación local). Catálogo (🤖 *learn*, hot), OG y gate de a11y. Escapa siempre la entrada del usuario (anti-XSS).
✨ Novedad
SEO programático: referencia MITRE ATT&CK (/attack)
- Nuevas páginas de referencia renderizadas en servidor (indexables) para todo el corpus MITRE ATT&CK que ya mantenía el cron (
mitre-cache.json): 697 técnicas (/attack/<TID>, p. ej. /attack/T1059), 15 tácticas (/attack/tactic/<slug>) y un hub (/attack). Cada técnica trae descripción de MITRE (atribuida), táctica, plataformas, relación padre↔sub-técnicas, técnicas relacionadas y un bloque "cómo detectar/mitigar" (→ tool-sigma-forge, tool-loganalyzer, mapeador ATT&CK), con migas y datos estructurados TechArticle.
- Lógica pura y testeada en
lib/attack.php (+20 tests, 333 en total). Rutas en .htaccess + router de dev; 712 URLs nuevas en el sitemap; enlace recíproco desde el mapeador ATT&CK; framework reutilizable para futuras familias (puertos, CVE…). Muestras añadidas al gate de a11y. Es la primera palanca de SEO programático del sitio.
✨ Novedad
Radar de Postura: escáner de seguridad de dominio con nota A–F
- Nueva herramienta (
/tool-posture) que, dado un dominio, ejecuta comprobaciones pasivas sobre datos públicos y devuelve una nota A–F con arreglos priorizados en 5 dimensiones: antisuplantación de correo (SPF/DMARC), certificado TLS, cabeceras de seguridad, higiene DNS (CAA/DNSSEC/redundancia NS) y exposición (puertos de riesgo / CVE).
- Endpoint
api/scan.php (server-side): DNS por DoH (dns.google) + ce_tls_probe + ce_http_headers (con pin anti-SSRF) + Shodan internetdb, todo vía lib/http.php. Anti-SSRF (rechaza dominios que resuelven a IP privada con ce_tls_ip_public) + rate-limit (ce_rate_ok, 20/h). Solo pasivo/público.
- Puntuación pura y testeada en
lib/scorecard.php (+25 tests, 313 en total). Cliente scorecard.js pinta el scorecard y lo exporta a PDF (html2pdf). i18n ES/EN, catálogo (🛰️, *hot*), OG y gate de a11y. Cruza con tool-osint-report, tool-headers, tool-tls y tool-email-records.
✨ Novedad
Threat Radar: inteligencia de vulnerabilidades en vivo
- El antiguo
cve-tracker (un fetch() cliente que listaba 20 CVE recientes de Red Hat) se eleva a un radar de inteligencia que rankea a diario las CVE más peligrosas cruzando tres señales: explotación activa (CISA KEV), probabilidad de explotación (EPSS) y severidad (CVSS), con frescura. Puntuación = 0.45·EPSS + 0.30·CVSS + 0.15·KEV + 0.10·recencia.
- Arquitectura ETL (patrón del proyecto):
scripts/sync-threat-radar.php (cron, vía lib/http.php) enriquece por CIRCL en lotes con reserva NVD y escribe assets/data/threat-radar.json; la página renderiza en servidor desde ese snapshot (indexable, rápido, degradable). Lógica de scoring pura y testeada en lib/threat-radar.php (+25 tests, 284 en total).
- Página (
projects/cve-tracker.php, alias limpio /threat-radar): top 40 con badges de severidad (color), KEV «explotada activamente» y ransomware, % EPSS, CVSS, vendor·producto, resumen; filtros (todas/KEV/críticas/altas); enlaces por CVE a NVD, /tool-cvss y /tool-patch-priority; datos estructurados ItemList; atribución de fuentes. i18n ES/EN.
- Snapshot vivo gitignored + semilla versionada (
threat-radar.seed.json) para el primer render antes del cron. Catálogo refrescado (📡, *hot*). CSS muerto del radar viejo eliminado de style.css. Añadida al gate de a11y.
- Teaser en la home: banda "Inteligencia en vivo" tras el Arsenal con stats reales del snapshot (nº en el radar, nº explotadas/KEV, "actualizado hace X"), las 3 CVE top como chips con color de severidad y CTA a
/threat-radar.
- Requiere en prod: una tarea de cron diaria en Plesk (con el PHP del dominio) que ejecute
scripts/sync-threat-radar.php.
✨ Novedad
Post del blog: «Nmap sin miedo»
- Nuevo artículo bilingüe (
blog/nmap-sin-miedo.php) que enseña Nmap como seis decisiones (qué hosts, qué puertos, tipo de escaneo -sS/-sT/-sU, cuánto averiguar -sV/-O/-sC, ritmo -T0..T5, salida -oA), decodifica un comando real, explica -A y las trampas habituales (-Pn, UDP, -sS sin root), con aviso de uso autorizado (legalidad). Enlaza al Constructor de comandos Nmap (/tool-nmap) y cruza con el post de reconocimiento pasivo. Convención blog.css. OG generada. Ya son 22 posts.
✨ Novedad
Post del blog: «Tu primera regla YARA»
- Nuevo artículo bilingüe (
blog/tu-primera-regla-yara.php) sobre reglas YARA: anatomía (meta/strings/condition), los tres tipos de string (texto/hex/regex) y sus modificadores (nocase/wide/fullword), la lógica de condition (N of them, filesize, uint16(0) anclando por magic bytes), y cómo escribir reglas robustas que cacen variantes sin falsos positivos. Enlaza al Generador de reglas YARA (/tool-yara) y cruza con los posts de Sigma y magic bytes. Estrena la convención blog.css (sin <style> inline). Tarjeta OG generada. Ya son 21 posts.
✨ Novedad
Post del blog: «La extensión miente» (magic bytes)
- Nuevo artículo bilingüe (
blog/la-extension-miente.php) sobre firmas de fichero / magic bytes: por qué la extensión, el icono y el Content-Type no prueban nada; tabla de firmas (PNG/JPEG/PDF/ZIP/ELF/PE/PHP…), el truco del políglota GIF+PHP en subidas, los contenedores ZIP que engañan (docx/apk/jar), cómo comprobarlo (hexdump/file) y la defensa por firma en el servidor. Enlaza al Identificador de firmas (/tool-magic) y a la Zona CTF. Registrado en lib/posts.php (aparece en índice, feed, sitemap y relacionados) con su tarjeta OG generada. Ya son 20 posts.
✨ Novedad
Identificador de firmas de fichero / magic bytes (/tool-magic)
- Nueva herramienta forense/CTF que identifica el formato real de un fichero por su firma (magic bytes), ignorando la extensión: pega los bytes en hex o suelta el archivo (se leen solo sus primeros 64 bytes, no se sube). Reconoce ~30 formatos (PNG/JPEG/GIF/PDF/ZIP/GZIP/7z/RAR/ELF/PE-MZ/Mach-O/WASM/SQLite/OGG/MP4-ftyp/RIFF-WEBP/WAV/AVI/PHP/XML…), avisa de los contenedores ZIP (docx/apk/jar) y de casos sospechosos (p. ej. un «.jpg» que en realidad es
<?php). Motor dual-mode con 7 tests. Catálogo (Cripto y datos), pa11y y OG.
✨ Novedad
Write-ups de los 3 labs (WAF, SQLi, Prompt Injection)
- El Archivo de Write-ups suma los solucionarios de los laboratorios WAF Bypass, SQL Injection y Prompt Injection (misiones
OP-WAF-LAB, OP-SQLI-LAB, OP-LLM-LAB): pista escalonada, un paso por nivel con el payload real y flag oculta, en ES/EN. Total: 23 write-ups. Los payloads (<img onerror>, </user><system>…) se muestran escapados con e(), sin riesgo de XSS en la propia página.
✨ Novedad
Generador de reglas YARA (/tool-yara)
- Nueva herramienta blue-team que compone una regla YARA válida a partir de cadenas (texto, hex o regex) con sus modificadores (nocase/wide/fullword) y una condición (
any/all/N of them, con filesize opcional). Filas de cadena dinámicas (añadir/quitar), normalización de hex (deadbeef → DE AD BE EF, respeta comodines ?? [n-m]), escape de comillas y copia. El primo forense de sigma-forge. Motor dual-mode con 6 tests. Client-side, catálogo (Defensa y SOC), pa11y y OG.
✨ Novedad
Constructor de comandos Nmap (/tool-nmap)
- Nueva herramienta que compone un comando de Nmap eligiendo opciones (tipo de escaneo, puertos, timing, detección de versión/SO, scripts NSE, salida) y explica qué hace cada flag en tiempo real. Incluye recetas (rápido, TCP completo, vuln NSE, sigiloso, UDP), copia al portapapeles y aviso de uso autorizado.
-A sustituye y desactiva -sV/-O/-sC; el barrido -sn ignora puertos/detección. Motor dual-mode con 7 tests. Client-side, encaja en la familia de generadores (revshell/ssh/httpbuilder). Catálogo (Ofensiva) + pa11y + OG.
✨ Novedad
Hub de la Zona CTF (/ctf/)
- Nueva página que reúne los 27 retos de la serie
ctf/ (antes solo descubribles dispersos): tarjetas con categoría (web · sistema · defensa · OSINT · ofensiva · móvil), dificultad y el ID de envío (submit <ID>), con filtro por categoría en el cliente. Los datos salen de ce_ctf_series() en lib/ctf-flags.php; explica cómo puntuar (submit <ID> <FLAG> → ranking) y cruza con perfil/leaderboard/misiones. Enlazado desde el Centro de Misiones y añadido al gate de a11y.
✨ Novedad
esquema SQL del leaderboard (db/schema.sql)
- Añadido
db/schema.sql con la tabla ce_solves (faltaba en el repo) para crear o reparar la base de datos del «Salón de Agentes». Documenta el montaje en local (mysql -u root < db/schema.sql) y en Plesk. Nota: db-config.php está en .gitignore (no llega por el git pull), así que en el VPS hay que crearlo a mano desde db-config.example.php con las credenciales reales de la BD de Plesk. Es la causa del mensaje «El ranking no está disponible» en producción cuando falta la BD o la config.
📝 Blog
nuevo artículo: inyección de prompts
- «Inyección de prompts: el "XSS" de las aplicaciones con IA» (bilingüe, IA · AppSec). Explica el nº1 del OWASP Top 10 para LLM, las cuatro formas de secuestrar un bot (petición directa, evasión de filtro, override, inyección de contexto), por qué es tan difícil de parar (no hay «prepared statement» del prompt) y cómo defenderse (jerarquía de instrucciones, entrada como datos, cero secretos, restringir herramientas). Cose el Prompt Injection Lab, el Auditor de System Prompts y el explicador offline del Triage de Email. Con tarjeta OG propia.
✨ Novedad
Explicación con IA en el Analizador de Logs (Modelo B)
- El Analizador de Logs (
/tool-loganalyzer) suma un panel «✨ Explicación con IA» que convierte los patrones detectados en un triage SOC en lenguaje claro: qué es cada ataque (con su técnica MITRE), qué implica verlo en tus logs y qué hacer. Mismo Modelo B que el Triage de Email: se compone en el navegador desde una base de conocimiento pre-generada con Claude (assets/data/ai-logtriage.json); tus logs no salen de tu equipo.
- El motor de explicación (
ai-phishing.js) se ha generalizado (alias aiExplain/aiExplainLoad, loader con parámetro de fichero) para reutilizarse entre herramientas. Cobertura de las 9 categorías de ataque verificada por test (CI). Precache del SW ce-v8.
✨ Novedad
Auditor de System Prompts (/tool-prompt-audit)
- Nueva herramienta que audita el system prompt de una aplicación LLM y detecta las debilidades que explota la inyección de prompts: secretos incrustados, confianza ciega en el usuario, invitación a revelar instrucciones, falta de límites/negativas, entrada de usuario sin delimitar, acciones potentes sin restringir, rol sin definir. Da una nota (A–F) y, por cada hallazgo, cómo arreglarlo; incluye un ejemplo de prompt endurecido. Todo en el navegador (el prompt no se sube). Motor dual-mode con 9 tests. Complementa (cara defensiva) el Prompt Injection Lab.
✨ Novedad
Prompt Injection Lab (/lab-llm)
- Nuevo laboratorio interactivo de inyección de prompts (OWASP LLM01), que cierra la trilogía WAF → SQLi → LLM. Hablas con un chatbot guardián simulado (sin LLM real, todo en el navegador) que esconde un secreto; 4 niveles con una técnica de jailbreak cada uno: petición directa, evasión de filtro de palabras, anulación de instrucciones (roleplay/«ignore previous instructions») e inyección de contexto/delimitadores. Interfaz tipo chat. Motor dual-mode con 6 tests; bandera
FLAG{pr0mpt_1nj3ct10n_pwn3d} (misión OP-LLM-LAB). En catálogo, pa11y y con tarjeta OG propia.
✨ Novedad
Explicación con IA en el Triage de Email (Modelo B: offline)
- El Triage de Email (
/tool-eml) ahora incluye un panel «✨ Explicación con IA» que traduce el veredicto técnico a lenguaje claro: qué es el correo, por qué cada señal detectada es sospechosa y qué hacer. Modelo B (sin coste, sin clave, offline): la explicación NO se genera en vivo; se compone en el navegador desde una base de conocimiento pre-generada con Claude (assets/data/ai-phishing.json) a partir de las señales que ya detecta el motor. Tu correo nunca sale de tu navegador.
- Cascada de respuesta: párrafo holístico para el conjunto exacto de señales → composición (intro por veredicto + explicación por señal) → si no hay datos, quedan las señales deterministas. Precacheado por el service worker (
ce-v7) para funcionar sin conexión.
- Infra:
eml-triage.js ahora etiqueta sus 29 señales con id estable; nueva capa ai-phishing.js (dual-mode, 8 tests, incl. guard de cobertura 29/29 en CI); scripts/ai-phishing-gaps.php reporta señales sin explicación para ampliar la base en local («entrenar» = crecer el JSON con Claude, commitear).
✨ Novedad
SQL Injection Lab (/lab-sqli)
- Nuevo laboratorio interactivo de inyección SQL por niveles, complementario del WAF Bypass Lab. 4 niveles con backend simulado en el navegador: (1) bypass de login booleano (
' OR 1=1), (2) comentar la comprobación de contraseña (admin'--), (3) UNION para exfiltrar la tabla users, (4) blind basada en tiempo (SLEEP/WAITFOR). Muestra la query reconstruida en cada intento. Motor dual-mode con 7 tests (node --test); bandera FLAG{bl1nd_sql1_pwn3d} (misión OP-SQLI-LAB). En catálogo, pa11y y con tarjeta OG propia.
scripts/gen-og.php acepta ahora un 2º argumento de slug (gen-og.php tools lab-sqli) para regenerar una sola tarjeta OG sin tocar las ~120.
✨ Novedad
guard de regresión «herramienta muerta»
- Nuevo test estático (
tests/php/tool-engine.test.php, en el paso bloqueante del CI) que recorre las 51 tools del catálogo y verifica que cada una: existe como fichero, referencia su motor JS ($pageScripts o <script src=…assets/js/…>) y que cada .js referenciado existe en disco. Habría cazado el caso auditor/bloodhound. El suite PHP pasa de 46 a 234 aserciones.
📝 Blog
nuevo artículo: cómo se evade un WAF
- «Cómo se evade un WAF (y por qué un blocklist casi nunca basta)» (bilingüe, Red Team · Web), enlazado con el WAF Bypass Lab, el mutador tool-waf y el post de encoding. Las cuatro evasiones clásicas (XSS sin
<script>, codificación URL, comentarios/mayúsculas en SQLi, doble codificación), por qué una lista negra siempre tiene huecos, y qué sí cierra el agujero (consultas parametrizadas, output encoding + CSP, canonicalizar rutas). Con tarjeta OG propia.
✨ Novedad
Laboratorio nuevo: WAF Bypass Lab
- Nuevo laboratorio interactivo (
/lab-waf): evade un WAF por niveles. Cuatro niveles, cada uno enseña una técnica de evasión — XSS sin <script> (manejadores on…), path traversal URL-encoded, SQLi sin espacios (comentarios //) y doble codificación — con un motor cliente que comprueba si tu payload pasa el filtro y cumple el objetivo. Al superar los 4, revela una flag que suma al ranking (OP-WAF-LAB). Motor dual-mode con 7 tests (tests/lab-waf.test.js: por nivel, naïve bloqueado vs evasión resuelta). Añadido al gate de a11y; complementa el mutador tool-waf.
✨ Novedad
write-ups de las 3 misiones nuevas
- El archivo de write-ups (
/missions/writeups) suma las guías de OP: SECRET_LEAK, OP: X509 y OP: CRAWLER, con el mismo formato de pistas escalonadas (pista → solución paso a paso → flag oculta en spoiler). Cada guía dirige a la herramienta que la resuelve: Escáner de Secretos + Multi Decoder, Decodificador de Certificados X.509 y Analizador robots.txt. Ya son 20 write-ups.
✨ Novedad
3 misiones CTF nuevas (atadas a las herramientas recientes)
- Tres misiones nuevas en el Centro de Operaciones (
/missions), cada una se resuelve con una herramienta del propio sitio: OP: SECRET_LEAK (un .env filtrado con un token codificado → Escáner de Secretos + Multi Decoder), OP: X509 (un certificado TLS interceptado con la flag oculta en el sujeto → Decodificador de Certificados X.509) y OP: CRAWLER (un robots.txt que expone un directorio sensible sin querer → Analizador robots.txt). Flags validadas en servidor (api/validate-mission.php, hash_equals, puntos solo tras acierto). El contador del hub pasa de 17 a 20 misiones.
🧰 Herramienta
Analizador de robots.txt / security.txt
- Nueva tool en «OSINT y reconocimiento»: Analizador robots.txt / security.txt (
/tool-robots). Del robots.txt saca user-agents, reglas y sitemaps, y marca las rutas sensibles que revela sin querer (admin, backup, .git…) — recon puro, recordando que no es control de acceso. Del security.txt (RFC 9116) valida Contact y Expires (obligatorios), avisa si está caducado y resalta lo que falta (Canonical, Encryption, firma PGP). 100% en el navegador. Dual-mode con 5 tests (tests/webfiles.test.js); añadida al gate de a11y; SoftwareApplication/FAQ/OG automáticos.
🧰 Herramienta
Decodificador de Certificados X.509
- Nueva tool en «Gestión de vulnerabilidades»: Decodificador de Certificados (
/tool-cert). Pega un certificado PEM y lo desmonta con un parser ASN.1/DER propio (sin librerías ni CDN): sujeto, emisor, validez (y si está caducado, aún no válido o autofirmado), número de serie, algoritmo de firma, tipo y tamaño de clave (RSA/EC) y los SAN. Avisa de firmas SHA-1 y claves RSA < 2048. 100% en el navegador (el certificado no se sube). Dual-mode con 3 tests (tests/cert-decode.test.js) validados campo a campo contra openssl x509. Añadida al gate de a11y; SoftwareApplication/FAQ/OG automáticos. Complementa el Analizador TLS/SSL (que valida cadena/host en vivo).
📝 Blog
nuevo artículo: los secretos en git no se borran, se rotan
- «Un secreto en git no se borra: se rota (y por qué)» (bilingüe, Blue Team · DevSecOps), enlazado con el Escáner de Secretos: por qué borrar el commit no borra el secreto (historial, forks, bots que lo usan en minutos), por qué lo primero es rotar/revocar (no borrar), los límites de reescribir el historial (
git filter-repo/BFG), dónde se esconden (.env, logs de CI) y cómo prevenirlo (.gitignore, pre-commit hooks, push protection). Con tarjeta OG propia.
🧰 Herramienta
Escáner de Secretos
- Nueva tool en «Defensa y SOC»: Escáner de Secretos (
/tool-secrets). Pega código, un .env o un log y detecta credenciales filtradas: claves de AWS, tokens de GitHub/Slack/Stripe, claves de API de Google, claves privadas (PEM), JWT, asignaciones tipo password="…" y cadenas de alta entropía. Se ejecuta 100% en el navegador (el texto no se sube) y los hallazgos se muestran enmascarados para no reexponer el secreto. Dedup por valor (una regla específica gana a las genéricas) y orden por severidad. Dual-mode con 6 tests (tests/secret-scan.test.js: patrones, entropía, redacción, dedup) y añadida al gate de a11y. SoftwareApplication/FAQ/OG automáticos por el catálogo.
✨ Novedad
«Novedades» más clara y de cara al usuario
- La página
/novedades deja de ser un muro técnico: ahora destaca lo que le importa a un visitante —herramientas y funciones nuevas, artículos del blog, cambios de la portada— con un chip de categoría por entrada (🧰 Herramienta, 📝 Blog, ✨ Novedad, 🎨 Portada), y agrupa el detalle técnico (tests, rendimiento, accesibilidad, SEO, CI, refactors) en un desplegable colapsado al final. La fuente única sigue siendo CHANGELOG.md: solo cambia la presentación (clasificación por el prefijo de cada entrada).
📝 Blog
3 artículos nuevos (JWT, Sigma, subdomain takeover)
- «JWT: los 3 fallos que veo en cada pentest» (Red Team · Web) → enlaza el Decodificador JWT: por qué un JWT se lee sin clave, y
alg:none, secreto HMAC débil y confusión de algoritmo (RS256→HS256).
- «De un log a una detección que funciona en cualquier SIEM» (Blue Team · Detección) → enlaza Sigma Forge: anatomía de una regla Sigma, del log a la regla y control de falsos positivos; encaja con el perfil KQL/Sentinel.
- «Subdomain takeover: cómo un CNAME olvidado acaba siendo tuyo» (Red Team · OSINT) → enlaza el detector de Subdomain Takeover, el post de reconocimiento pasivo y el DNS Lookup.
- Los tres bilingües (ES/EN), con tarjeta OG propia y registrados en
lib/posts.php. link-check 0 rotos.
🎨 Portada
contadores del hero: animación restaurada + centrado
- Al reenfocar la portada se perdió la animación de los contadores; restaurada la cuenta ascendente (0 → valor) que entra al cargar vía
IntersectionObserver y respeta *reduce-motion* (si está activo, muestra el valor real ya renderizado, sin flash). Además la fila (.hero-stats) ahora va centrada bajo el hero (le faltaba justify-content:center).
🎨 Portada
sistema visual unificado (tarjeta + tokens)
- Las tarjetas de la portada hardcodeaban el color (
rgba(0,255,255)), el borde (#222) y el radio (12px) — por eso el cian y los bordes bailaban entre secciones. Ahora todas usan los tokens del sistema (--cyan, --border, --bg-card2, --radius, --cyan-glow), así la home lee como un único sistema. El CSS inline de index.php (featured-card, hero-stats, author-strip, botones) pasa a style.css, y .hp-post se tokeniza también. style.min.css regenerado.
🎨 Portada
reenfoque «producto primero»
- La portada dejaba de decir qué es CyberEscudo: eran 12 secciones compitiendo (portfolio + toolkit + gamificación + blog + feed). Ahora se centra en el producto: hero limpio (propuesta + contadores) → el Arsenal (5 familias, con su nº de herramientas) como protagonista → 3 laboratorios destacados → últimos del blog → strip fino de autor → footer.
- Nada se pierde ni queda huérfano: el perfil profesional se concentra en
/sobre-mi (con los testimonios movidos allí), los proyectos guiados + manuales pasan a la nueva página /proyectos (enlazada desde el menú «Aprender»; el catálogo index#manuals → proyectos), y el feed de amenazas a /threat-intel. Se quita el *glitch* del título; el mesh 3D se mantiene. link-check 0 rotos.
📝 Blog
nuevo artículo: CVSS v3.1 vs v4.0
- «CVSS v3.1 vs v4.0: qué cambia y cómo puntuar bien» (bilingüe), enlazado con la Calculadora CVSS: el impacto sobre dos sistemas (vulnerable/posteriores) en vez del *Scope*, la separación de complejidad (AC) y requisitos del ataque (AT), User Interaction None/Passive/Active, Temporal→Threat, entorno con *Safety* para OT/ICS, las Supplemental informativas, y el mensaje de fondo: la nota base no es el riesgo (nomenclatura CVSS-B/BT/BE/BTE). Cierra enlazando con «Qué CVE parchear primero» (EPSS/KEV). Con tarjeta OG propia.
🧰 Herramienta
soporte de CVSS v4.0 (completo)
- La Calculadora estrena selector de versión (3.1 ↔ 4.0). El modo v4.0 cubre los cuatro grupos de la spec: Base (AV, AC, AT, PR, UI; C/I/A del sistema vulnerable y de sistemas posteriores), Threat (madurez del exploit), Entorno (requisitos C/I/A del negocio + las 11 métricas base modificadas, incluido el valor *Safety* de MSI/MSA) y Supplemental (Seguridad, Automatizable, Recuperación, Densidad de valor, Esfuerzo de respuesta y Urgencia del proveedor). Las Supplemental son informativas: no cambian la nota, solo enriquecen el vector para priorizar. El cálculo lo hace la implementación de referencia de FIRST/Red Hat (BSD-2-Clause), self-hosted en
assets/js/cvss40-engine.js (sin CDN, atribución intacta); la UI (cvss4.js) solo le pasa el vector. El pegado de vectores tolera mayúsculas/minúsculas y normaliza los valores con capitalización fija de *Provider Urgency* (Clear/Green/Amber/Red). Tests (tests/cvss4.test.js, 4 casos) contra vectores conocidos (10.0, 9.3, 0.0), de entorno (MSI:S válido) y Supplemental (no alteran la nota; valor inválido rechazado).
🧰 Herramienta
ahora completa (Temporal + Entorno)
- La Calculadora CVSS v3.1 (
/tool-cvss) deja de dar solo la base: calcula también las puntuaciones temporal (madurez del exploit, remediación, confianza del informe) y de entorno (requisitos C/I/A del negocio + métricas base modificadas), mostrando las tres notas y su severidad a la vez, según la spec de FIRST.org (incluida la corrección 0.9731/^13 del impacto modificado con *scope* cambiado). El vector incluye ahora temporal/entorno y se puede pegar uno para cargarlo. Tests ampliados a 7 casos (invariantes «todo X ⇒ entorno = base» + valores calculados a mano).
🧰 Herramienta
Calculadora CVSS v3.1
- Nueva tool en «Gestión de vulnerabilidades»: Calculadora CVSS v3.1 (
/tool-cvss). Se construye el vector métrica a métrica (radios accesibles, con cada opción explicada) y da en vivo la puntuación base, su severidad (con color) y el vector; también carga un vector pegado. Cálculo según la especificación de FIRST.org (incluye el *roundup* oficial), 100% en el navegador. Hecha en dual-mode con 5 tests (tests/cvss.test.js: vectores conocidos, incl. Log4Shell = 10.0) y añadida al gate de a11y (pasa). SoftwareApplication/OG automáticos por el catálogo.
- De paso: generadas las tarjetas OG que faltaban para
tool-email-check y tool-email-records (usaban la genérica).
📝 Blog
nuevo artículo: encoding no es cifrado (cripto/CTF)
- «Encoding no es cifrado: reconocer y encadenar decodificaciones» (bilingüe), enlazado con el Multi Decoder y Base64: encoding vs cifrado vs hashing, reconocer Base64/hex/URL/ROT por su forma, decodificar capas anidadas, cuándo hay XOR, y el ángulo de triaje de malware. Estrena la categoría Cripto/CTF del blog. Con tarjeta OG propia.
📝 Blog
nuevo artículo: reconocimiento pasivo (OSINT)
- «Reconocimiento pasivo: la huella pública de tu dominio (y cómo reducirla)» (bilingüe), enlazado con OSINT Quick Recon y DNS Lookup: DNS/MX/SPF, Certificate Transparency (subdominios), WHOIS, Wayback y Shodan, y cómo reducir la superficie. Estrena la categoría OSINT del blog. Con tarjeta OG propia.
📝 Blog
nuevo artículo: persistencia por cron
- «Persistencia por cron: cómo la usan los atacantes y cómo cazarla» (bilingüe), enlazado con el Analizador Cron: los seis sitios donde vive el cron, patrones de tarea maliciosa (
curl|sh, Base64, /dev/tcp, @reboot), enumeración de todos los crontabs y vigilancia con auditd. Doble ángulo red/blue, con tarjeta OG propia.
📝 Blog
nuevo artículo de Red Team
- «Reverse shells: cómo estabilizar la TTY (de shell tonta a terminal real)» (bilingüe), enlazado con el Generador de Reverse Shells: PTY con
pty.spawn, el baile de stty raw -echo, la vía cómoda con socat, y una sección para el Blue Team (cómo se detecta). Diversifica el blog hacia la ofensiva. Con tarjeta OG propia.
🎨 Portada
fondo del hero en 3D (canvas) reactivo al scroll
- El hero de la home estrena un fondo 3D en
<canvas> sin librerías (~5 KB, assets/js/hero-3d.js, diferido y solo en la home): las luces del .ce-mesh (teal/púrpura/azul) ahora con profundidad y paralaje, más una rejilla que fluye con el scroll y se desvanece al pasar el hero para no estorbar la lectura.
- Progressive enhancement: si no hay
<canvas>/2D o se pide menos movimiento (ajuste del SO o el toggle «reducir animaciones» del footer), no se activa y se mantiene el .ce-mesh CSS de siempre como fallback (conmutación en vivo). aria-hidden, pointer-events:none, se pausa con la pestaña oculta y respeta la CSP (JS de primera parte por asset(), sin CDNs).
📝 Blog
nuevo artículo: CSP en la práctica
- «CSP en la práctica: una Content-Security-Policy que frena el XSS sin romper tu web» (bilingüe), enlazado con el Constructor de CSP y el Analizador de Cabeceras HTTP. Explica el error del
'unsafe-inline', la política estricta por nonces con 'strict-dynamic', las directivas que importan y el despliegue en Report-Only. Con tarjeta OG propia.
📝 Blog
compartir artículos
- Al pie de cada post: compartir en LinkedIn y X (enlaces nativos, sin JS de terceros ni tracking) y copiar enlace. Verificado (URLs correctas y copiado al portapapeles).
📝 Blog
UX de lectura
- Nuevo
reading.js: barra de progreso de lectura arriba y botón «volver arriba» que aparece al bajar, solo en artículos. Respeta reduce-motion.
📝 Blog
últimos artículos en la home
- Nueva sección «Del blog / Últimos artículos» en la portada (las 3 entradas más recientes de
lib/posts.php), para dar salida al blog, mejorar el enlazado interno y la señal de frescura.
📝 Blog
páginas de etiqueta (/blog/tag/<slug>)
- Archivo por tema: cada etiqueta (Blue Team, KQL, Email, SSH, SOC…) tiene su página con los artículos que la llevan, título, canonical y meta propios. Nuevos helpers
ce_tag_slug(), ce_blog_tags(), ce_posts_by_tag(); reescritura en .htaccess; nube de etiquetas enlazable en el listado del blog. Una etiqueta inexistente devuelve 404. En el sitemap.
✨ Novedad
página pública de novedades (/novedades)
- Renderiza el
CHANGELOG.md como página («Novedades» / «What's new») con un conversor Markdown mínimo y seguro (escapa todo; solo permite enlaces http/https/mailto/relativos). Da transparencia de lo que se va añadiendo. Enlazada en el footer y en el sitemap.
📝 Blog
nuevo artículo de blog
- «Triaje en un SOC: los primeros 15 minutos de una alerta» (ES/EN, ~8 min): método validar → enriquecer → acotar → decidir, con enlaces a Defang, Info de IP, DNS, Triage de Email y «¿Me afecta a mí?». Tarjeta OG propia + related posts.
🧭 Web
breadcrumbs visibles
- Migas de pan en la UI de herramientas/labs (Inicio › Arsenal › Herramienta) y artículos (Inicio › Blog › Artículo), en
templates/header.php, acompañando al BreadcrumbList del JSON-LD que ya existía. Se calculan de las mismas variables, así que no hay que tocar cada página; solo aparecen donde corresponde.
📝 Blog
nuevo artículo de blog
- «Endurecer SSH: checklist práctico contra fuerza bruta y accesos indebidos» (ES/EN, ~9 min): claves vs contraseñas, sin root, restricción de usuarios, fail2ban, cifrados modernos, 2FA y monitorización, con comandos. Enlaza el Analizador SSH; tarjeta OG propia y related posts automáticos.
🎨 Portada
above-the-fold
- El héroe ahora ofrece una CTA principal a las herramientas («Explorar herramientas» →
/tools, botón relleno) junto a la secundaria de scroll a proyectos: la acción primaria es el arsenal, que es el valor del sitio.
- El enlace de «Últimas Alertas y 0-Days» usaba manejadores
onmouseover/onmouseout en línea que el CSP bloqueaba (hover muerto + incumplía la regla de no-inline-handlers): movido a una clase con :hover. La home queda con 0 handlers en línea. Verificado: sin overflow a 375 px.
✨ Novedad
panel privado (/panel) y mecanismo de secretos
- Nueva página
/panel protegida por contraseña para ver suscriptores y mensajes de contacto sin abrir los .log a mano. Login con hash bcrypt (password_verify), CSRF, session_regenerate_id, rate-limit (8 intentos/15 min), noindex + no-store, fuera del sitemap y bloqueada en robots.txt. Todo lo mostrado va escapado con e().
- Secretos al estilo del proyecto: nuevo
secrets.php (raíz, gitignored y bloqueado por .htaccess) que devuelve un array, leído con el helper ce_secret(). Plantilla en secrets.example.php y generador de hash scripts/make-admin-hash.php.
✨ Novedad
newsletter con envío real vía Brevo (opcional)
- La suscripción del blog puede integrarse con Brevo (
lib/newsletter.php): si hay claves en secrets.php, se dispara un doble opt-in por su API (el contacto se añade a la lista solo al confirmar por email) y el mensaje pasa a «revisa tu correo». Sin claves, sigue funcionando en local (log + respaldo). El registro local es siempre la fuente de verdad, aunque falle el ESP.
✨ Novedad
artículos relacionados en el blog
- Al pie de cada post, bloque de artículos relacionados (hasta 3, por tags compartidos y luego por fecha) vía
templates/related-posts.php + helper ce_related_posts(). Mejora el enlazado interno y el tiempo en página. Verificado en un post (3 tarjetas relevantes, dentro del artículo antes de la firma).
✨ Novedad
suscripción por email (newsletter) con consentimiento
- Formulario opcional en el blog para avisar de nuevas herramientas/artículos. Opt-in RGPD: casilla de consentimiento obligatoria + enlace a la política de privacidad. Mismo blindaje que el formulario de contacto: CSRF, honeypot, rate-limit por sesión (30 s) y validación de email. Los correos se guardan (en minúsculas, con timestamp y consentimiento) en
assets/data/subscribers.log, que va gitignored y bloqueado por .htaccess (\.log$); dedupe básico para no duplicar. La política de privacidad explica qué se guarda y cómo darse de baja. Verificado: alta válida se guarda, y se rechazan sin consentimiento/CSRF/email inválido.
📝 Blog
nuevo artículo de blog
- «Qué CVE parchear primero: prioriza con EPSS, KEV y contexto» (ES/EN, ~8 min): por qué el CVSS no basta y cómo combinar EPSS (probabilidad de explotación), la lista KEV de CISA y la exposición real. Enlaza el Buscador de CVE, «¿Me afecta a mí?» y «¿Qué parcheo primero?», con su tarjeta OG propia.
📝 Blog
nuevo artículo de blog
- «Cómo analizar un correo de phishing (.eml) paso a paso» (ES/EN, ~9 min): triaje sin abrir enlaces/adjuntos (cabeceras,
Authentication-Results, remitente, URLs, adjuntos, IOCs y reporte). Enlaza cinco herramientas (Triage de Email, Auditor SPF/DMARC/DKIM, Generador, Defang, Hash) con su tarjeta OG propia.
✨ Novedad
imágenes Open Graph por artículo de blog
- Los posts ahora se comparten con su propia tarjeta OG (1200×630, acento violeta para distinguirlas de las de herramientas) en vez de la portada genérica.
scripts/gen-og.php admite modo (all/tools/blog) para no regenerar las de herramientas; genera assets/img/og/blog-<slug>-<lang>.png desde lib/posts.php. templates/header.php las usa en og:image/twitter:image y en BlogPosting.image. Verificado (carga 200, se ve bien).
📝 Blog
nuevo artículo de blog
- «SPF, DKIM y DMARC en la práctica» (ES/EN, ~10 min): guía de despliegue en el orden correcto, errores típicos (límite de 10 consultas,
p=none eterno, alineación) y cómo verificarlo, enlazando con el Generador y el Auditor de email. Aprovecha el nuevo schema BlogPosting. Añadido a lib/posts.php (aparece solo en el listado, el RSS y el sitemap).
✨ Novedad
Panel de estado de servicios (/estado)
- Nueva página que muestra en vivo la disponibilidad y latencia de las 11 APIs públicas que usan las herramientas, agrupadas por área (vulnerabilidades, DNS/red, OSINT): EPSS, CISA KEV, CIRCL, NVD, OSV, Cloudflare DoH, ipwho.is, Shodan InternetDB, NetworkCalc, HIBP y Wayback.
- Endpoint
api/status.php: sondea todas en paralelo (curl_multi, con reserva por streams si no hay cURL) midiendo código HTTP y tiempo de respuesta. Sin entrada de usuario (destinos fijos → sin SSRF); descarga mínima (HEAD/Range: 0-0) y caché en disco de 120 s para no golpear las APIs ni servir de proxy gratuito. Semáforo por servicio (operativo/degradado/caído) y global.
- Página
estado.php + assets/js/status.js: semáforo global, tarjetas con latencia y código, leyenda, autorrefresco cada 60 s y botón manual. Enlace en el footer; entra sola en el sitemap. La caché del panel va al .gitignore.
- Verificado en vivo: 11/11 operativas (206/200), 3 grupos, sin overflow a 375 px, 0 errores de consola, refresco manual OK, canonical correcto.
✨ Novedad
Auditor SPF / DMARC / DKIM (tool-email-check)
- Nueva herramienta que consulta en vivo (por DNS-over-HTTPS de Cloudflare, ya en el CSP) el SPF, DMARC y DKIM de cualquier dominio y los audita con nota A–F, fallos concretos y cómo arreglarlos. Cierra el ciclo con el generador.
- SPF: detecta registro duplicado (permerror), mecanismo
all (-/~/?/+) y cuenta las consultas DNS de nivel superior (límite 10).
- DMARC: puntúa la política
p (none/quarantine/reject), rua, pct y sp.
- DKIM: autodetecta la clave probando ~15 selectores comunes (default, google, selector1/2, k1/2, s1/2…) o el que indiques; reconoce claves directas y delegadas por CNAME, y avisa de claves revocadas o de 1024 bits.
- Nota global ponderada (DMARC 40%, SPF 35%, DKIM 25%) y enlace directo al generador para corregir.
- Todo en el navegador (el dominio consultado es público). Entrada en catálogo (Defensa y SOC), 4 FAQ bilingües (+FAQPage) y conmutador. Catálogo: 45 → 46 herramientas.
- Verificado en vivo: SPF/DMARC de google.com y DKIM de microsoft.com (CNAME + clave directa) detectados, 0 errores de consola, sin overflow a 375 px.
✨ Novedad
Generador SPF / DMARC / DKIM (tool-email-records)
- Nueva herramienta defensiva que construye los tres registros DNS anti-suplantación de correo, con explicación y avisos, 100% en el navegador. Empareja con el DNS Lookup y el Triage de Email.
- SPF: marca tus servicios de envío (Google Workspace, Microsoft 365, Amazon SES, SendGrid, Mailgun, Mailchimp, Zoho, Brevo), tu MX, IPs e includes extra, y elige el mecanismo
all. Cuenta las consultas DNS en vivo y avisa si supera el límite de 10 (permerror), si usas +all/?all, o si el SPF rechazaría todo tu correo.
- DMARC: política
p (none/quarantine/reject), sp, rua/ruf, pct y alineación estricta (adkim/aspf=s), con la guía de subir gradualmente de none → quarantine → reject y de configurar rua.
- DKIM: genera un par de claves RSA (2048/1024) con WebCrypto en el propio navegador; publica la pública como TXT
v=DKIM1; k=rsa; p=… y entrega la privada en PEM PKCS8 para el servidor de correo. La privada no sale del navegador y se pierde al recargar (aviso incluido). Avisa de dividir el valor si supera 255 caracteres.
- Entrada en el catálogo (categoría Defensa y SOC), 4 FAQ bilingües (+FAQPage), y en el conmutador de herramientas. Catálogo: 44 → 45 herramientas.
- Verificado en vivo: SPF/DMARC en tiempo real, keygen DKIM válido (record 410 chars + PEM con
END PRIVATE KEY), 0 errores de consola y sin overflow a 375 px.
✨ Novedad
FAQ en todas las herramientas (cobertura 100%)
- Completadas las FAQ de las 21 herramientas que faltaban (sigma-forge, arsenal SOC, log analyzer, sandbox, threat-map, buscador CVE, escáner, bastioning, code auditor, SSH, HTTP builder, C2, BloodHound, takeover, puertos, recon, reporte OSINT, cloud enum, MAC, chmod, regex). Ahora 44/44 herramientas tienen acordeón visible + nodo
FAQPage en el JSON-LD (133 preguntas en total, bilingües). Los simuladores se marcan como tales con honestidad. Verificado el render y el schema en varias.
✨ Novedad
anti-spam del formulario de contacto
- El formulario ya tenía CSRF, honeypot y rate-limit, pero se colaba spam de bots que hacen un GET normal y mandan un email válido (p. ej. mensajes en cirílico con asuntos plantilla tipo «wrote about your the prices»). Añadido en
contacto.php:
- Puntuación de spam por contenido (
ce_contact_spam_score): penaliza alfabetos no latinos (cirílico, árabe, CJK, hebreo, griego — la audiencia es ES/EN), enlaces y TLDs de spam, plantillas/keywords típicas (SEO, backlinks, «your the prices», crypto, casino…) y nombres tipo «RobertDag». A partir de 4 puntos se descarta.
- Trampa de tiempo: un envío en menos de 3 s desde que se muestra el formulario se descarta (los bots envían al instante).
- Descarte silencioso: ante spam se finge éxito (para no dar pistas al bot) y se registra en
assets/data/contact-spam.log (cubierto por .gitignore) por si quieres revisarlo.
- Verificado: el spam real (cirílico) puntúa 17 y un spam SEO en inglés 15 (ambos descartados); mensajes legítimos ES/EN puntúan 0 (pasan). Sin falsos positivos.
✨ Novedad
herramientas nuevas
- Analizador TLS/SSL (
tool-tls.php + lib/tls.php + assets/js/tls.js): introduces un host y el servidor sondea su TLS (con stream_socket_client + contexto SSL, ya que el PHP del VPS no tiene cURL) y devuelve un informe con nota A–F: protocolo y cifrador negociados, validez y días restantes, coincidencia de nombre (CN/SAN con comodines), tipo y tamaño de clave, algoritmo de firma, autofirmado, cadena de confianza y SANs. Comprobaciones por severidad (caducado, no-válido-aún, sin coincidencia, autofirmado, firma SHA-1/MD5, RSA<2048, protocolo antiguo, caducidad próxima). Protección anti-SSRF: resuelve el host y rechaza IPs privadas/reservadas (RFC1918, loopback, link-local incl. 169.254.169.254, CGNAT), conectando solo a IPs públicas con SNI. Verificado: github.com → A, expired.badssl.com → F (caducado), self-signed → F, 127.0.0.1/metadata → bloqueado.
- Constructor de CSP (
tool-csp.php + assets/js/csp.js): arma una Content-Security-Policy directiva a directiva (un clic por fuente + hosts propios), genera la cabecera y el <meta>, y audita en vivo las decisiones débiles (script-src 'unsafe-inline' → aviso ALTO, comodines, y directivas de refuerzo ausentes: object-src, base-uri, frame-ancestors, default-src). Con presets Estricta/Moderada/Bloqueo total. Empareja con el analizador de cabeceras. Verificado: preset estricto sin avisos, unsafe-inline dispara el aviso correcto.
- Defang / Refang de IOCs (
tool-defang.php + assets/js/defang.js): neutraliza indicadores (http→hxxp, .→[.], @→[@]) de forma consciente del contexto —solo dentro de URLs/IPs/dominios/correos, sin destrozar la prosa—, los restaura, y extrae y clasifica los IOCs de un texto (URLs, IPv4, dominios, correos, MD5/SHA-1/SHA-256, CVE) con copia por categoría. Utilidad diaria de SOC. Verificado en ambos sentidos.
- Conversor de Timestamps (
tool-epoch.php + assets/js/epoch.js): epoch Unix ↔ fecha con autodetección de unidad (s/ms/µs/ns por magnitud), hora en UTC y tu zona, tiempo relativo, día del año y semana ISO, botón «Ahora» y conversión inversa desde datetime-local. Verificado: 1700000000 s y su equivalente en ms/ns dan el mismo instante; fecha→epoch respeta la zona.
- Añadidas al catálogo (
tool-tls y tool-csp en Gestión de vulnerabilidades; tool-defang en Defensa y SOC; tool-epoch en Cripto y datos), con lo que el Arsenal pasa a 44 herramientas.
✨ Novedad
SEO / datos estructurados
templates/header.php: cuando la página es herramienta o lab, el grafo JSON-LD incluye ahora un nodo SoftwareApplication (gratuita, isAccessibleForFree, Offer a 0 €, categoría SecurityApplication, autor y editor enlazados) y un BreadcrumbList (Inicio › Arsenal › Herramienta), generados desde lib/catalog.php. Objetivo: resultados enriquecidos en Google. Verificado: aparece en páginas de herramienta y no en las de contenido.
- Preguntas frecuentes (FAQ) por herramienta: nueva fuente única
lib/faqs.php (preguntas reales, bilingües) que alimenta a la vez un acordeón visible (templates/faq.php, renderizado sobre «Sigue por aquí» vía templates/related.php) y un nodo FAQPage en el JSON-LD del header, cumpliendo el requisito de Google de que el contenido esté visible. Cobertura: 23 herramientas, 71 preguntas (las de la ronda 4 y nuevas, más passcheck, passgen, jwt, cidr, ip, url, multidecode, hashcrack, eml, patch-priority, exposure, dns, headers, base64, hash). Estilos .ce-faq en style.css. Verificado: acordeón y FAQPage presentes en las herramientas con FAQ y ausentes en las que no la tienen; ampliable añadiendo una clave a lib/faqs.php.
- Imágenes OpenGraph por herramienta: generador CLI
scripts/gen-og.php (GD + FreeType) que produce una tarjeta de marca 1200×630 por herramienta/lab en español e inglés (assets/img/og/<slug>-<lang>.png, 110 imágenes) — fondo con malla cian/violeta, kicker, título, categoría y sello «gratis · sin registro». templates/header.php usa la tarjeta del slug e idioma actuales en og:image/twitter:image (con og:image:alt = título) y cae a la portada de marca en las páginas de contenido. Verificado: la herramienta sirve su tarjeta es/en (200, image/png) y las páginas de contenido conservan la portada.
✨ Novedad
capa de plataforma (favoritos, recientes y «enviar a»)
- Nuevo módulo
assets/js/ce-platform.js (window.CE) que da memoria compartida a las herramientas sin backend, en localStorage: favoritos (★) y herramientas recientes (registra la visita si la página es tool/lab). Clave = URL del catálogo, leída del JSON #ce-catalog que ya emitía el servidor.
- Enviar a… (encadenar herramientas): al seleccionar texto en la salida de una herramienta aparece un chip flotante «→ Enviar a…» que abre la herramienta elegida con el valor ya cargado (vía
?in=, que el receptor prefija y luego limpia de la URL). Cubre 12 herramientas encadenables (Base64, Multi Decoder, JWT, hashes, Defang, Epoch, URL, CIDR, Cron, WAF, Fortaleza). Verificado de punta a punta: Defang → Conversor de Timestamps con el valor puesto.
- Catálogo (
tools.php + assets/js/catalog.js): estrella ★ en cada tarjeta (no navega al pulsarla) y dos tiras arriba, ★ Favoritos y 🕘 Recientes (con «limpiar»), que se re-renderizan en vivo vía el evento ce:platformchange.
- Paleta ⌘K (
assets/js/command-palette.js): al abrir sin escribir, ahora encabeza con tus Favoritos y Recientes antes de las destacadas.
templates/footer.php: el JSON del catálogo incluye ahora el type de cada elemento (para filtrar recientes a solo herramientas) y carga ce-platform.js antes de la paleta.
- Descripciones del catálogo actualizadas tras la ronda 4 (revshell 30+ lenguajes/bind/msfvenom, WAF mutador+biblioteca, wordlist con leet/afijos/política, cron con auditoría).
✨ Novedad
paleta de comandos (⌘K)
- Buscador global (
assets/js/command-palette.js) que se abre con ⌘K, Ctrl+K, la tecla / o el botón del menú. Busca por nombre, descripción y etiquetas ignorando acentos —«contrasena» encuentra «Contraseñas» y «kev» encuentra «¿Qué parcheo primero?»—, agrupa por familia y se recorre con el teclado. Con 40 herramientas, llegar a cualquiera cuesta dos teclas.
- Se alimenta del catálogo incrustado desde
lib/catalog.php, así que una herramienta nueva aparece sola y sigue funcionando sin conexión gracias al service worker.
✨ Novedad
¿Me afecta a mí?
- Nueva herramienta (
tool-exposure.php + assets/js/exposure.js + api/exposure.php): pegas el fichero de dependencias de tu proyecto y sale qué actualizar esta semana y a qué versión exacta. Cruza OSV.dev (correspondencia versión↔vulnerabilidad y versión que corrige) con EPSS y el catálogo KEV, y ordena por explotación real en vez de por número de avisos. Con el ejemplo de Java: 5 dependencias → 1 exige acción en 24–48 h (Log4Shell, CVSS 10, EPSS 99,9%+, en KEV y usada en ransomware) → «actualiza a 2.15.0»; las otras 59 vulnerabilidades pueden esperar.
- Nueve formatos de inventario reconocidos automáticamente, todos analizados en el navegador:
package-lock.json (v1/v2/v3), package.json, pip freeze, requirements.txt, go.mod, composer.lock, Gemfile.lock, Cargo.lock, mvn dependency:list y la salida de dpkg -l (con selector de distribución). Al servidor solo viajan pares nombre+versión: ni el código ni las rutas del proyecto salen del equipo.
assets/js/vuln-score.js: motor de priorización compartido por «¿Qué parcheo primero?» y la herramienta nueva, para que la tesis de puntuación (KEV 40 · EPSS 35 · CVSS 15 · contexto ±33) esté definida una sola vez. Incluye el cálculo de la puntuación CVSS 3.x a partir del vector, validado contra cinco valores oficiales conocidos.
lib/vuln-intel.php: EPSS y KEV extraídos a una librería común, usada por los dos endpoints.
lib/http.php gana ce_http_post_json(), con el mismo respaldo por streams para servidores sin cURL.
✨ Novedad
PWA
- Compartir un correo desde el móvil al Triage de Email (Web Share Target): con la app instalada, «Compartir» en Gmail u Outlook ofrece CyberEscudo; el service worker recibe el
.eml, lo deja en una caché local y la herramienta lo analiza sola al abrirse. El correo no sale del dispositivo y el contenido se borra en cuanto se consume.
- Atajos en el icono (
shortcuts): pulsación larga sobre el icono de la app → Triage de Email, Arsenal o Retos CTF.
✨ Novedad
Añadido
- Sigma Forge (
tool-sigma-forge.php + assets/js/sigma-forge.js): el camino inverso al habitual — de un evento de log real a la regla de detección. Autodetecta el formato (XML del Visor de eventos, JSON/JSON lines de EDR, texto de un 4688 en español o en inglés, key=value y access logs de nginx/apache), normaliza los campos al estándar Sigma, avisa de los que son volátiles y romperían la detección, recorta el fragmento estable de la línea de comandos, sugiere técnicas MITRE ATT&CK a partir del propio evento y transpila a KQL, SPL, Lucene, EQL y regla de Wazuh. Todo en el navegador.
- ¿Qué parcheo primero? (
tool-patch-priority.php + assets/js/patch-priority.js + api/patch-priority.php): triage masivo de CVEs que cruza CVSS, EPSS de FIRST y el catálogo KEV de CISA, ponderando la explotación real por encima de la gravedad teórica, y ajusta el orden con el contexto del activo (exposición, criticidad, ausencia de parche, control compensatorio). Devuelve cubos P1–P4 con plazo, justificación por CVE y exportación a Markdown y CSV. Extrae los CVE de cualquier pegote, incluido un CSV de escáner.
scripts/sync-kev.php: ETL diario del catálogo KEV de CISA a assets/data/kev-cache.json (solo los campos necesarios: 1.656 CVEs en 365 KB en vez de 1,5 MB).
- Triage de Email (.eml) (
tool-eml.php + assets/js/eml-triage.js): analiza un correo completo en el navegador y emite un veredicto de phishing estilo SOC L1. Parser MIME propio (RFC 2045/2047/2231), cadena Received en orden cronológico con retardos, SPF/DKIM/DMARC con comprobación de alineación frente al dominio del From, 20+ heurísticas puntuadas (typosquatting por distancia de Levenshtein, nombre visible falso, punycode, Reply-To desviado, hilo de respuesta falso, doble extensión en adjuntos, texto del enlace que no coincide con el destino), extracción de IOCs defanged, desenvoltura de Safelinks / Proofpoint / Barracuda / Google, consulta KQL de caza para Defender-Sentinel e informe en Markdown descargable. El correo nunca sale del navegador.
- Entrada de las tres herramientas nuevas en los mega menús Plataforma y Arsenal, en el menú móvil y en el selector de
tools.php.
✨ Novedad
Añadido
- Feed RSS del blog (
blog/feed.php → /blog/feed.xml) con <link rel="alternate"> en el header.
- Segundo artículo del blog: hardening de M365 anti-phishing.
- Sistema de testimonios (
lib/testimonials.php), que solo se muestran si hay datos reales.
✨ Novedad
Añadido
- Fase 3: leaderboard con backend seguro anti-trampas (BD
cyberescudo_ctf, puntuación server-side, validación en api/validate-mission.php).
- Fase 4: reto diario rotativo y laboratorio de detección Sigma (
lab-sigma.php).
✨ Novedad
Añadido
- Fase 1 (conversión reclutadores): PWA (
manifest.webmanifest, sw.js, offline.php), página de contacto, JSON-LD y bloque de disponibilidad.
- Fase 2: writeups con pistas escalonadas, blog técnico y proyectos en profundidad (deep dives, 36 fichas en
projects/).
✨ Novedad
Añadido
- Backup del progreso del usuario, proyectos destacados con contadores, tarjetas Open Graph y soporte de
reduce-motion.
✨ Novedad
Añadido
- ~40 herramientas
tool-*.php: DNS, headers, wordlist, multidecode, cloud enum, log analyzer, payload, OSINT report, takeover, SSH, ports, escáner, phishing sandbox, generador de informes, CVE tracker, mapa de amenazas, MITRE ATT&CK mapper, SOC Arsenal, entre otras.
- 27 retos CTF (
ctf/ctf-01 … ctf-27) con validación y writeups.
- Misiones (
missions/), skill tree, barra de progreso y easter egg de terminal en el footer.
- Soporte multiidioma
es / en (lang/).
- SEO,
404.php, búsqueda rápida y modo PDF del CV.
---
✨ Novedad
Añadido
- Versión inicial del sitio:
bootstrap.php, plantillas (header, footer, content-page), estilos y primeras herramientas.
Ver detalle técnico y correcciones
Corregido — Espera del examen en singular
- Con menos de un minuto de espera, el aviso de «muchos exámenes seguidos» decía «dentro de 1 minutos»; ahora concuerda (ES/EN).
Corregido — La redirección de «.php» se comía los dominios que lo llevan
- La regla de
.htaccess que quita el .php de las URL buscaba «.php» en toda la petición, consulta incluida: /email/www.php.net/ o ?domain=www.php.net acababan en «www». Ahora solo mira el final de la ruta (comprobado en el Apache local con las reglas reales); lo encontró la revisión de la fase 14.
Corregido — El catálogo KEV llevaba parado desde el 3 de agosto
kev-cache.json estaba versionado en git y ningún cron lo actualizaba en producción (si lo hubiera hecho, habría chocado con el git pull del despliegue), así que el Threat Radar llevaba siete semanas sin marcar como explotadas las vulnerabilidades que CISA añadió desde entonces. Ahora sigue el patrón del radar: scripts/sync-kev.php escribe un fichero vivo fuera de git (con escritura atómica) y en git va una semilla, kev-cache.seed.json (actualizada hoy: catálogo 2026.09.23, 1721 vulnerabilidades), que se lee mientras el cron no haya corrido. El radar, «¿Qué parcheo primero?», «¿Me afecta a mí?» y el boletín leen lo mismo (ce_kev()).
Corregido — El auditor penalizaba los SPF que delegan con redirect= (metodología 2026.2)
v=spf1 redirect=spf.iberdrola.com perdía 40 puntos por «no termina en un mecanismo all», cuando sin all manda el SPF del destino (RFC 7208 §6.1), y el de Iberdrola termina en -all. Salía con B; ahora, A. Es un patrón habitual en empresas grandes.
- Los dos motores (navegador y servidor, con la paridad bloqueándolos) resuelven el destino: se evalúa su
all; si no publica un único SPF, es permerror; si no se puede consultar, no se penaliza y se avisa. Con all propio, redirect= se ignora, como dice el RFC. Siete casos nuevos en los fixtures de paridad.
- Cambia la nota, así que la metodología sube a
2026.2: la caché de /email/ se invalida sola.
Corregido — El minificador de CSS rompía selectores
scripts/minify.php quitaba el espacio delante de «:», y .x :is(a) (un descendiente) pasaba a ser .x:is(a) (el propio elemento): once selectores de cinco hojas dejaban de aplicarse en el .min.css, entre ellos estilos :focus-visible. Como asset() sirve el .min cuando no es más viejo que el .css, lo probado en local podía no ser lo servido.
- Ahora solo se quita el espacio de después, y un test comprueba que cada
.min.css es exactamente lo que genera el minificador desde su .css.
Corregido — En el informe
- Textos que la revisión encontró exagerados: «reciben informes DMARC» pasa a «piden» (dos de ellas mandan los informes a otro dominio que no lo autoriza, RFC 7489 §7.1); «no reciben correo» pasa a «sin registro MX»; «ninguna conexión con sus servidores» pasa a «nunca nos conectamos» (Google sí consulta sus DNS autoritativos), y la nota suma también la higiene de DMARC. La tarjeta para redes, que suele verse sola, dice «en el dominio de su web corporativa», y su texto alternativo describe la cifra.
- Los enlaces dentro de los párrafos se distinguían solo por el color: el control de accesibilidad del CI (axe,
link-in-text-block) los rechazó en las dos versiones. Ahora van subrayados.
build-ibex35-report.php escribe primero todos los temporales y renombra después, con reintentos: en Windows, el servidor de desarrollo retenía el CSV que había servido y la instantánea quedaba a medias.
- Las fechas sin hora («2024-07-22») salían con un día menos: se interpretaban como medianoche en hora de Madrid y se formateaban en UTC. Visto en la captura de la propia página, antes de publicar; ahora hay un test para ese caso.
scripts/gen-og.php no tenía candado de CLI. El .htaccess ya bloquea /scripts/; ahora tiene la segunda capa, como los crons.
Corregido — El aviso de «sin SPF» exageraba
- Ante un dominio sin SPF, el auditor decía «cualquiera puede enviar correo en nombre de tu dominio». No siempre es verdad: si el DMARC —propio o heredado del dominio principal— está en
reject, ese correo se rechaza igual (www.paypal.com es el caso real). Ahora dice lo que de verdad falta —los receptores no pueden comprobar qué servidores envían en nombre del dominio— y qué publicar: un SPF con los servidores de envío o, si el dominio no envía correo, v=spf1 -all. La nota no cambia: un nombre sin SPF sigue siendo un hueco.
- La misma frase estaba en el analizador DNS, en el triaje de correos y en la explicación pregenerada del explicador de phishing («…cualquiera puede enviar en su nombre sin dejar rastro»); corregida en los cinco sitios (con el texto adaptado al triaje y al explicador, donde el dominio es el del remitente), y un test que recorre los cinco impide que vuelva. +1 test JS (697 → 698), guard falsificado 4/4.
Corregido — Las auditorías de dominios .com daban 403 en producción
- La verificación en producción, nada más desplegar la fase 6, lo encontró:
/email/google.com daba 403, y con él cualquier .com —cyberescudo.com, casi todo el IBEX 35—, mientras /email/example.org o /email/ejemplo.es daban 200. El 403 no salía del código (la página nunca lo devuelve) sino de una capa anterior: todo encaja con el cortafuegos de aplicaciones del servidor (ModSecurity), que bloquea las rutas cuyo último tramo acaba en una extensión restringida, y .com lo es (ejecutables de MS-DOS). En local no hay cortafuegos que lo reprodujera.
- La URL pasa a terminar en barra (
/email/google.com/): el último tramo queda vacío y no hay extensión que bloquear —comprobado en producción: con barra, 200—. Sin barra o en mayúsculas redirige con 301 a esa forma, conservando los selectores. El enlace «Compartir» del auditor, los códigos de la insignia y el radar ya la usan. La configuración de seguridad del servidor no se ha tocado.
- Verificación: +3 tests PHP (1596 → 1599), guards falsificados 5/5; en local, 301 a la forma con barra (con los selectores), 200 con barra y el enlace «Compartir» del auditor con barra.
Seguridad — Todos los crons rechazan ejecutarse desde la web
/scripts/ ya devuelve 403 por .htaccess (verificado en producción), pero dos de los cinco crons no tenían la segunda capa —el candado PHP_SAPI !== 'cli' que sí llevaban los otros tres—: sync-wp-vulns.php y sync-kev.php. El primero es justo el que tiene detrás una API con límite de peticiones y una descarga de 117 MB: si algún día el servidor no aplicara el .htaccess, cualquiera podría dispararlo.
- Añadido el candado a los dos, y un test que recorre todos los
scripts/sync-*.php y update-*.php: el quinto cron sin candado lo encontró el propio test, no yo. Si alguien añade un sexto sin él, se verá en rojo.
Corregido — Fallos de la auditoría de correo que habría publicado el informe
- Las claves Ed25519 y las RSA de 2048 bits que acababan en
p== se marcaban como «clave débil»: el heurístico buscaba un patrón en vez de medir la longitud de la clave.
-ALL en mayúsculas se leía como «no hay mecanismo all»; los mecanismos SPF no distinguen mayúsculas (RFC 7208).
- El radar no unía los fragmentos de un TXT partido (los SPF largos de las grandes empresas llegan así).
- La media se calculaba en coma flotante (
90 * 0.35 = 31,4999…) y el redondeo dependía del lenguaje; ahora es entera.
- A una A con
~all y DKIM no detectado se le ofrecía ayuda («¿te ayudo a arreglarlo?»): el DKIM no detectado ya no cuenta para esa oferta.
- Los subdominios salían sin DMARC:
www.paypal.com sacaba F aunque hereda el p=reject de paypal.com. Un receptor que no encuentra _dmarc en el subdominio aplica el del dominio organizativo, con su sp= si lo tiene (RFC 7489 §6.6.3). Ahora el auditor sube por los dominios superiores (hasta 5, nunca hasta el TLD), aplica el primero que encuentra y dice de cuál viene.
- Con dos registros DMARC se leía el primero, y podía salir una A. Los receptores no aplican ninguno: ahora es F, con su propio motivo.
- Cualquier CNAME en un selector DKIM contaba como «DKIM delegado 👌», aunque el destino no existiera o no tuviera clave. Ahora solo cuentan las claves que existen, y un CNAME colgante se avisa: si otro pudiera registrar ese destino, firmaría correo como tu dominio.
- Retirar bien una clave DKIM restaba 40 puntos y salía como hallazgo grave:
p= vacío es justo como el RFC 6376 dice que se revoca una clave. Ahora se reconoce y no penaliza; lo que sí se avisa es una clave sin p= (registro roto).
- Un fallo del DNS se puntuaba como «sin registros»: si el resolvedor devolvía SERVFAIL, el auditor del navegador calculaba una nota sobre una consulta fallida. Ahora dice que no ha podido consultar y no pone nota (el servidor responde 503).
Seguridad
- Las cachés por dominio (
assets/data/badge-cache/ y la nueva email-cache/) no tenían ninguna regla que las bloqueara desde la web: si un dominio se había escaneado, su JSON se podía pedir directamente. No encontré ninguno en producción para demostrarlo (los que probé daban 404: no existían), pero nada lo impedía. Ahora devuelven 403 (.htaccess, router y lib/web-path.php).
- Todo lo que llega del DNS se escapa en HTML y SVG: quien controla un dominio puede publicar
<script> en su SPF. Demostrado con un caso malicioso en los tests.
- Techo global de auditorías nuevas (300 por hora para todo el sitio): el límite por sesión se salta sin cookies, y cada auditoría nueva lanza una veintena de consultas DNS. Lo que está en caché no cuenta y no se guarda nada de quien consulta; al superarlo, 503 con
Retry-After.
- Verificación: +432 tests PHP y +60 JS (1159 → 1591, 637 → 697); guards falsificados 36/36 (15 + 17 de la segunda ronda + 4 del sitemap y el smoke test); revisión separada por un revisor sin el contexto de la implementación, cuyos hallazgos son las cinco últimas correcciones de arriba, la insignia con selectores y el techo global; auditoría delta de toda la fase sin bloqueantes nuevos (4 avisos de tamaño). Comprobado en vivo:
www.paypal.com hereda y sale D (no F), un dominio con SERVFAIL no recibe nota ni en el navegador ni en el servidor, y página e insignia coinciden con ?s=.
Corregido — Los crons del radar y de Wordfence escriben de forma atómica y dicen por qué fallan
- Threat Radar llevaba 21 días sin actualizarse en producción. Al crear por fin el cron en Plesk, el script hizo todo el trabajo (263 candidatas, CIRCL, NVD, 215 s) y falló en el último paso con un «no se pudo escribir» sin motivo. La causa:
threat-radar.json está ignorado por git y el que había en el servidor lo creó otro usuario a mano; el cron, que corre como el usuario del dominio, no podía sobrescribirlo.
- Ahora
sync-threat-radar.php y sync-wp-vulns.php escriben temporal + rename(): nadie lee un JSON a medias mientras el cron corre, y rename solo necesita permiso sobre el directorio, no sobre el fichero anterior. Si aun así falla, el mensaje trae el motivo del sistema («Permission denied»…) y qué hacer — la lección del fallo silencioso, otra vez.
assets/data/*.tmp ignorado en git por si un temporal quedara huérfano. Patrón verificado en local (escribe, reemplaza, no deja basura, y el fallo explica el motivo).
Corregido — El filtro anti-spam dejaba pasar el spam de marketing, y el panel no enseñaba lo que descartaba
- El filtro de contenido del formulario cazaba los mensajes en cirílico pero puntuaba 0 los de marketing en inglés («video promotion», «Instagram followers») y el galimatías de teclado. Probado contra los 7 mensajes de spam reales que llegaron al panel: antes cazaba 3, ahora 7 de 7.
- Dos reglas nuevas: frases de plantilla de marketing (exactas a propósito — «video» o «results» sueltas no puntúan) y texto sin vocales (por debajo del 18 % no es ningún idioma; el texto real ronda el 40-55 %). 0 falsos positivos en 6 mensajes legítimos diseñados para parecerse al spam: siglas
SPF DKIM DMARC v=spf1, un cliente que menciona «your video on LinkedIn» de pasada, un presupuesto.
- La función sale de la página a
lib/contact-spam.php para poder probarla sin renderizar nada. +16 tests; guards falsificados, incluida la mutación realista de «simplificar las frases a palabras sueltas», que es como suelen romperse estos filtros.
- Nueva sección «Spam descartado» en el panel. Hasta ahora lo descartado se registraba en un log que nadie miraba: si el filtro se equivocaba con un cliente real, era invisible. Ahora se ve, con el motivo (honeypot, tiempo o contenido).
Corregido — Tres cosas que los datos del panel tenían mal
- La portada salía partida en dos filas (
/ y /index): el logo de la cabecera enlazaba a /index. Ahora enlaza a / y el contador normaliza /index por si queda en marcadores.
- «Últimos 7 días» eran 8: la consulta usaba
>= hoy − 7, que incluye 8 días, mientras el periodo anterior tenía 7. El porcentaje de variación partía con sesgo al alza. Las cuatro ventanas miden ahora exactamente lo que dicen.
- El «+298 %» era ilusión de arranque: el contador empezó el 5 de septiembre, así que «los 7 días anteriores» tenían 3 con datos reales. Ahora solo se compara cuando el periodo anterior está completo en los datos (
ce_stats_prev_complete()); hasta entonces, «sin comparación». +14 tests (1101 → 1131).
Corregido — El panel confundía páginas vistas con visitantes
- La tarjeta ponía «visitas» sobre una cifra que son páginas vistas. Al compararla con los «usuarios» de Google Analytics parecía que el contador inflaba el tráfico —632 frente a 5 al día—, cuando lo que pasaba es que son unidades distintas: quien entra y mira 12 páginas suma 12 aquí y *un* usuario en GA.
- Lo agravaba que la navegación interna se guarda con origen vacío (correcto: venir de tu propia web no es una fuente de tráfico) y en la tabla sale como
(directo). En los datos reales eran 588 de 632: casi todo tránsito interno, no llegadas.
- Nueva tarjeta «desde fuera» (
ce_stats_external()): páginas vistas con origen externo, la única cifra de aquí comparable con los usuarios de GA. En los datos reales daban ~9/día frente a los 5 de GA — esa diferencia de 1,8× es la gente que rechaza el banner, que es exactamente lo que este contador existe para ver.
- La tarjeta grande pasa a llamarse «páginas vistas», la sección a «Tráfico» y las columnas a «Vistas», con un aviso explicando que
(directo) incluye la navegación interna. +5 tests PHP (1096 → 1101).
Corregido — El banner de cookies era ilegible sobre el hero
- El aviso de consentimiento usaba
var(--bg-card2), que es rgba(255,255,255,0.022): un 2,2 % de blanco y sin backdrop-filter. Sobre el hero, los botones «Ver mi trabajo», «CV», «LinkedIn» y la píldora de Recruiter se veían a través del texto legal, que quedaba mezclado con ellos.
- No es solo estética: es el aviso que hay que leer para decidir sobre el consentimiento. Un texto que depende de lo que tenga detrás no cumple su función.
- Ahora usa el patrón que el propio sitio ya aplica a los paneles flotantes: fondo
rgba(10,14,20,.97) con blur(16px), borde cyan tenue y sombra más marcada, para que se lea como una capa por encima y no como parte de la página.
- Contraste medido en el navegador sobre el fondo compuesto: 15,50 el texto, 10,78 el enlace «Más info» y 8,58 el botón «Rechazar» — todos muy por encima del 4,5 que exige AA. Comprobado también en móvil (375 px): el banner se apila sin desbordar.
style.min.css regenerado.
Cambiado — Hero de la home: recuperado el eslogan «Protegiendo el Mundo Digital»
- Ajuste de marca: el titular sigue siendo el nombre (identifica de inmediato a quien selecciona), y vuelve el eslogan «Protegiendo el Mundo Digital» / «Protecting the Digital World» como línea bajo él, con el degradado original en la palabra destacada. Se reaprovecha
$t['hero']['title1']/titleHighlight (bilingüe ES/EN), que seguían en lang/. Híbrido: identidad + eslogan, sin perder ninguno de los dos.
Cambiado — Espaciado del pill «Recruiter» en el hero
- El botón «👔 ¿Recruiter? Vista de 60 segundos» de la portada baja un poco: más aire entre la fila de CTAs y el pill (margen superior 0.2rem → 1.1rem), para que respire y no quede pegado a los botones.
Mejorado — Descubribilidad: nav resalta el portfolio + LinkedIn en el footer
- Cierre de la Fase 1: el dropdown «Sobre Mí» ya resalta (
is-hot) las piezas de empleabilidad —Proof of Work y Recruiter Mode, que salen primero— igual que hacen los menús de Arsenal/Labs/Aprender; antes ese bucle ignoraba el flag hot y quedaban visualmente planas. Así, desde cualquier página interior, un técnico de selección ve el camino a la evidencia.
- LinkedIn ahora está en el footer (junto a Contacto), presente en todo el sitio; hasta ahora solo aparecía en el hero de la portada. Sin tocar el resto de la barra ni CTAs (el hero ya tenía CV·LinkedIn·Contacto·«Ver mi trabajo»·pill Recruiter).
Cambiado — Home reorientada a empleabilidad (analista + proof of work)
- El hero de la portada pasa de «producto primero» (vende las tools) a «analista primero»: nombre + rol (Analista de Ciberseguridad · SOC · Detection Engineering · Sentinel · KQL · eCPPT) y el lema *«no lo digo: lo construyo»*, con 4 CTAs de empleabilidad — Ver mi trabajo, CV, LinkedIn, Contacto. Primer paso de la Fase 1 del plan de evolución.
- Métricas del hero ahora reales y derivadas del catálogo, cada una clicable hacia su evidencia: 65 herramientas → /tools, 14 laboratorios → /tools, 22 técnicas ATT&CK → /cobertura-attack, 26 artículos → /blog (nada inventado; es la filosofía «demuestra, no digas»). Se mantiene la estética terminal y las secciones que son la prueba (Arsenal, Threat Radar, labs, blog). Bilingüe ES/EN, sin cambios de rendimiento.
Mejorado — Auditor SPF/DMARC/DKIM: plan de migración a p=reject
- El auditor (
/tool-email-check) ya no solo pone nota: tras la consulta en vivo, traza el plan de migración por fases hasta DMARC p=reject sin romper el correo legítimo, según en qué punto esté el dominio. Publicar p=none + rua (monitorizar) → alinear SPF/DKIM de todos los remitentes → p=quarantine; pct=25 → 50 → 100 → p=reject; sp=reject. Cada paso trae el registro DMARC a publicar (copiable, con el dominio real en el rua), cuánto esperar y qué vigilar en los informes (ojo a reenvíos/listas que rompen SPF → DKIM/ARC). Si el dominio ya está en p=reject, lo felicita y avisa si sp= es más débil.
- Es la otra cara del Triage de Email: uno analiza el correo que recibes, este protege el que envías. No es tool nueva: se potencia la existente reutilizando su consulta DoH y el parseo de DMARC. Motor puro
dmarcPlan(state) en assets/js/email-check.js (dual-mode; devuelve los pasos que faltan; i18n y el registro en el DOM). +5 tests (email-check 6 → 11): sin DMARC, none, quarantine escalonado por pct, reject/sp débil, y sp=reject en el registro final.
Cambiado — Consolidación de los flagships «Cómo funciona» (DRY, sin cambios visibles)
- Los tres flagships de la serie «Cómo funciona» (
/handshake-tls, /login-con-google, /viaje-de-una-consulta-dns) repetían, casi calcados, el mismo stepper (acumular pasos, colapsar los pasados con [hidden], contador aria-live, flechas del teclado, foco al encabezado activo) y el mismo CSS del carril (actores/carril/cabecera/cuerpo/controles). Se ha consolidado en una sola pieza reutilizable.
- Nuevo motor
assets/js/flow-stepper.js: función genérica ceFlowStepper() + clamp pura flowClampStep() (dual-mode, exportada para test). Auto-inicializa cada .flow-demo en DOMContentLoaded, así que las páginas no necesitan <script> inline (compatible con la CSP con nonce).
- Nueva hoja
assets/css/flow.css con clases genéricas .flow-* (carril + shell de página), cargada por $pageStyles. Lados neutros .a/.b/.both y modificador .act.
- Migradas las tres páginas a
.flow-* y data-flow; eliminados los <style> inline y los tres JS por página (handshake-tls.js, oauth-flow.js, dns-flow.js).
- Sin regresiones: DOM, ARIA, orden de encabezados (h1→h2→h3), mejora progresiva (sin JS = artículo) y comportamiento del stepper idénticos. Verificado en navegador (las 3, escritorio y móvil) y con axe (0 errores en las 3). No cambian slugs/URLs, catálogo ni la tira «Nuevas».
- Tests: los tres tests de clamp se unifican en
tests/flow-stepper.test.js; guards PHP actualizados (flow-stepper.js, flow-lane). Suite intacta: PHP 576, JS 443.
Mejorado — tool-url: radiografía de engaño (anti-phishing)
- La herramienta de URL (
/tool-url) ya no solo codifica/decodifica y desmonta: ahora emite un veredicto de engaño (🔴 alto / 🟠 sospechoso / 🟡 menor / 🟢 sin señales) con el dominio real y una lista de señales explicadas. Detecta: truco de credenciales @ (el destino real es el host, no lo de antes de la @), subdominios que imitan marcas (paypal.com.sitio-falso.ru → dominio real sitio-falso.ru), Punycode/homográficos, TLD abusados (.zip, .tk…), acortadores, redirecciones abiertas (parámetros que llevan otra URL), IP como host, puerto no estándar, doble codificación, HTTP y parámetros de rastreo.
- No se creó una tool nueva: se potenció la existente (ya hacía decode + desglose + Punycode) reutilizando su parseo. Motor puro
urlDeception()/registrableDomain() en assets/js/url-tools.js (dual-mode, devuelve señales por código; el DOM traduce ES/EN). Veredicto por peso de señales; el rastreo es informativo (no eleva el riesgo).
- De paso, arreglado que los botones «Ejemplo» y «Limpiar» no funcionaban con el campo vacío. +7 tests JS (
tests/url-tools.test.js, → 11): truco @, marca en subdominio, punycode/acortador/TLD/redirección, rastreo=info, URL limpia y entrada inválida.
Corregido — a11y intermitente en «Anatomía de un ataque»
- El revelado por scroll de las tarjetas usaba
opacity (0→1). axe evalúa el contraste de un texto a opacity<1 y lo contaba como fallo, de forma intermitente según cuántas tarjetas se hubieran revelado al correr el gate. Ahora el revelado es solo transform (las tarjetas son siempre opacas) → contraste determinista, mismo efecto visual (deslizamiento). Añadido de paso un cross-link recíproco de la Anatomía → Cobertura ATT&CK.
Eliminado — Asset sin usar logo-cyberescudo.png
- Se borra
assets/img/logo-cyberescudo.png (497×502, ~201 KB): no lo referencia ningún fichero (el logo del JSON-LD usa icon-512.png, la tarjeta OG es og-cover.png y el logo de la nav/footer es texto/SVG). Ya se había quitado del precache del service worker (ce-v6); esto completa esa limpieza eliminando el fichero del repo. El máster vectorial de marca sigue en logo-bisel.svg.
Cambiado — Blog: cross-link del post de CSP al evaluador
- El artículo «CSP en la práctica» ahora enlaza también al evaluador de CSP (
/tool-csp-eval) en su sección «Verifícala» (ES + EN), cerrando el último hueco de linking del lote 2 (crear → verificar cabeceras → juzgar la CSP).
Cambiado — Auditor JWT: lógica pura testeable + confusión de algoritmo y jku/x5u
- El análisis de seguridad de JWT (
jwt-analyze.js) se extrae a una función pura y testeable ce_jwt_audit(header, payload, parts, {now, weakSecret}) que devuelve hallazgos con códigos neutros ({code, sev, data}); el mapa código→texto (i18n) queda en el DOM (patrón dual-mode con separación de i18n). Determinista (se le inyecta now). +12 tests JS (tests/jwt-audit.test.js) → 295.
- Nuevos chequeos: confusión de algoritmo (aviso en algoritmos asimétricos RS/ES/PS: el servidor debe fijar el algoritmo esperado o el token es falsificable) y claves externas
jku/x5u en la cabecera (vector de SSRF e inyección de clave). Se mantienen los existentes: alg=none, secreto débil por diccionario HMAC en el navegador, exp/nbf/iat, kid inyectable, datos sensibles y claims de autorización.
- Comportamiento del navegador intacto (verificado: ejemplo con secreto débil caducado y token RS256+
jku), sin errores de consola.
Corregido — Threat Radar: calidad del snapshot diario (CIRCL 429)
- El ETL (
scripts/sync-threat-radar.php) dejaba >50% del top-40 sin CVSS ni resumen: CIRCL (cve.circl.lu) devuelve 429 ante la ráfaga de 60 peticiones en 6 lotes de 10 en paralelo (su API corta ráfagas de ~>10). El fallback de NVD, topado a 15, no llegaba a cubrir todos los huecos.
- Arreglo híbrido: se estrangula CIRCL (lotes de 6 + pausa de 6 s entre lotes, buen ciudadano; ya recupera de 11 a 23 de 60 aun con la IP penalizada) y se sube el tope de relleno NVD de 15 a 40, de modo que cubra el top-40 completo aunque CIRCL falle del todo. Verificado en el peor caso (CIRCL saturado): el snapshot pasa de 21/40 sin CVSS a 0/40 (0 sin resumen, 0 sin vector). Coste: el ETL diario tarda ~3-4,5 min (irrelevante en un cron). La degradación con gracia sigue intacta (si no hay candidatos, conserva el snapshot anterior).
Cambiado — Dashboard de /ctf/ coherente con el Perfil (Nivel unificado)
- El dashboard de la Zona CTF mostraba un «Rango» calculado por retos resueltos, mientras el Perfil muestra el Nivel por XP: dos escalas para el mismo agente. Ahora el dashboard muestra el mismo Nivel de AgentCore que el Perfil y el rango temático (RECRUIT → OPERATOR → SPECIALIST → GHOST_HACKER) se deriva del nivel, no de los resueltos. Cierra la coherencia iniciada con la unificación de XP. Verificado: con XP 2200 → Nivel 7 en Perfil y en el dashboard.
Cambiado — style.css al formato compacto de la casa (lint limpio)
- Reformateado
assets/css/style.css al estilo de la casa (sin espacio tras : ni ;): 1827 → 0 avisos de lint. Es un cambio solo de whitespace del fuente (no une líneas ni toca valores multi-token, cadenas, url() ni comentarios de código). Verificado con red de seguridad: style.min.css queda byte a byte idéntico al anterior, así que lo que se sirve en producción no cambia. Los 7 comentarios que contenían :/; se reformularon.
Cambiado — Estilos del blog extraídos a blog.css
- El bloque
<style> del artículo (.article), que se duplicaba en los 20 posts (~2 KB por página), se extrae a assets/css/blog.css (copia superset que cubre todas las variantes que había). Se carga solo en el blog mediante un nuevo hook $pageStyles en el header (espejo de $pageScripts): cada post declara $pageStyles = ['blog.css'] antes de incluir el header. Menos peso por página y una única fuente de estilo. blog.min.css generado y formato conforme a la guía de estilo (lint limpio).
Cambiado — XP unificado entre el Perfil y la Zona CTF
- Había dos contadores de XP que divergían: el antiguo
cyber_xp (lo subían solo los retos, desde la terminal) y el de AgentCore (cyberescudo_agent_v1, lo suben solo algunas herramientas y es el que muestra el Perfil). Ahora hay un único contador (AgentCore):
- Resolver un reto/misión suma al mismo XP que ve el Perfil (
grantXP → AgentCore.addXP), con su aviso emergente y subida de nivel.
- El dashboard de
/ctf/ lee el XP de AgentCore (así refleja también el XP ganado con herramientas).
- Migración única e idempotente: el
cyber_xp acumulado se funde en AgentCore la primera vez que se carga la página y se borra la clave antigua (sin doble conteo).
Cambiado — Misiones y CTF unificados en una sola Zona CTF (/ctf/)
/missions/ y /ctf/ se fusionan en una única página (/ctf/ = «Zona CTF»), porque ya eran lo mismo: retos validados en el servidor con submit <ID> <FLAG> que puntúan en el mismo ranking. Ahora conviven los 47 retos (20 misiones guiadas + 27 retos CTF) con:
- Dashboard arriba: anillo de progreso sobre el total (47), XP, rango (RECRUIT → OPERATOR → SPECIALIST → GHOST_HACKER) y contador de resueltos, calculado en el cliente desde
cyber_missions/cyber_xp.
- Filtros combinables por Tipo (Todos · Misión guiada · Reto CTF) y Dificultad (Easy/Medium/Hard/Insane), más toggle de vista Tarjetas ↔ Lista.
- Estado resuelto reflejado en tarjetas y tabla (detección por cualquiera de los IDs del reto).
- Las 20 misiones guiadas pasan a
ce_missions() en lib/ctf-flags.php (fuente única, junto a ce_ctf_series()). missions/index.php ahora hace 301 → /ctf/ conservando el idioma; las páginas de briefing (missions/*.php) siguen intactas.
- Enlaces entrantes actualizados a
/ctf/ (leaderboard, perfil, atajo PWA del manifest) y /missions/ retirado del gate de a11y (ya redirige). Paleta verde HackTheBox (#9fef00), pills de dificultad y a11y (enlaces en prosa subrayados) conservados.
Cambiado — rediseño del Centro de Misiones (estilo HackTheBox)
missions/index.php rediseñado con look HackTheBox en verde neón (#9fef00): cabecera dashboard (anillo de progreso, rango, barra XP, contador), pills de dificultad color-coded (Easy/Medium/Hard/Insane), filtro por dificultad y toggle de vista Tarjetas ↔ Lista (tabla), todo desde una fuente de datos única ($missions, 20 misiones). El contador es ahora dinámico (cuenta las tarjetas; antes fijo en 17). Preserva el marcado de resueltas (.mission-completed) y las stats del agente (XP/rango) del JS existente; el estado resuelto se refleja en ambas vistas. Enlace inline a /ctf/ subrayado (a11y).
Cambiado — CTF unificado: todo server-side y submit ID FLAG
- Migradas las 30 misiones de la serie
ctf/ del validador en cliente (flags hardcodeadas en missions.js, que no puntuaban y quedaban expuestas) al servidor. Nueva fuente única lib/ctf-flags.php (id→flag) que comparten api/validate-mission.php y las páginas ctf/ (muestran submit <ID> <FLAG> con ce_ctf_id_for($flag), solo tras resolver el reto).
- Eliminado el bloque cliente y la ruta
submit FLAG; ahora se exige submit <ID> <FLAG> (puntuación server-authoritative). Las 30 antiguas ya cuentan para el ranking. Guard de cobertura en CI (tests/php/ctf-flags.test.php): toda flag de ctf/ tiene ID en el mapa; IDs y flags únicos.
Corregido — el perfil avisa si el alias no vale para el ranking
profile.php: validación de alias en cliente con el mismo criterio que el servidor (ce_valid_nick: 2-24 caracteres, letras/números/espacio/_ . -). Antes, un alias con caracteres no válidos o longitud fuera de rango se aceptaba en silencio, pero el servidor lo rechazaba (reason: invalid_nick), así que el jugador nunca puntuaba y el perfil le decía «resuelve un reto para entrar» en bucle. Ahora sale un aviso claro y el badge se oculta hasta corregirlo.
Legal/cumplimiento — uso autorizado y divulgación de terceros
- Análisis de legalidad de las 51 herramientas: ninguna ejecuta un acto ilegal. Las «ofensivas» (revshell, waf, wordlist, cloud, hashcrack, c2, bloodhound) son generadores/simuladores en el navegador; el OSINT es pasivo (APIs públicas: Cloudflare DoH, Shodan InternetDB, HIBP con k-anonymity, ipwho.is, archive.org); y los sondeos server-side (TLS y cabeceras) tienen anti-SSRF (rechazan IPs privadas/reservadas/metadatos, conexión fijada a la IP pública validada) y rate-limit.
- Añadido aviso global de uso educativo/autorizado en el pie de página (ES/EN).
- Política de privacidad: nueva sección «Herramientas y servicios de terceros» que divulga qué llamadas hacen algunas tools desde el navegador y que TLS/cabeceras salen del servidor solo hacia IPs públicas.
- Pendiente (requiere datos del responsable): un Aviso Legal (LSSI) con identidad y contacto.
Corregido — dos herramientas más estaban muertas (no cargaban su JS)
- Auditoría funcional de las 51 tools tras el caso del bastionado.
/tool-auditor (Visual Code Auditor) y /tool-bloodhound (BloodHound AD Simulator) tenían su markup (#auditor-code, #bh-canvas) pero no cargaban labs-bundle.js, el motor que las hace funcionar: escanear código o dibujar el grafo no hacía nada. Añadido $pageScripts = ['labs-bundle.js'] en ambas. Verificado en navegador: el auditor detecta 3 vulnerabilidades en la plantilla PHP; el simulador dibuja el grafo AD y añade nodos.
- De paso, el panel de info de bloodhound usaba un icono FontAwesome (que no carga): migrado a emoji (👤/🖥️/👥/🌐). Barrido confirmado: 0 iconos FontAwesome activos en todo el sitio; ningún otro tool sufre el patrón de «JS no cargado» ni el early-return del handler compartido.
Corregido — la herramienta de bastionado no funcionaba
/tool-bastioning estaba muerta: el botón «Generar Hardening» no hacía nada. arch-bastion.js comparte un único handler DOMContentLoaded con el diseñador de arquitectura y salía antes de tiempo (if(!btnGenerate) return;) en cuanto no encontraba #btn-generate — que solo existe en arch-designer, no en la página del tool. Eso impedía que se inicializara el motor de bastionado. Ahora initOSBastion() se ejecuta siempre (tiene su propio guard), antes del early-return. Verificado: el checklist rinde y el script se genera en /tool-bastioning, y arch-designer sigue igual (sin doble init).
Mantenimiento — iconos FontAwesome inyectados por JS
- Sustituidos los
<i class="fas fa-…"> que inyectaban arch-bastion.js, labs-bundle.js y profile.js (FontAwesome no carga en el sitio y salían como huecos vacíos): ahora emoji (✅/⚠️/💀/🖥️/🏅) y, para los spinners de carga, el .cyber-spinner CSS ya existente.
Mantenimiento — barrido de calidad
- Eliminado CSS muerto: sección *About/contact* huérfana (
.about-wrap, .profile-card, .contact-card, .contact-icon/-label/-email) y restos responsive de banners retirados (.banner-skill-tree*, .banner-phishing*, .btn-road, .bg-code, .phishing-btn). Detector de CSS sin uso: de 24 a 14 clases (las 14 restantes son falsos positivos que sí se componen dinámicamente en JS: bg-*, pill-*, status-*, tlp-*).
- Limpiado el selector huérfano
.profile-card, .contact-card del observer de main.js.
- Quitadas dos líneas de depuración (
console.log) del gestor de toasts XP en agent-core.js.
- Los toasts de gamificación (XP/nivel/logro) ya no usan iconos FontAwesome (que no carga y dejaban un hueco vacío): ahora emoji ⚡/🆙/🏆.
- Normalizados a formato compacto varios selectores pequeños en media queries.
style.min.css regenerado.
Seguridad — rate-limit en la API de misiones (endurecimiento)
- El endpoint de validación de flags (
api/validate-mission.php) no tenía límite de envíos: se podían probar flags sin freno. Ahora aplica ce_rate_ok (40 intentos/hora por sesión → HTTP 429), como defensa en profundidad sobre una puntuación que ya era *server-authoritative*. También se añade un límite generoso a player-stats.php (120/h, lectura del ranking) y la cabecera X-Content-Type-Options: nosniff a ambas respuestas JSON. Verificado end-to-end: el envío nº 41 devuelve 429 y una sesión nueva sigue validando sin problema.
Tests — cobertura del backend PHP (nuevo frente)
- Hasta ahora solo el JS tenía tests; el backend PHP no tenía ninguno. Nuevo runner propio (sin composer/phpunit) en
tests/php/ (harness.php + run.php) con 46 aserciones sobre las funciones clave: ce_valid_nick (la barrera anti-XSS/inyección del nick del leaderboard, testeada a fondo), los helpers del catálogo (ce_item/ce_url/ce_title/ce_cat_items/ce_tools + integridad de todos los items), el blog (ce_tag_slug/ce_blog_post/ce_related_posts) y las FAQs (ce_faq + que toda Q&A trae las 4 variantes bilingües). Nuevo paso bloqueante en CI (php tests/php/run.php).
Tools — últimos restos de cian «duro» al token
- 5 herramientas (OSINT Report, BloodHound, Patch Priority, Perimeter Scanner, Sigma Forge) usaban aún el cian puro
#00ffff en su CSS; ahora usan var(--cyan) (el token del sitio, un teal algo más suave). El resto del arsenal ya estaba tokenizado, así que el cian queda uniforme en todas las páginas. Sin cambios de comportamiento (solo color).
Iconos — set antiguo unificado a currentColor
- Los 12 iconos «antiguos» de
icon() (shield, network, scan, code, alert, server, globe, sword, firewall, email, heart, logo) hardcodeaban #00ffff y un tamaño fijo en px. Ahora usan currentColor + 1em como los nuevos: heredan color y tamaño del contexto, así todo el set es coherente (p. ej. en la fila de botones de BloodHound, server ya no desentonaba en color/tamaño frente a user). Los contenedores de catálogo (.card-icon, .manual-cat-icon) fijan color:var(--cyan) + tamaño para conservar el cian y el tamaño de esos iconos. style.min.css regenerado.
Iconos — FontAwesome roto → SVG inline (CSP-safe)
- Bug encontrado: FontAwesome nunca estaba cargado (ni CSS, ni kit, ni fuentes), así que los
<i class="fa…"> de ~10 páginas (nav, /threat-hunting, /tool-*, architecture, profile…) salían invisibles. Sustituidos los 46 usos por SVG inline vía el helper icon(), ampliado con ~19 iconos *line-style* en currentColor (heredan color y tamaño del contexto, sin dependencias ni licencia). El badge de nivel del nav (presente en todas las páginas) también: agent-core.js ahora actualiza solo el número (#agent-nav-lvl) sin reintroducir el <i>. Los emoji del catálogo se mantienen.
CSS — barrido de código muerto + consistencia de tarjetas entre páginas
- Eliminadas ~150 líneas de CSS muerto que dejaron las secciones retiradas de la home:
.hero-alerts-link, .pulse-dot, .cyber-rss-*/.rss-* y todo el bloque .training-* (skill-tree / simulator / arsenal / scanner). Confirmado sin uso en PHP ni JS; @keyframes pulse se conserva (lo usan los retos CTF). style.min.css regenerado.
- La tarjeta de
/casos hardcodeaba el borde (#222) y el fondo (rgba(10,10,15,.5)): ahora usa los tokens (--border, --bg-card2, --radius) como el resto (home, /tools, /proyectos), así el sitio entero lee como un único sistema al navegar entre páginas.
Tests — cobertura del Multi Decoder (códecs + Magia + fuerza bruta)
multi-decode.js pasa a dual-mode: los códecs, el motor de Magia (cadena automática de decodificación hasta 12 capas) y la fuerza bruta (ROT-n y XOR de un byte) salen al scope del módulo; el render queda tras typeof document. tests/multi-decode.test.js (8 casos): los base-N (Base64/32/58, url), texto (hex/binario/decimal/Morse con separador de palabra), cifrados clásicos (ROT13 involutivo, Atbash), rechazo de entradas ajenas, el bonus de score/FLAG_RE, y —lo importante— que la Magia deshace una cadena de dos capas (Hex → Base64) hasta la flag, que ROT-n saca el desplazamiento correcto y que el XOR de un byte recupera la clave. 48 tests en total. Tool reverificada en navegador (Magia, códec manual, ROT y XOR) sin regresión.
Tests — cobertura del Conversor de timestamps y del Analizador de URL
epoch.js y url-tools.js pasan a dual-mode: el núcleo puro (parseEpoch/dayOfYear/isoWeek y encodeComponent/decodeDeep/tryB64/toUnicode) sale al scope del módulo para testearlo sin DOM; el render queda tras typeof document. tests/epoch.test.js (3 casos: autodetección de unidad s/ms/µs/ns por magnitud, unidad forzada, día del año y semana ISO en los límites de año) y tests/url-tools.test.js (4 casos: codificación de componente, doble codificación pelada en capas, base64 legible dentro de un parámetro y Punycode/IDN homográfico). 40 tests en total. Ambas herramientas verificadas en navegador tras el refactor (sin cambios de comportamiento).
Seguridad — revisión del backend CTF/gamificación (sin vulnerabilidades)
- Revisado el backend con estado en servidor/BD (
validate-mission, user-api, leaderboard, player-stats): sin vulnerabilidades reales. SQLi cubierto por *prepared statements* (el único LIMIT va con clamp a entero); XSS del nick bloqueado en 3 capas (regex de ce_valid_nick + e() en salida + badge de perfil solo con enteros del servidor); puntuación server-authoritative (hash_equals en tiempo constante, puntos solo tras flag correcta, INSERT IGNORE idempotente → el cliente no puede inflar); el IDOR de user-api es intencional y aislado (usuarios falsos, sin BD ni PII). Degradación elegante si cae la BD. Sin cambios necesarios.
Tests — cobertura del Auditor SPF/DMARC
email-check.js a dual-mode: auditSpf/auditDmarc/grade puros (hallazgos como datos {sev,es,en}). tests/email-check.test.js (6 casos): mecanismo all del SPF, múltiples registros (permerror), política DMARC (p/rua/pct/sp) y umbrales de nota A–F. 23 tests en total. Tool verificada en navegador (google.com → nota real).
Tests — cobertura ampliada a cron y hash-id
- La red de tests (
node --test, patrón dual-mode) cubre dos herramientas más: Analizador Cron (tests/cron.test.js: expansión de campos con rangos/pasos/listas/nombres, normalización domingo 7→0, y la semántica OR de día-del-mes/día-de-semana) e Identificador de Hashes (tests/hash-id.test.js: clasificación por prefijo y por longitud hex, MD5 vs NTLM, casos desconocidos). 17 tests en total (con CIDR). chmod se deja fuera: su lógica (r/w/x=4/2/1) es trivial y testearla obligaría a recablear el DOM sin ganancia real.
i18n — auditoría de fugas de idioma
- Escaneadas las 174 páginas en modo EN buscando español colado (signos
¿/¡ y palabras de UI inequívocas). Resultado: limpio. La única aparición es /novedades, que renderiza el CHANGELOG (en español por diseño, artefacto de desarrollo); añadida una nota en la intro EN aclarándolo.
Tests — primera cobertura del núcleo de las herramientas
- Arnés de tests con el runner nativo de Node (
node --test, sin dependencias). Primera suite sobre la Calculadora CIDR (tests/cidr.test.js, 6 casos: red/broadcast/máscara IPv4, round-trip y compresión IPv6, límites de prefijo). Para ello cidr-tools.js pasa a dual-mode (núcleo puro exportable en Node + cableado DOM guardado por typeof document), sin cambiar cómo carga la página. Paso Unit tests añadido al CI. Patrón listo para extender a más herramientas.
Accesibilidad — teclado y foco en los widgets interactivos
- Terminal (easter-egg): cierra con
Esc y devuelve el foco al botón que la abre; role="dialog" + aria-label.
- Paleta ⌘K: atrapa
Tab dentro del diálogo (ya tenía role/aria-modal, Esc y retorno de foco).
- Modal de donación (OSINT Report):
role="dialog"/aria-modal/aria-labelledby, cierre con Esc, foco al abrir, retorno al cerrar y trampa de Tab.
- Corregido de paso un bug latente en
hero-3d.js: con viewport de 0 px el degradado del horizonte generaba rgba(…,NaN); ahora render() corta si no hay tamaño.
Rendimiento (LCP) y APIs — auditados, ya sólidos
- LCP: la home ya está optimizada en cliente (CSS minificado sin bloquear render, fuentes precargadas con
swap, JS diferido, sin imágenes pesadas). El ~3,8 s de Lighthouse es TTFB local (XAMPP sin opcache) + throttling, no representativo de producción. Palanca de servidor: confirmar opcache en Plesk.
- APIs externas: los endpoints (
exposure, cve, mac, tls, EPSS/KEV) ya llevan rate-limit por sesión, timeouts, validación de entrada y degradación; KEV cacheado a fichero. Los fetch de cliente (HIBP, ipwho, DoH) van con try/catch. Sin cambios necesarios.
Rendimiento — hero 3D verificado (sin regresión)
- Lighthouse en la home con el fondo 3D nuevo: rendimiento 85 (antes 80), TBT 0 ms, CLS 0, LCP ~3,8 s (igual). El canvas va diferido y no penaliza la carga; confirmado, no supuesto.
Imágenes / PWA — menos peso precacheado + link-check más fino
- El service worker precacheaba
logo-cyberescudo.png (201 KB) que no se renderiza en ninguna página: eliminado del precache (sw.js, versión → ce-v6). Las imágenes de contenido (foto y avatar) ya se sirven en WebP con <picture>; las tarjetas OG y los iconos PWA se mantienen en PNG a propósito.
scripts/link-check.php ahora también comprueba los srcset (cazaría un WebP o *responsive* ausente que antes se colaba).
SEO — auditoría de datos estructurados
- Revisado el JSON-LD del sitio:
SoftwareApplication (gratis, isAccessibleForFree, offers 0 €), BlogPosting (imagen, fechas, autor, migas, keywords), Organization/Person/WebSite/ItemList/SearchAction — ya completo. Se decide no añadir FAQPage/HowTo: Google deprecó sus resultados enriquecidos en 2023 (FAQ solo para webs gov/salud), así que serían *cruft* sin beneficio para este sitio.
Calidad — barrido de auditoría y limpieza de CSS muerto
- Auditoría con los scripts internos (seguridad, enlaces, CSS sin usar). Sin vulnerabilidades reales: los 142 avisos del
security_audit son falsos positivos esperables en una web de herramientas de seguridad (payloads y ejemplos de código vulnerable como *contenido*); los ficheros que tocan BD no dieron ni un hallazgo (consultas preparadas). Sin enlaces rotos reales.
- Eliminado CSS muerto: 61 clases sin uso en ningún php/js/html (87 reglas, −324 líneas;
style.min.css −27%). Se conservan las familias que se construyen dinámicamente en JS/PHP (pill-*, status-*, tlp-*, bg-{SEVERIDAD}). Quitado también un comentario muerto que apuntaba a una imagen inexistente.
Accesibilidad — gate ampliado a 10 plantillas + arreglos reales
- El chequeo axe/pa11y del CI pasa de 4 a 10 páginas: home, catálogo, herramienta, post, índice de blog, etiqueta, formulario de contacto, contenido, lab y hub de misiones (todas las plantillas distintas).
- Corregidas las *violations* que destapó: contraste bajo en
.blog-soon y estilos inline #333/#666 sobre fondo oscuro (soc-simulator, missions); y enlaces en prosa distinguibles solo por color (consentimiento del newsletter, leaderboard) ahora subrayados. El pulso de .status-live pasa de animar la opacidad —que hacía el contraste no determinista— a un *glow* estable.
scripts/router.php emula ahora también el rewrite /blog/tag/<slug> de .htaccess, para poder auditar esa plantilla en CI.
CI — chequeo de enlaces internos rotos
- Nuevo
scripts/link-check.php: recorre todas las páginas, extrae sus enlaces internos (<a>, <link>, <script>, <img>), resuelve relativos y URLs limpias, y falla ante cualquier destino interno ≥400. Autónomo (PHP + cURL, sin dependencias nuevas de CI). Añadido como paso tras el smoke-test. Estado: 725 destinos, 0 rotos.
i18n — autodetección de idioma en la primera visita
- Sin
?lang= y sin cookie previa, el idioma se elige por Accept-Language del navegador (empieza en EN si el navegador lo prefiere; si no, ES). La elección manual sigue mandando. Los bots reciben ES (idioma canónico) para no confundir canonical/hreflang. Verificado: navegador EN→EN, ES→ES, Googlebot→ES.
Accesibilidad — chequeo en CI (pa11y/axe) ahora bloqueante
- El chequeo axe/pa11y (
.pa11yci.js) pasa de informativo a bloqueante: si aparece una violation real de a11y (contraste medible, roles, labels…), el build falla.
- Al medir la «deuda de contraste» pendiente se vio que no eran fallos: los ~100 avisos eran resultados
incomplete de axe («no puedo medir el contraste porque hay un degradado detrás / un elemento superpuesto»), que pa11y contaba como error. Se auditó a mano todo el texto sobre degradados (peor stop del gradiente compuesto sobre --bg) y todo pasa AA con holgura (p. ej. estrella de favoritos 8,4:1; banner de cookies 8–12:1; hero 5,9–16,5:1; hero-highlight con degradado recortado 5,7:1 sobre umbral 3:1 de texto grande).
- Por eso los
incomplete se degradan a *warning* (levelCapWhenNeedsReview) y no tumban el build; las violations reales siguen bloqueando. Versión de pa11y-ci fijada (@4.1.1) para que el gate no cambie solo, y el paso falla si no encuentra Chrome en el runner (sin fallback silencioso).
- Correcciones de contraste previas que se mantienen: separadores del footer, contadores del catálogo (
.cat-chip .n, .cat-group-head .n), estrella de favoritos y textos del banner de cookies.
SEO — sitemap con etiquetas e imágenes
- El
sitemap.xml incluye ahora las URLs de etiqueta y la extensión de imágenes (image:image): cada herramienta/lab/artículo declara su tarjeta OG y la home su portada (63 imágenes), para mejorar la indexación visual.
CI — mantenimiento
permissions: contents: read (mínimo privilegio), timeout-minutes en el job y Node 22 para los checks de JS. Excluidos del smoke-test/sitemap los handlers que necesitan parámetros (tag.php) o no son páginas.
Seguridad — security.txt (RFC 9116)
- Añadido
/.well-known/security.txt (y /security.txt) para divulgación responsable: generado por security-txt.php con Expires dinámico (+1 año, nunca caduca), Contact (email + formulario), Canonical, Preferred-Languages y Policy. Muy apropiado para una web de ciberseguridad. Servido por reescritura en .htaccess; fuera del sitemap.
Páginas de error 403 y 500 útiles
- 403 ahora está al nivel del 404: estética de terminal, mensaje de acceso denegado, chips de herramientas a las que sí se puede entrar, buscador (⌘K) y «Volver a la Base». Reutiliza clases compartidas (
nf-*) movidas a style.css.
- 500 rediseñada manteniéndola autónoma (sin
bootstrap.php, porque puede ser lo que ha fallado): estética de terminal en línea, bilingüe y con enlace de contacto.
Rendimiento — Core Web Vitals reales (RUM) a GA4
- Nuevo
assets/js/web-vitals.js (global): mide LCP, CLS, INP (aprox.) y TTFB de visitantes reales con PerformanceObserver y los envía a GA4 (evento web_vitals con nombre, valor y rating good/ni/poor) al ocultarse la página. Solo envía con consentimiento (usa window.gtag). Así se ve el rendimiento real de campo, no la estimación throttled de Lighthouse.
SEO — ItemList del arsenal
/tools emite ahora un ItemList con las 57 herramientas y labs (posición, nombre y URL absoluta), para que Google entienda el catálogo como una lista ordenada.
SEO — datos estructurados Organization + sitelinks searchbox
- Añadido el nodo
Organization al JSON-LD (nombre, url, logo 512×512, founder → la Person, sameAs) y publisher en WebSite, para que Google entienda la marca y pueda mostrar el logo.
- Sitelinks searchbox:
WebSite.potentialAction (SearchAction) apunta a /tools?q={search_term_string}. Para que sea real, catalog.js ahora lee ?q= al cargar y filtra el arsenal (p. ej. /tools?q=nmap muestra solo Nmap). Verificado el filtrado y el JSON-LD.
i18n — auditoría bilingüe
- Repasadas las páginas de más tráfico y las de salida dinámica en modo EN (portada, catálogo, patch-priority): sin fugas de español. Corregido el único leak real encontrado: el
placeholder del nombre de regla en el laboratorio de Sentinel (sentinel-lab.php) estaba fijo en español y ahora es bilingüe. (El panel privado y los retos CTF son ES por diseño.)
Accesibilidad — pestañas con teclado (resto de herramientas)
- Extendido
aria-tabs.js al resto de herramientas con pestañas: Defang, Generador de contraseñas, SSH, HTTP Builder y Enumeración Cloud (ya estaban SPF/DMARC/DKIM, WAF y Reverse Shell). El helper ahora detecta también la clase activa .on (además de .active), que usa el generador de contraseñas. Verificado el cambio de pestaña por teclado y la sincronización de aria-selected.
Accesibilidad — pestañas con teclado y ARIA
- Nuevo
assets/js/aria-tabs.js (global): mejora cualquier grupo de pestañas marcado con data-ce-tabs con role=tablist/tab, aria-selected, tabindex itinerante y navegación por flechas/Home/End, sin tocar la lógica de cada herramienta (el cambio de panel lo sigue haciendo el tool; el activo se detecta por su clase .active). Aplicado al Generador SPF/DMARC/DKIM, al WAF Bypass y al Reverse Shell. Verificado: al pulsar flechas cambia el panel y se refleja aria-selected.
Rendimiento — limpieza de CSS y evaluación de CSS crítico
- CSS muerto: eliminadas reglas de una sola línea confirmadas sin uso (donaciones movidas al footer, medidor de contraseñas antiguo, banners retirados). El resto del CSS marcado como «sin usar» por las herramientas se aplica dinámicamente desde JS (
sev-*, waf-*, cvss-*, cron-*…), así que no se toca para no romper nada; además va comprimido y cacheado un año, con coste real ~0.
- CSS crítico (descartado tras medir): se probó a inyectar el CSS crítico en línea y cargar
style.css de forma asíncrona para bajar el LCP. Medido con Lighthouse, empeoraba el rendimiento (portada 87→80; LCP igual o peor), porque los ~20 KB de crítico en línea inflan el HTML más de lo que ahorra el desbloqueo del render bajo el perfil móvil throttled. Se revirtió: el montaje actual (hoja bloqueante pero comprimida + cacheada, con precarga de fuentes) rinde mejor aquí.
Analítica — eventos GA4 bajo consentimiento
- Nuevo
assets/js/analytics.js (global, no-op si no hay consentimiento) que, por delegación, mide qué herramientas se usan de verdad: copy_result (clic en cualquier botón de copiar), outbound_click (enlaces externos) y eventos explícitos vía data-ga en acciones clave (p. ej. dkim_generate, email_audit). Cada evento lleva el tool (slug de la página). Solo envía si el visitante aceptó cookies. Verificado que dispara los tres tipos y que es no-op sin consentimiento.
Auditoría Lighthouse (portada, herramienta y artículo)
- Resultados: best-practices 100, SEO 100, accesibilidad 97–100, CLS 0, TBT 0; rendimiento 87–90 (limitado por LCP ~3,5 s en el perfil móvil throttled de Lighthouse). Corregidos los fallos accionables:
- Accesibilidad — orden de encabezados (portada): las tarjetas de «Últimas Amenazas» eran
<h4> bajo un <h2> (salto de nivel); ahora <h3>.
- Accesibilidad — enlaces por color (artículos): los enlaces dentro del texto ahora van subrayados (WCAG «uso del color»), no solo en cian.
- 404 en consola:
/favicon.ico (y /apple-touch-icon.png) que el navegador pide por convención devolvían 404; añadida una reescritura a los iconos existentes.
Corregido — herramientas que no funcionaban en producción (allow_url_fopen off)
- El
.user.ini de producción desactiva allow_url_fopen, así que el Escáner de seguridad y el Buscador MAC estaban rotos en el VPS (usaban get_headers()/file_get_contents() sobre HTTP). Migrados a cURL vía lib/http.php, que funciona con allow_url_fopen off (verificado con -d allow_url_fopen=0).
- Nuevo helper
ce_http_headers() (petición HEAD por cURL): el Escáner lo usa fijando la conexión a la IP pública ya validada (CURLOPT_RESOLVE) y sin seguir redirecciones → arregla el fallo en prod y cierra del todo el SSRF por rebinding/redirección que señaló la auditoría.
- Buscador MAC: proxy migrado a
ce_http_get(), con validación del formato MAC (solo hex/separadores) y rate-limit por sesión (40/min).
Pulido — CTAs y formularios
- Formulario de contacto: al enviar, el botón se deshabilita y muestra un spinner con texto «Enviando…» (feedback de envío). Estado
:disabled con cursor:progress.
- Botones de portada (
.pb-btn): pequeña elevación al pasar el ratón, para el mismo tacto que el nuevo CTA de contacto.
Corregido — botón de envío del formulario de contacto
- El botón usaba clases (
pb-btn) que no existían en el CSS, así que salía con el estilo por defecto del navegador. Nuevo .contact-submit con el lenguaje visual del sitio: degradado cian→violeta, glow y elevación al pasar el ratón, flecha que desliza y barrido de brillo (respeta reduce-motion).
Privacidad — consentimiento de cookies (RGPD/ePrivacy)
- Google Analytics ya no se carga sin permiso. Nuevo
assets/js/consent.js (origen propio, sin nonce inline) que muestra un banner y solo inyecta GA tras aceptar; si se rechaza no se carga nada de terceros ni se instala ninguna cookie. La elección se guarda en localStorage y se puede cambiar desde el enlace «Cookies» del footer. GA se configura con anonymize_ip y sin señales de publicidad.
- La política de privacidad se corrige: antes decía «no se usan cookies de terceros» (falso, GA las ponía). Ahora distingue la cookie necesaria (idioma) de la analítica opcional y explica cómo cambiar la decisión.
- Verificado: sin decisión no se carga GA; Aceptar lo carga y persiste; Rechazar lo bloquea y no vuelve a molestar; el enlace del footer reabre el banner; sin overflow a 375 px ni errores de consola.
Seguridad — auditoría y correcciones
- Revisión completa de la superficie de ataque (endpoints, cabeceras, CSP, sesión, secretos). El servidor estaba bien protegido (consultas preparadas, validación por lista blanca, salida a hosts fijos, CSP con nonce). Corregido lo detectado:
- Inyección de cabeceras de correo (medio) en
contacto.php: el asunto llegaba a mail() sin limpiar saltos de línea → permitía inyectar Bcc: y usar el formulario como relay de spam. Ahora se eliminan CR/LF y caracteres de control y se acota a 150.
- SSRF por redirección (medio) en
tool-scanner.php: get_headers() seguía redirecciones que esquivaban el filtro anti-IP-interna. Se desactiva el seguimiento de redirecciones.
- Sin límite de abuso:
tool-tls.php (sondas TLS a host:port arbitrarios) ahora tiene rate-limit por sesión (20/min → 429, verificado); tool-takeover.php acota la lista de dominios a 50 por petición (evita consultas DNS ilimitadas).
- HSTS a nivel de servidor en
.htaccess (always set): cubre las respuestas que no pasan por bootstrap.php (api/*, sitemap.php, blog/feed.xml).
CI — validación automática en GitHub Actions
- Nuevo
.github/workflows/ci.yml que en cada push/PR ejecuta: php -l de todos los .php, node --check de todo el JS de primera parte, y el smoke-test de las 166 páginas contra el servidor integrado de PHP. Así no llega nada roto a main.
scripts/router.php: enrutador para php -S que emula las reescrituras de .htaccess (URLs limpias→.php, /→index, /dir/→index, sitemap/feed, 404) para poder levantar el sitio sin Apache en CI. No se usa en producción.
scripts/smoke-test.php admite SMOKE_SKIP_API=1 para no depender en CI de que las APIs externas estén arriba. Verificado localmente reproduciendo el entorno de CI (con php -S + router y sin db-config.php): 166/166 (la web degrada con elegancia sin base de datos).
SEO — datos estructurados del blog (Article/BlogPosting)
- Los artículos del blog emiten ahora
BlogPosting (titular, datePublished/dateModified, autor, editor, imagen, sección y keywords) y BreadcrumbList (Inicio › Blog › Artículo) en el JSON-LD, además de og:type=article y metas article:published_time/article:author/article:tag.
- Fecha y metadatos salen de la fuente única
lib/posts.php (nuevo helper ce_blog_post()), la misma que alimenta el listado y el RSS, así que no hay que duplicar nada. Verificado: la ficha del artículo sale correcta y el listado del blog sigue siendo website.
PWA — service worker y página offline
sw.js a ce-v5: el precache incluye ahora las fuentes latin críticas (para que el texto se vea sin conexión) y varias herramientas 100% cliente (Base64, Hash, Generador de contraseñas, Generador SPF/DMARC/DKIM) además de las que ya estaban.
- Página offline rediseñada: mantiene el estilo de marca y lista las páginas realmente cacheadas (leyendo la Cache Storage) para que el usuario sepa qué puede abrir sin red, con enlaces directos. Enlaces corregidos a la URL limpia
/. Verificado: SW instala v5 (24 recursos precacheados, cachés viejas purgadas) y la lista offline se rellena sola.
Rendimiento — cache de estáticos (arreglo importante), fuentes propias e imágenes
- Bug de caché corregido: un bloque de cabeceras en
.htaccess rotulado «CTF pages» se aplicaba a todo el sitio y metía un segundo Cache-Control: no-store en cada CSS/JS/fuente/imagen, anulando el cache inmutable de un año → todo se re-descargaba en cada visita. Ahora va acotado con <If> a /ctf/ y /missions/; el resto de estáticos ya se sirven con public, max-age=31536000, immutable (verificado). También devuelve el Referrer-Policy global a strict-origin-when-cross-origin (antes forzado a no-referrer en todas partes).
- Fuentes auto-hospedadas: Inter y JetBrains Mono (subconjuntos latin/latin-ext, OFL) pasan de Google Fonts a
assets/fonts/ con @font-face en el CSS. Se eliminan dos peticiones bloqueantes a terceros (googleapis + gstatic), se precargan los subconjuntos latin críticos (cuerpo, título del héroe, monoespaciada) y se retiran esos orígenes del CSP (style-src/font-src más estrictos). Verificado: 0 peticiones a gstatic, sin doble descarga (la URL del preload coincide con la del @font-face).
- Imágenes:
mifoto.jpg (89→47 KB) y hacker_avatar (84→62 KB) ahora se sirven en WebP con <picture> y respaldo JPEG (el navegador elige WebP; el JPEG no se descarga). Todas las <img> llevan width/height (evita CLS), las de bajo del pliegue loading="lazy", y la foto principal de «Sobre mí» fetchpriority="high". El <picture> usa display:contents para no alterar el layout existente.
Accesibilidad — teclado y ARIA
- Enlace «Saltar al contenido» (oculto hasta recibir foco) al inicio del
<body>, con destino tras la barra de navegación. Antes no había forma rápida de saltarse el menú con teclado.
- Menús desplegables operables por teclado: se añade
:focus-within (además del :hover) para que al tabular por «Arsenal», «Laboratorios», etc. se abra el menú. Los disparadores sin página propia (Laboratorios, Aprender) pasan de <a href="#"> a <button> de descripción; todos llevan aria-haspopup.
- Botón hamburguesa con
aria-expanded (sincronizado por JS al abrir/cerrar) y aria-controls.
Robustez — smoke-test y página 404 útil
scripts/smoke-test.php: recorre las 167 URLs públicas (mismas reglas que el sitemap) más /api/status y una ruta inexistente, y verifica 200 + ausencia de rastros de error PHP (Fatal/Warning/Notice/Uncaught…). Detectó y se dejó fuera missions/user-api.php (endpoint JSON de la misión IDOR, no una página). Resultado: 167/167 OK. Útil para CI (código de salida != 0 si algo falla).
- 404 rediseñada: mantiene la estética de terminal y añade buscador (abre la paleta de comandos ⌘K) y sugerencias de herramientas destacadas (desde el catálogo, siempre al día) + enlace al arsenal. Enlaces corregidos a la URL limpia
/ (antes /index.php) y el REQUEST_URI reflejado ahora se escapa con el helper e().
SEO técnico — canonical, hreflang y robots.txt
<link rel="canonical"> autorreferente en todas las páginas (en templates/header.php), apuntando a la URL limpia y sin query. Deduplica de golpe las variantes que antes competían por el mismo contenido: .php (que hacían 301), ?lang=, y parámetros de estado (?in=, ?shared=1, utm_*). La home canoniza a / (colapsa /index).
- Clúster
hreflang (es, en, x-default). La i18n va por sesión/?lang=, así que Googlebot (sin cookie) solo veía el idioma por defecto y la versión inglesa era invisible para el buscador. Ahora cada idioma tiene canonical autorreferente (/ruta para ES, /ruta?lang=en para EN) y el par se enlaza por hreflang → ambas se indexan. Añadido también og:url canónico y og:locale:alternate.
robots.txt saneado: eliminados los Allow: /tool-*.php obsoletos (ahora son URLs limpias). Se deja de bloquear CSS/JS (Googlebot los necesita para renderizar y evaluar la página): Allow: /assets/css/, /assets/js/, /assets/img/ por delante del Disallow: /assets/, que sigue protegiendo assets/data/ (logs, caché). Bloqueados además /lib/ y /scripts/.
- Verificado en vivo: canonical y hreflang correctos en vista ES y EN, home →
/, params de estado eliminados del canonical, JSON-LD sigue válido, y sitemap.xml incluye las herramientas nuevas (165 URLs).
Accesibilidad — objetivos táctiles en móvil
- Los controles del header eran pequeños para tocar en móvil (hamburguesa 30×24, búsqueda 32×32, idioma ×27). En
@media (max-width:767px) se amplían: hamburguesa 44×44 y idioma/búsqueda a 44px de alto (el ancho del search se deja natural para que la fila no desborde). Verificado: sin overflow horizontal (375 px), la hamburguesa sigue abriendo el menú, y el arreglo es global (también en páginas de herramienta). style.min.css regenerado.
Rendimiento — compresión + minificación de assets
- Compresión brotli/gzip en
.htaccess (mod_brotli/mod_deflate) para HTML, CSS, JS, JSON, SVG y XML. Reduce ~75-85% los bytes transferidos de texto, sin tocar el código. Verificado en vivo: Content-Encoding: br en el CSS.
- Minificación de CSS: nuevo
scripts/minify.php (sin dependencias) genera style.min.css (131 KB → 91.6 KB, −30%, con calc() y strings intactos). asset() sirve automáticamente el .min si existe y no está obsoleto respecto al fuente (guarda anti-.min-viejo: si editas el CSS sin re-minificar, sirve el original). sw.js precachea el .min (versión → ce-v4).
- El JS de primera parte se sirve comprimido (gzip/brotli ≈ el mismo ahorro que minificar) en vez de minificarlo con regex, que es arriesgado (ASI, comentarios dentro de strings/regex). Los vendor (
mermaid.min.js, html2pdf.min.js) ya venían minificados.
- Verificado: la home enlaza
style.min.css, renderiza igual (871 reglas, navbar sticky, 0 fallos de contraste, sin overflow).
Corregido — terminal (easter-egg) en móvil
- El terminal del footer aparecía medio abierto al cargar en móvil y su botón
>_ no hacía nada. Causas y arreglos:
- El
#term-input tenía autofocus: al cargar, el móvil enfocaba ese campo del footer y hacía scroll hasta revelar el terminal fijo. Quitado autofocus (el JS ya enfoca solo al abrirlo).
- El widget del terminal vive en el footer global (todas las páginas), pero su JS estaba en
missions.js, que solo se cargaba en las páginas de misiones → el botón no respondía en el resto. Ahora missions.js se carga en el footer global (y se quita de los $pageScripts de misiones para no duplicarlo).
- CSS del terminal: el estado oculto queda inerte (
pointer-events:none) y se añade max-height:75vh + padding-bottom: env(safe-area-inset-bottom) para móviles con notch/barra inferior.
- Verificado: oculto por defecto (fuera de pantalla), abre a pantalla completa inferior y cierra; sin robar foco al cargar; el motor de comandos responde.
Accesibilidad — contraste AA
- Auditado el contraste real (componiendo alfa sobre el fondo oscuro, como Lighthouse) en portada, catálogo, herramientas y contenido. Corregidos los textos por debajo de 4.5:1:
--gray-dark subido de rgba(237,241,246,0.32) (ratio 2.6) a 0.52 (~5.2) y --gray de 0.58 a 0.70 — arreglo global, ya que estas variables se usan en labels/hints de todas las herramientas.
- Footer:
.donate-note, .footer-links y .footer-copy dejan de usar blancos translúcidos (rgba(255,255,255,.18/.25), ratio 1.7–2.2) y pasan a las variables ya compatibles; .rss-source deja #666.
- CTA del reto diario: fondo
#ff2a2a→#dc2626 para que el texto blanco pase AA (era 3.5).
- Enlaces de texto de LinkedIn (
#0077b5/#0a66c2, ratio ~4.0) → #4aa3e0 (~6.8); los botones (blanco sobre azul) ya pasaban.
aria-label en el input del terminal (#term-input), que no tenía nombre accesible.
- Verificado: portada,
/tools, tool-tls y sobre-mi con 0 fallos de contraste; sin enlaces/botones sin nombre.
Cambiado — donaciones más discretas (profesionalidad)
- Quitado el botón ♥ «Apóyame» de la barra de navegación (escritorio y móvil) y la tarjeta de donación destacada de la portada. El acceso se mantiene, pero discreto en el footer con copy más suave: «☕ Invítame a un café» → enlaza directamente al PayPal. Menos «proyecto que pide dinero» y más portfolio/producto cuidado, sin perder la opción de apoyar.
Rendimiento — split de main.js (303 KB → 12 KB) ✅ completado
main.js cargaba 303 KB en todas las páginas con código de ~15 páginas distintas. Partido por completo en módulos por página (mecanismo $pageScripts), dejando main.js como núcleo global de ~12 KB (navbar/scroll, copia de bloques de código, barra de lectura, TOC, filtros del índice, fade-in y menú móvil) — lo único que de verdad se usa en todas las páginas.
- Módulos extraídos y cargados solo en su(s) página(s):
labs-bundle.js (100 KB — Sentinel, Threat Map, Sandbox, C2), soc-arsenal.js (61 KB, con su BD bilingüe), report-generator.js (18 KB), ir-tabletop.js (18 KB), arch-bastion.js (Arch Designer + OS Bastioning + init de mermaid), dashboards.js (escáner perimetral + simulador SOC + feed CVE/noticias), missions.js (misiones/CTF: XP/HUD, spoilers, progreso; incluye el terminal easter-egg), threat-hunting.js (SIEM), mitre-mapper.js (9 KB), architecture.js (partículas), profile.js (3 KB).
tool-threat-map.php (que no incluye footer) recibe su bundle con un <script> directo; el resto vía $pageScripts. En report-generator.php se conserva html2pdf.min.js junto al módulo.
- Verificado en vivo: las 7 páginas afectadas funcionan (Arsenal 244 entradas + filtro, MITRE 1139 celdas, IR tabletop, report + PDF, profile, C2, threat-map canvas, sandbox, sentinel/KQL) sin errores de consola; la portada carga solo
main.js (ningún módulo de lab). Núcleo global intacto.
- Con esto, una página normal (portada, herramientas, contenido) pasa de arrastrar ~400 KB de JS (tools.js 100 KB + main.js 303 KB) a ~15 KB de núcleo. Cada lab/simulador carga solo su módulo. Todas las páginas afectadas verificadas en navegador sin errores de consola.
Rendimiento — fin del monolito tools.js
- Eliminado
assets/js/tools.js (~100 KB) que cargaban las 29 páginas de herramienta. Su lógica compartida (navegación del selector + copiado al portapapeles, ahora window.copyToClipboard) vive en un core.js mínimo (~2 KB), y cada herramienta que aún tenía su código dentro del monolito pasa a su propio módulo: hash-gen.js, base64.js, chmod.js, regex.js, mac.js, headers.js, httpbuilder.js, cloud.js, takeover.js, recon.js, ssh.js, ports.js.
- Las ~16 páginas que ya tenían módulo propio (cidr, jwt, cve, dns, ip, url, passcheck…) soltaron los ~100 KB de peso muerto que arrastraban además de su módulo.
- Impacto por página: de 100 KB fijos a ~3,5–7 KB en la mayoría (−93 a −96 %); las dos más densas (headers 17 KB, cloud 23 KB) siguen muy por debajo del monolito. Menos JS que descargar, parsear y ejecutar = mejor TBT/LCP.
- Limpieza: eliminado código muerto que quedaba en el monolito (bloque viejo de reverse shell con
rs-ip, ya sustituido por revshell.js; y el bloque PDF/donación que en realidad maneja osint.js). sw.js precachea core.js en vez de tools.js (versión → ce-v3). agent-core.js pasa a cargarse con defer.
- Verificado en vivo: las 12 herramientas extraídas funcionan (base64, chmod, hash, mac, regex, headers→nota, httpbuilder→curl, ssh→análisis, cloud, takeover, recon, ports) y el selector navega, sin errores de consola y sin
tools.js en ninguna página.
Cambiado — ampliación de herramientas (ronda 4)
- Analizador de Cron (
tool-cron.php + assets/js/cron.js): reescrito. Parser correcto que expande cada campo admitiendo pasos, rangos, rangos con paso (8-18/2), listas, nombres de mes/día (jan-mar, mon) y atajos (@daily, @hourly, @reboot…). Corrige un bug real: la semántica de cron es OR entre día-del-mes y día-de-semana cuando ambos están restringidos (el código anterior usaba AND, dando resultados vacíos o erróneos). Descripción en lenguaje humano, próximas 6 ejecuciones con tiempo relativo en zona horaria local y validación con mensaje de campo inválido. Nuevo: auditoría de seguridad del comando con severidad (rm -rf, curl|sh, Base64→shell, reverse shells /dev/tcp, ficheros sensibles, rutas temporales, IP en crudo, frecuencia excesiva). Verificado: 0 0 1 * 1 lista lunes y el día 1 (OR), curl|sh → 3 avisos, campo 99 → error.
- Generador de Wordlist (
tool-wordlist.php + assets/js/wordlist.js): reescrito. Además de las mutaciones básicas, ahora tiene modos de mayúsculas combinables (minúsculas, Capitalizada, MAYÚSCULAS, aLtErNa, tal cual), leetspeak básico y agresivo, prefijos/sufijos con separadores (_ - . @) aplicables al principio o al final, combinación de dos palabras (admin_empresa) y un filtro por política de contraseña (longitud mín./máx. y exigir dígito/mayúscula/símbolo) que descarta candidatos inválidos antes de generarlos. Recuento y tamaño en vivo, con tope de 250 000 candidatos avisado (sin recorte silencioso), copia y descarga .txt. Verificado: exige-dígito 133→96 (todos con dígito), combinación real de dos palabras, sin overflow a 375 px.
- WAF Bypass (
tool-waf.php + assets/js/waf-tools.js): pasa de biblioteca estática a laboratorio de evasión. Nuevo Mutador: pegas cualquier payload y genera variantes de evasión aplicando transformaciones reales (cambio de mayúsculas, comentarios en línea //, comentario versionado /*!50000*/, partir palabra clave, espacios alternativos, operadores equivalentes &&/||, literales a hex 0x…, URL/doble-URL/URL-total, entidades HTML dec/hex, escape Unicode JS), cada una con la explicación de por qué salta el filtro y con autodetección de contexto (XSS/SQLi/genérico). Se conserva la Biblioteca de 158 payloads en 8 categorías (filtro, copia y descarga). Verificado: SQLi → 11 variantes, XSS → 9 (alert\1\, tab en etiqueta…), sin overflow a 375 px.
- Generador de Reverse Shell (
tool-revshell.php + assets/js/revshell.js): reescrito a fondo. Tres modos —reverse shell, bind shell y MSFVenom— con 34 payloads en 30+ lenguajes (Bash 196/read-line/UDP, nc/ncat/BusyBox, OpenSSL TLS, Python 2/3, PHP en cinco variantes, Perl, Ruby, Go, Node, Java, Groovy, Lua, Awk, Telnet, socat, PowerShell y PowerShell Base64). Selector de shell (/bin/bash, sh, zsh…), codificación Base64 / URL / doble-URL para evasión de WAF, listener conmutable (nc, rlwrap, ncat, ncat-TLS, socat con TTY, pwncat, msfconsole multi/handler), validación de IP/puerto y chuleta de estabilización de TTY. Verificado: Base64 decodifica al comando exacto, PS Base64 en UTF-16LE, msfvenom + handler emparejados, sin overflow a 375 px.
Cambiado — ampliación de herramientas (ronda 3)
- Generador de Contraseñas (
tool-passgen.php + assets/js/passgen.js): tres modos con crypto.getRandomValues (nunca Math.random) y entropía en vivo — aleatoria (alfabeto configurable, sin ambiguos opcional, garantiza un carácter de cada conjunto), frase de paso diceware (~471 palabras es/en, 6 palabras + número ≈ 60 bits) y pronunciable. Con generación en lote de 12. Verificado: aleatoria 103 bits, frase 60 bits.
- ¿Cuál es mi IP? (
tool-ip.php + assets/js/ip-privacy.js): pasa a chequeo de privacidad. Geolocalización + ISP (ipwho.is), huella del navegador (UA, motor, pantalla, CPU/RAM, GPU WebGL, hash de canvas, idiomas, DNT), prueba de fuga WebRTC y veredicto que avisa del desajuste de zona horaria navegador↔IP (señal de VPN). Verificado: detecta la IP pública real por WebRTC.
- URL Encoder/Decoder (
tool-url.php + assets/js/url-tools.js): además de codificar/decodificar (con desmontado de capas múltiples), analizador de URL que desglosa esquema/host/puerto/ruta/parámetros en tabla, detecta base64 oculto en los parámetros y avisa de HTTP, Punycode (homográfico), credenciales embebidas y doble codificación. Verificado con un enlace de phishing: descubre redir → https://evil.com/phish.
- Calculadora CIDR (
tool-cidr.php + assets/js/cidr-tools.js): IPv4 e IPv6 con BigInt (red, broadcast, máscara, wildcard, rango, hosts, ámbito RFC1918/ULA/CGNAT…), división en subredes y comprobación de si una IP cae dentro de un rango. Verificado: /26 → 4 subredes /28, IPv6 /48 con 1,2×10²⁴ direcciones.
- Los bloques de estas cuatro salieron de
tools.js a ficheros propios; tools.js baja a ~139 KB.
Cambiado — ampliación de herramientas (ronda 2)
- Buscador CVE (
tool-cve.php + assets/js/cve-finder.js): deja de ser un proxy fino a NVD. El servidor enriquece cada CVE con EPSS + KEV (reutilizando lib/vuln-intel.php) y la ficha muestra CVSS con vector, EPSS, percentil, KEV con fecha límite de CISA, CWE y enlaces a Exploit-DB/GitHub/NVD. Acepta CVE-ID exacto o palabra clave, ordena por riesgo real y admite ?q= para compartir búsquedas. Verificado: Log4Shell → CVSS 10, EPSS 99,9%+, KEV+ransomware.
- Analizador de Logs (
tool-loganalyzer.php + assets/js/log-analyzer.js): reescrito. 9 firmas de ataque (SQLi, XSS, LFI, RCE, SSRF, XXE, SSTI, fuerza bruta, webshell), decodificación doble para evasión, top de IPs atacantes y rutas, detección de escáneres por User-Agent, sparkline de actividad por minuto, extracción de IOCs y generación de regla Sigma/KQL del patrón dominante (ponderado por severidad) enlazando con Sigma Forge.
- Multi Decoder (
tool-multidecode.php + assets/js/multi-decode.js): «CyberChef ligero». 17 códecs (Base64/32/58, Hex, URL, HTML, Unicode, binario, decimal, ROT13/47, Atbash, Morse, JWT, gzip/zlib, invertir), modo Magia que deshace hasta 12 capas puntuando por texto con sentido y forma de flag, receta manual encadenable con deshacer, y fuerza bruta de ROT-n (×25) y XOR de un byte. Verificado: flag en 2 capas y flag{rot13_is_weak} como primer acierto del ROT brute.
- DNS Lookup (
tool-dns.php + assets/js/dns-tools.js): además de los registros (con «ALL» en paralelo y flag DNSSEC), nuevo modo postura de seguridad del correo que analiza SPF (política ~all/-all y límite de lookups), DMARC (p=none/quarantine/reject y rua), DKIM (selectores comunes), DNSSEC y MX, con nota A–F. Enlaza con el Triage de Email.
- Los bloques de estas cuatro salieron de
tools.js a ficheros propios; tools.js baja de ~183 KB (inicio de sesión) a ~147 KB.
Corregido
- El endpoint del Buscador CVE se sirve por URL limpia (
/tool-cve), no .php, para no perder el POST/GET en la redirección 301 del .htaccess.
Cambiado — efecto del título del hero
- Sustituida la aberración cromática permanente (RGB rojo/cian temblando sin parar, con
clip:rect() obsoleto) por dos efectos que encajan con el estilo nuevo: aurora en reposo —degradado cian→violeta que recorre despacio «Mundo Digital», a juego con la malla del fondo— y el glitch solo al pasar el ratón por el título. Ambos respetan «reducir movimiento» (media query y toggle del sitio).
Cambiado — herramientas de más uso, a fondo
- Fortaleza de Contraseña (
assets/js/passcheck.js): reescrita. Deja de contar requisitos —el método que daba verde a «P@ssw0rd123»— y mide entropía real descontando patrones que un atacante prueba primero (recorridos de teclado, sustituciones l33t, fechas, secuencias, repeticiones, palabras comunes). Añade tiempo de crack en tres escenarios (online, bcrypt, granja de GPUs) y comprobación contra Have I Been Pwned por k-anonymity: solo salen del navegador los 5 primeros caracteres del hash SHA-1, nunca la contraseña. Verificado: «password» → 52.372.427 filtraciones.
- Decodificador de JWT (
assets/js/jwt-analyze.js): además de descodificar, audita el token — alg=none, expiración vencida o eterna, nbf/iat, kid inyectable, datos sensibles y claims de autorización — y prueba secretos débiles HS256/384/512 calculando el HMAC en el navegador contra un diccionario. Explica los claims registrados y traduce las fechas.
- Analizador de Hashes (
assets/js/hash-id.js): identifica por prefijo y por longitud+alfabeto con candidatos ordenados, distingue casos ambiguos con pistas (MD5 vs NTLM vs LM), da el modo exacto de Hashcat (-m) y el formato de John para cada uno, marca si el algoritmo es rápido/lento, y mantiene el crack por diccionario en el navegador para SHA-1/256/384/512 (con aviso honesto de que MD5/NTLM no los calcula el navegador).
- Cabeceras HTTP (
assets/js/tools.js): añadido análisis fino del CSP (detecta unsafe-inline/unsafe-eval, comodines en las fuentes de script y ausencia de object-src/base-uri/frame-ancestors) y dos cabeceras cross-origin modernas (COOP y CORP).
bootstrap.php: api.pwnedpasswords.com añadido a connect-src en la CSP, necesario para la comprobación de filtraciones.
- Los bloques de estas herramientas se sacaron de
assets/js/tools.js a ficheros propios; tools.js baja de ~183 KB a ~169 KB.
Corregido
- El botón de búsqueda global desbordaba la barra en móvil (~360-375 px): en móvil se colapsa a solo la lupa y se ajusta la separación del nav.
Cambiado — nueva marca «el bisel»
- Logotipo nuevo (
assets/img/logo-bisel.svg): escudo con mordeduras hexagonales en los flancos, resuelto en dos planos de luz —el izquierdo iluminado, el derecho en sombra— para que se lea como una pieza tallada. Sustituye al escudo con check anterior en la cabecera, el pie, el favicon y la PWA.
- Juego de iconos completo generado desde la misma geometría del SVG:
favicon-16/32.png, apple-touch-icon.png (180 px, sin transparencia, que iOS no la admite), icon-192/512.png con fondo y esquinas redondeadas, y icon-maskable-512.png con margen suficiente para sobrevivir al recorte circular de Android.
- Portada Open Graph rehecha (
og-cover.png, 1200×630) con la marca nueva, la malla del sitio y el titular «Encontrar el fallo es fácil. Saber cuál importa, no.». Es la imagen que se ve al compartir en LinkedIn.
theme-color y el manifest sincronizados con el fondo nuevo (#0b0c10).
Cambiado — identidad visual «J + M»
- Nueva paleta base, aplicada retocando los valores de las variables que ya existían en
:root, así que las 60 páginas heredan el aspecto nuevo sin tocarlas una a una: fondo #0b0c10, cian #2dd4e8 (más suave que el #00ffff anterior), violeta de apoyo #8b7cff, radio 14 px y una definición única de elevación, filo de luz y anillo de foco.
- Malla de fondo en movimiento: tres manchas de color muy desenfocadas que se desplazan en ciclos de 19, 23 y 27 segundos sin sincronizar, al 35 % de opacidad. En móvil se reduce el desenfoque y se detienen, porque un
blur de 64 px es caro; también se detienen con «reducir movimiento».
- Las tarjetas pasan a vidrio con filo de luz en el borde superior, y el foco de teclado es visible y coherente en todo lo que se puede teclear o pulsar.
Corregido
- La primera versión de la malla subía el contenido con una regla
body > * que, por especificidad, machacaba el position:fixed del terminal flotante, del modal de donación y de la propia paleta de comandos. Ahora el fondo vive en <html> y la malla va detrás con z-index:-1, sin tocar ningún otro elemento.
Corregido — PWA
- El service worker no precacheaba las herramientas: se prometía análisis sin conexión y sin red el script no llegaba a cargar. Ahora precachea 13 recursos, incluidos
tool-eml, tool-sigma-forge, el catálogo y sus JS, verificado con cero errores de descarga.
- El precache usaba un
addAll() cuyo fallo quedaba silenciado por un catch vacío: un solo recurso caído dejaba la caché entera vacía sin avisar. Ahora cada recurso se cachea por separado.
start_url apuntaba a /index.php, que el .htaccess redirige con un 301 a la URL limpia: cada arranque de la app se comía una redirección. Ahora es /.
- Añadidos
id y display_override al manifest para que la identidad de la app sea estable entre actualizaciones.
Cambiado — reestructuración del contenido
- Catálogo único en
lib/catalog.php: 50 elementos declarados en un solo sitio. De ahí se generan el menú de escritorio, el de móvil, el selector de herramientas, el catálogo, las relacionadas y la prioridad del sitemap. Añadir una herramienta pasa de tocar 34 ficheros a tocar 1.
- Clasificación por lo que el visitante viene a hacer, en 5 familias (Defensa y SOC · Gestión de vulnerabilidades · Ofensiva y pentesting · OSINT y reconocimiento · Cripto y datos) más Laboratorios, Aprender y Perfil. Las 7 aplicaciones interactivas que estaban enterradas entre los writeups de
projects/ aparecen ya en Laboratorios.
templates/tool-switcher.php: un único selector con <optgroup> por familia sustituye a las 33 copias a mano que nunca coincidían entre sí (993 líneas eliminadas, 860 <option> menos que mantener).
- Menú de escritorio y menú móvil se generan del mismo array, así que dejan de poder divergir. Antes tenían taxonomías distintas.
tools.php deja de ser un callejón sin salida («> Esperando selección…») y pasa a ser el catálogo del Arsenal, con buscador que ignora acentos, filtros por familia y enlace directo por ancla (/tools#osint).
templates/related.php: bloque «Sigue por aquí» al pie de 42 páginas, con sugerencias de la misma familia.
- El mega del Arsenal se ancla al viewport y se adapta de 5 a 3 columnas; los enlaces destacados usan la clase
.is-hot en vez de estilos en línea repetidos.
sitemap.php toma la prioridad del catálogo en vez de deducirla del prefijo del nombre de fichero.
- 5 herramientas y 1 simulador que no se podían alcanzar desde ningún menú (
tool-chmod, tool-cron, tool-passcheck, tool-regex, tool-url, soc-simulator) quedan enlazados.
Corregido
- El menú móvil no se abría.
main.js y templates/header.php registraban cada uno un manejador de clic sobre el mismo botón y alternaban las mismas clases: uno abría el menú y el otro lo cerraba en el mismo clic. Desde que main.js se carga con defer se registra antes, así que el stopImmediatePropagation() del header llegaba tarde. Eliminado el manejador duplicado de main.js.
Cambiado
.gitignore excluye assets/data/cvss-cache.json: lo llena el uso de la herramienta, no es contenido del repo.
- «¿Qué parcheo primero?» muestra el vector CVSS de cada CVE y advierte de que la puntuación es la que publica el CNA, que a veces no coincide con NVD (el registro de Zerologon publica un 5.5 con
AV:L). Por eso el CVSS pesa solo 15 de 100 puntos: con EPSS y KEV, Zerologon sigue saliendo P1.
Corregido
curl_init() no existe en todos los binarios de PHP (el php del PATH en el VPS de Plesk no trae la extensión), lo que mataba scripts/sync-kev.php con un fatal. Nuevo lib/http.php: cliente HTTP con cURL cuando está disponible y respaldo por envoltorios de streams cuando no, en paralelo o en serie según el caso. sync-kev.php y api/patch-priority.php ya no dependen de cURL, y el script indica qué binario usar si tampoco hay allow_url_fopen.
- Este
CHANGELOG.md. A partir de ahora se actualiza en cada cambio.
Pendiente en el working tree
assets/data/rss-cache.json regenerado (caché del feed, sin cambios de código).
---
Corregido
- El Centro de Inteligencia y el Simulador SOC no cargaban por
main.js sin defer.
- Desbordamiento horizontal en móvil.
- Sitemap pasa a ser dinámico (
sitemap.php).
- Onboarding del ranking.
Eliminado
- Soporte de GitHub en el perfil (se descarta su uso).
---
Eliminado
- Modo claro. Se revierte y queda solo el tema oscuro de marca.
---
Corregido
templates/footer.php provocaba un fatal cuando una página machacaba la variable $t.
---
Corregido
- Generador de CV devolvía 500.
- Enlaces rotos y riesgo de DoS en los scripts ETL.
- Optimización de imágenes.
---
Seguridad
- Contenido el path traversal en el módulo CTF.
- Bloqueado el SSRF hacia direcciones internas.
---