Incluyo columna 'Necesidad Actualizacion'

This commit is contained in:
Luis Gutierrez Lopez 2026-09-03 15:09:21 +02:00
parent 2c789e9d45
commit d30a0163a9

View File

@ -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): def parse_single_version(v_str):
""" """
Limpia y extrae la versión semántica limpia de una cadena. 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): 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. 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", "*"]: if not product_version_str or product_version_str.lower() in ["n/a", "unknown", "*"]:
return True, "Versión indeterminada en la fuente (Revisar manualmente)" 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}" f"Versión marcada como corregida mediante parche {patch_version}"
) )
if not t_ver: if not t_ver:
if target_version_str.strip().lower() in product_version_str.lower(): if target_version_str.strip().lower() in product_version_str.lower():
return True, f"Coincidencia directa por cadena '{product_version_str}'" 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 ## 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: 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 += """ md_content += """
--- ---
@ -244,12 +313,13 @@ def generate_markdown_report(total_consolidados, total_analizadas, total_aplicab
just = app['Justificación'].strip() just = app['Justificación'].strip()
score_vector = app.get('BaseScoreVector', 'N/A') score_vector = app.get('BaseScoreVector', 'N/A')
parcheo_ind = app.get('Parcheo', 'Diferible')
md_content += f""" md_content += f"""
### {badge} - {app['ID']} - {app['CVE']} ### {badge} - {app['ID']} - {app['CVE']}
* **Producto / Versión:** `{app['Producto']}` (v`{app['Versión']}`) | **Fuente:** {app['Fuente']} * **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}` * **Vector CVSS:** `{score_vector}`
* **Regla versión:** {app['Regla versión']} * **Regla versión:** {app['Regla versión']}
* **Justificación / Descripción:** * **Justificación / Descripción:**
@ -297,6 +367,7 @@ def process_targets(config_path="targets.yaml"):
num_aplicables = 0 num_aplicables = 0
num_no_aplicables = 0 num_no_aplicables = 0
num_revision = 0 num_revision = 0
parcheo_vulnerabilidades_target = []
for item in items: for item in items:
score = item.get("baseScore", 0.0) or 0.0 score = item.get("baseScore", 0.0) or 0.0
@ -329,6 +400,9 @@ def process_targets(config_path="targets.yaml"):
if is_affected: if is_affected:
num_aplicables += 1 num_aplicables += 1
parcheo_vuln = evaluar_parcheo_cvss(score_vector)
parcheo_vulnerabilidades_target.append(parcheo_vuln)
aplicables_severidad.append({ aplicables_severidad.append({
"Fuente": "ENISA", "Fuente": "ENISA",
"ID": euvd_id, "ID": euvd_id,
@ -338,7 +412,8 @@ def process_targets(config_path="targets.yaml"):
"BaseScore": f"{score} {score_ver}".strip(), "BaseScore": f"{score} {score_ver}".strip(),
"BaseScoreVector": score_vector, "BaseScoreVector": score_vector,
"Regla versión": match_rule, "Regla versión": match_rule,
"Justificación": desc_short "Justificación": desc_short,
"Parcheo": parcheo_vuln
}) })
else: else:
num_no_aplicables += 1 num_no_aplicables += 1
@ -348,6 +423,9 @@ def process_targets(config_path="targets.yaml"):
total_no_aplicables += num_no_aplicables total_no_aplicables += num_no_aplicables
total_revision += num_revision total_revision += num_revision
# Evaluar el parcheo acumulado para el producto
parcheo_global_target = calcular_parcheo_global(parcheo_vulnerabilidades_target)
resumen_objetivos.append({ resumen_objetivos.append({
"Fuente": "ENISA", "Fuente": "ENISA",
"Producto": product, "Producto": product,
@ -355,10 +433,11 @@ def process_targets(config_path="targets.yaml"):
"Analizadas": num_analizadas, "Analizadas": num_analizadas,
"Aplicables": num_aplicables, "Aplicables": num_aplicables,
"No aplicables": num_no_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) time.sleep(0.5)
reporte_json = { reporte_json = {