From 2a1ffab19f1b102ad4ecf38a49f71faaecbb7689 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Sun, 21 Jun 2026 10:26:14 -0700 Subject: [PATCH] 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) --- .../skills/bitdefender/references/BUILDOUT.md | 11 ++- .../bitdefender/references/api-reference.md | 14 ++- .claude/skills/bitdefender/scripts/gz.py | 88 ++++++++++++++++++- .../skills/bitdefender/scripts/gz_client.py | 55 ++++++++++++ .../skills/bitdefender/scripts/selftest.py | 8 ++ 5 files changed, 168 insertions(+), 8 deletions(-) diff --git a/.claude/skills/bitdefender/references/BUILDOUT.md b/.claude/skills/bitdefender/references/BUILDOUT.md index 3dfc328d..c03d9c04 100644 --- a/.claude/skills/bitdefender/references/BUILDOUT.md +++ b/.claude/skills/bitdefender/references/BUILDOUT.md @@ -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 diff --git a/.claude/skills/bitdefender/references/api-reference.md b/.claude/skills/bitdefender/references/api-reference.md index cca05454..e9a60e19 100644 --- a/.claude/skills/bitdefender/references/api-reference.md +++ b/.claude/skills/bitdefender/references/api-reference.md @@ -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`) diff --git a/.claude/skills/bitdefender/scripts/gz.py b/.claude/skills/bitdefender/scripts/gz.py index f914b013..e679d923 100644 --- a/.claude/skills/bitdefender/scripts/gz.py +++ b/.claude/skills/bitdefender/scripts/gz.py @@ -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, diff --git a/.claude/skills/bitdefender/scripts/gz_client.py b/.claude/skills/bitdefender/scripts/gz_client.py index 4379f4c0..60b80396 100644 --- a/.claude/skills/bitdefender/scripts/gz_client.py +++ b/.claude/skills/bitdefender/scripts/gz_client.py @@ -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", diff --git a/.claude/skills/bitdefender/scripts/selftest.py b/.claude/skills/bitdefender/scripts/selftest.py index 124a4b25..30e974d2 100644 --- a/.claude/skills/bitdefender/scripts/selftest.py +++ b/.claude/skills/bitdefender/scripts/selftest.py @@ -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")