#!/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()