mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
fix(cve-triage): is_kev reads info.tags (real nuclei format) + CLI main() tests
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 <devel@cybermind.fr>
This commit is contained in:
parent
5f459b1de1
commit
dc190aa049
|
|
@ -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(",")]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
89
packages/secubox-cve-triage/tests/test_cli.py
Normal file
89
packages/secubox-cve-triage/tests/test_cli.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# 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.
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user