From d90f74dd8f0f1c8126cc0b70812c6269db430dc4 Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 03:59:47 +0200 Subject: [PATCH 1/7] fix(os): use VERSION_ID from /etc/os-release for full version number in LXC --- full_updater/backend/scanner.py | 49 +++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index a721c21..27aee27 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -140,36 +140,45 @@ def scan_os(target: Target) -> str: """Récupère le nom, la version complète et le codename de l'OS.""" prefix = [] if target.is_host else ["pct", "exec", target.target_id, "--"] - # 1. Récupérer le nom (Debian, Ubuntu...) - ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) - os_name = name_out.strip() if ok else "" + # 1. Lire /etc/os-release pour VERSION_ID (ex: 12.9) et PRETTY_NAME + ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30) + version_id = "" + pretty_name = "" + if ok: + ver_match = re.search(r'VERSION_ID="([^"]+)"', osrel_out) + if ver_match: + version_id = ver_match.group(1) + pretty_match = re.search(r'PRETTY_NAME="([^"]+)"', osrel_out) + if pretty_match: + pretty_name = pretty_match.group(1) - # 2. Récupérer le codename (bookworm, trixie...) + # 2. Récupérer le codename via lsb_release ok, code_out, _ = run_cmd(prefix + ["lsb_release", "-cs"], timeout=30) codename = code_out.strip() if ok else "" - # 3. Récupérer la version complète depuis /etc/debian_version (ex: 12.9) - ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) - version = ver_out.strip() if ok else "" + # 3. Fallback sur /etc/debian_version si VERSION_ID vide + if not version_id: + ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) + version_id = ver_out.strip() if ok else "" - # 4. Fallback sur lsb_release -rs si /etc/debian_version vide - if not version: + # 4. Fallback sur lsb_release -rs + if not version_id: ok, ver_out, _ = run_cmd(prefix + ["lsb_release", "-rs"], timeout=30) - version = ver_out.strip() if ok else "" + version_id = ver_out.strip() if ok else "" - # 5. Fallback sur /etc/os-release si tout le reste échoue - if not os_name or not version: - ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30) - if ok: - pretty = re.search(r'PRETTY_NAME="([^"]+)"', osrel_out) - if pretty: - return pretty.group(1) - return "OS inconnu" + # 5. Récupérer le nom via lsb_release + ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) + os_name = name_out.strip() if ok else "" + + if not os_name and pretty_name: + 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} ({codename})" - return f"{os_name} GNU/Linux {version}" + 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]: From f0f59f04682c395a669864b9c6c58a61a9ea4a8d Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:05:45 +0200 Subject: [PATCH 2/7] fix(os): read DEBIAN_VERSION_FULL from /etc/os-release first - Check DEBIAN_VERSION_FULL (ex: 13.4) as primary source - Fallback to VERSION, then VERSION_ID, then /etc/debian_version --- full_updater/backend/scanner.py | 40 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 27aee27..486b9fe 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -140,33 +140,41 @@ def scan_os(target: Target) -> str: """Récupère le nom, la version complète et le codename de l'OS.""" prefix = [] if target.is_host else ["pct", "exec", target.target_id, "--"] - # 1. Lire /etc/os-release pour VERSION_ID (ex: 12.9) et PRETTY_NAME + # 1. Lire /etc/os-release ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30) version_id = "" pretty_name = "" + codename = "" if ok: - ver_match = re.search(r'VERSION_ID="([^"]+)"', osrel_out) - if ver_match: - version_id = ver_match.group(1) + # Priorité 1 : DEBIAN_VERSION_FULL (ex: 13.4) + ver_full_match = re.search(r'DEBIAN_VERSION_FULL="?([^"\n]+)"?', osrel_out) + if ver_full_match: + version_id = ver_full_match.group(1).strip() + # Priorité 2 : VERSION (ex: "13 (trixie)") + if not version_id: + ver_match = re.search(r'VERSION="([^"]+)"', osrel_out) + if ver_match: + version_id = ver_match.group(1).strip() + # Priorité 3 : VERSION_ID (ex: "13") + if not version_id: + ver_match = re.search(r'VERSION_ID="([^"]+)"', osrel_out) + if ver_match: + version_id = ver_match.group(1).strip() + # Nom affichable pretty_match = re.search(r'PRETTY_NAME="([^"]+)"', osrel_out) if pretty_match: - pretty_name = pretty_match.group(1) + pretty_name = pretty_match.group(1).strip() + # Codename + code_match = re.search(r'VERSION_CODENAME="?([^"\n]+)"?', osrel_out) + if code_match: + codename = code_match.group(1).strip() - # 2. Récupérer le codename via lsb_release - ok, code_out, _ = run_cmd(prefix + ["lsb_release", "-cs"], timeout=30) - codename = code_out.strip() if ok else "" - - # 3. Fallback sur /etc/debian_version si VERSION_ID vide + # 2. Fallback sur /etc/debian_version si tout vide if not version_id: ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) version_id = ver_out.strip() if ok else "" - # 4. Fallback sur lsb_release -rs - if not version_id: - ok, ver_out, _ = run_cmd(prefix + ["lsb_release", "-rs"], timeout=30) - version_id = ver_out.strip() if ok else "" - - # 5. Récupérer le nom via lsb_release + # 3. Récupérer le nom via lsb_release ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) os_name = name_out.strip() if ok else "" From c1f462d2093e16ee495164865a79f57afb8350be Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:10:01 +0200 Subject: [PATCH 3/7] fix(os): parse /etc/os-release line-by-line instead of regex - pct exec returns stdout differently than local cat - Line-by-line parsing is more robust across LXC boundaries - Handles quotes correctly --- full_updater/backend/scanner.py | 55 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 486b9fe..818a35c 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -140,43 +140,38 @@ def scan_os(target: Target) -> str: """Récupère le nom, la version complète et le codename de l'OS.""" prefix = [] if target.is_host else ["pct", "exec", target.target_id, "--"] - # 1. Lire /etc/os-release + # 1. Lire /etc/os-release et parser ligne par ligne ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30) - version_id = "" - pretty_name = "" - codename = "" + os_data = {} if ok: - # Priorité 1 : DEBIAN_VERSION_FULL (ex: 13.4) - ver_full_match = re.search(r'DEBIAN_VERSION_FULL="?([^"\n]+)"?', osrel_out) - if ver_full_match: - version_id = ver_full_match.group(1).strip() - # Priorité 2 : VERSION (ex: "13 (trixie)") - if not version_id: - ver_match = re.search(r'VERSION="([^"]+)"', osrel_out) - if ver_match: - version_id = ver_match.group(1).strip() - # Priorité 3 : VERSION_ID (ex: "13") - if not version_id: - ver_match = re.search(r'VERSION_ID="([^"]+)"', osrel_out) - if ver_match: - version_id = ver_match.group(1).strip() - # Nom affichable - pretty_match = re.search(r'PRETTY_NAME="([^"]+)"', osrel_out) - if pretty_match: - pretty_name = pretty_match.group(1).strip() - # Codename - code_match = re.search(r'VERSION_CODENAME="?([^"\n]+)"?', osrel_out) - if code_match: - codename = code_match.group(1).strip() + for line in osrel_out.splitlines(): + line = line.strip() + if "=" in line: + key, val = line.split("=", 1) + # Supprimer les guillemets + val = val.strip().strip('"').strip("'") + os_data[key] = val - # 2. Fallback sur /etc/debian_version si tout vide + # 2. Extraire les champs + 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", "") + pretty_name = os_data.get("PRETTY_NAME", "") + os_name = os_data.get("NAME", "") + + # 3. Fallback sur /etc/debian_version si tout vide if not version_id: ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) version_id = ver_out.strip() if ok else "" - # 3. Récupérer le nom via lsb_release - ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) - os_name = name_out.strip() if ok else "" + # 4. Fallback sur lsb_release pour le nom + if not os_name: + ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) + os_name = name_out.strip() if ok else "" if not os_name and pretty_name: return pretty_name From 721e677fa648afc1004f0c000d3e4f0b3e1af57a Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:15:57 +0200 Subject: [PATCH 4/7] fix(os): use NAME directly, avoid double 'GNU/Linux' - NAME already contains 'Debian GNU/Linux', don't append it again - With DEBIAN_VERSION_FULL: NAME + DEBIAN_VERSION_FULL + (codename) - Without: NAME + VERSION_ID + (codename) - Fixes duplication bug --- full_updater/backend/scanner.py | 39 +++++++++++---------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 818a35c..4d2d768 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -148,40 +148,27 @@ def scan_os(target: Target) -> str: line = line.strip() if "=" in line: key, val = line.split("=", 1) - # Supprimer les guillemets val = val.strip().strip('"').strip("'") os_data[key] = val # 2. Extraire les champs - 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", "") - + name = os_data.get("NAME", "") codename = os_data.get("VERSION_CODENAME", "") - pretty_name = os_data.get("PRETTY_NAME", "") - os_name = os_data.get("NAME", "") + debian_version_full = os_data.get("DEBIAN_VERSION_FULL", "") + version_id = os_data.get("VERSION_ID", "") - # 3. Fallback sur /etc/debian_version si tout vide - if not version_id: - ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) - version_id = ver_out.strip() if ok else "" + # 3. Construire le libellé final + # Avec DEBIAN_VERSION_FULL : NAME DEBIAN_VERSION_FULL (VERSION_CODENAME) + # Sans : NAME VERSION_ID (VERSION_CODENAME) + version = debian_version_full if debian_version_full else version_id - # 4. Fallback sur lsb_release pour le nom - if not os_name: - ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) - os_name = name_out.strip() if ok else "" + if name and version and codename: + return f"{name} {version} ({codename})" + if name and version: + return f"{name} {version}" - if not os_name and pretty_name: - 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}" + # Fallback : PRETTY_NAME ou "OS inconnu" + return os_data.get("PRETTY_NAME", "OS inconnu") def ensure_debsecan_installed(is_host: bool, vmid: str = "") -> tuple[bool, str]: From 13d826ecd2322bc78b74719a954f9a1568640d39 Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:20:14 +0200 Subject: [PATCH 5/7] feat(cve): add severity and vector columns to CVE list - Parse [remote|local] and [severity] from debsecan output - Display Severity and Vector columns in CVEListScreen - Severity values: unimportant, low, medium, high, critical --- full_updater/backend/scanner.py | 11 +++++++++-- full_updater/ui/detail_screens.py | 6 ++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 4d2d768..2ae08ad 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -226,9 +226,16 @@ def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: cves = [] for line in stdout.splitlines(): - m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line) + # Format: CVE-XXXX-XXXX package [remote|local] [severity] - description + m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)(?:\s+\[(remote|local)\])?(?:\s+\[(unimportant|low|medium|high|critical)\])?", line) if m: - cves.append({"id": m.group(1), "package": m.group(2), "url": f"https://security-tracker.debian.org/tracker/{m.group(1)}"}) + cves.append({ + "id": m.group(1), + "package": m.group(2), + "vector": m.group(3) or "?", + "severity": m.group(4) or "?", + "url": f"https://security-tracker.debian.org/tracker/{m.group(1)}" + }) return True, cves, "" diff --git a/full_updater/ui/detail_screens.py b/full_updater/ui/detail_screens.py index 78b31b4..5e95e10 100644 --- a/full_updater/ui/detail_screens.py +++ b/full_updater/ui/detail_screens.py @@ -90,15 +90,17 @@ class CVEListScreen(Screen): with Horizontal(id="toolbar"): yield Button("⬅ Retour", id="cve-back", variant="default") table = DataTable(id="cve-table") - table.add_columns("CVE-ID", "Paquet", "Corrigeable", "Lien") + table.add_columns("CVE-ID", "Paquet", "Severite", "Vecteur", "Corrigeable", "Lien") table.cursor_type = "row" for i, cve in enumerate(self.cves): cve_id = cve.get("id", "?") pkg = cve.get("package", "?") url = cve.get("url", "") + severity = cve.get("severity", "?") + vector = cve.get("vector", "?") fixable = "🟢 Oui" if cve.get("fixable") else "🔴 Non" self.urls[i] = url - table.add_row(cve_id, pkg, fixable, url) + table.add_row(cve_id, pkg, severity, vector, fixable, url) yield table def on_data_table_row_selected(self, event: DataTable.RowSelected): From 9107009370de3b4f2827a3d2849f1ed0234c3e34 Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:23:45 +0200 Subject: [PATCH 6/7] fix(cve): robust parsing of [remote|local] and [severity] flags - Use re.findall to extract all bracketed flags instead of positional regex - Fixes issue where optional groups were not captured correctly --- full_updater/backend/scanner.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 2ae08ad..9e317bb 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -227,13 +227,22 @@ def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: cves = [] for line in stdout.splitlines(): # Format: CVE-XXXX-XXXX package [remote|local] [severity] - description - m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)(?:\s+\[(remote|local)\])?(?:\s+\[(unimportant|low|medium|high|critical)\])?", line) + m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line) if m: + # Extraire tous les flags entre crochets + flags = re.findall(r"\[(\w+)\]", line) + vector = "?" + severity = "?" + for f in flags: + if f in ("remote", "local"): + vector = f + elif f in ("unimportant", "low", "medium", "high", "critical"): + severity = f cves.append({ "id": m.group(1), "package": m.group(2), - "vector": m.group(3) or "?", - "severity": m.group(4) or "?", + "vector": vector, + "severity": severity, "url": f"https://security-tracker.debian.org/tracker/{m.group(1)}" }) return True, cves, "" From 11ada690a121c3759b334a66cc692a37dd46de9a Mon Sep 17 00:00:00 2001 From: enzo Date: Wed, 13 May 2026 04:33:05 +0200 Subject: [PATCH 7/7] fix(cve): enrich CVEs with severity/vector from Debian Security Tracker HTML - debsecan default format has no [remote|local] or [severity] flags - Now fetches severity and vector from security-tracker.debian.org HTML - scan_cve uses default format (no --format report) - enrich_cves replaces filter_actionable_cves, adds severity+vector --- full_updater/backend/scanner.py | 58 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 9e317bb..f00185a 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -41,6 +41,29 @@ def _fetch_cve_html(cve_id: str) -> str: 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: remote ou local + vector_match = re.search(r']*>\s*(remote|local)\s*', 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: """Retourne True si la CVE est marquée 'fixed' pour le suite donné dans le HTML.""" html = _fetch_cve_html(cve_id) @@ -54,16 +77,20 @@ def _is_cve_actionable(cve_id: str, suite: str = "bookworm") -> bool: return bool(pattern.search(html)) -def filter_actionable_cves(cves: list[dict]) -> tuple[list[dict], int]: - """Filtre la liste des CVE pour ajouter un flag 'fixable'. - Retourne (cve_avec_flag, nombre_actionnables).""" +def enrich_cves(cves: list[dict]) -> tuple[list[dict], int]: + """Enrichit les CVEs avec fixable, severity, vector via l'API Debian. + Retourne (cve_enrichies, nombre_actionnables).""" if not cves: return [], 0 def check(cve: dict) -> dict: try: - is_fixable = _is_cve_actionable(cve["id"]) - cve["fixable"] = is_fixable + # Vérifie si corrigeable + cve["fixable"] = _is_cve_actionable(cve["id"]) + # Enrichit avec severity et vector + enriched = _enrich_cve(cve["id"]) + cve["severity"] = enriched["severity"] + cve["vector"] = enriched["vector"] return cve except Exception: cve["fixable"] = False @@ -219,30 +246,21 @@ def scan_apt(target: Target) -> tuple[bool, list[dict[str, str]], str]: def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: - cmd = ["debsecan", "--format", "report", "--suite", "bookworm"] if target.is_host else ["pct", "exec", target.target_id, "--", "debsecan", "--format", "report", "--suite", "bookworm"] + # Format par défaut (pas de --format) retourne: CVE-ID package + 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) if not ok: return False, [], stderr or stdout cves = [] for line in stdout.splitlines(): - # Format: CVE-XXXX-XXXX package [remote|local] [severity] - description m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line) if m: - # Extraire tous les flags entre crochets - flags = re.findall(r"\[(\w+)\]", line) - vector = "?" - severity = "?" - for f in flags: - if f in ("remote", "local"): - vector = f - elif f in ("unimportant", "low", "medium", "high", "critical"): - severity = f cves.append({ "id": m.group(1), "package": m.group(2), - "vector": vector, - "severity": severity, + "vector": "?", + "severity": "?", "url": f"https://security-tracker.debian.org/tracker/{m.group(1)}" }) return True, cves, "" @@ -273,9 +291,9 @@ def scan_target(target: Target, progress_cb: Callable) -> ScanResult: if not cve_ok: result.error = cve_err - # Filtrer les CVE actionnables via l'API Debian + # Enrichir les CVE via l'API Debian (severity, vector, fixable) if cve_list: - all_cves, actionable_count = filter_actionable_cves(cve_list) + all_cves, actionable_count = enrich_cves(cve_list) result.cve_list = all_cves result.cve_count = actionable_count result.cve_total = len(all_cves)