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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user