feat(bitdefender): complete Companies module (build-out 2/N)

- Completed Companies module for bitdefender GravityZone Public API
- Implemented: getCompanyDetails, getCompanyDetailsByUser, createCompany, suspendCompany, activateCompany, deleteCompany
- Discovered updateCompany and getCompaniesList not available; companies retrieved via network inventory
- Company types: 0=Partner, 1=Customer; createCompany accepts nested licenseSubscription via JSON passthrough
- All write operations require --confirm; raw also restricts createCompany/suspendCompany/activateCompany
- selftest 49 -> 55 passing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 10:26:14 -07:00
parent 8a64bc48e6
commit 2a1ffab19f
5 changed files with 168 additions and 8 deletions

View File

@@ -22,10 +22,13 @@ Per-module workflow: fetch module doc -> list methods + params -> add to
- [x] getLicenseInfo
- [ ] (enumerate: getMonthlyUsage? / others)
## companies
- [x] getCompanyDetails
- [ ] getCompaniesList / getCompanyDetailsByUser
- [ ] createCompany / updateCompany / deleteCompany / suspend / activate (enumerate; gated)
## companies — COMPLETE (6; no updateCompany/getCompaniesList — those don't exist)
- [x] getCompanyDetails (no id = own)
- [x] getCompanyDetailsByUser (username)
- [x] createCompany (gated; type 0=Partner/1=Customer + name)
- [x] suspendCompany (gated)
- [x] activateCompany (gated)
- [x] deleteCompany (gated)
## network
- [x] getNetworkInventoryItems · getEndpointsList · getManagedEndpointDetails

View File

@@ -66,12 +66,20 @@ In `getNetworkInventoryItems` results, `type == 1` denotes a company node.
|---|---|---|---|
| `getLicenseInfo` | `{}` | VERIFIED | Seats, expiry, usage. |
## companies (`/companies`)
## companies (`/companies`) — COMPLETE (6 methods; no updateCompany/getCompaniesList)
> `updateCompany` and `getCompaniesList` return "method not found" — they do NOT
> exist. Enumerate companies via `network.getNetworkInventoryItems` (the `companies`
> CLI cmd). `type`: 0=Partner, 1=Customer.
| Method | Params | Status | Notes |
|---|---|---|---|
| `getCompanyDetails` | `{}` or `{companyId}` | VERIFIED | Own company when no arg; a specific company when `companyId` given. |
| `getCompanyDetailsByUser` | uncertain | UNVERIFIED | Param shape not confirmed. Use `raw` if needed. |
| `getCompanyDetails` | `{}` or `{companyId}` | VERIFIED LIVE | Own company when no arg. CLI `company [id]`. |
| `getCompanyDetailsByUser` | `username` | VERIFIED LIVE | Company that owns a user. CLI `company-by-user`. |
| `createCompany` | `type (req), name (req), parentId?, address?, country?, state?, phone?, industry?, canBeManagedByAbove?, assignedProductType?, licenseSubscription{type,reservedSlots,endSubscription,autoRenewPeriod,...}` | VERIFIED (docs + probe) | CLI `company-create`, gated. STATE-CHANGING. Docs: 77211-126236-createcompany.html |
| `suspendCompany` | `companyId` | VERIFIED (probe) | CLI `company-suspend`, gated. STATE-CHANGING. |
| `activateCompany` | `companyId` | VERIFIED (probe) | CLI `company-activate`, gated. STATE-CHANGING. |
| `deleteCompany` | `companyId` | VERIFIED (probe) | CLI `company-delete`, gated. STATE-CHANGING. |
## network (`/network`)

View File

