From dc190aa049d83ffc650be146cddb3a11d4f4ffb3 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 18 Jul 2026 08:06:20 +0200 Subject: [PATCH] fix(cve-triage): is_kev reads info.tags (real nuclei format) + CLI main() tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_kev() read tags at the template's top level while extract() already read classification/metadata from under info: — real upstream nuclei-templates nest tags under info: too, so a real curation run silently produced zero candidates. Read info.tags first with a top-level fallback for legacy fixtures, and realign the two vendored KEV templates to the real nested format. Also add direct coverage of cli.main() (dry-run no-op, --apply writing only product_absent_probes, and the incomplete-inventory fail-safe returning 3 without touching the rules file). Co-Authored-By: Gerald KERMA --- .../secubox-cve-triage/api/wafgen/extract.py | 9 +- .../nuclei-subset/CVE-2024-21887.yaml | 2 +- .../nuclei-subset/CVE-2024-3400.yaml | 2 +- packages/secubox-cve-triage/tests/test_cli.py | 89 +++++++++++++++++++ .../secubox-cve-triage/tests/test_extract.py | 26 ++++++ 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 packages/secubox-cve-triage/tests/test_cli.py diff --git a/packages/secubox-cve-triage/api/wafgen/extract.py b/packages/secubox-cve-triage/api/wafgen/extract.py index dabc9bbd..98c244d9 100644 --- a/packages/secubox-cve-triage/api/wafgen/extract.py +++ b/packages/secubox-cve-triage/api/wafgen/extract.py @@ -43,8 +43,13 @@ class Rejection: def is_kev(template: dict) -> bool: - """True si le template porte le tag KEV (exploité pour de vrai).""" - tags = template.get("tags", "") + """True si le template porte le tag KEV (exploité pour de vrai). + + Le nuclei-templates réel place `tags` sous `info:` (comme `classification` + et `metadata`, lus plus bas). On lit `info.tags` en priorité, avec un + repli sur `tags` au niveau racine pour les fixtures/anciens formats.""" + info = template.get("info", {}) or {} + tags = info.get("tags", template.get("tags", "")) if isinstance(tags, list): tags = ",".join(tags) return "kev" in [t.strip().lower() for t in str(tags).split(",")] diff --git a/packages/secubox-cve-triage/nuclei-subset/CVE-2024-21887.yaml b/packages/secubox-cve-triage/nuclei-subset/CVE-2024-21887.yaml index 2cfb402a..ed67afff 100644 --- a/packages/secubox-cve-triage/nuclei-subset/CVE-2024-21887.yaml +++ b/packages/secubox-cve-triage/nuclei-subset/CVE-2024-21887.yaml @@ -8,7 +8,7 @@ info: metadata: vendor: ivanti product: connect_secure -tags: cve,cve2024,ivanti,rce,kev,vuln + tags: cve,cve2024,ivanti,rce,kev,vuln http: - method: GET path: diff --git a/packages/secubox-cve-triage/nuclei-subset/CVE-2024-3400.yaml b/packages/secubox-cve-triage/nuclei-subset/CVE-2024-3400.yaml index 3e35a107..1e6af85a 100644 --- a/packages/secubox-cve-triage/nuclei-subset/CVE-2024-3400.yaml +++ b/packages/secubox-cve-triage/nuclei-subset/CVE-2024-3400.yaml @@ -8,7 +8,7 @@ info: metadata: vendor: paloaltonetworks product: pan-os -tags: cve,cve2024,panos,rce,kev,vuln + tags: cve,cve2024,panos,rce,kev,vuln http: - method: GET path: diff --git a/packages/secubox-cve-triage/tests/test_cli.py b/packages/secubox-cve-triage/tests/test_cli.py new file mode 100644 index 00000000..7e25f695 --- /dev/null +++ b/packages/secubox-cve-triage/tests/test_cli.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma +# Source-Disclosed License — All rights reserved except as expressly granted. +# See LICENCE-CMSD-1.0.md for terms. +import json +import textwrap + +import api.wafgen.cli as cli_mod +from api.wafgen.emit import CATEGORY_KEY + +# One appliance/KEV/clean template (real nested-tags format — tags under +# info:, like the vendored nuclei-subset fixtures after the extract.py fix). +_APPLIANCE_KEV_TEMPLATE = """ + id: CVE-2024-3400 + info: + severity: critical + classification: {cve-id: CVE-2024-3400, cpe: "cpe:2.3:a:paloaltonetworks:pan-os:*"} + metadata: {vendor: paloaltonetworks, product: pan-os} + tags: cve,cve2024,panos,rce,kev,vuln + http: + - method: GET + path: ["{{BaseURL}}/api/v1/totp/user-backup"] +""" + + +def _subset_dir(tmp_path): + subset = tmp_path / "nuclei-subset" + subset.mkdir() + (subset / "keep.yaml").write_text(textwrap.dedent(_APPLIANCE_KEV_TEMPLATE)) + return subset + + +def _rules_file(tmp_path): + p = tmp_path / "waf-rules.json" + p.write_text(json.dumps({ + "_meta": {"version": "1.2.0"}, + "categories": { + "sqli": {"name": "SQLi", "severity": "critical", + "patterns": [{"id": "sqli-001", "pattern": "union select", "desc": "x"}]}, + }, + }, indent=2)) + return p + + +def test_default_run_returns_0_and_writes_nothing(tmp_path, monkeypatch): + monkeypatch.setattr(cli_mod, "gather_present", lambda: (set(), True)) + subset = _subset_dir(tmp_path) + rules = _rules_file(tmp_path) + before = rules.read_text() + + rc = cli_mod.main(["waf-rules", "generate", "--subset", str(subset), "--rules", str(rules)]) + + assert rc == 0 + assert rules.read_text() == before + + +def test_apply_returns_0_and_adds_only_product_absent_probes(tmp_path, monkeypatch): + monkeypatch.setattr(cli_mod, "gather_present", lambda: (set(), True)) + subset = _subset_dir(tmp_path) + rules = _rules_file(tmp_path) + + rc = cli_mod.main(["waf-rules", "generate", "--apply", + "--subset", str(subset), "--rules", str(rules)]) + + assert rc == 0 + data = json.loads(rules.read_text()) + # Other category preserved untouched. + assert data["categories"]["sqli"]["patterns"][0]["id"] == "sqli-001" + # Only the generated category was added. + assert set(data["categories"].keys()) == {"sqli", CATEGORY_KEY} + assert data["categories"][CATEGORY_KEY]["mode"] == "detect" + ids = [p["id"] for p in data["categories"][CATEGORY_KEY]["patterns"]] + assert any("2024-3400" in i for i in ids) + + +def test_apply_with_incomplete_inventory_returns_3_and_leaves_rules_untouched(tmp_path, monkeypatch): + # complete=False must be a hard guard: dropping it would let --apply + # write a category built under an unproven presence union (the exact + # fail-safe should_generate()/gather_present() are meant to enforce). + monkeypatch.setattr(cli_mod, "gather_present", lambda: (set(), False)) + subset = _subset_dir(tmp_path) + rules = _rules_file(tmp_path) + before = rules.read_text() + + rc = cli_mod.main(["waf-rules", "generate", "--apply", + "--subset", str(subset), "--rules", str(rules)]) + + assert rc == 3 + assert rules.read_text() == before diff --git a/packages/secubox-cve-triage/tests/test_extract.py b/packages/secubox-cve-triage/tests/test_extract.py index 7e59b5fd..9ece8da2 100644 --- a/packages/secubox-cve-triage/tests/test_extract.py +++ b/packages/secubox-cve-triage/tests/test_extract.py @@ -96,3 +96,29 @@ def test_path_is_regex_escaped_ready(): def test_is_kev_reads_the_tag(): assert is_kev(CLEAN) is True assert is_kev(dict(CLEAN, tags="cve,cve2024,rce")) is False + + +def test_is_kev_reads_info_nested_tags_real_upstream_format(): + # Real upstream projectdiscovery/nuclei-templates nests `tags` under + # `info:` (alongside classification/metadata, which extract() already + # reads from there). is_kev must read info.tags FIRST — a version that + # only reads the top-level `tags` key would miss every real template + # and silently generate nothing. + t = { + "id": "CVE-2024-3400", + "info": {"severity": "critical", "tags": "cve,cve2024,panos,rce,kev,vuln"}, + } + assert is_kev(t) is True + + +def test_is_kev_falls_back_to_top_level_tags_legacy_fixture_form(): + # Backward-compat: older-style fixtures with `tags` at the top level + # (no info.tags present) must still be detected. + t = {"id": "CVE-2024-1", "info": {"severity": "high"}, "tags": "cve,kev"} + assert is_kev(t) is True + + +def test_is_kev_false_when_neither_level_carries_the_kev_token(): + t = {"id": "CVE-2024-2", "info": {"severity": "high", "tags": "cve,rce"}, + "tags": "cve,rce"} + assert is_kev(t) is False