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

@@ -4,10 +4,12 @@ description: >-
Manage the ACG Bitdefender GravityZone Cloud MSP tenant (Public JSON-RPC API):
inventory/audit endpoints, live security sweeps (infected / outdated-signature /
outdated-product), client companies, install packages, custom groups, scans,
move/delete endpoints (gated), policies (read-only), quarantine. Live production
partner tenant — treat destructive actions conservatively. Triggers: bitdefender,
gravityzone, install bitdefender on, list endpoints, infected machines, av coverage,
security sweep, endpoint protection, quarantine.
move/delete endpoints (gated), policies (full read + assign), reports, accounts,
scan tasks, notifications, push event service, quarantine, EDR (isolate /
blocklist). Live production partner tenant — treat destructive actions
conservatively. Triggers: bitdefender, gravityzone, install bitdefender on, list
endpoints, infected machines, av coverage, security sweep, endpoint protection,
assign policy, quarantine, reports, accounts.
---
# Bitdefender GravityZone Skill
@@ -64,14 +66,19 @@ PII).
cache with the new id immediately, so you don't need a full refresh to
reference it.
## Policy API limitation
## Policy control (corrected 2026-06-21)
The Public API exposes policies only shallowly. You CAN list policies, read
their id/name, audit which endpoints carry which policy (via endpoint detail),
and — via the UNVERIFIED `assignPolicy` — assign an existing policy. You CANNOT
read the granular module configuration of a policy, and there is NO create /
edit / clone policy method in the Public API. For policy authoring, use the
GravityZone console.
Earlier docs claimed policy detail was "shallow." That was WRONG. Verified live:
- **READ — full config.** `policy <id> --json` returns the COMPLETE granular
module configuration (general/antimalware/firewall/content-control/etc.), not
a shallow subset. `policies` lists id+name; `policy <id>` dumps the full tree.
- **ASSIGN — supported.** `assign-policy --policy <id> --targets <ep/group ids>`
pushes an EXISTING policy onto endpoints/groups (gated behind `--confirm`).
Param shape (`policyId` + `targetIds`) verified live.
- **AUTHOR — still console-only.** The Public API has NO create / edit / clone
policy method. You can read and assign, but to CREATE or MODIFY a policy body
you still use the GravityZone console. (This is the one true API limitation.)
## Safety gating
@@ -85,6 +92,8 @@ what they would do and exit non-zero:
- `unisolate --endpoints <id> ... --confirm`
- `blocklist-add --company <id> --hashes <h> ... --confirm`
- `blocklist-remove --id <hashItemId> --confirm`
- `assign-policy --policy <id> --targets <id> ... --confirm` (applies an existing policy to endpoints/groups)
- `push-set --status 1 --url <receiver> --confirm` (configures the GravityZone push event service)
Never run destructive calls casually against this tenant. UNVERIFIED methods
(assignPolicy, uninstall/reconfigure tasks, quarantine remove/restore, set
@@ -114,9 +123,22 @@ $GZ endpoint <endpointId>
$GZ sweep --company <companyId> # readable table
$GZ sweep --company <companyId> --json # machine output
# Policies (read-only, shallow)
# Policies (full read + assign; authoring is console-only)
$GZ policies
$GZ policy <policyId>
$GZ policy <policyId> --json # FULL granular config
$GZ assign-policy --policy <policyId> --targets <epId> ... --confirm
# Reports / accounts / scan tasks / notifications (read)
$GZ reports
$GZ accounts
$GZ scan-tasks
$GZ notif-settings
# Push event service (event-driven alerts instead of polling)
$GZ push-settings # current config (or "not configured")
$GZ push-stats
$GZ push-set --status 1 --url https://<receiver> --confirm # enable
$GZ push-set --status 0 --confirm # disable
# Quarantine
$GZ quarantine --company <companyId>
@@ -149,14 +171,32 @@ $GZ raw --module network --method getEndpointsList --params '{"page":1,"perPage"
$GZ delete-endpoint <id> --confirm
```
## Phase-2 hooks (not yet implemented)
## Enabled API scopes (live key, 2026-06-21)
- **GuruRMM push-deploy:** use `install-links` to fetch the platform installer
URL, then push the installer to a target via the GuruRMM agent fleet (`/rmm`)
for one-step Bitdefender rollout from RMM.
- **Push webhook:** subscribe to GravityZone Push events (new malware /
endpoint state changes) and surface them through the coord API / RMM alerts
instead of polling `sweep`.
`companies, licensing, packages, network, integrations, policies,
maintenancewindows, reports, accounts, incidents, push, quarantine, phasr,
patchmanagement`.
**Wrapped & verified:** companies, licensing, packages, network (endpoints/
groups/scan/move/delete/assignPolicy), policies (read+assign), reports,
accounts, incidents (blocklist + isolate), quarantine, push (get/stats/set).
**Dead on this tenant (license/feature OFF — `raw` only, returns errors):**
`patchmanagement`, `phasr`, `maintenancewindows`, `integrations`. `incidents.
getIncidentsList` returns "Method not found" (blocklist + isolate on the same
module DO work).
## Phase-2 hooks
- **Push webhook (half-built):** `push-set` now configures the GravityZone push
event service over the API (verified the `status` param + gating). Remaining:
stand up the RECEIVER — an HTTPS endpoint (coord API or an RMM route) that
accepts GravityZone's event POSTs and fans them into coord/RMM alerts — then
`push-set --status 1 --url <receiver> --confirm`. Without a receiver URL there
is nothing to enable yet.
- **GuruRMM push-deploy (not yet wired):** use `install-links` to fetch the
platform installer URL, then push the installer to a target via the GuruRMM
agent fleet (`/rmm`) for one-step Bitdefender rollout from RMM. Needs the
cross-skill call into `/rmm` against a chosen agent.
## Reference

View File

@@ -96,20 +96,50 @@ In `getNetworkInventoryItems` results, `type == 1` denotes a company node.
| `getInstallationLinks` | `packageName, companyId?` | VERIFIED | Returns Windows / Mac / Linux installer download URLs for a package. |
| `deletePackage` | `packageName, companyId?` | VERIFIED (destructive) | Delete a package. CLI-gated behind `--confirm`. |
## policies (`/policies`) — READ ONLY, SHALLOW
## policies (`/policies`) — FULL READ + ASSIGN (authoring is console-only)
> **Important limitation:** The Public API exposes policies only at a shallow
> level. `getPolicyDetails` returns id / name / a limited subset of settings —
> **NOT** the granular module configuration shown in the console. There is **no
> create / edit / clone** policy method in the Public API. You can: list
> policies, read their names/ids, and (via the UNVERIFIED `assignPolicy`) assign
> an existing policy to endpoints. You CANNOT author or modify policy bodies
> programmatically.
> **Corrected 2026-06-21:** the earlier "shallow only" claim was WRONG.
> `getPolicyDetails` returns the COMPLETE granular module configuration
> (general/antimalware/firewall/content-control/etc.), confirmed live on a real
> policy. You CAN: list policies, read the full config, and assign an existing
> policy to endpoints/groups (`network.assignPolicy`, param shape now verified).
> You still CANNOT **create / edit / clone** a policy body via the Public API —
> authoring stays in the GravityZone console.
| Method | Params | Status | Notes |
|---|---|---|---|
| `getPoliciesList` | `page?, perPage?` | VERIFIED | List policies (id, name). |
| `getPolicyDetails` | `policyId` | VERIFIED | Shallow detail only. Not the full config. |
| `getPolicyDetails` | `policyId` | VERIFIED | **Full** granular config (not shallow). |
| `assignPolicy` (`/network`) | `policyId, targetIds[], forcePolicyInheritance?` | VERIFIED LIVE (param shape) | Assign existing policy to endpoints/groups. Param shape confirmed via validation probe 2026-06-21. CLI `assign-policy`, gated. STATE-CHANGING. |
## reports (`/reports`) — VERIFIED LIVE
| Method | Params | Status | Notes |
|---|---|---|---|
| `getReportsList` | `page?, perPage?` | VERIFIED LIVE | List saved reports. CLI `reports`. |
| `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)
| Method | Params | Status | Notes |
|---|---|---|---|
| `getAccountsList` | `page?, perPage?` | VERIFIED LIVE | List console accounts/users. CLI `accounts`. |
| `getNotificationsSettings` | `{}` | VERIFIED LIVE | Notification config. CLI `notif-settings`. |
| `createAccount` / `updateAccount` / `deleteAccount` | uncertain | UNVERIFIED (state-changing) | Not exposed; `raw` only after confirming shape. |
## push (`/push`) — event push service (VERIFIED reachable)
> Powers event-driven alerting (GravityZone POSTs security events to a receiver
> you specify) instead of polling `sweep`. `get`/`stats` error with "…were not
> set" until configured — that is an EXPECTED unconfigured state, handled cleanly
> by the CLI (rc0 + INFO), NOT a failure.
| Method | Params | Status | Notes |
|---|---|---|---|
| `getPushEventSettings` | `{}` | VERIFIED LIVE | Current settings. CLI `push-settings`. |
| `getPushEventStats` | `{}` | VERIFIED LIVE | Delivery stats. CLI `push-stats`. |
| `setPushEventSettings` | `status (req), serviceType, serviceSettings{url,requireValidSslCertificate,authorization}, subscribeToEventTypes?` | `status` VERIFIED (probe); nested shape UNVERIFIED | Configure the service. CLI `push-set`, gated. STATE-CHANGING. Needs a receiver URL.
## quarantine (`/quarantine`)
@@ -142,15 +172,18 @@ In `getNetworkInventoryItems` results, `type == 1` denotes a company node.
| `getCustomRulesList` | uncertain | UNVERIFIED | Not implemented. `raw` only. |
| `deleteCustomRule` | uncertain | UNVERIFIED (destructive) | Not implemented. `raw` only. |
## Other modules — raw-reachable only
## Dead / unavailable modules on this tenant (probed 2026-06-21)
The following modules are reachable via `raw --module <name>` but have no
dedicated CLI methods and no verified signatures here:
In the API-key scope but NOT usable — calls return "not available" / "method
not found". Do not build against these without a license/feature change:
- `patchmanagement`raw only. NOTE: the patchmanagement / PHASR license
features are OFF on this tenant, so these calls will not return useful data.
- `integrations` — raw only, UNVERIFIED.
- `maintenancewindows` — raw only, UNVERIFIED.
- `patchmanagement`license OFF (`managePatchManagement: false`). "not available".
- `phasr` — license/feature OFF. `getStatus` → method not found.
- `maintenancewindows``getMaintenanceWindows(List)` → "not available".
- `integrations``getPSAIntegrationList` → method not found (correct method
name unconfirmed).
- `incidents.getIncidentsList` — "Method not found" (yet `getBlocklistItems` and
the isolate tasks on the SAME module work — likely an EDR sub-feature gate).
---
@@ -169,7 +202,11 @@ quarantine.getQuarantineItemsList, incidents.getBlocklistItems,
incidents.createIsolateEndpointTask (gated),
incidents.createRestoreEndpointFromIsolationTask (gated),
incidents.addToBlocklist (gated), incidents.removeFromBlocklist (gated;
param name UNVERIFIED).
param name UNVERIFIED), network.getScanTasksList, network.assignPolicy (gated;
param shape verified 2026-06-21), reports.getReportsList, accounts.getAccountsList,
accounts.getNotificationsSettings, push.getPushEventSettings,
push.getPushEventStats, push.setPushEventSettings (gated; `status` verified,
nested shape UNVERIFIED).
> NOTE: `incidents.getIncidentsList` is wired into the CLI (`incidents`
> subcommand) but returned `Method not found` on live re-test (2026-05-30) —

