diff --git a/check_EUVD.py b/check_EUVD.py deleted file mode 120000 index 4faf8e5..0000000 --- a/check_EUVD.py +++ /dev/null @@ -1 +0,0 @@ -check_EUVD_v2.0.py \ No newline at end of file diff --git a/check_EUVD.py b/check_EUVD.py new file mode 100755 index 0000000..ccc622f --- /dev/null +++ b/check_EUVD.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 + +import time +import json +import yaml +import requests +import re +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 matches_product_version(target_version, product_version_str): + """ + Compara la versión del objetivo contra la cadena product_version de ENISA. + Maneja coincidencia exacta, rangos "X to Y", "<= X", y patrones simples. + """ + if not product_version_str or product_version_str.lower() in ["n/a", "unknown", "*"]: + return True, "Versión indeterminada en la fuente (Revisar manualmente)" + + p_ver_clean = product_version_str.strip().lower() + t_ver = target_version.strip().lower() + + # Coincidencia exacta o contención directa + if t_ver == p_ver_clean or t_ver in p_ver_clean: + return True, f"Coincidencia directa con patrón '{product_version_str}'" + + # Evaluador de rangos "X to Y" + if " to " in p_ver_clean: + parts = p_ver_clean.split(" to ") + if len(parts) == 2: + start_ver = parts[0].replace("openjdk", "").replace("v", "").strip() + end_ver = parts[1].replace("openjdk", "").replace("v", "").strip() + # Si target empieza igual que la rama del rango + if t_ver.startswith(start_ver.split('_')[0]) or t_ver.startswith(start_ver.split('.')[0]): + return True, f"Afecta al rango: {product_version_str}" + + return False, f"No coincide con la versión/rango indicado ({product_version_str})" + + +def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=730): + vulnerabilities = [] + from_date = (datetime.now(timezone.utc) - timedelta(days=days_back)).strftime("%Y-%m-%d") + + # Búsqueda usando directamente vendor y product + params = { + "vendor": vendor, + "product": product, + "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) + + if response.status_code == 429: + wait_time = (attempt + 1) * 5 + 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 + + # Si no devolvió nada buscando con vendor/product exactos, probar fallback con "text" + if not items and product: + params_fallback = { + "text": product, + "fromScore": from_score, + "fromDate": from_date, + "page": 0, + "size": 100 + } + res_fb = requests.get(BASE_URL, headers=HEADERS, params=params_fallback, timeout=15) + if res_fb.status_code == 200: + items = res_fb.json().get("items", []) + + vulnerabilities.extend(items) + break + + except Exception as e: + if attempt == max_retries - 1: + print(f" ❌ Error final 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 compatible con Pandoc/XeLaTeX. + """ + 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 + +* **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 en el período indicado.*\n" + else: + # Ordenar vulnerabilidades aplicables de mayor a menor puntuación CVSS + aplicables_ordenadas = sorted( + aplicables_severidad, + key=lambda x: float(x["BaseScore"].split()[0]) if x.get("BaseScore") and x["BaseScore"].split()[0].replace('.', '', 1).isdigit() else 0.0, + reverse=True + ) + + for app in aplicables_ordenadas: + score_val = app['BaseScore'].split()[0] if app.get('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**]" + + 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 exitosamente 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 análisis de precisión 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=0.0, days_back=730) + + num_analizadas = len(items) + num_aplicables = 0 + num_no_aplicables = 0 + num_revision = 0 + + for item in items: + score = item.get("baseScore", 0.0) or 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 + + # --- EXTRACCIÓN Y EVALUACIÓN DE product_version DESDE enisaIdProduct --- + enisa_products = item.get("enisaIdProduct", []) + is_affected = False + match_rule = "Sin información de versión en la API" + + if enisa_products: + for prod_entry in enisa_products: + p_version = prod_entry.get("product_version", "") + affected, rule_msg = matches_product_version(version, p_version) + if affected: + is_affected = True + match_rule = f"`product_version`: \"{p_version}\" -> ({rule_msg})" + break + else: + # Si no hay metadatos de producto, se marca para revisión manual o aplicable por defecto + is_affected = True + match_rule = "Evaluación por coincidencia general de producto" + + if is_affected: + 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": match_rule, + "Justificación": desc_short + }) + else: + num_no_aplicables += 1 + + 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" 📊 Analizadas: {num_analizadas} | Aplicables: {num_aplicables} | Descartadas por versión: {num_no_aplicables}\n") + time.sleep(0.5) + + # 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 el informe en Markdown + generate_markdown_report( + total_consolidados=total_consolidados, + total_analizadas=total_analizadas, + total_aplicables=total_aplicables, + total_no_aplicables=total_no_aplicables, + total_revision=total_revision, + resumen_objetivos=resumen_objetivos, + aplicables_severidad=aplicables_severidad, + output_file="informe_vulnerabilidades.md" + ) + + print("✅ Análisis finalizado y archivo 'informe_vulnerabilidades.md' actualizado con éxito.") + +if __name__ == "__main__": + process_targets()