Compare commits

...

6 commits

Author SHA1 Message Date
11ada690a1 fix(cve): enrich CVEs with severity/vector from Debian Security Tracker HTML
All checks were successful
Build and Release .deb / build-deb (push) Successful in 21s
- 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
2026-05-13 04:33:05 +02:00
9107009370 fix(cve): robust parsing of [remote|local] and [severity] flags
All checks were successful
Build and Release .deb / build-deb (push) Successful in 22s
- Use re.findall to extract all bracketed flags instead of positional regex
- Fixes issue where optional groups were not captured correctly
2026-05-13 04:23:45 +02:00
13d826ecd2 feat(cve): add severity and vector columns to CVE list
All checks were successful
Build and Release .deb / build-deb (push) Successful in 21s
- Parse [remote|local] and [severity] from debsecan output
- Display Severity and Vector columns in CVEListScreen
- Severity values: unimportant, low, medium, high, critical
2026-05-13 04:20:14 +02:00
721e677fa6 fix(os): use NAME directly, avoid double 'GNU/Linux'
All checks were successful
Build and Release .deb / build-deb (push) Successful in 21s
- 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
2026-05-13 04:15:57 +02:00
c1f462d209 fix(os): parse /etc/os-release line-by-line instead of regex
All checks were successful
Build and Release .deb / build-deb (push) Successful in 22s
- pct exec returns stdout differently than local cat
- Line-by-line parsing is more robust across LXC boundaries
- Handles quotes correctly
2026-05-13 04:10:01 +02:00
f0f59f0468 fix(os): read DEBIAN_VERSION_FULL from /etc/os-release first
All checks were successful
Build and Release .deb / build-deb (push) Successful in 21s
- Check DEBIAN_VERSION_FULL (ex: 13.4) as primary source
- Fallback to VERSION, then VERSION_ID, then /etc/debian_version
2026-05-13 04:05:45 +02:00
2 changed files with 70 additions and 44 deletions

View file

@ -41,6 +41,29 @@ 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)
@ -54,16 +77,20 @@ def _is_cve_actionable(cve_id: str, suite: str = "bookworm") -> bool:
return bool(pattern.search(html)) return bool(pattern.search(html))
def filter_actionable_cves(cves: list[dict]) -> tuple[list[dict], int]: def enrich_cves(cves: list[dict]) -> tuple[list[dict], int]:
"""Filtre la liste des CVE pour ajouter un flag 'fixable'. """Enrichit les CVEs avec fixable, severity, vector via l'API Debian.
Retourne (cve_avec_flag, nombre_actionnables).""" Retourne (cve_enrichies, nombre_actionnables)."""
if not cves: if not cves:
return [], 0 return [], 0
def check(cve: dict) -> dict: def check(cve: dict) -> dict:
try: try:
is_fixable = _is_cve_actionable(cve["id"]) # Vérifie si corrigeable
cve["fixable"] = is_fixable 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 return cve
except Exception: except Exception:
cve["fixable"] = False cve["fixable"] = False
@ -140,45 +167,35 @@ def scan_os(target: Target) -> str:
"""Récupère le nom, la version complète et le codename de l'OS.""" """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, "--"] 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 et parser ligne par ligne
ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30) ok, osrel_out, _ = run_cmd(prefix + ["cat", "/etc/os-release"], timeout=30)
version_id = "" os_data = {}
pretty_name = ""
if ok: if ok:
ver_match = re.search(r'VERSION_ID="([^"]+)"', osrel_out) for line in osrel_out.splitlines():
if ver_match: line = line.strip()
version_id = ver_match.group(1) if "=" in line:
pretty_match = re.search(r'PRETTY_NAME="([^"]+)"', osrel_out) key, val = line.split("=", 1)
if pretty_match: val = val.strip().strip('"').strip("'")
pretty_name = pretty_match.group(1) os_data[key] = val
# 2. Récupérer le codename via lsb_release # 2. Extraire les champs
ok, code_out, _ = run_cmd(prefix + ["lsb_release", "-cs"], timeout=30) name = os_data.get("NAME", "")
codename = code_out.strip() if ok else "" codename = os_data.get("VERSION_CODENAME", "")
debian_version_full = os_data.get("DEBIAN_VERSION_FULL", "")
version_id = os_data.get("VERSION_ID", "")
# 3. Fallback sur /etc/debian_version si VERSION_ID vide # 3. Construire le libellé final
if not version_id: # Avec DEBIAN_VERSION_FULL : NAME DEBIAN_VERSION_FULL (VERSION_CODENAME)
ok, ver_out, _ = run_cmd(prefix + ["cat", "/etc/debian_version"], timeout=30) # Sans : NAME VERSION_ID (VERSION_CODENAME)
version_id = ver_out.strip() if ok else "" version = debian_version_full if debian_version_full else version_id
# 4. Fallback sur lsb_release -rs if name and version and codename:
if not version_id: return f"{name} {version} ({codename})"
ok, ver_out, _ = run_cmd(prefix + ["lsb_release", "-rs"], timeout=30) if name and version:
version_id = ver_out.strip() if ok else "" return f"{name} {version}"
# 5. Récupérer le nom via lsb_release # Fallback : PRETTY_NAME ou "OS inconnu"
ok, name_out, _ = run_cmd(prefix + ["lsb_release", "-is"], timeout=30) return os_data.get("PRETTY_NAME", "OS inconnu")
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_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]:
@ -229,7 +246,8 @@ 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]:
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) ok, stdout, stderr = run_cmd(cmd, timeout=120)
if not ok: if not ok:
return False, [], stderr or stdout return False, [], stderr or stdout
@ -238,7 +256,13 @@ 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({"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": "?",
"severity": "?",
"url": f"https://security-tracker.debian.org/tracker/{m.group(1)}"
})
return True, cves, "" return True, cves, ""
@ -267,9 +291,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
# Filtrer les CVE actionnables via l'API Debian # Enrichir les CVE via l'API Debian (severity, vector, fixable)
if cve_list: 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_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,15 +90,17 @@ 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", "Corrigeable", "Lien") table.add_columns("CVE-ID", "Paquet", "Severite", "Vecteur", "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, fixable, url) table.add_row(cve_id, pkg, severity, vector, 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):