mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Aggregator-served panel (User=secubox) cannot read/write 0750 root:root /etc/secubox/waf. Both routes now shell out to the root CLI (--json preview, --apply --json write) via scoped exact-command sudoers grants. Adds CLI --json. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
98 lines
3.5 KiB
Python
98 lines
3.5 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 — CLI waf-rules generate
|
|
CyberMind — https://cybermind.fr
|
|
|
|
Dry-run par défaut : montre ce qui SERAIT généré (retenus + rejets avec raison)
|
|
sans écrire. --apply écrit la catégorie product_absent_probes en mode detect.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from .emit import existing_patterns, write_category
|
|
from .generate import generate
|
|
from .inventory import gather_present
|
|
|
|
SUBSET_DIR = Path("/usr/lib/secubox/cve-triage/nuclei-subset")
|
|
RULES_PATH = Path("/etc/secubox/waf/waf-rules.json")
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
p = argparse.ArgumentParser(prog="secubox-cvectl")
|
|
sub = p.add_subparsers(dest="group", required=True)
|
|
wr = sub.add_parser("waf-rules")
|
|
wsub = wr.add_subparsers(dest="cmd", required=True)
|
|
gen = wsub.add_parser("generate", help="generate product-absent probe rules")
|
|
gen.add_argument("--apply", action="store_true",
|
|
help="write the category (default: dry-run, write nothing)")
|
|
gen.add_argument("--json", action="store_true",
|
|
help="emit a JSON payload instead of the human text (used by the panel)")
|
|
gen.add_argument("--subset", default=str(SUBSET_DIR))
|
|
gen.add_argument("--rules", default=str(RULES_PATH))
|
|
args = p.parse_args(argv)
|
|
|
|
present, complete = gather_present()
|
|
kept, rejected = generate(Path(args.subset), present, complete,
|
|
existing=existing_patterns(Path(args.rules)))
|
|
|
|
# Shared payload for --json (the panel routes parse this over sudo).
|
|
payload = {
|
|
"present_count": len(present),
|
|
"complete": complete,
|
|
"kept": [{"cve": c.cve, "vendor": c.vendor, "product": c.product,
|
|
"path": c.path} for c in kept],
|
|
"rejected": [{"file": n, "reason": r} for n, r in rejected],
|
|
"applied": False,
|
|
}
|
|
|
|
if not args.json:
|
|
print(f"presence union: {len(present)} product(s), complete={complete}")
|
|
print(f"kept: {len(kept)} probe(s), rejected: {len(rejected)}")
|
|
for c in kept:
|
|
print(f" ✓ {c.cve:<16} {c.vendor}/{c.product} {c.path}")
|
|
for name, why in rejected:
|
|
print(f" ✗ {name}: {why}")
|
|
|
|
if not args.apply:
|
|
if args.json:
|
|
print(json.dumps(payload))
|
|
else:
|
|
print("dry-run — nothing written (use --apply to write).")
|
|
return 0
|
|
|
|
# --apply: refuse when the presence inventory is incomplete (fail-safe).
|
|
if not complete:
|
|
if args.json:
|
|
print(json.dumps({**payload,
|
|
"error": "presence inventory incomplete — refusing (fail-safe)"}))
|
|
else:
|
|
print("refusing to write: presence inventory incomplete (fail-safe).",
|
|
file=sys.stderr)
|
|
return 3
|
|
|
|
write_category(Path(args.rules), kept, now=_now())
|
|
payload["applied"] = True
|
|
payload["written"] = len(kept)
|
|
if args.json:
|
|
print(json.dumps(payload))
|
|
else:
|
|
print(f"wrote {len(kept)} probe(s) to product_absent_probes (mode=detect) in {args.rules}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|