From d30a0163a93b90b57b79531c9acb2856751d4f39 Mon Sep 17 00:00:00 2001 From: Luis Gutierrez Lopez Date: Thu, 3 Sep 2026 15:09:21 +0200 Subject: [PATCH] Incluyo columna 'Necesidad Actualizacion' --- check_EUVD.py | 105 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/check_EUVD.py b/check_EUVD.py index af7744a..e3eb6da 100755 --- a/check_EUVD.py +++ b/check_EUVD.py @@ -14,6 +14,70 @@ HEADERS = { } +def evaluar_parcheo_cvss(vector_str): + """ + Evalúa el vector CVSS v3.x y devuelve la calificación de Parcheo: + 'Urgente', 'Diferible' o 'Descartable'. + + Reglas aplicadas: + - Descartable: AV:L / AV:P o PR:H + - Urgente: AV:N + AC:L + PR:N + UI:N y además (C:H o I:H o A:H o S:C) + - Diferible: Resto de casos + """ + if not vector_str or vector_str == "N/A": + return "Diferible" + + # Convertir el vector en un diccionario de componentes (ej: {'AV': 'N', 'AC': 'L', ...}) + metrics = {} + parts = vector_str.split('/') + for part in parts: + if ':' in part: + k, v = part.split(':', 1) + metrics[k.upper()] = v.upper() + + av = metrics.get('AV', '') + ac = metrics.get('AC', '') + pr = metrics.get('PR', '') + ui = metrics.get('UI', '') + s = metrics.get('S', '') + c = metrics.get('C', '') + i = metrics.get('I', '') + a = metrics.get('A', '') + + # 1. Reglas de Descartable + if av in ['L', 'P'] or pr == 'H': + return "Descartable" + + # 2. Reglas de Urgente + es_remoto_facil = (av == 'N' and ac == 'L' and pr == 'N' and ui == 'N') + impacto_alto = (c == 'H' or i == 'H' or a == 'H' or s == 'C') + + if es_remoto_facil and impacto_alto: + return "Urgente" + + # 3. Resto de escenarios -> Diferible + return "Diferible" + + +def calcular_parcheo_global(lista_parcheo_vulnerabilidades): + """ + Determina la calificación de Parcheo del producto según la máxima criticidad encontrada: + Si hay al menos una Urgente -> Urgente + Sino, si hay al menos una Diferible -> Diferible + Sino, si todas son Descartables -> Descartable + Sin vulnerabilidades aplicables -> Sin Acción + """ + if not lista_parcheo_vulnerabilidades: + return "Sin Acción" + + if "Urgente" in lista_parcheo_vulnerabilidades: + return "Urgente" + elif "Diferible" in lista_parcheo_vulnerabilidades: + return "Diferible" + else: + return "Descartable" + + def parse_single_version(v_str): """ Limpia y extrae la versión semántica limpia de una cadena. @@ -34,11 +98,6 @@ def parse_single_version(v_str): def matches_product_version(target_version_str, product_version_str): """ Evalúa matemáticamente si target_version_str se ve afectada por product_version_str. - Soporta: - - Expresiones con operadores: "<19.1.1", "<=19.1.1", ">19.0", ">=19.0" - - Rangos combinados: "19.1 <19.1.1", "19.1 <19.2.3" - - Rangos con palabra "to": "1.7.0 to 1.7.10" - - Coincidencia exacta o comodines '*' """ if not product_version_str or product_version_str.lower() in ["n/a", "unknown", "*"]: return True, "Versión indeterminada en la fuente (Revisar manualmente)" @@ -69,7 +128,6 @@ def matches_product_version(target_version_str, product_version_str): f"Versión marcada como corregida mediante parche {patch_version}" ) - if not t_ver: if target_version_str.strip().lower() in product_version_str.lower(): return True, f"Coincidencia directa por cadena '{product_version_str}'" @@ -204,12 +262,23 @@ def generate_markdown_report(total_consolidados, total_analizadas, total_aplicab ## Resumen por objetivo -| Fuente | Producto | Versión | Analizadas | Aplicables | No aplicables | Revisión manual | -|---|---|---|---|---|---|---| +| Fuente | Producto | Versión | Analizadas | Aplicables | No aplicables | Revisión manual | Necesidad Parcheo | +|---|---|---|---|---|---|---|---| """ 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" + parcheo_val = obj.get('Parcheo', 'Sin Acción') + # Formato visual destacado según urgencia + if parcheo_val == "Urgente": + parcheo_str = "**URGENTE**" + elif parcheo_val == "Diferible": + parcheo_str = "Diferible" + elif parcheo_val == "Descartable": + parcheo_str = "Descartable" + else: + parcheo_str = "Sin Acción" + + md_content += f"| {obj['Fuente']} | {obj['Producto']} | {obj['Versión']} | {obj['Analizadas']} | {obj['Aplicables']} | {obj['No aplicables']} | {obj['Revisión manual']} | {parcheo_str} |\n" md_content += """ --- @@ -244,12 +313,13 @@ def generate_markdown_report(total_consolidados, total_analizadas, total_aplicab just = app['Justificación'].strip() score_vector = app.get('BaseScoreVector', 'N/A') + parcheo_ind = app.get('Parcheo', 'Diferible') md_content += f""" ### {badge} - {app['ID']} - {app['CVE']} * **Producto / Versión:** `{app['Producto']}` (v`{app['Versión']}`) | **Fuente:** {app['Fuente']} -* **BaseScore:** {app['BaseScore']} +* **BaseScore:** {app['BaseScore']} | **Parcheo Vulnerabilidad:** **{parcheo_ind}** * **Vector CVSS:** `{score_vector}` * **Regla versión:** {app['Regla versión']} * **Justificación / Descripción:** @@ -297,6 +367,7 @@ def process_targets(config_path="targets.yaml"): num_aplicables = 0 num_no_aplicables = 0 num_revision = 0 + parcheo_vulnerabilidades_target = [] for item in items: score = item.get("baseScore", 0.0) or 0.0 @@ -329,6 +400,9 @@ def process_targets(config_path="targets.yaml"): if is_affected: num_aplicables += 1 + parcheo_vuln = evaluar_parcheo_cvss(score_vector) + parcheo_vulnerabilidades_target.append(parcheo_vuln) + aplicables_severidad.append({ "Fuente": "ENISA", "ID": euvd_id, @@ -338,7 +412,8 @@ def process_targets(config_path="targets.yaml"): "BaseScore": f"{score} {score_ver}".strip(), "BaseScoreVector": score_vector, "Regla versión": match_rule, - "Justificación": desc_short + "Justificación": desc_short, + "Parcheo": parcheo_vuln }) else: num_no_aplicables += 1 @@ -348,6 +423,9 @@ def process_targets(config_path="targets.yaml"): total_no_aplicables += num_no_aplicables total_revision += num_revision + # Evaluar el parcheo acumulado para el producto + parcheo_global_target = calcular_parcheo_global(parcheo_vulnerabilidades_target) + resumen_objetivos.append({ "Fuente": "ENISA", "Producto": product, @@ -355,10 +433,11 @@ def process_targets(config_path="targets.yaml"): "Analizadas": num_analizadas, "Aplicables": num_aplicables, "No aplicables": num_no_aplicables, - "Revisión manual": num_revision + "Revisión manual": num_revision, + "Parcheo": parcheo_global_target }) - print(f" Analizadas: {num_analizadas} | Aplicables: {num_aplicables} | Descartadas por versión: {num_no_aplicables}\n") + print(f" Analizadas: {num_analizadas} | Aplicables: {num_aplicables} | Descartadas por versión: {num_no_aplicables} | Parcheo: {parcheo_global_target}\n") time.sleep(0.5) reporte_json = {