---
name: bitdefender
description: "Manage the ACG Bitdefender GravityZone Cloud MSP tenant (JSON-RPC API): endpoint inventory/audit, security sweeps, packages, policies, quarantine, EDR isolate/blocklist; destructive ops gated. Triggers: bitdefender, gravityzone, infected machines, av coverage, assign policy."
---

# 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`.

```bash
# 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

```bash
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`.