View File

@@ -168,6 +168,31 @@ def _print_incidents_table(data: dict) -> None:
f"{i.get('severity', i.get('status',''))}")
def _print_reports_table(data: dict) -> None:
items = data.get("items", [])
print(f"Reports: {data.get('total', len(items))}")
for r in items:
print(f" {str(r.get('id','?')):26} {str(r.get('name','')):40} "
f"type={r.get('type','')}")
def _print_accounts_table(data: dict) -> None:
items = data.get("items", [])
print(f"Accounts: {data.get('total', len(items))}")
for a in items:
prof = a.get("profile", {}) or {}
print(f" {str(a.get('id','?')):26} {str(a.get('email','')):34} "
f"{prof.get('fullName','')}")
def _print_scan_tasks_table(data: dict) -> None:
items = data.get("items", [])
print(f"Scan tasks: {data.get('total', len(items))}")
for t in items:
print(f" {str(t.get('id','?')):26} {str(t.get('name','')):30} "
f"status={t.get('status','')}")
def _print_inventory_table(cache: dict) -> None:
print(f"Inventory cached_at: {cache.get('fetched_at')}")
print(f" companies: {len(cache.get('companies', {}))}")
@@ -220,11 +245,90 @@ def cmd_policies(client, args):
def cmd_policy(client, args):
print("[WARNING] Public API returns shallow policy detail only "
"(no granular config).", file=sys.stderr)
# getPolicyDetails returns the FULL granular module configuration (verified
# live 2026-06-21). Use --json for the complete settings tree; the table
# view shows the top-level keys only.
_emit(client.get_policy_details(args.policy_id), args.json, _print_kv)
def cmd_reports(client, args):
_emit(client.list_reports(page=args.page, per_page=args.per_page),
args.json, _print_reports_table)
def cmd_accounts(client, args):
_emit(client.list_accounts(page=args.page, per_page=args.per_page),
args.json, _print_accounts_table)
def cmd_notif_settings(client, args):
_emit(client.get_notifications_settings(), args.json, _print_kv)
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)
def cmd_assign_policy(client, args):
desc = (f"assign policy {args.policy} to {len(args.targets)} target(s): "
f"{','.join(args.targets)}")
if not _gated(desc, args.confirm):
return 3
result = client.assign_policy(
args.policy, args.targets, force_inheritance=args.force_inheritance
)
_emit({"assignedPolicy": args.policy, "targets": args.targets,
"result": result}, args.json, _print_kv)
return 0
def _push_read(emit_fn) -> int:
"""Run a push read, treating 'never configured' as an expected (non-error)
state rather than a failure (so it does not pollute errorlog)."""
try:
emit_fn()
return 0
except GravityZoneError as exc:
msg = str(exc).lower()
if "not set" in msg or "are not" in msg or "not available" in msg:
print("[INFO] Push event service is not configured on this tenant.")
return 0
raise
def cmd_push_settings(client, args):
return _push_read(
lambda: _emit(client.get_push_settings(), args.json, _print_kv)
)
def cmd_push_stats(client, args):
return _push_read(
lambda: _emit(client.get_push_stats(), args.json, _print_kv)
)
def cmd_push_set(client, args):
state = "ENABLE" if args.status == 1 else "DISABLE"
if args.status == 1 and not args.url:
print("[ERROR] --url is required to enable the push event service.",
file=sys.stderr)
return 2
desc = f"{state} GravityZone push event service (url={args.url or '-'})"
if not _gated(desc, args.confirm):
return 3
result = client.set_push_settings(
status=args.status,
service_type=args.service_type,
url=args.url,
require_valid_ssl=not args.allow_insecure_ssl,
authorization=args.authorization,
)
_emit({"pushService": state, "result": result}, args.json, _print_kv)
return 0
def cmd_packages(client, args):
_emit(client.list_packages(), args.json, _print_package_table)
@@ -288,7 +392,8 @@ def cmd_make_group(client, args):
# (EDR) module — gate them in `raw` as well as via the dedicated subcommands.
DESTRUCTIVE_RAW_PATTERNS = ("delete", "createuninstall", "createremove",
"createreconfigure", "isolat", "addtoblocklist",
"removefromblocklist")
"removefromblocklist", "assignpolicy",
"setpushevent")
def _is_destructive_method(method: str) -> bool:
@@ -419,12 +524,34 @@ def build_parser() -> argparse.ArgumentParser:
sp.add_argument("--company", help="Parent id (defaults to ACG container).")
sub.add_parser("policies", help="List policies (id + name).", parents=[common])
sp = sub.add_parser("policy", help="Shallow detail for one policy.",
sp = sub.add_parser("policy",
help="Full granular config for one policy (use --json).",
parents=[common])
sp.add_argument("policy_id")
sub.add_parser("packages", help="List installation packages.", parents=[common])
sp = sub.add_parser("reports", help="List saved reports.", parents=[common])
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=100)
sp = sub.add_parser("accounts", help="List GravityZone console accounts.",
parents=[common])
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=100)
sub.add_parser("notif-settings", help="Show notification settings.",
parents=[common])
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)
sub.add_parser("push-settings",
help="Show push event service settings.", parents=[common])
sub.add_parser("push-stats",
help="Show push event service delivery stats.", parents=[common])
sp = sub.add_parser("quarantine", help="List quarantine items for a company.",
parents=[common])
sp.add_argument("--company", required=True)
@@ -533,6 +660,32 @@ def build_parser() -> argparse.ArgumentParser:
help="hashItemId — the 'id' from `blocklist` output.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("assign-policy",
help="Assign an existing policy to endpoints/groups (gated).",
parents=[common])
sp.add_argument("--policy", required=True, help="policyId to assign.")
sp.add_argument("--targets", nargs="+", required=True,
help="One or more endpoint/group ids.")
sp.add_argument("--force-inheritance", action="store_true",
help="Force policy inheritance to sub-items.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("push-set",
help="Configure the push event service (gated).",
parents=[common])
sp.add_argument("--status", type=int, required=True, choices=[0, 1],
help="1=enable, 0=disable.")
sp.add_argument("--url",
help="Receiver URL GravityZone POSTs events to "
"(required to enable).")
sp.add_argument("--service-type", default="jsonRPC",
help="jsonRPC|splunk|cef (default jsonRPC).")
sp.add_argument("--authorization",
help="Optional Authorization header the receiver expects.")
sp.add_argument("--allow-insecure-ssl", action="store_true",
help="Do not require a valid SSL cert on the receiver.")
sp.add_argument("--confirm", action="store_true")
return p
@@ -545,6 +698,14 @@ HANDLERS = {
"policies": cmd_policies,
"policy": cmd_policy,
"packages": cmd_packages,
"reports": cmd_reports,
"accounts": cmd_accounts,
"notif-settings": cmd_notif_settings,
"scan-tasks": cmd_scan_tasks,
"push-settings": cmd_push_settings,
"push-stats": cmd_push_stats,
"assign-policy": cmd_assign_policy,
"push-set": cmd_push_set,
"quarantine": cmd_quarantine,
"blocklist": cmd_blocklist,
"incidents": cmd_incidents,

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)
# ======================================================================

