From 446d25c66bcda79aabb33749e3a740065ff9c5fc Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Sat, 30 May 2026 00:34:07 -0700 Subject: [PATCH] fix(bitdefender): gate raw destructive calls, allow --json after subcommand - raw now refuses destructive methods (delete/uninstall/remove/reconfigure) without --confirm (it previously bypassed all gating) - --json is now accepted after the subcommand (shared via a common parent parser), matching the documented usage - drop a placeholder-less f-string - SKILL.md: document raw gating + that raw echoes upstream responses verbatim Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/bitdefender/SKILL.md | 5 ++ .claude/skills/bitdefender/scripts/gz.py | 74 +++++++++++++++++------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/.claude/skills/bitdefender/SKILL.md b/.claude/skills/bitdefender/SKILL.md index 7676920..023ed7d 100644 --- a/.claude/skills/bitdefender/SKILL.md +++ b/.claude/skills/bitdefender/SKILL.md @@ -92,6 +92,11 @@ label) are intentionally NOT exposed as dedicated subcommands — reach them onl through `raw` after confirming the correct params against `references/api-reference.md` and the official Bitdefender docs. +`raw` itself refuses destructive method names (delete/uninstall/remove/ +reconfigure) unless `--confirm` is passed. Note that `raw` prints the upstream +response verbatim — it can carry data from the called method, so do not paste +raw output into tickets/logs without review. + ## Common commands ```bash diff --git a/.claude/skills/bitdefender/scripts/gz.py b/.claude/skills/bitdefender/scripts/gz.py index 640e7bf..97e0cc3 100644 --- a/.claude/skills/bitdefender/scripts/gz.py +++ b/.claude/skills/bitdefender/scripts/gz.py @@ -207,7 +207,22 @@ def cmd_make_group(client, args): _emit({"createdGroup": args.name, "result": result}, args.json, _print_kv) +# Substrings that mark a JSON-RPC method as state-destroying. `raw` can reach +# any method (incl. UNVERIFIED ones), so gate these behind --confirm too. +DESTRUCTIVE_RAW_PATTERNS = ("delete", "createuninstall", "createremove", + "createreconfigure") + + +def _is_destructive_method(method: str) -> bool: + m = method.lower() + return any(pat in m for pat in DESTRUCTIVE_RAW_PATTERNS) + + def cmd_raw(client, args): + if _is_destructive_method(args.method) and not args.confirm: + print(f"[WARNING] '{args.method}' looks destructive; refusing without " + "--confirm.", file=sys.stderr) + return 3 try: params = json.loads(args.params) if args.params else {} except json.JSONDecodeError as exc: @@ -224,7 +239,7 @@ def cmd_raw(client, args): # --- destructive (gated) ------------------------------------------------------ def _gated(action_desc: str, confirm: bool) -> bool: if not confirm: - print(f"[WARNING] Refusing destructive action without --confirm.") + print("[WARNING] Refusing destructive action without --confirm.") print(f"[INFO] Would: {action_desc}") return False return True @@ -261,74 +276,91 @@ def build_parser() -> argparse.ArgumentParser: prog="gz.py", description="GravityZone Cloud Public API CLI (ACG MSP tenant).", ) - p.add_argument("--json", action="store_true", help="Emit raw JSON output.") + # --json is shared by every subcommand (via parents) so it is accepted + # after the subcommand, e.g. `gz.py companies --json`. + common = argparse.ArgumentParser(add_help=False) + common.add_argument("--json", action="store_true", help="Emit raw JSON output.") sub = p.add_subparsers(dest="command", required=True) - sub.add_parser("status", help="API key + license status.") - sub.add_parser("companies", help="List client companies.") + sub.add_parser("status", help="API key + license status.", parents=[common]) + sub.add_parser("companies", help="List client companies.", parents=[common]) - sp = sub.add_parser("endpoints", help="List endpoints under a company/group.") + sp = sub.add_parser("endpoints", help="List endpoints under a company/group.", + parents=[common]) sp.add_argument("--company", help="Parent company/group id.") sp.add_argument("--per-page", type=int, default=100) - sp = sub.add_parser("endpoint", help="Full detail for one endpoint.") + sp = sub.add_parser("endpoint", help="Full detail for one endpoint.", + parents=[common]) sp.add_argument("endpoint_id") - sp = sub.add_parser("sweep", help="Live security posture sweep.") + sp = sub.add_parser("sweep", help="Live security posture sweep.", parents=[common]) sp.add_argument("--company", help="Parent id (defaults to ACG container).") - sub.add_parser("policies", help="List policies (id + name).") - sp = sub.add_parser("policy", help="Shallow detail for one policy.") + sub.add_parser("policies", help="List policies (id + name).", parents=[common]) + sp = sub.add_parser("policy", help="Shallow detail for one policy.", + parents=[common]) sp.add_argument("policy_id") - sub.add_parser("packages", help="List installation packages.") + sub.add_parser("packages", help="List installation packages.", parents=[common]) - sp = sub.add_parser("quarantine", help="List quarantine items for a company.") + sp = sub.add_parser("quarantine", help="List quarantine items for a company.", + parents=[common]) sp.add_argument("--company", required=True) - sp = sub.add_parser("inventory", help="Show cached identity/structure.") + sp = sub.add_parser("inventory", help="Show cached identity/structure.", + parents=[common]) sp.add_argument("--refresh", action="store_true", help="Force a full re-pull.") - sp = sub.add_parser("create-package", help="Create an installer package.") + sp = sub.add_parser("create-package", help="Create an installer package.", + parents=[common]) sp.add_argument("--name", required=True) sp.add_argument("--company") sp.add_argument("--description") sp.add_argument("--language") - sp = sub.add_parser("install-links", help="Get installer download URLs.") + sp = sub.add_parser("install-links", help="Get installer download URLs.", + parents=[common]) sp.add_argument("--package", required=True) sp.add_argument("--company") - sp = sub.add_parser("scan", help="Create a scan task.") + sp = sub.add_parser("scan", help="Create a scan task.", parents=[common]) sp.add_argument("--targets", nargs="+", required=True) sp.add_argument("--type", type=int, required=True, help="1=Quick 2=Full 3=Memory 4=Custom (verify in console).") sp.add_argument("--name") - sp = sub.add_parser("move", help="Move endpoints into a group.") + sp = sub.add_parser("move", help="Move endpoints into a group.", parents=[common]) sp.add_argument("--endpoints", nargs="+", required=True) sp.add_argument("--group", required=True) - sp = sub.add_parser("make-group", help="Create a custom group.") + sp = sub.add_parser("make-group", help="Create a custom group.", parents=[common]) sp.add_argument("--name", required=True) sp.add_argument("--parent") - sp = sub.add_parser("raw", help="Call any method directly (power use).") + sp = sub.add_parser("raw", help="Call any method directly (power use).", + parents=[common]) sp.add_argument("--module", required=True) sp.add_argument("--method", required=True) sp.add_argument("--params", default="{}", help="JSON object of params.") + sp.add_argument("--confirm", action="store_true", + help="Required for destructive methods (delete/uninstall/" + "remove/reconfigure).") # destructive (gated) - sp = sub.add_parser("delete-endpoint", help="Delete an endpoint (gated).") + sp = sub.add_parser("delete-endpoint", help="Delete an endpoint (gated).", + parents=[common]) sp.add_argument("endpoint_id") sp.add_argument("--confirm", action="store_true") - sp = sub.add_parser("delete-package", help="Delete a package (gated).") + sp = sub.add_parser("delete-package", help="Delete a package (gated).", + parents=[common]) sp.add_argument("--package", required=True) sp.add_argument("--company") sp.add_argument("--confirm", action="store_true") - sp = sub.add_parser("delete-group", help="Delete a custom group (gated).") + sp = sub.add_parser("delete-group", help="Delete a custom group (gated).", + parents=[common]) sp.add_argument("--group", required=True) sp.add_argument("--confirm", action="store_true")