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) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 00:34:07 -07:00
parent 8ba92bf02b
commit 446d25c66b
2 changed files with 58 additions and 21 deletions

View File

@@ -92,6 +92,11 @@ label) are intentionally NOT exposed as dedicated subcommands — reach them onl
through `raw` after confirming the correct params against through `raw` after confirming the correct params against
`references/api-reference.md` and the official Bitdefender docs. `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 ## Common commands
```bash ```bash

View File

@@ -207,7 +207,22 @@ def cmd_make_group(client, args):
_emit({"createdGroup": args.name, "result": result}, args.json, _print_kv) _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): 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: try:
params = json.loads(args.params) if args.params else {} params = json.loads(args.params) if args.params else {}
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
@@ -224,7 +239,7 @@ def cmd_raw(client, args):
# --- destructive (gated) ------------------------------------------------------ # --- destructive (gated) ------------------------------------------------------
def _gated(action_desc: str, confirm: bool) -> bool: def _gated(action_desc: str, confirm: bool) -> bool:
if not confirm: if not confirm:
print(f"[WARNING] Refusing destructive action without --confirm.") print("[WARNING] Refusing destructive action without --confirm.")
print(f"[INFO] Would: {action_desc}") print(f"[INFO] Would: {action_desc}")
return False return False
return True return True
@@ -261,74 +276,91 @@ def build_parser() -> argparse.ArgumentParser:
prog="gz.py", prog="gz.py",
description="GravityZone Cloud Public API CLI (ACG MSP tenant).", 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 = p.add_subparsers(dest="command", required=True)
sub.add_parser("status", help="API key + license status.") sub.add_parser("status", help="API key + license status.", parents=[common])
sub.add_parser("companies", help="List client companies.") 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("--company", help="Parent company/group id.")
sp.add_argument("--per-page", type=int, default=100) 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.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).") sp.add_argument("--company", help="Parent id (defaults to ACG container).")
sub.add_parser("policies", help="List policies (id + name).") sub.add_parser("policies", help="List policies (id + name).", parents=[common])
sp = sub.add_parser("policy", help="Shallow detail for one policy.") sp = sub.add_parser("policy", help="Shallow detail for one policy.",
parents=[common])
sp.add_argument("policy_id") 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.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.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("--name", required=True)
sp.add_argument("--company") sp.add_argument("--company")
sp.add_argument("--description") sp.add_argument("--description")
sp.add_argument("--language") 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("--package", required=True)
sp.add_argument("--company") 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("--targets", nargs="+", required=True)
sp.add_argument("--type", type=int, required=True, sp.add_argument("--type", type=int, required=True,
help="1=Quick 2=Full 3=Memory 4=Custom (verify in console).") help="1=Quick 2=Full 3=Memory 4=Custom (verify in console).")
sp.add_argument("--name") 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("--endpoints", nargs="+", required=True)
sp.add_argument("--group", 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("--name", required=True)
sp.add_argument("--parent") 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("--module", required=True)
sp.add_argument("--method", required=True) sp.add_argument("--method", required=True)
sp.add_argument("--params", default="{}", help="JSON object of params.") 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) # 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("endpoint_id")
sp.add_argument("--confirm", action="store_true") 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("--package", required=True)
sp.add_argument("--company") sp.add_argument("--company")
sp.add_argument("--confirm", action="store_true") 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("--group", required=True)
sp.add_argument("--confirm", action="store_true") sp.add_argument("--confirm", action="store_true")