Convergence-pass LOW/NIT cleanup:
- cmd_companies uses list_all_companies() so a >100-company tenant isn't truncated
in the listing (was page-1 only); matches sweep/inventory.
- removed unused 'field' import from dataclasses.
Deliberately NOT changed: id validation on delete-package/report-delete/blocklist-
remove/quarantine-remove/restore - those ids are not pinned 24-hex format, so
validating could reject valid input; they are --confirm-gated and bad ids match
the expected-error markers (no mislog). 81/81 selftest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From a third review pass (converging - all MEDIUM/LOW):
- urllib fallback: a post-send reset (RemoteDisconnected/ConnectionReset, which
urllib wraps in URLError) was misclassified as always-safe 'connect' and could
retry a non-idempotent write after a server commit. Now only ConnectionRefused/
DNS (socket.gaierror) -> 'connect'; everything else -> 'timeout' (write-gated).
- _retry_delay clamps a negative numeric Retry-After to 0 (was -> time.sleep(-1) ValueError).
- cmd_sweep + cmd_install_links now validate --company; cmd_company_create validates
--parent (finished _require_oid consistency - these mislogged as errorlog noise).
- cmd_push_test parses --extra-json before gating (validate->gate order, matches siblings).
- selftest: +sweep/install-links bad-company assertions. 81/81. Units: clamp + reset classification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remaining LOW/NIT items from the second review pass:
- list_all_companies() paginates the company list; sweep-all + refresh_inventory
no longer truncate a >100-company tenant.
- Pre-send connection failures (httpx ConnectError/ConnectTimeout; urllib URLError
not wrapping a timeout) are now retried as 'connect' - always safe (no side
effect) even for non-idempotent writes; ambiguous read-timeouts stay idempotent-gated.
- Explicit Retry-After honored up to RETRY_AFTER_MAX_SECONDS (120s) instead of the
30s exponential cap, so a server-mandated cooldown isn't cut short.
- GravityZoneClient is now a context manager (__enter__/__exit__ -> close()).
- incident-status/note reject an empty --set-json (rc2), matching account-update/notif.
- selftest: +connect/Retry-After/ctx-mgr unit coverage, incident empty-json assertion. 79/79.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Audit fix M3: _write_cache did a non-atomic CACHE_FILE.write_text and the
write-through helpers did unlocked read-modify-write, so a crash mid-write could
truncate inventory.json and two concurrent gz.py runs could lose an update.
- _write_cache now writes a temp file (fsync) then os.replace() - atomic on the
same filesystem; a reader/crash can never see a partial file, and a failed
write leaves the prior cache intact and no .tmp residue.
- Added a best-effort cross-platform advisory lock (_cache_lock) around the
read-modify-write in _cache_add_group/_cache_add_package; steals a stale lock,
proceeds unlocked on timeout (a lost update is tolerable, a hang is not).
- Dropped the dead cache.setdefault('companies', ...) line in _cache_add_group.
- Verified: compile clean; unit tests for round-trip, lock acquire/release/steal,
write-through, temp cleanup on failure, and prior-cache survival.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit fix H2 (+ M2): the live GravityZone tenant is rate-limited and sweeps fan
out one getManagedEndpointDetails per endpoint across every company, which hit a
real HTTP 429 (errorlog 2026-06-21). _post had zero retry and opened a fresh
httpx.Client (new TLS handshake) per request.
- _post now retries 429/500/502/503/504/timeout up to RETRY_MAX_ATTEMPTS with
bounded exponential backoff + jitter, honoring Retry-After (numeric or HTTP-date).
Retry notices go to stderr (don't pollute --json). Terminal errors still raise.
- M2: a single httpx.Client is created lazily and reused (connection pooling),
closed via client.close() in main()'s finally. Makes the docstring's pooling
claim true and cuts handshake overhead + 429 pressure during sweeps.
- Verified: compile clean; offline unit tests (persistent 429 -> 4 attempts then
raise, flaky 503 -> recovers, Retry-After honored); live status read OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found during the RMM-TEST-MACHINE full-function test (live tenant):
- assignPolicy: assigning a policyId REQUIRES inheritFromAbove:false in the same
call, else the API rejects with a misleading "inheritFromAbove should not be
used with policyId" error. Fixed assign_policy to always send it; dropped the
wrong --inherit-from-above flag.
- isolate/unisolate: the API takes a SINGLE endpointId per call, NOT an
endpointIds array (errored "not expected"). Client now loops per endpoint.
unisolate fails while the isolate task is in progress — wait + retry.
- api-reference updated with the live-verified shapes.
Full function test PASSED on RMM-TEST-MACHINE: install(offline kit/SYSTEM) ->
enroll -> move(ZZ-RMM-TEST) -> assign-policy(GPS Base, applied) -> set-label ->
scan -> reconfigure -> isolate -> unisolate -> quarantine/blocklist read ->
managed uninstall(deleteEndpoint). selftest 75/75.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Completed Companies module for bitdefender GravityZone Public API
- Implemented: getCompanyDetails, getCompanyDetailsByUser, createCompany, suspendCompany, activateCompany, deleteCompany
- Discovered updateCompany and getCompaniesList not available; companies retrieved via network inventory
- Company types: 0=Partner, 1=Customer; createCompany accepts nested licenseSubscription via JSON passthrough
- All write operations require --confirm; raw also restricts createCompany/suspendCompany/activateCompany
- selftest 49 -> 55 passing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Completed Accounts module for bitdefender skill (GravityZone Public API)
- Added 5 methods: getAccountDetails, createAccount, updateAccount, deleteAccount, configureNotificationsSettings
- Write methods require --confirm; raw also gates createAccount/updateAccount/configureNotificationsSettings
- Param shapes validated against official docs and safe validation probes
- configureNotificationsSettings is a setter with no required param; warning documented against empty payload on live tenant
- selftest 42 -> 49 passing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- assign_policy: add inheritFromAbove option; mark VERIFIED via official docs
(policyId/targetIds/forcePolicyInheritance/inheritFromAbove; not applied to
ENFORCED-policy targets).
- setPushEventSettings: documented serviceType (splunk/cef/jsonRPC), TLS 1.2+
receiver requirement, subscribeToEventTypes event-flag map; webhook receiver
pattern noted.
- api-reference.md: cite GravityZone Support Center as authoritative source.
- add references/BUILDOUT.md — master checklist to implement every API method
module-by-module; seeded with current done/todo/dead state.
- memory: reference_gravityzone_support (+ index).
selftest 42/42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Several bugs found and fixed during live testing against the ACG GravityZone
tenant:
- security_sweep_all_clients: iterate each company (the companies container is
not a valid endpoint parent; passing it 400'd the whole sweep)
- list_quarantine: use service-scoped path quarantine/computers with companyId
(bare quarantine module 404'd; param is companyId not parentId)
- rename GZEndpointSummary.detection_active -> threat_detected with corrected
semantics (True = active threat, tracks with infected; not an engine-on flag)
- status: readable sectioned table renderer for the nested apiKey/license dict
- portable CLAUDETOOLS_ROOT resolution (derive from file path, not a Windows
literal) so it works on the Mac/Linux fleet
Adds scripts/selftest.py: a 29-check read-only harness (all passing) covering
every read command, --json, error exit codes, and destructive-action gating.
EDR/incident commands (blocklist, isolate/unisolate, blocklist-add/remove) and
raw destructive-method gating are included from this session's work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a /bitdefender skill that drives the ACG GravityZone partner tenant
via the JSON-RPC Public API. Read + management ops (companies, endpoints,
live security sweep, policies [read-only/shallow], packages, quarantine,
scans, groups, move/delete). Identity-tier JSON cache (24h TTL,
--refresh); volatile status is always pulled live, never cached.
Security hardening: API key loaded from SOPS vault at runtime (never on
disk/logs/argv/cache); destructive deletes gated behind --confirm; `raw`
also gates destructive methods; upstream error bodies truncated. UNVERIFIED
API methods reachable only via `raw`. Reuses the auth/JSON-RPC pattern from
api/services/gravityzone_service.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>