View File

@@ -68,6 +68,19 @@ check("blocklist json", ["blocklist", "--json"], want_rc=0, out_json_ok=True)
check("blocklist page2", ["blocklist", "--page", "2", "--per-page", "3"], want_rc=0, out_has="Blocklist items:")
check("inventory cached", ["inventory"], want_rc=0, out_has="Inventory cached_at:")
# --- expanded control surface (read) ---
check("reports", ["reports"], want_rc=0, out_has="Reports:")
check("reports json", ["reports", "--json"], want_rc=0, out_json_ok=True)
check("accounts", ["accounts"], want_rc=0, out_has="Accounts:")
check("accounts json", ["accounts", "--json"], want_rc=0, out_json_ok=True)
check("notif-settings", ["notif-settings"], want_rc=0)
check("scan-tasks", ["scan-tasks"], want_rc=0, out_has="Scan tasks:")
# push read: unconfigured tenant must be treated as expected (rc0 + INFO), NOT an error
check("push-settings (unconfigured -> rc0)", ["push-settings"], want_rc=0, out_has="not configured")
check("push-stats (unconfigured -> rc0)", ["push-stats"], want_rc=0, out_has="not configured")
# policy detail must NOT carry the old false 'shallow' warning anymore
check("policy no shallow warning", ["policy", "5c42940b6e16d61a0c8b4568"], want_rc=0, err_has=None)
# --- error handling: a MALFORMED id (not valid hex/ObjectId) makes the API
# error, which must exit non-zero (1). Note: a well-formed but non-existent
# hex id is ACCEPTED by GravityZone and returns a stub (rc 0) -- that is the
@@ -87,6 +100,10 @@ check("blocklist-remove no confirm -> rc3", ["blocklist-remove", "--id", "x"], w
check("delete-endpoint no confirm -> rc3", ["delete-endpoint", "x"], want_rc=3)
check("delete-package no confirm -> rc3", ["delete-package", "--package", "x"], want_rc=3)
check("delete-group no confirm -> rc3", ["delete-group", "--group", "x"], want_rc=3)
check("assign-policy no confirm -> rc3", ["assign-policy", "--policy", "p", "--targets", "x"], want_rc=3, out_has="Would")
check("push-set no confirm -> rc3", ["push-set", "--status", "1", "--url", "https://x/y"], want_rc=3)
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)
# --- raw gating ---
check("raw destructive no confirm -> rc3", ["raw", "--module", "network",