feat(bitdefender): expand GravityZone control surface + correct policy docs

Re-verified the live tenant's full API scope and wrapped the modules the key
allows but the skill didn't expose. New CLI subcommands:
- assign-policy (gated) — apply an existing policy to endpoints/groups
  (param shape policyId+targetIds verified live)
- reports, accounts, notif-settings, scan-tasks — read
- push-settings / push-stats / push-set (gated) — push event service
  (status param verified; needs a receiver URL to enable)

Corrections from live probing:
- policies are NOT shallow: getPolicyDetails returns the FULL granular config.
  Removed the false "shallow" warning; documented read+assign, console-only authoring.
- raw now gates assignPolicy + setPushEventSettings.
- documented dead modules (patchmanagement/phasr/maintenancewindows/integrations,
  incidents.getIncidentsList) and unconfigured-push handled cleanly (rc0, no errorlog).

selftest 29/29 -> 42/42, all green against the live tenant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 10:02:25 -07:00
parent 1f65facb6f
commit d622a05b84
5 changed files with 417 additions and 41 deletions

View File

@@ -620,6 +620,127 @@ class GravityZoneClient:
"incidents", "removeFromBlocklist", {"hashItemId": hash_item_id}
)
# ======================================================================
# POLICY ASSIGNMENT (state-changing; gate behind --confirm at call site)
# ----------------------------------------------------------------------
# NOTE: getPolicyDetails (above) returns the FULL granular module config
# (verified live 2026-06-21 — the earlier "shallow only" claim was wrong).
# The Public API still has NO create/edit/clone policy method — authoring
# stays in the console — but assigning an EXISTING policy is supported here.
# ======================================================================
def assign_policy(
self,
policy_id: str,
target_ids: list[str],
force_inheritance: bool = False,
) -> Any:
"""Assign an existing policy to endpoints/groups (network.assignPolicy).
Param shape VERIFIED LIVE via validation probe (2026-06-21): requires
`policyId` and `targetIds` (a list of endpoint/group ids).
`forcePolicyInheritance` is optional. STATE-CHANGING — gate at the call
site behind --confirm.
"""
params: dict = {"policyId": policy_id, "targetIds": target_ids}
if force_inheritance:
params["forcePolicyInheritance"] = True
return self._jsonrpc_request("network", "assignPolicy", params)
def list_scan_tasks(
self,
page: int = 1,
per_page: int = 100,
name: Optional[str] = None,
status: Optional[int] = None,
) -> dict:
"""List scan tasks (network.getScanTasksList). VERIFIED LIVE."""
params: dict = {"page": page, "perPage": per_page}
if name is not None:
params["name"] = name
if status is not None:
params["status"] = status
return self._jsonrpc_request("network", "getScanTasksList", params) or {}
# ======================================================================
# REPORTS (module `/reports`) — VERIFIED LIVE
# ======================================================================
def list_reports(self, page: int = 1, per_page: int = 100) -> dict:
"""List saved reports (reports.getReportsList)."""
return self._jsonrpc_request(
"reports", "getReportsList", {"page": page, "perPage": per_page}
) or {}
def get_report_links(self, report_id: str) -> Any:
"""Get download links for a generated report (reports.getDownloadLinks).
Param name `reportId` is the candidate — confirm against the console if
it errors.
"""
return self._jsonrpc_request(
"reports", "getDownloadLinks", {"reportId": report_id}
)
# ======================================================================
# ACCOUNTS (module `/accounts`) — VERIFIED LIVE (read)
# ======================================================================
def list_accounts(self, page: int = 1, per_page: int = 100) -> dict:
"""List GravityZone console accounts/users (accounts.getAccountsList)."""
return self._jsonrpc_request(
"accounts", "getAccountsList", {"page": page, "perPage": per_page}
) or {}
def get_notifications_settings(self) -> dict:
"""Notification configuration (accounts.getNotificationsSettings)."""
return self._jsonrpc_request(
"accounts", "getNotificationsSettings", {}
) or {}
# ======================================================================
# PUSH EVENT SERVICE (module `/push`)
# ----------------------------------------------------------------------
# `get`/`stats` are read (but error when the service was never configured —
# that is an EXPECTED state, not a failure; the CLI handles it cleanly).
# `set` is STATE-CHANGING (it configures where GravityZone POSTs security
# events) — gate behind --confirm at the call site.
# ======================================================================
def get_push_settings(self) -> dict:
"""Current push event service settings (push.getPushEventSettings)."""
return self._jsonrpc_request("push", "getPushEventSettings", {}) or {}
def get_push_stats(self) -> dict:
"""Push event service delivery stats (push.getPushEventStats)."""
return self._jsonrpc_request("push", "getPushEventStats", {}) or {}
def set_push_settings(
self,
status: int,
service_type: str = "jsonRPC",
url: Optional[str] = None,
require_valid_ssl: bool = True,
authorization: Optional[str] = None,
subscribe_event_types: Optional[dict] = None,
) -> Any:
"""Configure the GravityZone Push event service (push.setPushEventSettings).
`status` (1=on / 0=off) is REQUIRED (verified via validation probe
2026-06-21). When enabling, `serviceSettings.url` is the receiver
endpoint GravityZone POSTs events to. The nested shape
(serviceType/serviceSettings/subscribeToEventTypes) follows Bitdefender's
documented push API and is UNVERIFIED beyond `status` on this tenant —
confirm the first successful enable against the live response.
STATE-CHANGING — gate at the call site behind --confirm.
"""
params: dict = {"status": status, "serviceType": service_type}
service_settings: dict = {"requireValidSslCertificate": require_valid_ssl}
if url is not None:
service_settings["url"] = url
if authorization is not None:
service_settings["authorization"] = authorization
params["serviceSettings"] = service_settings
if subscribe_event_types is not None:
params["subscribeToEventTypes"] = subscribe_event_types
return self._jsonrpc_request("push", "setPushEventSettings", params)
# ======================================================================
# CACHE LAYER (identity / structure only — never volatile status)
# ======================================================================