mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 09:14:33 +00:00
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>
116 lines
4.4 KiB
Python
116 lines
4.4 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
|
|
|
|
|
|
def test_cli_dedups_against_existing_rules(tmp_path, monkeypatch, capsys):
|
|
from api.wafgen import cli
|
|
subset = tmp_path / "subset"
|
|
subset.mkdir()
|
|
(subset / "ivanti.yaml").write_text(
|
|
'id: CVE-2024-21887\n'
|
|
'info:\n severity: critical\n'
|
|
' classification: {cve-id: CVE-2024-21887, cpe: "cpe:2.3:a:ivanti:connect_secure:*"}\n'
|
|
' metadata: {vendor: ivanti, product: connect_secure}\n'
|
|
' tags: cve,kev\n'
|
|
'http:\n - method: GET\n path: ["{{BaseURL}}/api/v1/totp/user-backup"]\n')
|
|
rules = tmp_path / "waf-rules.json"
|
|
# Literal hyphen (unescaped) — this is the real live cve_2024 rule form,
|
|
# a hand-written regex, NOT re.escape() output.
|
|
rules.write_text(json.dumps({"categories": {"cve_2024": {"patterns": [
|
|
{"pattern": "/api/v1/totp/user-backup"}]}}}))
|
|
monkeypatch.setattr(cli, "gather_present", lambda: (set(), True))
|
|
|
|
rc = cli.main(["waf-rules", "generate",
|
|
"--subset", str(subset), "--rules", str(rules)]) # dry-run
|
|
out = capsys.readouterr().out
|
|
assert rc == 0
|
|
assert "kept: 0 probe(s)" in out
|
|
assert "already covered by cve_2024" in out
|