feat(bitdefender): complete Accounts module (build-out 1/N)

- Completed Accounts module for bitdefender skill (GravityZone Public API)
- Added 5 methods: getAccountDetails, createAccount, updateAccount, deleteAccount, configureNotificationsSettings
- Write methods require --confirm; raw also gates createAccount/updateAccount/configureNotificationsSettings
- Param shapes validated against official docs and safe validation probes
- configureNotificationsSettings is a setter with no required param; warning documented against empty payload on live tenant
- selftest 42 -> 49 passing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 10:22:01 -07:00
parent 4cf34f5221
commit 8a64bc48e6
5 changed files with 217 additions and 9 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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`)
# ----------------------------------------------------------------------

View File

@@ -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)