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