Files
claudetools/.claude/skills/bitdefender/scripts/gz.py
Mike Swanson ef55121d95 errorlog lint follow-ups: bitdefender log-gap fix + submodule memory + Windows CORE rules
- bitdefender gz.py: add "missing name" to _EXPECTED_ERROR_MARKERS — closes the last gap in
  Howard's errorlog suppression ("Missing name 'X' in 'options' object" validation errors were
  still logged). Verified all 10 real spam messages now suppressed; genuine errors still log.
- memory feedback_submodule_autosync_discipline: capture the recurring auto-synced-submodule
  rule (worktree or push-by-SHA + ls-remote verify; assert HEAD==origin/main before audits;
  never checkout-- shared files). Recurred on Howard-Home x3 + GURU-5070 this session.
- CLAUDE.md CORE Windows bullet: promote the two top recurring mechanical traps (/tmp path
  mismatch, curl.exe/plink quote-stripping) to always-loaded hard rules so they stop repeating.

Lint of errorlog.md: bitdefender expected-validation spam was ~70% of entries (Howard's
suppression now complete); fabb3421/Mail.Send drift closed earlier this session; wiki-compile
lock-release doc already fixed (entries predate the fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:48:00 -07:00

1261 lines
47 KiB
Python

#!/usr/bin/env python3
"""CLI for the bitdefender skill - GravityZone Cloud Public API.
Read-only subcommands run freely. Destructive subcommands (delete-endpoint,
delete-package, delete-group) refuse to run unless --confirm is passed; without
it they print what they WOULD do and exit non-zero.
Output: --json emits raw JSON; otherwise a readable table/summary.
Usage examples:
python gz.py status
python gz.py companies
python gz.py endpoints --company 5c4280716c0318f3478b456e
python gz.py endpoint <endpointId>
python gz.py sweep --company <id>
python gz.py policies
python gz.py policy <policyId>
python gz.py packages
python gz.py quarantine --company <id>
python gz.py inventory --refresh
python gz.py create-package --name "Win Default" --company <id>
python gz.py install-links --package "Win Default" --company <id>
python gz.py scan --targets <id1> <id2> --type 2 --name "Full scan"
python gz.py move --endpoints <id1> <id2> --group <groupId>
python gz.py make-group --name "New Group" --parent <parentId>
python gz.py delete-endpoint <id> --confirm
python gz.py blocklist --company <id>
python gz.py incidents --company <id>
python gz.py isolate --endpoints <id1> <id2> --confirm
python gz.py unisolate --endpoints <id1> <id2> --confirm
python gz.py blocklist-add --company <id> --hashes <h1> <h2> --confirm
python gz.py blocklist-remove --id <hashItemId> --confirm
python gz.py raw --module network --method getEndpointsList --params '{"page":1}'
"""
from __future__ import annotations
import argparse
import dataclasses
import json
import os
import subprocess
import sys
from gz_client import GravityZoneClient, GravityZoneError, GZEndpointSummary
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
# Substrings that mark an error as an EXPECTED API response (validation, probing,
# not-configured, transient rate-limit, expected state) rather than a genuine skill
# failure. The errorlog rule (CLAUDE.md) forbids logging expected/handled
# conditions - only real failures worth pattern-spotting. These are NOT logged.
_EXPECTED_ERROR_MARKERS = (
"required parameter is missing",
"missing name", # e.g. "Missing name 'reportingInterval' in 'options' object" (validation)
"invalid value",
"not expected",
"method not found",
"is not available",
"not available yet",
"were not set",
"not configured",
"should not be used with",
"you must specify a value",
"cannot be restored from isolation",
"429",
"too many requests",
)
def _is_expected_error(msg: str) -> bool:
m = (msg or "").lower()
return any(marker in m for marker in _EXPECTED_ERROR_MARKERS)
def _should_log_error(command: str, msg: str) -> bool:
"""Decide whether a GravityZoneError is a genuine skill failure worth logging.
Skips: explicit suppression (selftest/probes set GZ_SUPPRESS_ERRORLOG), the
exploratory `raw` subcommand, and expected API validation/probe responses.
"""
if os.environ.get("GZ_SUPPRESS_ERRORLOG"):
return False
if command == "raw":
return False
return not _is_expected_error(msg)
def _emit(obj, as_json: bool, table_fn=None) -> None:
if as_json or table_fn is None:
print(json.dumps(obj, indent=2, default=_json_default))
else:
table_fn(obj)
def _json_default(o):
if dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
return str(o)
# --- table renderers ----------------------------------------------------------
def _print_kv(d) -> None:
# Tolerant: some API methods return a list (e.g. installation links,
# endpoint tags) rather than a dict. Render either cleanly.
if isinstance(d, list):
for i, item in enumerate(d):
if isinstance(item, dict):
if i:
print(" ---")
for k, v in item.items():
print(f" {k}: {v}")
else:
print(f" {item}")
return
if not isinstance(d, dict):
print(f" {d}")
return
for k, v in d.items():
print(f" {k}: {v}")
def _print_status(status: dict) -> None:
"""Render the nested API-status dict as readable sections.
get_api_status() returns {"apiKey": {...}, "license": {...}} and possibly
"_licenseWarning". Print a [section] header per top-level key, then indented
key: value lines for each sub-field. Non-dict top-level values (e.g. the
license warning string) print as a single indented line under their header.
"""
for section, body in status.items():
print(f"[{section}]")
if isinstance(body, dict):
for k, v in body.items():
print(f" {k}: {v}")
else:
print(f" {body}")
def _print_company_table(data: dict) -> None:
items = data.get("items", [])
print(f"Companies: {data.get('total', len(items))}")
for c in items:
print(f" {c.get('id','?'):26} {c.get('name','')}")
def _print_endpoint_table(data: dict) -> None:
items = data.get("items", [])
print(f"Endpoints: {data.get('total', len(items))}")
for e in items:
print(f" {e.get('id','?'):26} {e.get('name',''):30} "
f"{e.get('operatingSystemVersion', e.get('os',''))}")
def _print_sweep_table(summaries: list) -> None:
print(f"Endpoints swept: {len(summaries)}")
print(f" {'STATUS':10} {'NAME':30} {'AGENT':14} {'LAST SEEN'}")
for s in summaries:
flags = []
if s.infected:
flags.append("INFECTED")
if s.signature_outdated:
flags.append("SIG-OLD")
if s.product_outdated:
flags.append("PROD-OLD")
status = ",".join(flags) if flags else "OK"
print(f" {status:10} {s.name[:30]:30} {str(s.agent_version or '-'):14} "
f"{s.last_seen or '-'}")
def _print_policy_table(data: dict) -> None:
items = data.get("items", [])
print(f"Policies: {data.get('total', len(items))}")
for p in items:
print(f" {p.get('id','?'):26} {p.get('name','')}")
def _print_package_table(data: dict) -> None:
items = data.get("items", [])
print(f"Packages: {data.get('total', len(items))}")
for p in items:
print(f" {str(p.get('id','?')):26} {p.get('name','')}")
def _print_quarantine_table(data: dict) -> None:
items = data.get("items", [])
print(f"Quarantine items: {data.get('total', len(items))}")
for q in items:
print(f" {q.get('threatName','?'):30} {q.get('endpointName','')} "
f"{q.get('detectionTime','')}")
def _print_blocklist_table(data: dict) -> None:
items = data.get("items", [])
print(f"Blocklist items: {data.get('total', len(items))}")
print(f" {'ID':26} {'HTYPE':6} {'HASH':66} SOURCE-INFO")
for b in items:
print(f" {str(b.get('id','?')):26} {str(b.get('hashType','?')):6} "
f"{str(b.get('hash','')):66} {b.get('sourceInfo','')}")
def _print_incidents_table(data: dict) -> None:
items = data.get("items", [])
print(f"Incidents: {data.get('total', len(items))}")
for i in items:
print(f" {str(i.get('id','?')):26} {i.get('name', i.get('title','')):40} "
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', {}))}")
print(f" endpoints: {len(cache.get('endpoints', {}))}")
print(f" policies: {len(cache.get('policies', {}))}")
print(f" packages: {len(cache.get('packages', []))}")
print(f" groups: {len(cache.get('groups', {}))}")
# --- command handlers ---------------------------------------------------------
def cmd_status(client, args):
_emit(client.get_api_status(), args.json, _print_status)
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)
def cmd_endpoint(client, args):
_emit(client.get_endpoint_details(args.endpoint_id), args.json, _print_kv)
def cmd_sweep(client, args):
if args.company:
summaries = client.security_sweep(args.company)
else:
print("[INFO] No --company given; sweeping ALL client companies "
"(this makes many live API calls).", file=sys.stderr)
summaries = client.security_sweep_all_clients()
if args.json:
print(json.dumps([dataclasses.asdict(s) for s in summaries], indent=2))
else:
_print_sweep_table(summaries)
def _require_company_for_sweep() -> str:
from gz_client import ACG_COMPANIES_CONTAINER_ID
print("[INFO] No --company given; sweeping the ACG companies container.",
file=sys.stderr)
return ACG_COMPANIES_CONTAINER_ID
def cmd_policies(client, args):
_emit(client.list_policies(), args.json, _print_policy_table)
def cmd_policy(client, args):
# 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_account(client, args):
_emit(client.get_account_details(args.account_id), args.json, _print_kv)
def _load_json_arg(raw, label):
"""Parse a JSON-object CLI arg; returns (obj, error_rc). error_rc is None on ok."""
if raw is None:
return {}, None
try:
obj = json.loads(raw)
except json.JSONDecodeError as exc:
print(f"[ERROR] --{label} is not valid JSON: {exc}", file=sys.stderr)
return None, 2
if not isinstance(obj, dict):
print(f"[ERROR] --{label} must be a JSON object.", file=sys.stderr)
return None, 2
return obj, None
def cmd_account_create(client, args):
rights, rc = _load_json_arg(args.rights_json, "rights-json")
if rc:
return rc
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
profile = {}
if args.full_name:
profile["fullName"] = args.full_name
if args.language:
profile["language"] = args.language
if args.timezone:
profile["timezone"] = args.timezone
if not _gated(f"create account {args.email} (role={args.role})", args.confirm):
return 3
result = client.create_account(
email=args.email, password=args.password, username=args.username,
role=args.role, profile=profile or None, rights=rights or None,
extra=extra or None,
)
_emit({"createdAccount": args.email, "result": result}, args.json, _print_kv)
return 0
def cmd_account_update(client, args):
fields, rc = _load_json_arg(args.set_json, "set-json")
if rc:
return rc
if not fields:
print("[ERROR] --set-json (object of fields to change) is required.",
file=sys.stderr)
return 2
if not _gated(f"update account {args.id} fields={list(fields)}", args.confirm):
return 3
result = client.update_account(args.id, fields)
_emit({"updatedAccount": args.id, "result": result}, args.json, _print_kv)
return 0
def cmd_account_delete(client, args):
if not _gated(f"delete account {args.id}", args.confirm):
return 3
result = client.delete_account(args.id)
_emit({"deletedAccount": args.id, "result": result}, args.json, _print_kv)
return 0
def cmd_notif_configure(client, args):
settings, rc = _load_json_arg(args.settings_json, "settings-json")
if rc:
return rc
if not settings:
print("[ERROR] --settings-json (the settings object) is required.",
file=sys.stderr)
return 2
if not _gated(f"configure notification settings ({list(settings)})", args.confirm):
return 3
result = client.configure_notifications_settings(settings)
_emit({"notificationsConfigured": True, "result": result}, args.json, _print_kv)
return 0
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_push_test(client, args):
if not _gated(f"send test push event '{args.event_type}'", args.confirm):
return 3
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
result = client.send_test_push_event(args.event_type, extra=extra or None)
_emit({"testEvent": args.event_type, "result": result}, args.json, _print_kv)
return 0
def cmd_package_details(client, args):
_emit(client.get_package_details(args.package_id), args.json, _print_kv)
def cmd_report_create(client, args):
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
if not _gated(f"create report '{args.name}'", args.confirm):
return 3
result = client.create_report(args.name, extra=extra or None)
_emit({"createdReport": args.name, "result": result}, args.json, _print_kv)
return 0
def cmd_report_links(client, args):
_emit(client.get_report_links(args.id), args.json, _print_kv)
def cmd_report_delete(client, args):
if not _gated(f"delete report {args.id}", args.confirm):
return 3
_emit({"deletedReport": args.id, "result": client.delete_report(args.id)},
args.json, _print_kv)
return 0
def cmd_quarantine_remove(client, args):
if not _gated(f"remove {len(args.items)} quarantine item(s)", args.confirm):
return 3
result = client.remove_quarantine_items(args.items)
_emit({"removedQuarantine": args.items, "result": result}, args.json, _print_kv)
return 0
def cmd_quarantine_restore(client, args):
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
if not _gated(f"restore {len(args.items)} quarantine item(s)", args.confirm):
return 3
result = client.restore_quarantine_items(args.items, extra=extra or None)
_emit({"restoredQuarantine": args.items, "result": result}, args.json, _print_kv)
return 0
def cmd_custom_rules(client, args):
_emit(client.list_custom_rules(page=args.page, per_page=args.per_page),
args.json, _print_kv)
def cmd_custom_rule_create(client, args):
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
if not _gated(f"create custom rule '{args.name}'", args.confirm):
return 3
result = client.create_custom_rule(args.name, extra=extra or None)
_emit({"createdRule": args.name, "result": result}, args.json, _print_kv)
return 0
def cmd_custom_rule_delete(client, args):
if not _gated(f"delete custom rule {args.id}", args.confirm):
return 3
_emit({"deletedRule": args.id, "result": client.delete_custom_rule(args.id)},
args.json, _print_kv)
return 0
def cmd_incident_status(client, args):
fields, rc = _load_json_arg(args.set_json, "set-json")
if rc:
return rc
if not _gated(f"change incident status (type={args.type})", args.confirm):
return 3
result = client.change_incident_status(args.type, fields)
_emit({"incidentStatus": "changed", "result": result}, args.json, _print_kv)
return 0
def cmd_incident_note(client, args):
fields, rc = _load_json_arg(args.set_json, "set-json")
if rc:
return rc
if not _gated(f"update incident note (type={args.type})", args.confirm):
return 3
result = client.update_incident_note(args.type, fields)
_emit({"incidentNote": "updated", "result": result}, args.json, _print_kv)
return 0
def cmd_monthly_usage(client, args):
_emit(client.get_monthly_usage(), args.json, _print_kv)
def cmd_integrations(client, args):
_emit(client.get_configured_integrations(page=args.page, per_page=args.per_page),
args.json, _print_kv)
def cmd_packages(client, args):
_emit(client.list_packages(), args.json, _print_package_table)
def cmd_quarantine(client, args):
_emit(client.list_quarantine(args.company), args.json, _print_quarantine_table)
def cmd_blocklist(client, args):
_emit(client.list_blocklist(args.company, page=args.page, per_page=args.per_page),
args.json, _print_blocklist_table)
def cmd_incidents(client, args):
_emit(client.list_incidents(args.company, page=args.page,
per_page=args.per_page),
args.json, _print_incidents_table)
def cmd_inventory(client, args):
_emit(client.get_inventory(refresh=args.refresh), args.json,
_print_inventory_table)
def cmd_create_package(client, args):
result = client.create_package(
package_name=args.name,
company_id=args.company,
description=args.description,
language=args.language,
)
_emit({"created": args.name, "result": result}, args.json, _print_kv)
def cmd_install_links(client, args):
_emit(client.get_installation_links(args.package, args.company),
args.json, _print_kv)
def cmd_scan(client, args):
result = client.create_scan_task(
target_ids=args.targets, scan_type=args.type, name=args.name
)
_emit({"scanTask": result}, args.json, _print_kv)
def cmd_move(client, args):
result = client.move_endpoints(args.endpoints, args.group)
_emit({"moved": args.endpoints, "to": args.group, "result": result},
args.json, _print_kv)
def _print_tags(tags) -> None:
items = tags if isinstance(tags, list) else (tags or {}).get("items", [])
print(f"Endpoint tags: {len(items)}")
for t in items:
print(f" {t}")
def cmd_endpoint_tags(client, args):
_emit(client.get_endpoint_tags(), args.json, _print_tags)
def cmd_set_label(client, args):
if not _gated(f"label endpoint {args.endpoint} = '{args.label}'", args.confirm):
return 3
result = client.set_endpoint_label(args.endpoint, args.label)
_emit({"labeled": args.endpoint, "label": args.label, "result": result},
args.json, _print_kv)
return 0
def cmd_reconfigure(client, args):
extra, rc = _load_json_arg(args.extra_json, "extra-json")
if rc:
return rc
if not _gated(f"reconfigure {len(args.targets)} agent(s): {','.join(args.targets)}",
args.confirm):
return 3
result = client.reconfigure_client(args.targets, extra=extra or None)
_emit({"reconfigured": args.targets, "result": result}, args.json, _print_kv)
return 0
def cmd_make_group(client, args):
result = client.create_custom_group(args.name, args.parent)
_emit({"createdGroup": args.name, "result": result}, args.json, _print_kv)
# Substrings that mark a JSON-RPC method as state-destroying. `raw` can reach
# any method (incl. UNVERIFIED ones), so gate these behind --confirm too.
# isolate / blocklist add+remove are NEW destructive verbs from the incidents
# (EDR) module - gate them in `raw` as well as via the dedicated subcommands.
DESTRUCTIVE_RAW_PATTERNS = ("delete", "createuninstall", "createremove",
"createreconfigure", "isolat", "addtoblocklist",
"removefromblocklist", "assignpolicy",
"setpushevent", "createaccount", "updateaccount",
"configurenotif", "createcompany", "suspendcompany",
"activatecompany", "setendpointlabel", "createreport",
"createrestore", "createcustomrule", "changeincident",
"updateincident", "sendtestpush")
def _is_destructive_method(method: str) -> bool:
m = method.lower()
return any(pat in m for pat in DESTRUCTIVE_RAW_PATTERNS)
def cmd_raw(client, args):
if _is_destructive_method(args.method) and not args.confirm:
print(f"[WARNING] '{args.method}' looks destructive; refusing without "
"--confirm.", file=sys.stderr)
return 3
try:
params = json.loads(args.params) if args.params else {}
except json.JSONDecodeError as exc:
print(f"[ERROR] --params is not valid JSON: {exc}", file=sys.stderr)
return 2
if not isinstance(params, dict):
print("[ERROR] --params must be a JSON object.", file=sys.stderr)
return 2
result = client._jsonrpc_request(args.module, args.method, params)
print(json.dumps(result, indent=2, default=_json_default))
return 0
# --- destructive (gated) ------------------------------------------------------
def _gated(action_desc: str, confirm: bool) -> bool:
if not confirm:
print("[WARNING] Refusing destructive action without --confirm.")
print(f"[INFO] Would: {action_desc}")
return False
return True
def cmd_delete_endpoint(client, args):
if not _gated(f"delete endpoint {args.endpoint_id}", args.confirm):
return 3
result = client.delete_endpoint(args.endpoint_id)
_emit({"deletedEndpoint": args.endpoint_id, "result": result},
args.json, _print_kv)
return 0
def cmd_delete_package(client, args):
if not _gated(f"delete package {args.id}", args.confirm):
return 3
result = client.delete_package(args.id)
_emit({"deletedPackage": args.id, "result": result}, args.json, _print_kv)
return 0
def cmd_delete_group(client, args):
if not _gated(f"delete custom group {args.group}", args.confirm):
return 3
result = client.delete_custom_group(args.group)
_emit({"deletedGroup": args.group, "result": result}, args.json, _print_kv)
return 0
# --- EDR / incident response (gated) ------------------------------------------
def cmd_isolate(client, args):
targets = ",".join(args.endpoints)
if not _gated(f"isolate endpoints {targets}", args.confirm):
return 3
result = client.isolate_endpoints(args.endpoints)
_emit({"isolated": args.endpoints, "result": result}, args.json, _print_kv)
return 0
def cmd_unisolate(client, args):
targets = ",".join(args.endpoints)
if not _gated(f"restore endpoints from isolation {targets}", args.confirm):
return 3
result = client.restore_endpoints_from_isolation(args.endpoints)
_emit({"unisolated": args.endpoints, "result": result}, args.json, _print_kv)
return 0
def cmd_blocklist_add(client, args):
desc = (f"add {len(args.hashes)} hash(es) to blocklist for company "
f"{args.company}: {','.join(args.hashes)}")
if not _gated(desc, args.confirm):
return 3
result = client.add_to_blocklist(
company_id=args.company,
hash_list=args.hashes,
hash_type=args.hash_type,
source_info=args.source_info,
)
_emit({"blocklistAdded": args.hashes, "company": args.company,
"result": result}, args.json, _print_kv)
return 0
def cmd_blocklist_remove(client, args):
if not _gated(f"remove blocklist item {args.id}", args.confirm):
return 3
result = client.remove_from_blocklist(args.id)
_emit({"blocklistRemoved": args.id, "result": result}, args.json, _print_kv)
return 0
# --- parser -------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="gz.py",
description="GravityZone Cloud Public API CLI (ACG MSP tenant).",
)
# --json is shared by every subcommand (via parents) so it is accepted
# after the subcommand, e.g. `gz.py companies --json`.
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--json", action="store_true", help="Emit raw JSON output.")
sub = p.add_subparsers(dest="command", required=True)
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.")
sp.add_argument("--per-page", type=int, default=100)
sp = sub.add_parser("endpoint", help="Full detail for one endpoint.",
parents=[common])
sp.add_argument("endpoint_id")
sp = sub.add_parser("sweep", help="Live security posture sweep.", parents=[common])
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="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("account",
help="Account detail (no id = the API key's own account).",
parents=[common])
sp.add_argument("account_id", nargs="?", help="Account id (optional).")
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("package-details", help="Installation package detail.",
parents=[common])
sp.add_argument("package_id")
sub.add_parser("monthly-usage", help="Monthly license usage.", parents=[common])
sp = sub.add_parser("integrations", help="List configured integrations.",
parents=[common])
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=100)
sp = sub.add_parser("custom-rules", help="List EDR custom rules.", parents=[common])
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=100)
sp = sub.add_parser("report-links", help="Get a report's download links.",
parents=[common])
sp.add_argument("--id", required=True, help="reportId.")
sp = sub.add_parser("quarantine", help="List quarantine items for a company.",
parents=[common])
sp.add_argument("--company", required=True)
sp = sub.add_parser("blocklist", help="List blocklisted hash items (EDR).",
parents=[common])
sp.add_argument("--company", help="Scope to one company id (optional).")
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=100)
sp = sub.add_parser("incidents", help="List incidents under a company (EDR).",
parents=[common])
sp.add_argument("--company", required=True,
help="Parent company/group id (parentId; required).")
sp.add_argument("--page", type=int, default=1)
sp.add_argument("--per-page", type=int, default=500,
help="incidents requires 500-10000 (API constraint).")
sp = sub.add_parser("inventory", help="Show cached identity/structure.",
parents=[common])
sp.add_argument("--refresh", action="store_true", help="Force a full re-pull.")
sp = sub.add_parser("create-package", help="Create an installer package.",
parents=[common])
sp.add_argument("--name", required=True)
sp.add_argument("--company")
sp.add_argument("--description")
sp.add_argument("--language")
sp = sub.add_parser("install-links", help="Get installer download URLs.",
parents=[common])
sp.add_argument("--package", required=True)
sp.add_argument("--company")
sp = sub.add_parser("scan", help="Create a scan task.", parents=[common])
sp.add_argument("--targets", nargs="+", required=True)
sp.add_argument("--type", type=int, required=True,
help="1=Quick 2=Full 3=Memory 4=Custom (verify in console).")
sp.add_argument("--name")
sp = sub.add_parser("move", help="Move endpoints into a group.", parents=[common])
sp.add_argument("--endpoints", nargs="+", required=True)
sp.add_argument("--group", required=True)
sp = sub.add_parser("make-group", help="Create a custom group.", parents=[common])
sp.add_argument("--name", required=True)
sp.add_argument("--parent")
sub.add_parser("endpoint-tags", help="List endpoint tags.", parents=[common])
sp = sub.add_parser("set-label", help="Set an endpoint's label (gated).",
parents=[common])
sp.add_argument("--endpoint", required=True, help="endpointId.")
sp.add_argument("--label", required=True)
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("reconfigure",
help="Reconfigure installed agents (gated).", parents=[common])
sp.add_argument("--targets", nargs="+", required=True, help="Endpoint ids.")
sp.add_argument("--extra-json",
help="JSON reconfigure body (modules/roles/scanMode).")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("raw", help="Call any method directly (power use).",
parents=[common])
sp.add_argument("--module", required=True)
sp.add_argument("--method", required=True)
sp.add_argument("--params", default="{}", help="JSON object of params.")
sp.add_argument("--confirm", action="store_true",
help="Required for destructive methods (delete/uninstall/"
"remove/reconfigure).")
# destructive (gated)
sp = sub.add_parser("delete-endpoint", help="Delete an endpoint (gated).",
parents=[common])
sp.add_argument("endpoint_id")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("delete-package", help="Delete a package by id (gated).",
parents=[common])
sp.add_argument("--id", required=True, help="packageId (from `packages`).")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("delete-group", help="Delete a custom group (gated).",
parents=[common])
sp.add_argument("--group", required=True)
sp.add_argument("--confirm", action="store_true")
# EDR / incident response (gated)
sp = sub.add_parser("isolate",
help="Isolate endpoints from the network (gated).",
parents=[common])
sp.add_argument("--endpoints", nargs="+", required=True,
help="One or more endpoint ids (max 1000).")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("unisolate",
help="Restore endpoints from isolation (gated).",
parents=[common])
sp.add_argument("--endpoints", nargs="+", required=True,
help="One or more endpoint ids (max 1000).")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("blocklist-add",
help="Add hashes to the blocklist (gated).",
parents=[common])
sp.add_argument("--company", required=True)
sp.add_argument("--hashes", nargs="+", required=True,
help="One or more hash strings to block.")
sp.add_argument("--hash-type", type=int, default=1,
help="Hash type int (1 common; see console / API docs).")
sp.add_argument("--source-info", default="",
help="Free-text description of the source.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("blocklist-remove",
help="Remove one blocklist entry (gated).",
parents=[common])
sp.add_argument("--id", required=True,
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("account-create", help="Create a console account (gated).",
parents=[common])
sp.add_argument("--email", required=True)
sp.add_argument("--password")
sp.add_argument("--username")
sp.add_argument("--role", type=int, help="Numeric role id (see docs/console).")
sp.add_argument("--full-name")
sp.add_argument("--language", help="e.g. en_US")
sp.add_argument("--timezone", help="e.g. America/Phoenix")
sp.add_argument("--rights-json", help="JSON object of rights flags.")
sp.add_argument("--extra-json", help="JSON object of any extra documented fields.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("account-update", help="Update a console account (gated).",
parents=[common])
sp.add_argument("--id", required=True, help="accountId.")
sp.add_argument("--set-json", required=True, help="JSON object of fields to change.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("account-delete", help="Delete a console account (gated).",
parents=[common])
sp.add_argument("--id", required=True, help="accountId.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("notif-configure",
help="Set notification settings (gated).", parents=[common])
sp.add_argument("--settings-json", required=True,
help="JSON object of the notification settings to apply.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("push-test", help="Send a test push event (gated).",
parents=[common])
sp.add_argument("--event-type", required=True)
sp.add_argument("--extra-json")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("report-create", help="Create a report (gated).",
parents=[common])
sp.add_argument("--name", required=True)
sp.add_argument("--extra-json", help="JSON: type, targetIds, recurrence, format...")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("report-delete", help="Delete a report (gated).",
parents=[common])
sp.add_argument("--id", required=True, help="reportId.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("quarantine-remove",
help="Delete quarantined items (gated).", parents=[common])
sp.add_argument("--items", nargs="+", required=True,
help="quarantineItemsIds.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("quarantine-restore",
help="Restore quarantined items (gated).", parents=[common])
sp.add_argument("--items", nargs="+", required=True,
help="quarantineItemsIds.")
sp.add_argument("--extra-json", help="JSON: addExclusionInPolicy, etc.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("custom-rule-create",
help="Create an EDR custom rule (gated).", parents=[common])
sp.add_argument("--name", required=True)
sp.add_argument("--extra-json", help="JSON: settings, companyId, tags...")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("custom-rule-delete",
help="Delete an EDR custom rule (gated).", parents=[common])
sp.add_argument("--id", required=True, help="ruleId.")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("incident-status",
help="Change an incident's status (gated).", parents=[common])
sp.add_argument("--type", required=True, help="Incident type/category.")
sp.add_argument("--set-json", required=True, help="JSON: id, status, ...")
sp.add_argument("--confirm", action="store_true")
sp = sub.add_parser("incident-note",
help="Update an incident note (gated).", parents=[common])
sp.add_argument("--type", required=True, help="Incident type/category.")
sp.add_argument("--set-json", required=True, help="JSON: id, note, ...")
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
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,
"policies": cmd_policies,
"policy": cmd_policy,
"packages": cmd_packages,
"reports": cmd_reports,
"accounts": cmd_accounts,
"notif-settings": cmd_notif_settings,
"account": cmd_account,
"account-create": cmd_account_create,
"account-update": cmd_account_update,
"account-delete": cmd_account_delete,
"notif-configure": cmd_notif_configure,
"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,
"push-test": cmd_push_test,
"package-details": cmd_package_details,
"monthly-usage": cmd_monthly_usage,
"integrations": cmd_integrations,
"custom-rules": cmd_custom_rules,
"custom-rule-create": cmd_custom_rule_create,
"custom-rule-delete": cmd_custom_rule_delete,
"incident-status": cmd_incident_status,
"incident-note": cmd_incident_note,
"report-create": cmd_report_create,
"report-links": cmd_report_links,
"report-delete": cmd_report_delete,
"quarantine-remove": cmd_quarantine_remove,
"quarantine-restore": cmd_quarantine_restore,
"quarantine": cmd_quarantine,
"blocklist": cmd_blocklist,
"incidents": cmd_incidents,
"inventory": cmd_inventory,
"create-package": cmd_create_package,
"install-links": cmd_install_links,
"scan": cmd_scan,
"move": cmd_move,
"make-group": cmd_make_group,
"endpoint-tags": cmd_endpoint_tags,
"set-label": cmd_set_label,
"reconfigure": cmd_reconfigure,
"raw": cmd_raw,
"delete-endpoint": cmd_delete_endpoint,
"delete-package": cmd_delete_package,
"delete-group": cmd_delete_group,
"isolate": cmd_isolate,
"unisolate": cmd_unisolate,
"blocklist-add": cmd_blocklist_add,
"blocklist-remove": cmd_blocklist_remove,
}
def main(argv=None) -> int:
args = build_parser().parse_args(argv)
handler = HANDLERS[args.command]
try:
client = GravityZoneClient()
rc = handler(client, args)
return rc if isinstance(rc, int) else 0
except GravityZoneError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
cmd = getattr(args, "command", "?")
if _should_log_error(cmd, str(exc)):
_log_skill_error("bitdefender", f"{exc}", context=f"cmd={cmd}")
return 1
except KeyboardInterrupt:
return 130
if __name__ == "__main__":
raise SystemExit(main())