secubox-deb/packages/secubox-cve-triage/api/wafgen/generate.py
CyberMind-FR 58afb624ff fix(cve-triage): dedup must regex-match live WAF patterns, not exact-string
The generator only rejected a candidate when re.escape(path) exactly
equaled a stored pattern key, so it never matched the live cve_2024
category's hand-written regexes (literal hyphens, \d classes) — the
dedup feature was inert against real WAF rules. Now a candidate is
covered when any existing pattern, treated as a regex (as the WAF
engine does), matches the raw path; invalid stored regexes are
skipped, never raised.

Also harden existing_patterns() against valid-but-non-object JSON
(e.g. "[]"/"null") which previously raised AttributeError on
data.get(...), breaking the read-only preview/dry-run best-effort
contract.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-18 12:15:13 +02:00

84 lines
3.1 KiB
Python

# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
"""
SecuBox-Deb :: cve-triage — orchestration du générateur
CyberMind — https://cybermind.fr
Parcourt le sous-ensemble Nuclei vendored, applique KEV + extraction + les deux
barrières de présence, retourne (retenus, rejets-avec-raison). N'écrit rien :
la CLI décide (dry-run par défaut).
"""
from __future__ import annotations
import re
from pathlib import Path
import yaml
from .extract import Candidate, Rejection, extract, is_kev
from .presence import is_appliance, should_generate
def _covered_by(path: str, existing: dict[str, str]) -> str | None:
"""Nom de la catégorie dont un pattern (traité comme regex, ce que fait le
moteur WAF) matche `path` — donc la sonde detect y serait redondante. Un
pattern stocké qui n'est pas une regex valide ne peut pas matcher : on le
saute (jamais d'erreur). None si aucune couverture."""
for pattern, category in existing.items():
try:
if re.search(pattern, path):
return category
except re.error:
continue
return None
def generate(subset_dir: Path, present: set[str], present_complete: bool,
*, existing: dict[str, str] | None = None
) -> tuple[list[Candidate], list[tuple[str, str]]]:
"""(retenus, [(fichier, raison de rejet)]). `existing` mappe pattern-string
-> catégorie (voir emit.existing_patterns) : un candidat dont le pattern y
figure déjà est rejeté (redondance ; une catégorie bloquante l'emporterait
sur notre detect). None/{} => aucune dédup."""
existing = existing or {}
kept: list[Candidate] = []
rejected: list[tuple[str, str]] = []
for p in sorted(Path(subset_dir).glob("*.yaml")):
try:
tmpl = yaml.safe_load(p.read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError) as exc:
rejected.append((p.name, f"unparseable: {exc}"))
continue
if not isinstance(tmpl, dict):
rejected.append((p.name, "not a template mapping"))
continue
if not is_kev(tmpl):
rejected.append((p.name, "not KEV (not known-exploited)"))
continue
result = extract(tmpl)
if isinstance(result, Rejection):
rejected.append((p.name, result.reason))
continue
if not should_generate(result, present, present_complete):
why = ("inventory incomplete → present" if not present_complete
else "not an appliance family" if not is_appliance(result)
else "product is present on this box")
rejected.append((p.name, why))
continue
covered_by = _covered_by(result.path, existing)
if covered_by is not None:
rejected.append((p.name, f"already covered by {covered_by}"))
continue
kept.append(result)
return kept, rejected