Versiones finales

This commit is contained in:
Luis Gutierrez Lopez 2026-08-11 18:06:48 +02:00
parent 1473bfa72f
commit a0f1c6b0bc
5 changed files with 629 additions and 0 deletions

7
INFORME.sh Normal file
View File

@ -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

135
README.md
View File

@ -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 <URL_DEL_REPOSITORIO>
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.
```
```

223
check_CVEs.py Normal file
View File

@ -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()

223
check_EUVD.py Normal file
View File

@ -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()

41
targets.yaml Normal file
View File

@ -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"