@@ -211,6 +211,51 @@ def cmd_companies(client, args):
_emit(client.list_companies(), args.json, _print_company_table)
def cmd_company(client, args):
_emit(client.get_company_details(args.company_id), args.json, _print_kv)
def cmd_company_by_user(client, args):
_emit(client.get_company_by_user(args.username), args.json, _print_kv)
def cmd_company_create(client, args):
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
label = {0: "Partner", 1: "Customer"}.get(args.type, str(args.type))
if not _gated(f"create {label} company '{args.name}'", args.confirm):
return 3
result = client.create_company(args.type, args.name, parent_id=args.parent,
extra=extra or None)
_emit({"createdCompany": args.name, "result": result}, args.json, _print_kv)
return 0
def cmd_company_suspend(client, args):
if not _gated(f"suspend company {args.id}", args.confirm):
return 3
_emit({"suspended": args.id, "result": client.suspend_company(args.id)},
args.json, _print_kv)
return 0
def cmd_company_activate(client, args):
if not _gated(f"activate company {args.id}", args.confirm):
return 3
_emit({"activated": args.id, "result": client.activate_company(args.id)},
args.json, _print_kv)
return 0
def cmd_company_delete(client, args):
if not _gated(f"delete company {args.id}", args.confirm):
return 3
_emit({"deletedCompany": args.id, "result": client.delete_company(args.id)},
args.json, _print_kv)
return 0
def cmd_endpoints(client, args):
_emit(client.list_endpoints(args.company, per_page=args.per_page),
args.json, _print_endpoint_table)
@@ -478,7 +523,8 @@ DESTRUCTIVE_RAW_PATTERNS = ("delete", "createuninstall", "createremove",
"createreconfigure", "isolat", "addtoblocklist",
"removefromblocklist", "assignpolicy",
"setpushevent", "createaccount", "updateaccount",
"configurenotif")
"configurenotif", "createcompany", "suspendcompany",
"activatecompany")
def _is_destructive_method(method: str) -> bool:
@@ -596,6 +642,40 @@ def build_parser() -> argparse.ArgumentParser:
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("company", help="Company detail (no id = own company).",
parents=[common])
sp.add_argument("company_id", nargs="?", help="Company id (optional).")
sp = sub.add_parser("company-by-user", help="Company that owns a username.",
parents=[common])
sp.add_argument("--username", required=True)
sp = sub.add_parser("company-create", help="Create a company (gated).",
parents=[common])
sp.add_argument("--type", type=int, required=True,
help="0=Partner, 1=Customer.")
sp.add_argument("--name", required=True)
sp.add_argument("--parent", help="parentId (defaults to the key's company).")
sp.add_argument("--extra-json",
help="JSON object of extra fields (licenseSubscription, "
"address, assignedProductType, ...).")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("company-suspend", help="Suspend a company (gated).",
parents=[common])
sp.add_argument("--id", required=True)
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("company-activate", help="Activate a company (gated).",
parents=[common])
sp.add_argument("--id", required=True)
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("company-delete", help="Delete a company (gated).",
parents=[common])
sp.add_argument("--id", required=True)
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("endpoints", help="List endpoints under a company/group.",
parents=[common])
sp.add_argument("--company", help="Parent company/group id.")
@@ -814,6 +894,12 @@ def build_parser() -> argparse.ArgumentParser:
HANDLERS = {
"status": cmd_status,
"companies": cmd_companies,
"company": cmd_company,
"company-by-user": cmd_company_by_user,
"company-create": cmd_company_create,
"company-suspend": cmd_company_suspend,
"company-activate": cmd_company_activate,
"company-delete": cmd_company_delete,
"endpoints": cmd_endpoints,
"endpoint": cmd_endpoint,
"sweep": cmd_sweep,

View File

@@ -254,6 +254,61 @@ class GravityZoneClient:
def get_own_company(self) -> dict:
return self._jsonrpc_request("companies", "getCompanyDetails", {}) or {}
def get_company_details(self, company_id: Optional[str] = None) -> dict:
"""Company detail (companies.getCompanyDetails). No id ⇒ own company."""
params: dict = {}
if company_id:
params["companyId"] = company_id
return self._jsonrpc_request("companies", "getCompanyDetails", params) or {}
def get_company_by_user(self, username: str) -> dict:
"""Company that owns a given user (companies.getCompanyDetailsByUser)."""
return self._jsonrpc_request(
"companies", "getCompanyDetailsByUser", {"username": username}
) or {}
def create_company(
self,
company_type: int,
name: str,
parent_id: Optional[str] = None,
extra: Optional[dict] = None,
) -> Any:
"""Create a company (companies.createCompany). STATE-CHANGING.
Required (verified): `type` (0=Partner, 1=Customer) and `name`. Documented
optional params: parentId, address, country, state, phone, industry,
canBeManagedByAbove, assignedProductType, and a nested `licenseSubscription`
{type (3=monthly subscription, 4=monthly trial), reservedSlots,
endSubscription, autoRenewPeriod, ...}. Pass those via `extra`. Gate at
the call site behind --confirm.
Docs: bitdefender.com/business/support/en/77211-126236-createcompany.html
"""
params: dict = {"type": company_type, "name": name}
if parent_id is not None:
params["parentId"] = parent_id
if extra:
params.update(extra)
return self._jsonrpc_request("companies", "createCompany", params)
def suspend_company(self, company_id: str) -> Any:
"""Suspend a company (companies.suspendCompany). STATE-CHANGING. Gated."""
return self._jsonrpc_request(
"companies", "suspendCompany", {"companyId": company_id}
)
def activate_company(self, company_id: str) -> Any:
"""Activate a suspended company (companies.activateCompany). STATE-CHANGING."""
return self._jsonrpc_request(
"companies", "activateCompany", {"companyId": company_id}
)
def delete_company(self, company_id: str) -> Any:
"""Delete a company (companies.deleteCompany). STATE-CHANGING. Gated."""
return self._jsonrpc_request(
"companies", "deleteCompany", {"companyId": company_id}
)
def list_companies(self, page: int = 1, per_page: int = 100) -> dict:
result = self._jsonrpc_request(
"network",

View File

@@ -105,6 +105,14 @@ 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)
# --- companies module ---
check("company (own, no id)", ["company"], want_rc=0)
check("company-create no confirm -> rc3", ["company-create", "--type", "1", "--name", "Test Co"], want_rc=3, out_has="Would")
check("company-suspend no confirm -> rc3", ["company-suspend", "--id", "x"], want_rc=3)
check("company-activate no confirm -> rc3", ["company-activate", "--id", "x"], want_rc=3)
check("company-delete no confirm -> rc3", ["company-delete", "--id", "x"], want_rc=3)
check("raw createCompany no confirm -> rc3", ["raw", "--module", "companies", "--method", "createCompany", "--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")