Afinado de busqueda de version mayor, menor o igual que
This commit is contained in:
parent
a4c1b058c5
commit
233054daa8
111
check_EUVD.py
111
check_EUVD.py
@ -6,45 +6,109 @@ import yaml
|
|||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from packaging.version import Version, InvalidVersion
|
||||||
|
|
||||||
BASE_URL = "https://euvdservices.enisa.europa.eu/api/search"
|
BASE_URL = "https://euvdservices.enisa.europa.eu/api/search"
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) EUVD-Auditor/1.0"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) EUVD-Auditor/1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
def matches_product_version(target_version, product_version_str):
|
|
||||||
|
def parse_single_version(v_str):
|
||||||
"""
|
"""
|
||||||
Compara la versión del objetivo contra la cadena product_version de ENISA.
|
Limpia y extrae la versión semántica limpia de una cadena.
|
||||||
Maneja coincidencia exacta, rangos "X to Y", "<= X", y patrones simples.
|
Ejemplos: 'v1.8.0' -> '1.8.0', 'openjdk 1.8.0_20' -> '1.8.0'
|
||||||
|
"""
|
||||||
|
if not v_str:
|
||||||
|
return None
|
||||||
|
cleaned = re.sub(r'^(openjdk|v|version)\s*', '', v_str.strip(), flags=re.IGNORECASE)
|
||||||
|
match = re.search(r'\b\d+(\.\d+)+', cleaned)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
return Version(match.group(0))
|
||||||
|
except InvalidVersion:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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", "*"]:
|
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)"
|
||||||
|
|
||||||
p_ver_clean = product_version_str.strip().lower()
|
t_ver = parse_single_version(target_version_str)
|
||||||
t_ver = target_version.strip().lower()
|
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}'"
|
||||||
|
return False, f"No coincide con '{product_version_str}'"
|
||||||
|
|
||||||
# Coincidencia exacta o contención directa
|
p_str = product_version_str.strip()
|
||||||
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"
|
# 1. Manejo de rangos con palabra "to" (ej. "1.7.0 to 1.8.2")
|
||||||
if " to " in p_ver_clean:
|
if " to " in p_str.lower():
|
||||||
parts = p_ver_clean.split(" to ")
|
parts = re.split(r'\s+to\s+', p_str, flags=re.IGNORECASE)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
start_ver = parts[0].replace("openjdk", "").replace("v", "").strip()
|
min_v = parse_single_version(parts[0])
|
||||||
end_ver = parts[1].replace("openjdk", "").replace("v", "").strip()
|
max_v = parse_single_version(parts[1])
|
||||||
# Si target empieza igual que la rama del rango
|
if min_v and max_v:
|
||||||
if t_ver.startswith(start_ver.split('_')[0]) or t_ver.startswith(start_ver.split('.')[0]):
|
if min_v <= t_ver <= max_v:
|
||||||
return True, f"Afecta al rango: {product_version_str}"
|
return True, f"Afecta al rango {min_v} <= {t_ver} <= {max_v}"
|
||||||
|
else:
|
||||||
|
return False, f"Fuera del rango ({min_v} a {max_v})"
|
||||||
|
|
||||||
return False, f"No coincide con la versión/rango indicado ({product_version_str})"
|
# 2. Extraer condiciones/operadores de la cadena product_version
|
||||||
|
tokens = re.findall(r'(<=|>=|<|>)?\s*(\d+(?:\.\d+)+[a-zA-Z0-9_\.-]*)', p_str)
|
||||||
|
|
||||||
|
if not tokens:
|
||||||
|
if t_ver.public in p_str:
|
||||||
|
return True, f"Coincidencia textual directa con '{product_version_str}'"
|
||||||
|
return False, f"No coincide con '{product_version_str}'"
|
||||||
|
|
||||||
|
all_conditions_met = True
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
for op, ver_str in tokens:
|
||||||
|
cond_ver = parse_single_version(ver_str)
|
||||||
|
if not cond_ver:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if op == "<":
|
||||||
|
met = t_ver < cond_ver
|
||||||
|
reasons.append(f"{t_ver} < {cond_ver} ({'Sí' if met else 'No'})")
|
||||||
|
elif op == "<=":
|
||||||
|
met = t_ver <= cond_ver
|
||||||
|
reasons.append(f"{t_ver} <= {cond_ver} ({'Sí' if met else 'No'})")
|
||||||
|
elif op == ">":
|
||||||
|
met = t_ver > cond_ver
|
||||||
|
reasons.append(f"{t_ver} > {cond_ver} ({'Sí' if met else 'No'})")
|
||||||
|
elif op == ">=":
|
||||||
|
met = t_ver >= cond_ver
|
||||||
|
reasons.append(f"{t_ver} >= {cond_ver} ({'Sí' if met else 'No'})")
|
||||||
|
else:
|
||||||
|
met = (t_ver == cond_ver) or (t_ver.public.startswith(cond_ver.public))
|
||||||
|
reasons.append(f"Rama/Coincidencia {cond_ver} ({'Sí' if met else 'No'})")
|
||||||
|
|
||||||
|
if not met:
|
||||||
|
all_conditions_met = False
|
||||||
|
|
||||||
|
rule_msg = " | ".join(reasons)
|
||||||
|
if all_conditions_met:
|
||||||
|
return True, f"Cumple condiciones: {rule_msg}"
|
||||||
|
else:
|
||||||
|
return False, f"No cumple condiciones: {rule_msg}"
|
||||||
|
|
||||||
|
|
||||||
def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=730):
|
def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=730):
|
||||||
vulnerabilities = []
|
vulnerabilities = []
|
||||||
from_date = (datetime.now(timezone.utc) - timedelta(days=days_back)).strftime("%Y-%m-%d")
|
from_date = (datetime.now(timezone.utc) - timedelta(days=days_back)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# Búsqueda usando directamente vendor y product
|
|
||||||
params = {
|
params = {
|
||||||
"vendor": vendor,
|
"vendor": vendor,
|
||||||
"product": product,
|
"product": product,
|
||||||
@ -69,7 +133,6 @@ def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=
|
|||||||
|
|
||||||
items = data.get("items", data.get("content", [])) if isinstance(data, dict) else data
|
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:
|
if not items and product:
|
||||||
params_fallback = {
|
params_fallback = {
|
||||||
"text": product,
|
"text": product,
|
||||||
@ -92,6 +155,7 @@ def fetch_fast_enisa_vulnerabilities(vendor, product, from_score=7.0, days_back=
|
|||||||
|
|
||||||
return vulnerabilities
|
return vulnerabilities
|
||||||
|
|
||||||
|
|
||||||
def generate_markdown_report(total_consolidados, total_analizadas, total_aplicables,
|
def generate_markdown_report(total_consolidados, total_analizadas, total_aplicables,
|
||||||
total_no_aplicables, total_revision, resumen_objetivos,
|
total_no_aplicables, total_revision, resumen_objetivos,
|
||||||
aplicables_severidad, output_file="informe_vulnerabilidades.md"):
|
aplicables_severidad, output_file="informe_vulnerabilidades.md"):
|
||||||
@ -130,7 +194,6 @@ def generate_markdown_report(total_consolidados, total_analizadas, total_aplicab
|
|||||||
if not aplicables_severidad:
|
if not aplicables_severidad:
|
||||||
md_content += "\n*No se encontraron vulnerabilidades aplicables con los criterios seleccionados en el período indicado.*\n"
|
md_content += "\n*No se encontraron vulnerabilidades aplicables con los criterios seleccionados en el período indicado.*\n"
|
||||||
else:
|
else:
|
||||||
# Ordenar vulnerabilidades aplicables de mayor a menor puntuación CVSS
|
|
||||||
aplicables_ordenadas = sorted(
|
aplicables_ordenadas = sorted(
|
||||||
aplicables_severidad,
|
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,
|
key=lambda x: float(x["BaseScore"].split()[0]) if x.get("BaseScore") and x["BaseScore"].split()[0].replace('.', '', 1).isdigit() else 0.0,
|
||||||
@ -154,12 +217,14 @@ def generate_markdown_report(total_consolidados, total_analizadas, total_aplicab
|
|||||||
badge = "[**BAJA**]"
|
badge = "[**BAJA**]"
|
||||||
|
|
||||||
just = app['Justificación'].strip()
|
just = app['Justificación'].strip()
|
||||||
|
score_vector = app.get('BaseScoreVector', 'N/A')
|
||||||
|
|
||||||
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']}
|
||||||
|
* **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:**
|
||||||
> {just}
|
> {just}
|
||||||
@ -210,6 +275,7 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
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
|
||||||
score_ver = item.get("baseScoreVersion", "3.1")
|
score_ver = item.get("baseScoreVersion", "3.1")
|
||||||
|
score_vector = item.get("baseScoreVector", "N/A")
|
||||||
euvd_id = item.get("id", "N/A")
|
euvd_id = item.get("id", "N/A")
|
||||||
|
|
||||||
aliases = item.get("aliases", "")
|
aliases = item.get("aliases", "")
|
||||||
@ -219,7 +285,6 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
desc = item.get("description", "Sin descripción disponible.")
|
desc = item.get("description", "Sin descripción disponible.")
|
||||||
desc_short = desc[:180].strip() + "..." if len(desc) > 180 else desc
|
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", [])
|
enisa_products = item.get("enisaIdProduct", [])
|
||||||
is_affected = False
|
is_affected = False
|
||||||
match_rule = "Sin información de versión en la API"
|
match_rule = "Sin información de versión en la API"
|
||||||
@ -233,7 +298,6 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
match_rule = f"`product_version`: \"{p_version}\" -> ({rule_msg})"
|
match_rule = f"`product_version`: \"{p_version}\" -> ({rule_msg})"
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Si no hay metadatos de producto, se marca para revisión manual o aplicable por defecto
|
|
||||||
is_affected = True
|
is_affected = True
|
||||||
match_rule = "Evaluación por coincidencia general de producto"
|
match_rule = "Evaluación por coincidencia general de producto"
|
||||||
|
|
||||||
@ -246,6 +310,7 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
"Producto": product,
|
"Producto": product,
|
||||||
"Versión": version,
|
"Versión": version,
|
||||||
"BaseScore": f"{score} {score_ver}".strip(),
|
"BaseScore": f"{score} {score_ver}".strip(),
|
||||||
|
"BaseScoreVector": score_vector,
|
||||||
"Regla versión": match_rule,
|
"Regla versión": match_rule,
|
||||||
"Justificación": desc_short
|
"Justificación": desc_short
|
||||||
})
|
})
|
||||||
@ -270,7 +335,6 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
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}\n")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# 1. Guardar JSON
|
|
||||||
reporte_json = {
|
reporte_json = {
|
||||||
"generado_en": datetime.now(timezone.utc).isoformat(),
|
"generado_en": datetime.now(timezone.utc).isoformat(),
|
||||||
"fuentes": ["ENISA"],
|
"fuentes": ["ENISA"],
|
||||||
@ -280,7 +344,6 @@ def process_targets(config_path="targets.yaml"):
|
|||||||
with open("reporte_vulnerabilidades.json", "w", encoding="utf-8") as f:
|
with open("reporte_vulnerabilidades.json", "w", encoding="utf-8") as f:
|
||||||
json.dump(reporte_json, f, indent=2, ensure_ascii=False)
|
json.dump(reporte_json, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
# 2. Generar el informe en Markdown
|
|
||||||
generate_markdown_report(
|
generate_markdown_report(
|
||||||
total_consolidados=total_consolidados,
|
total_consolidados=total_consolidados,
|
||||||
total_analizadas=total_analizadas,
|
total_analizadas=total_analizadas,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user