Compare commits

..

No commits in common. "master" and "v2026.05.13.c1f462d" have entirely different histories.

2 changed files with 37 additions and 60 deletions

View file

@ -41,29 +41,6 @@ def _fetch_cve_html(cve_id: str) -> str:
return "" return ""
def _enrich_cve(cve_id: str) -> dict[str, str]:
"""Récupère la sévérité et le vecteur d'attaque depuis le HTML Debian Security Tracker."""
html = _fetch_cve_html(cve_id)
if not html:
return {"severity": "?", "vector": "?"}
result = {"severity": "?", "vector": "?"}
# Extraction de la sévérité depuis le bloc "Vulnerable and fixed packages"
# Recherche du motif: <td>remote</td> ou <td>local</td>
vector_match = re.search(r'<td[^>]*>\s*(remote|local)\s*</td>', html, re.IGNORECASE)
if vector_match:
result["vector"] = vector_match.group(1).lower()
# Extraction de la sévérité depuis les notes ou le tableau
# Format courant: [low], [medium], [high], [critical] dans les notes
severity_match = re.search(r'\b(unimportant|low|medium|high|critical)\b', html, re.IGNORECASE)
if severity_match:
result["severity"] = severity_match.group(1).lower()
return result
def _is_cve_actionable(cve_id: str, suite: str = "bookworm") -> bool: def _is_cve_actionable(cve_id: str, suite: str = "bookworm") -> bool:
"""Retourne True si la CVE est marquée 'fixed' pour le suite donné dans le HTML.""" """Retourne True si la CVE est marquée 'fixed' pour le suite donné dans le HTML."""
html = _fetch_cve_html(cve_id) html = _fetch_cve_html(cve_id)
@ -77,20 +54,16 @@ def _is_cve_actionable(cve_id: str, suite: str = "bookworm") -> bool:
return bool(pattern.search(html)) return bool(pattern.search(html))
def enrich_cves(cves: list[dict]) -> tuple[list[dict], int]: def filter_actionable_cves(cves: list[dict]) -> tuple[list[dict], int]:
"""Enrichit les CVEs avec fixable, severity, vector via l'API Debian. """Filtre la liste des CVE pour ajouter un flag 'fixable'.
Retourne (cve_enrichies, nombre_actionnables).""" Retourne (cve_avec_flag, nombre_actionnables)."""
if not cves: if not cves:
return [], 0 return [], 0
def check(cve: dict) -> dict: def check(cve: dict) -> dict:
try: try:
# Vérifie si corrigeable is_fixable = _is_cve_actionable(cve["id"])
cve["fixable"] = _is_cve_actionable(cve["id"]) cve["fixable"] = is_fixable
# Enrichit avec severity et vector
enriched = _enrich_cve(cve["id"])
cve["severity"] = enriched["severity"]
cve["vector"] = enriched["vector"]
return cve return cve
except Exception: except Exception:
cve["fixable"] = False cve["fixable"] = False
@ -175,27 +148,40 @@ def scan_os(target: Target) -> str:
line = line.strip() line = line.strip()
if "=" in line: if "=" in line:
key, val = line.split("=", 1) key, val = line.split("=", 1)
# Supprimer les guillemets
val = val.strip().strip('"').strip("'") val = val.strip().strip('"').strip("'")
os_data[key] = val os_data[key] = val
# 2. Extraire les champs # 2. Extraire les champs
name = os_data.get("NAME", "") version_id = os_data.get("DEBIAN_VERSION_FULL", "")
if not version_id:
version_id = os_data.get("VERSION", "")
if not version_id:
version_id = os_data.get("VERSION_ID", "")
codename = os_data.get("VERSION_CODENAME", "") codename = os_data.get("VERSION_CODENAME", "")
debian_version_full = os_data.get("DEBIAN_VERSION_FULL", "") pretty_name = os_data.get("PRETTY_NAME", "")
version_id = os_data.get("VERSION_ID", "") os_name = os_data.get("NAME", "")
# 3. Construire le libellé final # 3. Fallback sur /etc/debian_version si tout vide
# Avec DEBIAN_VERSION_FULL : NAME DEBIAN_VERSION_FULL (VERSION_CODENAME) if not version_id:
# Sans : NAME VERSION_ID (VERSION_CODENAME) ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30)
version = debian_version_full if debian_version_full else version_id version_id = ver_out.strip() if ok else ""
if name and version and codename: # 4. Fallback sur lsb_release pour le nom
return f"{name} {version} ({codename})" if not os_name:
if name and version: ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30)
return f"{name} {version}" os_name = name_out.strip() if ok else ""
# Fallback : PRETTY_NAME ou "OS inconnu" if not os_name and pretty_name:
return os_data.get("PRETTY_NAME", "OS inconnu") return pretty_name
if not os_name or not version_id:
return pretty_name if pretty_name else "OS inconnu"
# Construire le libellé final
if codename:
return f"{os_name} GNU/Linux {version_id} ({codename})"
return f"{os_name} GNU/Linux {version_id}"
def ensure_debsecan_installed(is_host: bool, vmid: str = "") -> tuple[bool, str]: def ensure_debsecan_installed(is_host: bool, vmid: str = "") -> tuple[bool, str]:
@ -246,8 +232,7 @@ def scan_apt(target: Target) -> tuple[bool, list[dict[str, str]], str]:
def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]:
# Format par défaut (pas de --format) retourne: CVE-ID package cmd = ["debsecan", "--format", "report", "--suite", "bookworm"] if target.is_host else ["pct", "exec", target.target_id, "--", "debsecan", "--format", "report", "--suite", "bookworm"]
cmd = ["debsecan", "--suite", "bookworm"] if target.is_host else ["pct", "exec", target.target_id, "--", "debsecan", "--suite", "bookworm"]
ok, stdout, stderr = run_cmd(cmd, timeout=120) ok, stdout, stderr = run_cmd(cmd, timeout=120)
if not ok: if not ok:
return False, [], stderr or stdout return False, [], stderr or stdout
@ -256,13 +241,7 @@ def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]:
for line in stdout.splitlines(): for line in stdout.splitlines():
m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line) m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line)
if m: if m:
cves.append({ cves.append({"id": m.group(1), "package": m.group(2), "url": f"https://security-tracker.debian.org/tracker/{m.group(1)}"})
"id": m.group(1),
"package": m.group(2),
"vector": "?",
"severity": "?",
"url": f"https://security-tracker.debian.org/tracker/{m.group(1)}"
})
return True, cves, "" return True, cves, ""
@ -291,9 +270,9 @@ def scan_target(target: Target, progress_cb: Callable) -> ScanResult:
if not cve_ok: if not cve_ok:
result.error = cve_err result.error = cve_err
# Enrichir les CVE via l'API Debian (severity, vector, fixable) # Filtrer les CVE actionnables via l'API Debian
if cve_list: if cve_list:
all_cves, actionable_count = enrich_cves(cve_list) all_cves, actionable_count = filter_actionable_cves(cve_list)
result.cve_list = all_cves result.cve_list = all_cves
result.cve_count = actionable_count result.cve_count = actionable_count
result.cve_total = len(all_cves) result.cve_total = len(all_cves)

