- deploy-cmd: require explicit --regkey or --group; never auto-pick an arbitrary cross-client registration key (would enroll into wrong org). - raw: block POST to any */scan endpoint with no non-empty `where` (same tenant-wide footgun the scan command guards against). - main(): catch-all for unexpected exceptions -> [ERROR] + errorlog, plus clean KeyboardInterrupt (130). - isolate: forgiving extension-name match (exact, then substring), excludes the paired "Restore" ext; errors on ambiguous match. - detections: --site -> --target-group; Alert.targetGroupId is a scan-target id, not a Location id (distinct from `agents --site`). - status: relabel "Target groups (sites)" -> "Scan target groups". - SKILL.md + docstrings updated to match. Verified: py_compile clean, selftest green (216 agents), guards fire on no-key/empty-where/no-agent, deploy-cmd --group picks the group's key.
8.9 KiB
name, description
| name | description |
|---|---|
| datto-edr | Control the ACG Datto EDR / Datto AV tenant (azcomp4587 — Datto EDR is rebranded Infocyte HUNT; per-tenant LoopBack REST API). Read the whole MSP fleet from ONE token: per-client organizations, sites, agents (online/AV/isolation/version), detections (MITRE-tagged alerts), and a per-client security-posture sweep. Gated actions: trigger scans, invoke response extensions (host isolation), and emit the agent install one-liner for RMM-pushed deployment. Read-only by default; mutating ops require --confirm. Triggers: datto edr, datto av, infocyte, EDR detections, isolate endpoint, run a scan, deploy edr agent, edr security sweep, endpoint detection response, azcomp4587. |
Datto EDR (Infocyte HUNT) Skill
Standalone CLI for the Datto EDR REST API against the live ACG tenant
(azcomp4587.infocyte.com). Datto EDR is the rebranded Infocyte HUNT platform,
so the API is a per-tenant LoopBack service: https://<instance>.infocyte.com/api/<Model>.
Read-only by default; mutating operations are gated behind --confirm.
One API token sees the entire MSP fleet (13 client orgs, 40 scan groups, 215 agents, 1500+ alerts as of build) — segmented per-client by Organization.
Running the CLI
PY="bash $CLAUDETOOLS_ROOT/.claude/scripts/py.sh"
EDR="$PY C:/claudetools/.claude/skills/datto-edr/scripts/edr.py"
$EDR status # tenant rollup
$EDR orgs # clients (Organizations)
$EDR sites --org <orgId> # client sites (Locations)
$EDR agents --org <orgId> # endpoints for a client
$EDR detections --org <orgId> --days 7
$EDR sweep # per-client posture, sorted by recent detections
Transport auto-selects httpx if installed, else stdlib urllib (no hard dependency).
Credentials
The token is NEVER hardcoded. Loaded at runtime from the SOPS vault:
bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
get-field msp-tools/datto-edr.sops.yaml credentials.api_token
Auth model (critical): the token is passed in the raw Authorization header
— NOT Bearer <token>, no prefix. (Verified live 2026-06-25.) For testing you can
override with DATTO_EDR_TOKEN and DATTO_EDR_BASE_URL. The token expires ~1 year
after creation (~2027-06-25) — refresh it in the console (username menu → Admin →
Users & Tokens → API Tokens) and update the vault entry before it lapses.
Data model (how the tenant is structured)
- Organization = a client/company (
orgs). Carries agentCount/alertCount. - Location = a client site (
sites). CarriesorganizationId. Agents belong to a Location viaAgent.locationId. This is the inventory hierarchy: Organization → Location → Agent. - Target = a scan target group (
scan-targets), the scannable unit for thescancommand (POST targets/{id}/scan). Distinct from Location (though they may share an id when a site maps 1:1 to a scan group). - deviceGroup = a global category ("Servers", "Workstations") spanning all orgs — NOT a per-client grouping. Not used for client scoping.
- Alert = a detection. Carries
severity(0 info … 4 critical),mitreTactic/mitreId,hostname,organizationName,targetGroupName,eventTime.
Command surface
Reads (run freely — always live, never cached)
$EDR status # counts: orgs/sites/agents/active/isolated/alerts
$EDR orgs # clients + agent/alert/site counts
$EDR sites [--org <orgId>] # Locations (client sites)
$EDR scan-targets [--org <orgId>] # scan target groups (for `scan`)
$EDR agents [--org <orgId>] [--site <locationId>] [--limit N]
$EDR agent <agentId> # full agent detail
$EDR detections [--org <orgId>] [--site <locationId>] [--severity 0-4] [--days N] [--limit N]
$EDR detection <alertId> # full alert detail
$EDR sweep [--org <orgId>] # per-client posture rollup (headline view)
$EDR extensions # list response/collection extensions
$EDR tasks [--type "Scan - EDR"] [--limit N] # recent userTasks (scan/analysis jobs)
$EDR task <taskId> # one userTask (scan job) detail/status
$EDR deploy-cmd --regkey <key> | --group <targetId> # emit the agent install one-liner (see Deployment)
--json is a global flag (place before the subcommand): $EDR --json sweep.
Mutating (gated — refuse without --confirm)
Scan/isolate go through POST Agents/scan with a where filter selecting the
agents. The CLI REQUIRES explicit --agent id(s): an absent where scans the
ENTIRE tenant (the "156-host" footgun), so the client refuses an empty list.
# EDR-scan specific agent(s). Repeat --agent for several. POST Agents/scan
# {"where":{"and":[{"id":[ids]}]}, "options":{}, "taskName":"Scan - EDR"}
$EDR scan --agent <agentId> [--agent <agentId2> ...] --confirm
# Response extension (Host Isolation, etc.) on agent(s) — rides Agents/scan w/ options.extensions
$EDR isolate --agent <agentId> [--extension-name "Host Isolation [Win/Linux]"] --confirm
$EDR isolate --agent <agentId> --extension-id <extId> --confirm
# Cancel a running scan/task; create a group; mint a group registration key
$EDR cancel <taskId> --confirm
$EDR create-group --name "[TEST] Foo" --org <organizationId> --confirm
$EDR mint-key --group <targetId> --key <10char> --confirm
# Power tool — any endpoint. Non-GET requires --confirm.
$EDR raw GET Agents --filter '{"limit":1}'
$EDR raw POST Agents/scan --data '{"where":{"and":[{"id":["<id>"]}]},"options":{},"taskName":"Scan - EDR"}' --confirm
The single-agent scan body that works: {"where":{"and":[{"id":["<agentId>"]}]}, "options":{}, "taskName":"Scan - EDR"}. A bare {"id":{"inq":[...]}} returns HTTP
412 "column reference id is ambiguous" — the and-wrap is required. AV scans
(Scan - AV Quick/Full) are policy-driven, NOT callable via this endpoint.
Deployment
Agent install is not a REST call — it's the agent binary run on the endpoint.
deploy-cmd emits the official one-liner. It needs an explicit key: either
--regkey <key> (used verbatim) or --group <targetId> (looks up THAT group's key
from /agentKeys). It will not auto-pick an arbitrary key — keys are
per-target-group and cross-client, so a wrong key lands the agent in the wrong org.
To land an agent in a SPECIFIC group: create-group → mint-key (the key id is
caller-supplied) → deploy-cmd --group <targetId> → install with that -RegKey.
Install-EDR -URL "https://azcomp4587.infocyte.com" -RegKey <key>
Pass the full -URL, not -InstanceName azcomp4587 — the install script's loose
.com regex matches "zcom" inside the cname, leaving --url empty and the agent
erroring a value is required for '--url'. Push via GuruRMM (/rmm) or any
remote-exec channel. RegKey auto-approves + adds to the key's target group; without
it the agent registers but awaits manual approval.
Safety gating
- Reads never mutate and run without confirmation.
scan,isolate,cancel,create-group,mint-key, and any non-GETrawprint a[DRY-RUN]line and exit non-zero unless--confirmis passed.scan/isolateadditionally REFUSE an empty--agentlist (would scan the whole tenant). Never scan/isolate casually — these hit live client production endpoints. Confirm the agent id with the read commands first.
Verified vs. unverified
Live-verified 2026-06-25 (exercised end-to-end on the tenant): all reads
(status, orgs, sites, scan-targets, agents incl. per-org Location mapping,
agent, detections, detection, sweep, extensions, tasks, task); the full
deploy lifecycle (create-group → mint-key → install via /rmm → agent registered
into the group); scan (targeted single-agent — confirmed "Scanning 1 host"); cancel;
deploy-cmd; raw; and the --confirm + empty---agent guards.
RUN-unverified (shape correct, not fired at prod): isolate — it rides the same
Agents/scan call with the Host Isolation extension in options.extensions; confirm
the extension id via $EDR extensions and test on an ACG-internal box before relying
on it (it cuts the endpoint off the network). AV scans are policy-driven (not callable).
Relationship to GuruRMM (Feature 6)
This skill is the prototype for GuruRMM security-connector #2 (Datto EDR), exactly
as the bitdefender skill prototyped connector #1. Its read logic (per-client agents +
detections + AV/isolation state) ports into the GuruRMM SecurityVendor trait; the
live "EDR add-on" dashboard is best fed by EDR webhooks (Admin → Webhooks) into a
GuruRMM receiver, complemented by REST polling here. See
projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md Feature 6.
Reference
Full endpoint map, auth detail, LoopBack filter syntax, and the scan/response payload
shapes: references/api-reference.md.