Files
claudetools/.claude/skills/bitdefender/SKILL.md
Howard Enos 1852f755ad fix(bitdefender): doc LOWs + sweep pagination + selftest alignment
Batched the audit doc/LOW findings plus the two pagination LOWs:
- Pagination (gz_client): security_sweep and refresh_inventory stopped on a
  'total' field some responses omit, truncating after page 1. Now page until a
  short page (< per_page) - the reliable last-page signal.
- isolate/restore docstrings (gz_client): removed the stale 'v1.2 takes an ARRAY
  endpointIds' lines that contradicted the verified single-endpointId code.
- Cache 'no PII' wording corrected (gz_client header + SKILL.md): cache holds infra
  identifiers (hostnames/FQDNs); no secrets. Dead _require_company_for_sweep removed.
- Doc drift fixed: delete-package is '--id <packageId>' not '--package <name>'
  (SKILL.md + api-reference.md, verified live); module docstring + sweep --company
  help corrected (sweep with no --company fans out to ALL companies).
- selftest aligned to the improved behavior: malformed ids now exit rc2 client-side
  (H3) instead of rc1; gate-refusal 'Would' messages now assert on stderr (they
  moved off stdout so --json isn't polluted). 75/75 pass; live sweep verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:28:38 -07:00

9.7 KiB

name, description
name description
bitdefender 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 (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

Standalone CLI client for the GravityZone Cloud Public API (JSON-RPC). Talks to the live ACG partner tenant. Read-only by default; destructive operations are gated behind --confirm.

Running the CLI

This machine's Python launcher is py (per identity.json). The scripts also work with python/python3.

# from the scripts dir, or pass full paths
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" status
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" companies
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" sweep --company <id> --json

Transport auto-selects: uses httpx if installed, otherwise stdlib urllib (no third-party dependency required).

Credentials

The API key is NEVER hardcoded. At runtime the client loads it from the SOPS vault:

bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
     get-field msp-tools/gravityzone.sops.yaml credentials.api_key

CLAUDETOOLS_ROOT resolves from the env var, else claudetools_root in C:/claudetools/.claude/identity.json, else C:/claudetools. For testing you can override with GRAVITYZONE_API_KEY. Auth is HTTP Basic (key as username, empty password).

Cache model (important)

The CLI keeps a local cache at .claude/skills/bitdefender/.cache/inventory.json (gitignored — no secrets; it does hold infra identifiers: endpoint hostnames/FQDNs and id<->name maps).

  • Cached (identity / structure tier): company id<->name map, endpoint id<->name/company/fqdn map, policy id<->name map, package list, and custom groups created via this tool. TTL = 86400s (24h).
  • NEVER cached (volatile): infected status, last-seen, online/offline, signature/product freshness. Those are ALWAYS pulled live — sweep and endpoint always hit the API.
  • Refresh: inventory --refresh forces a full re-pull. get_inventory() auto-refreshes when the cache is stale.
  • Write-through: a successful create-package or make-group updates the cache with the new id immediately, so you don't need a full refresh to reference it.

Policy control (corrected 2026-06-21)

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

Destructive subcommands refuse to run without --confirm; without it they print what they would do and exit non-zero:

  • delete-endpoint <id> --confirm
  • delete-package --id <packageId> --confirm (param is packageId, NOT packageName)
  • delete-group --group <id> --confirm
  • isolate --endpoints <id> ... --confirm (cuts the endpoint off the network; reversible via unisolate)
  • 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)
  • move --endpoints <id> ... --group <id> --confirm (relocating endpoints changes their INHERITED policy — gated 2026-06-25)
  • scan --targets <id> ... --type <n> --confirm (starts a scan on live endpoints — gated 2026-06-25)
  • create-package --name <n> [--company <id>] --confirm (creates an installer package — gated 2026-06-25)
  • make-group --name <n> [--parent <id>] --confirm (creates a custom group — gated 2026-06-25)

Never run destructive calls casually against this tenant. UNVERIFIED methods (assignPolicy, uninstall/reconfigure tasks, quarantine remove/restore, set label) are intentionally NOT exposed as dedicated subcommands — reach them only through raw after confirming the correct params against references/api-reference.md and the official Bitdefender docs.

raw itself refuses destructive method names (delete/uninstall/remove/ reconfigure, plus the EDR verbs isolat*/addToBlocklist/removeFromBlocklist) unless --confirm is passed. Note that raw prints the upstream response verbatim — it can carry data from the called method, so do not paste raw output into tickets/logs without review.

Common commands

GZ="bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" C:/claudetools/.claude/skills/bitdefender/scripts/gz.py"

# Status / inventory
$GZ status
$GZ companies
$GZ inventory --refresh
$GZ endpoints --company <companyId>
$GZ endpoint <endpointId>

# Live security posture
$GZ sweep --company <companyId>          # readable table
$GZ sweep --company <companyId> --json   # machine output

# Policies (full read + assign; authoring is console-only)
$GZ policies
$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>

# Deployment
$GZ packages
$GZ create-package --name "Win Default" --company <companyId>
$GZ install-links --package "Win Default" --company <companyId>

# Org structure
$GZ make-group --name "New Site" --parent <parentId>
$GZ move --endpoints <id1> <id2> --group <groupId>

# Scans
$GZ scan --targets <id1> <id2> --type 2 --name "Full scan"

# EDR / incident response
$GZ blocklist                                  # list blocklisted hashes (whole tenant)
$GZ blocklist --company <companyId>            # scope to one company
$GZ incidents --company <companyId>            # list incidents (parentId required; method UNVERIFIED on this tenant - may return "Method not found")
$GZ isolate --endpoints <id1> <id2> --confirm  # cut endpoint(s) off the network (reversible via unisolate)
$GZ unisolate --endpoints <id1> <id2> --confirm   # restore endpoint(s) from isolation
$GZ blocklist-add --company <companyId> --hashes <h1> <h2> --hash-type 1 --source-info "..." --confirm
$GZ blocklist-remove --id <hashItemId> --confirm  # id comes from `blocklist` output

# Power use — call any method directly
$GZ raw --module network --method getEndpointsList --params '{"page":1,"perPage":50}'

# Destructive (gated)
$GZ delete-endpoint <id> --confirm

Enabled API scopes (live key, 2026-06-21)

companies, licensing, packages, network, integrations, policies, maintenancewindows, reports, accounts, incidents, push, quarantine, phasr, patchmanagement.

Wrapped & verified — ALL live modules complete (see references/BUILDOUT.md for the per-method checklist): general, licensing (+monthly-usage), companies (full CRUD-ish), network (endpoints/groups/scan/move/delete/assignPolicy/ reconfigure/setLabel/tags), policies (full read + assign), packages (+details), reports (create/links/delete), accounts (full 7), quarantine (list/remove/ restore), incidents/EDR (blocklist + isolate + custom-rules + incident status/note), push (get/stats/set/test), integrations (read). 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

Full verified vs unverified method spec, JSON-RPC envelope, auth, and the policy/deployment caveats: references/api-reference.md.