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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`)
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user