secubox-deb/packages/secubox-cve-triage/tests/test_cli.py
CyberMind-FR dc190aa049 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>
2026-07-18 08:06:20 +02:00

90 lines
3.3 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.
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