diff --git a/INFORME.sh b/INFORME.sh new file mode 100644 index 0000000..8ead6e2 --- /dev/null +++ b/INFORME.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo "Revisando vulnerabilidades de productos mediante ENISA" +./check_EUVD.py +echo "Transformando informe en formato Markdown en PDF" +pandoc -V lang=es -V geometry:a4paper -V geometry:margin=2cm --pdf-engine=xelatex -V monofont="DejaVu Sans Mono" --metadata author="Luis Gutiérrez López (Software Libre)" informe_vulnerabilidades.md -o informe_vulnerabilidades.pdf +echo + diff --git a/README.md b/README.md index e69de29..d27723b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,135 @@ +# Auditoría de Vulnerabilidades ENISA EUVD + +Herramienta en Python para la consulta y generación de informes ejecutivos de vulnerabilidades basados en la API oficial de **ENISA EUVD** (*European Union Vulnerability Database*). + +El script analiza los componentes de infraestructura definidos por el usuario, evalúa su impacto según métricas CVSS y genera automáticamente un informe detallado en formatos **JSON**, **Markdown** y **PDF**. + +--- + +## 🚀 Características + +* **Consulta eficiente a la API de ENISA:** Filtrado por ventana temporal y nivel de severidad (CVSS `> 7.0` por defecto). +* **Gestión de límites de tasa (*Rate Limiting*):** Control automático con reintentos para manejar respuestas `HTTP 429`. +* **Soporte multiobjetivo:** Definición ágil de entornos mediante archivos de configuración YAML. +* **Formatos de salida:** + * `reporte_vulnerabilidades.json`: Datos estructurados para integración con pipelines o SIEM. + * `informe_vulnerabilidades.md`: Informe en Markdown organizado mediante fichas ejecutivas. + * `informe_vulnerabilidades.pdf`: Documento compilado mediante Pandoc e listo para presentación. + +--- + +## Requisitos Previos + +### Entorno Linux (Debian) + +Asegúrate de contar con los paquetes base del sistema y el motor de compilación PDF: + +```bash +sudo apt update +sudo apt install -y python3 python3-pip pandoc texlive-xetex fonts-dejavu fonts-noto-color-emoji + +``` + +--- + +## Instalación + +1. **Clonar el repositorio:** +```bash +git clone +cd ENISA_CVEs + +``` + + +2. **Instalar las dependencias de Python:** + +Se necesita: + +* `requests` +* `pyyaml` +* `packaging` + + +--- + +## Configuración (`targets.yaml`) + +Define en el archivo `targets.yaml` el listado de tecnologías, proveedores y versiones que deseas auditar: + +```yaml +targets: + - vendor: "apache" + product: "tomcat" + version: "9.0.118" + + - vendor: "jenkins" + product: "jenkins" + version: "2.504.3" + + - vendor: "goharbor" + product: "harbor" + version: "2.11.0" + + - vendor: "oracle" + product: "openjdk" + version: "11.0.21" + + - vendor: "gitlab" + product: "gitlab" + version: "19.1.1" + +``` + +--- + +## Uso + +### 1. Ejecutar el análisis + +Ejecuta el script principal para consultar la API de ENISA y generar los archivos de reporte: + +```bash +python3 check_EUVD.py + +``` + +Al finalizar la ejecución se habrán creado dos archivos: + +* `reporte_vulnerabilidades.json` +* `informe_vulnerabilidades.md` + +### 2. Generar el informe en PDF + +Utiliza **Pandoc** junto con el motor XeLaTeX para compilar el Markdown a PDF con márgenes ajustados: + +```bash +pandoc informe_vulnerabilidades.md -o informe_vulnerabilidades.pdf \ + -V lang=es \ + -V geometry:a4paper \ + -V geometry:margin=2cm \ + -V mainfont="DejaVu Serif" \ + -V monofont="DejaVu Sans Mono" \ + --pdf-engine=xelatex + +``` + +--- + +## Estructura del Informe + +El informe generado incluye: + +1. **Métricas Globales:** Total de elementos auditados, vulnerabilidades analizadas y distribución por criticidad. +2. **Resumen Ejecutivo:** Tabla comparativa indicando los hallazgos por cada componente del archivo YAML. +3. **Detalle de Vulnerabilidades (Fichas):** Desglose individual clasificado por nivel de severidad (`[CRÍTICA]`, `[ALTA]`, etc.), identificadores CVE/EUVD, versión afectada y descripción detallada del impacto. + +--- + +## Licencia + +Uso interno para auditorías de seguridad y gestión de vulnerabilidades. + +``` + +``` diff --git a/check_CVEs.py b/check_CVEs.py new file mode 100644 index 0000000..416eea6 --- /dev/null +++ b/check_CVEs.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +import time +import json +import yaml +import requests +from datetime import datetime, timedelta, timezone + +BASE_URL = "https://euvdservices.enisa.europa.eu/api/search" +HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) EUVD-Auditor/1.0" +} + +def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=30): + vulnerabilities = [] + from_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d") + + params = { + "text": product if product else vendor, + "fromScore": from_score, + "fromDate": from_date, + "page": 0, + "size": 100 + } + + max_retries = 3 + for attempt in range(max_retries): + try: + response = requests.get(BASE_URL, headers=HEADERS, params=params, timeout=15) + + # Si se supera el rate limit, esperar y reintentar + if response.status_code == 429: + wait_time = (attempt + 1) * 5 + print(f" ⚠️ Rate limit (429) alcanzado para {product}. Esperando {wait_time}s...") + time.sleep(wait_time) + continue + + response.raise_for_status() + data = response.json() + + items = data.get("items", data.get("content", [])) if isinstance(data, dict) else data + vulnerabilities.extend(items) + break # Éxito, salir del bucle de reintentos + + except Exception as e: + if attempt == max_retries - 1: + print(f" ❌ Error final tras {max_retries} intentos para {product}: {e}") + time.sleep(2) + + return vulnerabilities + +def generate_markdown_report(total_consolidados, total_analizadas, total_aplicables, + total_no_aplicables, total_revision, resumen_objetivos, + aplicables_severidad, output_file="informe_vulnerabilidades.md"): + """ + Genera el informe ejecutivo en Markdown utilizando el formato de Bloques / Fichas Ejecutivas, + ideal para lectura clara y conversión a PDF mediante Pandoc. + """ + now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + md_content = f"""# Informe Ejecutivo de Vulnerabilidades + +**Generado en:** {now_iso} | **Fuentes:** ENISA + +**Grupo de Software Libre - Área de Plataformas** + +* **Informes consolidados:** {total_consolidados} +* **Vulnerabilidades analizadas:** {total_analizadas} +* **Aplicables:** {total_aplicables} +* **No aplicables:** {total_no_aplicables} +* **Revisión manual:** {total_revision} + +## Resumen por objetivo + +| Fuente | Producto | Versión | Analizadas | Aplicables | No aplicables | Revisión manual | +|---|---|---|---|---|---|---| +""" + + for obj in resumen_objetivos: + md_content += f"| {obj['Fuente']} | {obj['Producto']} | {obj['Versión']} | {obj['Analizadas']} | {obj['Aplicables']} | {obj['No aplicables']} | {obj['Revisión manual']} |\n" + + md_content += """ +## Detalle de Vulnerabilidades Aplicables +""" + + if not aplicables_severidad: + md_content += "\n*No se encontraron vulnerabilidades aplicables con los criterios seleccionados.*\n" + else: + for app in aplicables_severidad: + # Determinar nivel y badge según la puntuación CVSS + score_val = app['BaseScore'].split()[0] if app['BaseScore'] else "0.0" + try: + score_num = float(score_val) + except ValueError: + score_num = 0.0 + + if score_num >= 9.0: + badge = "[**CRÍTICA**] - " + elif score_num >= 7.0: + badge = "[**ALTA**] - " + elif score_num >= 4.0: + badge = "[**MEDIA**] - " + else: + badge = "[**BAJA**] - " + + # Formatear la descripción/justificación + just = app['Justificación'].strip() + + md_content += f""" + +### {badge} {app['ID']} - {app['CVE']} + +* **Producto / Versión:** `{app['Producto']}` (v`{app['Versión']}`) | **Fuente:** {app['Fuente']} +* **BaseScore:** {app['BaseScore']} +* **Regla versión:** {app['Regla versión']} +* **Justificación / Descripción:** + > {just} +""" + + with open(output_file, "w", encoding="utf-8") as f: + f.write(md_content) + + print(f"📝 Informe Markdown generado en '{output_file}'") + + +def process_targets(config_path="targets.yaml"): + try: + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + except FileNotFoundError: + print(f"❌ Archivo '{config_path}' no encontrado.") + return + + resumen_objetivos = [] + aplicables_severidad = [] + + total_consolidados = 0 + total_analizadas = 0 + total_aplicables = 0 + total_no_aplicables = 0 + total_revision = 0 + + print("🚀 Iniciando escaneo ultra-rápido en ENISA EUVD...\n") + + for target in config.get("targets", []): + vendor = target.get("vendor", "") + product = target.get("product", "") + version = target.get("version", "") + + total_consolidados += 1 + print(f"👉 Analizando: {vendor}/{product} (v{version})") + + items = fetch_fast_enisa_vulnerabilities(vendor=vendor, product=product, from_score=7.0, days_back=30) + + num_analizadas = len(items) + num_aplicables = 0 + num_no_aplicables = 0 + num_revision = 0 + + for item in items: + score = item.get("baseScore", 0.0) + score_ver = item.get("baseScoreVersion", "3.1") + euvd_id = item.get("id", "N/A") + + aliases = item.get("aliases", "") + aliases_list = aliases.strip().split("\n") if isinstance(aliases, str) else [] + cve_id = aliases_list[0] if aliases_list and aliases_list[0] else euvd_id + + desc = item.get("description", "Sin descripción disponible.") + desc_short = desc[:180].strip() + "..." if len(desc) > 180 else desc + + num_aplicables += 1 + aplicables_severidad.append({ + "Fuente": "ENISA", + "ID": euvd_id, + "CVE": cve_id, + "Producto": product, + "Versión": version, + "BaseScore": f"{score} {score_ver}".strip(), + "Regla versión": f"Afecta rama v{version.split('.')[0]}.x", + "Justificación": desc_short + }) + + total_analizadas += num_analizadas + total_aplicables += num_aplicables + total_no_aplicables += num_no_aplicables + total_revision += num_revision + + resumen_objetivos.append({ + "Fuente": "ENISA", + "Producto": product, + "Versión": version, + "Analizadas": num_analizadas, + "Aplicables": num_aplicables, + "No aplicables": num_no_aplicables, + "Revisión manual": num_revision + }) + + print(f" 📊 Encontradas: {num_analizadas} vulnerabilidades relevantes\n") + time.sleep(0.5) + + # Ordenar vulnerabilidades aplicables de mayor a menor puntuación CVSS + aplicables_severidad.sort(key=lambda x: float(x["BaseScore"].split()[0]) if x["BaseScore"].split()[0].replace('.', '', 1).isdigit() else 0.0, reverse=True) + + # 1. Guardar JSON + reporte_json = { + "generado_en": datetime.now(timezone.utc).isoformat(), + "fuentes": ["ENISA"], + "resumen_por_objetivo": resumen_objetivos, + "aplicables_por_severidad": aplicables_severidad + } + with open("reporte_vulnerabilidades.json", "w", encoding="utf-8") as f: + json.dump(reporte_json, f, indent=2, ensure_ascii=False) + + # 2. Generar Markdown + generate_markdown_report( + total_consolidados, total_analizadas, total_aplicables, + total_no_aplicables, total_revision, resumen_objetivos, + aplicables_severidad + ) + +if __name__ == "__main__": + process_targets() diff --git a/check_EUVD.py b/check_EUVD.py new file mode 100644 index 0000000..416eea6 --- /dev/null +++ b/check_EUVD.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +import time +import json +import yaml +import requests +from datetime import datetime, timedelta, timezone + +BASE_URL = "https://euvdservices.enisa.europa.eu/api/search" +HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) EUVD-Auditor/1.0" +} + +def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=30): + vulnerabilities = [] + from_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d") + + params = { + "text": product if product else vendor, + "fromScore": from_score, + "fromDate": from_date, + "page": 0, + "size": 100 + } + + max_retries = 3 + for attempt in range(max_retries): + try: + response = requests.get(BASE_URL, headers=HEADERS, params=params, timeout=15) + + # Si se supera el rate limit, esperar y reintentar + if response.status_code == 429: + wait_time = (attempt + 1) * 5 + print(f" ⚠️ Rate limit (429) alcanzado para {product}. Esperando {wait_time}s...") + time.sleep(wait_time) + continue + + response.raise_for_status() + data = response.json() + + items = data.get("items", data.get("content", [])) if isinstance(data, dict) else data + vulnerabilities.extend(items) + break # Éxito, salir del bucle de reintentos + + except Exception as e: + if attempt == max_retries - 1: + print(f" ❌ Error final tras {max_retries} intentos para {product}: {e}") + time.sleep(2) + + return vulnerabilities + +def generate_markdown_report(total_consolidados, total_analizadas, total_aplicables, + total_no_aplicables, total_revision, resumen_objetivos, + aplicables_severidad, output_file="informe_vulnerabilidades.md"): + """ + Genera el informe ejecutivo en Markdown utilizando el formato de Bloques / Fichas Ejecutivas, + ideal para lectura clara y conversión a PDF mediante Pandoc. + """ + now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + md_content = f"""# Informe Ejecutivo de Vulnerabilidades + +**Generado en:** {now_iso} | **Fuentes:** ENISA + +**Grupo de Software Libre - Área de Plataformas** + +* **Informes consolidados:** {total_consolidados} +* **Vulnerabilidades analizadas:** {total_analizadas} +* **Aplicables:** {total_aplicables} +* **No aplicables:** {total_no_aplicables} +* **Revisión manual:** {total_revision} + +## Resumen por objetivo + +| Fuente | Producto | Versión | Analizadas | Aplicables | No aplicables | Revisión manual | +|---|---|---|---|---|---|---| +""" + + for obj in resumen_objetivos: + md_content += f"| {obj['Fuente']} | {obj['Producto']} | {obj['Versión']} | {obj['Analizadas']} | {obj['Aplicables']} | {obj['No aplicables']} | {obj['Revisión manual']} |\n" + + md_content += """ +## Detalle de Vulnerabilidades Aplicables +""" + + if not aplicables_severidad: + md_content += "\n*No se encontraron vulnerabilidades aplicables con los criterios seleccionados.*\n" + else: + for app in aplicables_severidad: + # Determinar nivel y badge según la puntuación CVSS + score_val = app['BaseScore'].split()[0] if app['BaseScore'] else "0.0" + try: + score_num = float(score_val) + except ValueError: + score_num = 0.0 + + if score_num >= 9.0: + badge = "[**CRÍTICA**] - " + elif score_num >= 7.0: + badge = "[**ALTA**] - " + elif score_num >= 4.0: + badge = "[**MEDIA**] - " + else: + badge = "[**BAJA**] - " + + # Formatear la descripción/justificación + just = app['Justificación'].strip() + + md_content += f""" + +### {badge} {app['ID']} - {app['CVE']} + +* **Producto / Versión:** `{app['Producto']}` (v`{app['Versión']}`) | **Fuente:** {app['Fuente']} +* **BaseScore:** {app['BaseScore']} +* **Regla versión:** {app['Regla versión']} +* **Justificación / Descripción:** + > {just} +""" + + with open(output_file, "w", encoding="utf-8") as f: + f.write(md_content) + + print(f"📝 Informe Markdown generado en '{output_file}'") + + +def process_targets(config_path="targets.yaml"): + try: + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + except FileNotFoundError: + print(f"❌ Archivo '{config_path}' no encontrado.") + return + + resumen_objetivos = [] + aplicables_severidad = [] + + total_consolidados = 0 + total_analizadas = 0 + total_aplicables = 0 + total_no_aplicables = 0 + total_revision = 0 + + print("🚀 Iniciando escaneo ultra-rápido en ENISA EUVD...\n") + + for target in config.get("targets", []): + vendor = target.get("vendor", "") + product = target.get("product", "") + version = target.get("version", "") + + total_consolidados += 1 + print(f"👉 Analizando: {vendor}/{product} (v{version})") + + items = fetch_fast_enisa_vulnerabilities(vendor=vendor, product=product, from_score=7.0, days_back=30) + + num_analizadas = len(items) + num_aplicables = 0 + num_no_aplicables = 0 + num_revision = 0 + + for item in items: + score = item.get("baseScore", 0.0) + score_ver = item.get("baseScoreVersion", "3.1") + euvd_id = item.get("id", "N/A") + + aliases = item.get("aliases", "") + aliases_list = aliases.strip().split("\n") if isinstance(aliases, str) else [] + cve_id = aliases_list[0] if aliases_list and aliases_list[0] else euvd_id + + desc = item.get("description", "Sin descripción disponible.") + desc_short = desc[:180].strip() + "..." if len(desc) > 180 else desc + + num_aplicables += 1 + aplicables_severidad.append({ + "Fuente": "ENISA", + "ID": euvd_id, + "CVE": cve_id, + "Producto": product, + "Versión": version, + "BaseScore": f"{score} {score_ver}".strip(), + "Regla versión": f"Afecta rama v{version.split('.')[0]}.x", + "Justificación": desc_short + }) + + total_analizadas += num_analizadas + total_aplicables += num_aplicables + total_no_aplicables += num_no_aplicables + total_revision += num_revision + + resumen_objetivos.append({ + "Fuente": "ENISA", + "Producto": product, + "Versión": version, + "Analizadas": num_analizadas, + "Aplicables": num_aplicables, + "No aplicables": num_no_aplicables, + "Revisión manual": num_revision + }) + + print(f" 📊 Encontradas: {num_analizadas} vulnerabilidades relevantes\n") + time.sleep(0.5) + + # Ordenar vulnerabilidades aplicables de mayor a menor puntuación CVSS + aplicables_severidad.sort(key=lambda x: float(x["BaseScore"].split()[0]) if x["BaseScore"].split()[0].replace('.', '', 1).isdigit() else 0.0, reverse=True) + + # 1. Guardar JSON + reporte_json = { + "generado_en": datetime.now(timezone.utc).isoformat(), + "fuentes": ["ENISA"], + "resumen_por_objetivo": resumen_objetivos, + "aplicables_por_severidad": aplicables_severidad + } + with open("reporte_vulnerabilidades.json", "w", encoding="utf-8") as f: + json.dump(reporte_json, f, indent=2, ensure_ascii=False) + + # 2. Generar Markdown + generate_markdown_report( + total_consolidados, total_analizadas, total_aplicables, + total_no_aplicables, total_revision, resumen_objetivos, + aplicables_severidad + ) + +if __name__ == "__main__": + process_targets() diff --git a/targets.yaml b/targets.yaml new file mode 100644 index 0000000..88de8c5 --- /dev/null +++ b/targets.yaml @@ -0,0 +1,41 @@ +targets: + - vendor: "Apache" + product: "Tomcat" + version: "9.0.119" + + - vendor: "Apache" + product: "Tomcat" + version: "11.0.23" + + - vendor: "Jenkins" + product: "Jenkins" + version: "2.504.3" + + - vendor: "GoHarbor" + product: "Harbor" + version: "2.11.0" + + - vendor: "Oracle" + product: "Openjdk" + version: "21.0.21" + + - vendor: "Oracle" + product: "Openjdk" + version: "1.8.0" + + - vendor: "GitLab" + product: "GitLab CE" + version: "19.1.1" + + - vendor: "Nginx" + product: "Nginx" + version: "1.30.3" + + - vendor: "Apache" + product: "Artemis" + version: "2.54.0" + + - vendor: "Jasper" + product: "JasperReports" + version: "10.0.0" +