View file

@ -90,17 +90,15 @@ class CVEListScreen(Screen):
with Horizontal(id="toolbar"): with Horizontal(id="toolbar"):
yield Button("⬅ Retour", id="cve-back", variant="default") yield Button("⬅ Retour", id="cve-back", variant="default")
table = DataTable(id="cve-table") table = DataTable(id="cve-table")
table.add_columns("CVE-ID", "Paquet", "Severite", "Vecteur", "Corrigeable", "Lien") table.add_columns("CVE-ID", "Paquet", "Corrigeable", "Lien")
table.cursor_type = "row" table.cursor_type = "row"
for i, cve in enumerate(self.cves): for i, cve in enumerate(self.cves):
cve_id = cve.get("id", "?") cve_id = cve.get("id", "?")
pkg = cve.get("package", "?") pkg = cve.get("package", "?")
url = cve.get("url", "") url = cve.get("url", "")
severity = cve.get("severity", "?")
vector = cve.get("vector", "?")
fixable = "🟢 Oui" if cve.get("fixable") else "🔴 Non" fixable = "🟢 Oui" if cve.get("fixable") else "🔴 Non"
self.urls[i] = url self.urls[i] = url
table.add_row(cve_id, pkg, severity, vector, fixable, url) table.add_row(cve_id, pkg, fixable, url)
yield table yield table
def on_data_table_row_selected(self, event: DataTable.RowSelected): def on_data_table_row_selected(self, event: DataTable.RowSelected):