diff --git a/.claude/skills/bitdefender/references/BUILDOUT.md b/.claude/skills/bitdefender/references/BUILDOUT.md index 2e0b9b57..3dfc328d 100644 --- a/.claude/skills/bitdefender/references/BUILDOUT.md +++ b/.claude/skills/bitdefender/references/BUILDOUT.md @@ -68,14 +68,14 @@ Per-module workflow: fetch module doc -> list methods + params -> add to - [x] getPushEventSettings · getPushEventStats · setPushEventSettings - [ ] sendTestPushEvent (enumerate) -## accounts (fully enumerated from docs) +## accounts — COMPLETE (all 7) - [x] getAccountsList +- [x] getAccountDetails (no id = own account) - [x] getNotificationsSettings -- [ ] getAccountDetails -- [ ] createAccount (gated) -- [ ] updateAccount (gated) -- [ ] deleteAccount (gated) -- [ ] configureNotificationsSettings (gated) +- [x] createAccount (gated) +- [x] updateAccount (gated) +- [x] deleteAccount (gated) +- [x] configureNotificationsSettings (gated; setter w/ no required param — never probe empty) ## integrations - [ ] (enumerate; getPSAIntegrationList was a name-miss — find correct names) diff --git a/.claude/skills/bitdefender/references/api-reference.md b/.claude/skills/bitdefender/references/api-reference.md index 794cae8b..cca05454 100644 --- a/.claude/skills/bitdefender/references/api-reference.md +++ b/.claude/skills/bitdefender/references/api-reference.md @@ -125,13 +125,17 @@ In `getNetworkInventoryItems` results, `type == 1` denotes a company node. | `createReport` | `name, type, targetIds, ...` | param `name` required (probed) | Not yet a dedicated CLI command — `raw` only. | | `getDownloadLinks` | `reportId` *(candidate)* | UNVERIFIED param | Report download links. Client helper `get_report_links`. | -## accounts (`/accounts`) — VERIFIED LIVE (read) +## accounts (`/accounts`) — COMPLETE (all 7 methods wrapped) | Method | Params | Status | Notes | |---|---|---|---| | `getAccountsList` | `page?, perPage?` | VERIFIED LIVE | List console accounts/users. CLI `accounts`. | +| `getAccountDetails` | `accountId?` | VERIFIED LIVE | No id ⇒ the API key's own account. CLI `account [id]`. | | `getNotificationsSettings` | `{}` | VERIFIED LIVE | Notification config. CLI `notif-settings`. | -| `createAccount` / `updateAccount` / `deleteAccount` | uncertain | UNVERIFIED (state-changing) | Not exposed; `raw` only after confirming shape. | +| `createAccount` | `email (req), userName, password, role, profile{fullName,language,timezone}, phoneNumber{countryCode,subscriberNumber}, rights{manageInventory,managePoliciesRead/Write,...}` | VERIFIED (docs + probe) | CLI `account-create`, gated. STATE-CHANGING. Docs: 77212-125284-createaccount.html | +| `updateAccount` | `accountId (req) + fields` | VERIFIED (probe) | CLI `account-update --id --set-json`, gated. STATE-CHANGING. | +| `deleteAccount` | `accountId (req)` | VERIFIED (probe) | CLI `account-delete --id`, gated. STATE-CHANGING. | +| `configureNotificationsSettings` | settings object (NO required field — empty payload is accepted) | VERIFIED (probe) | CLI `notif-configure --settings-json`, gated. STATE-CHANGING. ⚠ never probe with `{}` on a live tenant — it is a setter. | ## push (`/push`) — event push service (VERIFIED reachable) diff --git a/.claude/skills/bitdefender/scripts/gz.py b/.claude/skills/bitdefender/scripts/gz.py index fbfff980..f914b013 100644 --- a/.claude/skills/bitdefender/scripts/gz.py +++ b/.claude/skills/bitdefender/scripts/gz.py @@ -265,6 +265,88 @@ def cmd_notif_settings(client, args): _emit(client.get_notifications_settings(), args.json, _print_kv) +def cmd_account(client, args): + _emit(client.get_account_details(args.account_id), args.json, _print_kv) + + +def _load_json_arg(raw, label): + """Parse a JSON-object CLI arg; returns (obj, error_rc). error_rc is None on ok.""" + if raw is None: + return {}, None + try: + obj = json.loads(raw) + except json.JSONDecodeError as exc: + print(f"[ERROR] --{label} is not valid JSON: {exc}", file=sys.stderr) + return None, 2 + if not isinstance(obj, dict): + print(f"[ERROR] --{label} must be a JSON object.", file=sys.stderr) + return None, 2 + return obj, None + + +def cmd_account_create(client, args): + rights, rc = _load_json_arg(args.rights_json, "rights-json") + if rc: + return rc + extra, rc = _load_json_arg(args.extra_json, "extra-json") + if rc: + return rc + profile = {} + if args.full_name: + profile["fullName"] = args.full_name + if args.language: + profile["language"] = args.language + if args.timezone: + profile["timezone"] = args.timezone + if not _gated(f"create account {args.email} (role={args.role})", args.confirm): + return 3 + result = client.create_account( + email=args.email, password=args.password, username=args.username, + role=args.role, profile=profile or None, rights=rights or None, + extra=extra or None, + ) + _emit({"createdAccount": args.email, "result": result}, args.json, _print_kv) + return 0 + + +def cmd_account_update(client, args): + fields, rc = _load_json_arg(args.set_json, "set-json") + if rc: + return rc + if not fields: + print("[ERROR] --set-json (object of fields to change) is required.", + file=sys.stderr) + return 2 + if not _gated(f"update account {args.id} fields={list(fields)}", args.confirm): + return 3 + result = client.update_account(args.id, fields) + _emit({"updatedAccount": args.id, "result": result}, args.json, _print_kv) + return 0 + + +def cmd_account_delete(client, args): + if not _gated(f"delete account {args.id}", args.confirm): + return 3 + result = client.delete_account(args.id) + _emit({"deletedAccount": args.id, "result": result}, args.json, _print_kv) + return 0 + + +def cmd_notif_configure(client, args): + settings, rc = _load_json_arg(args.settings_json, "settings-json") + if rc: + return rc + if not settings: + print("[ERROR] --settings-json (the settings object) is required.", + file=sys.stderr) + return 2 + if not _gated(f"configure notification settings ({list(settings)})", args.confirm): + return 3 + result = client.configure_notifications_settings(settings) + _emit({"notificationsConfigured": True, "result": result}, args.json, _print_kv) + return 0 + + def cmd_scan_tasks(client, args): _emit(client.list_scan_tasks(page=args.page, per_page=args.per_page), args.json, _print_scan_tasks_table) @@ -395,7 +477,8 @@ def cmd_make_group(client, args): DESTRUCTIVE_RAW_PATTERNS = ("delete", "createuninstall", "createremove", "createreconfigure", "isolat", "addtoblocklist", "removefromblocklist", "assignpolicy", - "setpushevent") + "setpushevent", "createaccount", "updateaccount", + "configurenotif") def _is_destructive_method(method: str) -> bool: @@ -545,6 +628,11 @@ def build_parser() -> argparse.ArgumentParser: sub.add_parser("notif-settings", help="Show notification settings.", parents=[common]) + sp = sub.add_parser("account", + help="Account detail (no id = the API key's own account).", + parents=[common]) + sp.add_argument("account_id", nargs="?", help="Account id (optional).") + sp = sub.add_parser("scan-tasks", help="List scan tasks.", parents=[common]) sp.add_argument("--page", type=int, default=1) sp.add_argument("--per-page", type=int, default=100) @@ -674,6 +762,36 @@ def build_parser() -> argparse.ArgumentParser: help="Inherit the policy from the parent group.") sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("account-create", help="Create a console account (gated).", + parents=[common]) + sp.add_argument("--email", required=True) + sp.add_argument("--password") + sp.add_argument("--username") + sp.add_argument("--role", type=int, help="Numeric role id (see docs/console).") + sp.add_argument("--full-name") + sp.add_argument("--language", help="e.g. en_US") + sp.add_argument("--timezone", help="e.g. America/Phoenix") + sp.add_argument("--rights-json", help="JSON object of rights flags.") + sp.add_argument("--extra-json", help="JSON object of any extra documented fields.") + sp.add_argument("--confirm", action="store_true") + + sp = sub.add_parser("account-update", help="Update a console account (gated).", + parents=[common]) + sp.add_argument("--id", required=True, help="accountId.") + sp.add_argument("--set-json", required=True, help="JSON object of fields to change.") + sp.add_argument("--confirm", action="store_true") + + sp = sub.add_parser("account-delete", help="Delete a console account (gated).", + parents=[common]) + sp.add_argument("--id", required=True, help="accountId.") + sp.add_argument("--confirm", action="store_true") + + sp = sub.add_parser("notif-configure", + help="Set notification settings (gated).", parents=[common]) + sp.add_argument("--settings-json", required=True, + help="JSON object of the notification settings to apply.") + sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("push-set", help="Configure the push event service (gated).", parents=[common]) @@ -705,6 +823,11 @@ HANDLERS = { "reports": cmd_reports, "accounts": cmd_accounts, "notif-settings": cmd_notif_settings, + "account": cmd_account, + "account-create": cmd_account_create, + "account-update": cmd_account_update, + "account-delete": cmd_account_delete, + "notif-configure": cmd_notif_configure, "scan-tasks": cmd_scan_tasks, "push-settings": cmd_push_settings, "push-stats": cmd_push_stats, diff --git a/.claude/skills/bitdefender/scripts/gz_client.py b/.claude/skills/bitdefender/scripts/gz_client.py index fae103f4..4379f4c0 100644 --- a/.claude/skills/bitdefender/scripts/gz_client.py +++ b/.claude/skills/bitdefender/scripts/gz_client.py @@ -700,6 +700,78 @@ class GravityZoneClient: "accounts", "getNotificationsSettings", {} ) or {} + def get_account_details(self, account_id: Optional[str] = None) -> dict: + """Account detail (accounts.getAccountDetails). With no id, returns the + API key owner's own account (verified live).""" + params: dict = {} + if account_id: + params["accountId"] = account_id + return self._jsonrpc_request("accounts", "getAccountDetails", params) or {} + + def create_account( + self, + email: str, + password: Optional[str] = None, + username: Optional[str] = None, + role: Optional[int] = None, + profile: Optional[dict] = None, + rights: Optional[dict] = None, + phone_number: Optional[dict] = None, + extra: Optional[dict] = None, + ) -> Any: + """Create a console account (accounts.createAccount). STATE-CHANGING. + + `email` is required (verified). Documented params: userName, password, + role (int), profile{fullName,language,timezone}, + phoneNumber{countryCode,subscriberNumber}, rights{manageInventory, + managePoliciesRead/Write,...}. `extra` merges any additional documented + fields verbatim. Gate at the call site behind --confirm. + Docs: bitdefender.com/business/support/en/77212-125284-createaccount.html + """ + params: dict = {"email": email} + if username is not None: + params["userName"] = username + if password is not None: + params["password"] = password + if role is not None: + params["role"] = role + if profile is not None: + params["profile"] = profile + if rights is not None: + params["rights"] = rights + if phone_number is not None: + params["phoneNumber"] = phone_number + if extra: + params.update(extra) + return self._jsonrpc_request("accounts", "createAccount", params) + + def update_account(self, account_id: str, fields: dict) -> Any: + """Update a console account (accounts.updateAccount). STATE-CHANGING. + + `accountId` is required (verified); `fields` are the documented + account attributes to change (same shape as create, minus email). + Gate at the call site behind --confirm. + """ + params: dict = {"accountId": account_id} + params.update(fields or {}) + return self._jsonrpc_request("accounts", "updateAccount", params) + + def delete_account(self, account_id: str) -> Any: + """Delete a console account (accounts.deleteAccount). STATE-CHANGING. + `accountId` required (verified). Gate at the call site behind --confirm.""" + return self._jsonrpc_request( + "accounts", "deleteAccount", {"accountId": account_id} + ) + + def configure_notifications_settings(self, settings: dict) -> Any: + """Set notification settings (accounts.configureNotificationsSettings). + STATE-CHANGING — there are NO required params, so an empty payload is + accepted; the caller must pass the intended settings object. Gate at the + call site behind --confirm.""" + return self._jsonrpc_request( + "accounts", "configureNotificationsSettings", settings or {} + ) + # ====================================================================== # PUSH EVENT SERVICE (module `/push`) # ---------------------------------------------------------------------- diff --git a/.claude/skills/bitdefender/scripts/selftest.py b/.claude/skills/bitdefender/scripts/selftest.py index 0522259d..124a4b25 100644 --- a/.claude/skills/bitdefender/scripts/selftest.py +++ b/.claude/skills/bitdefender/scripts/selftest.py @@ -105,6 +105,15 @@ check("push-set no confirm -> rc3", ["push-set", "--status", "1", "--url", "http check("push-set enable no url -> rc2", ["push-set", "--status", "1", "--confirm"], want_rc=2) check("raw assignPolicy no confirm -> rc3", ["raw", "--module", "network", "--method", "assignPolicy", "--params", "{}"], want_rc=3) +# --- accounts module --- +check("account (own, no id)", ["account"], want_rc=0) +check("account-create no confirm -> rc3", ["account-create", "--email", "t@x.io"], want_rc=3, out_has="Would") +check("account-update no confirm -> rc3", ["account-update", "--id", "a", "--set-json", "{\"role\":5}"], want_rc=3) +check("account-update bad json -> rc2", ["account-update", "--id", "a", "--set-json", "{bad", "--confirm"], want_rc=2) +check("account-delete no confirm -> rc3", ["account-delete", "--id", "a"], want_rc=3) +check("notif-configure no confirm -> rc3", ["notif-configure", "--settings-json", "{\"deleteAfter\":7}"], want_rc=3) +check("raw createAccount no confirm -> rc3", ["raw", "--module", "accounts", "--method", "createAccount", "--params", "{}"], want_rc=3) + # --- raw gating --- check("raw destructive no confirm -> rc3", ["raw", "--module", "network", "--method", "deleteEndpoint", "--params", "{}"], want_rc=3)