- Scan now uses POST Agents/scan with AND-wrapped where {and:[{id:[...]}]}
(the Infocyte targets/{id}/scan routes are dead/404; bare {id:{inq}} returns
HTTP 412 ambiguous-column). Verified live: single-agent scan -> 'Scanning 1 host'.
- scan/isolate REQUIRE explicit --agent ids; empty list refused (tenant-wide footgun).
- isolate rides Agents/scan with the Host Isolation extension in options.extensions;
resolves --extension-name -> id via /Extensions.
- New subcommands: tasks, task, cancel, create-group, mint-key.
- deploy-cmd emits full -URL (not -InstanceName; cname 'azcomp4587' trips the
install script's .com regex and leaves --url empty).
- Docs (SKILL.md + api-reference.md) rewritten to the verified endpoints + footguns.
Lifecycle verified end-to-end on RMM-TEST-MACHINE (create-group/mint-key/install/
register/scan/cancel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
Datto EDR (Infocyte HUNT) API reference
Datto EDR == rebranded Infocyte HUNT. The API is a per-tenant LoopBack REST
service. Everything below was verified live against azcomp4587.infocyte.com on
2026-06-25 unless marked otherwise.
Base URL & auth
- Base:
https://<instance>.infocyte.com/api(this tenant:azcomp4587). - Self-documenting LoopBack explorer:
https://<instance>.infocyte.com/explorer. - Auth: raw 64-char token in the
Authorizationheader. NOBearerprefix, no Basic, no OAuth.Authorization: <token>. (Source: KaseyaDEDR PowershellToolsrequestHelpers.ps1; confirmed live —Bearerprefix would fail.) - Token created in console: username menu → Admin → Users & Tokens → API Tokens → Create new token. Shown once. Expires 1 year after creation.
- Vault:
msp-tools/datto-edr.sops.yamlfieldcredentials.api_token.
LoopBack conventions
- Models are PascalCase collections:
Organizations,Locations,Targets,Agents,Alerts,Boxes,Reports,deviceGroups(lowercase),agentKeys. - List with a filter:
GET /<Model>?filter=<urlencoded JSON>where the JSON supports{"where":{...},"limit":N,"order":"field DIR","fields":{"x":true}}. whereoperators:{"field":{"gt":"..."}},{"field":{"inq":[...]}}, equality{"field":val}.- Count:
GET /<Model>/count?where=<urlencoded JSON>→{"count":N}. - Detail:
GET /<Model>/{id}. - A model whose route doesn't exist returns
{"error":{"statusCode":404,"message": "There is no method to handle GET /..."}}.
Data hierarchy
Organization (client) GET /Organizations [id,name,agentCount,alertCount,locationCount,tenantId]
└─ Location (site) GET /Locations [id,name,organizationId,agentCount,activeAgentCount,alertCount,lastScannedOn]
└─ Agent (endpoint) GET /Agents (Agent.locationId -> Location.id)
Target (SCAN group) GET /Targets [id,name,organizationId,agentCount,activeAgentCount,lastScannedOn] (scannable unit)
deviceGroup (global category) GET /deviceGroups [id,name,deviceType] ("Servers"/"Workstations" — spans all orgs)
Alert (detection) GET /Alerts (carries organizationId/Name, targetGroupId/Name, severity, mitre*)
To list agents for a client: resolve org → its Locations (where organizationId)
→ GET /Agents?filter={"where":{"locationId":{"inq":[<locIds>]}}}. (Agents do NOT
carry organizationId directly; deviceGroupId is a global category, not the client.)
Agent object (key fields)
id, hostname, name, os, osWindows/osLinux/osOsx/osOther, version, ip/ipPub, active (online), heartbeat, isolated (containment state), dattoAvEnabled, markedForUninstall, markedForUpdate, locationId, deviceGroupId, deviceId, authorized, eppData, rwdInfo.
Alert object (key fields)
id, name, description, severity (0 info,1 low,2 medium,3 high,4 critical), mitreTactic, mitreId, hostname, organizationId, organizationName, targetGroupId, targetGroupName, deviceId, agentId, hostId, eventTime, createdOn, sourceType, sourceName, responseData, signed, managed, archived.
Reads (verified)
| Op | Method/path |
|---|---|
| Tenant counts | GET /Organizations/count, /Targets/count, /Agents/count, /Alerts/count |
| Organizations | GET /Organizations |
| Locations (sites) | GET /Locations (filter where organizationId) |
| Scan target groups | GET /Targets |
| Agents | GET /Agents (filter where locationId / inq) |
| Agent detail | GET /Agents/{id} |
| Alerts | GET /Alerts (filter where org/severity/createdOn) |
| Alert detail | GET /Alerts/{id} |
| Extensions | GET /Extensions |
| Agent reg keys | GET /agentKeys (each is {id:<key>, targetId}) |
| Tasks (scan jobs) | GET /userTasks (filter where type, e.g. "Scan - EDR") |
| Task detail | GET /userTasks/{id} |
Mutating (VERIFIED LIVE 2026-06-25; gate behind --confirm)
The Infocyte-module scan routes (targets/{id}/scan, targets/scan, scans) are
DEAD (404). The live mechanism (read from the console JS bundle + run live) is a
single endpoint selecting agents by a LoopBack where:
| Op | Method/path | Body |
|---|---|---|
| Scan agent(s) | POST /Agents/scan |
{"where":{"and":[{"id":["<agentId>",...]}]}, "options":{}, "taskName":"Scan - EDR"} |
| Response ext (isolate/kill) | POST /Agents/scan |
{"where":{"and":[{"id":[...]}]}, "options":{"extensions":[{"id":"<extId>"}]}, "taskName":"Response"} |
| Cancel a scan/task | POST /userTasks/{id}/cancel |
— (204) |
| Create group | POST /Targets |
{"name":"...","organizationId":"..."} |
| Mint reg key | POST /agentKeys |
{"id":"<10char,caller-supplied>","targetId":"<group>"} |
CRITICAL footguns:
- The
whereis REQUIRED. An absent/emptywherescans the ENTIRE tenant ("Scanning 156 hosts"). The CLI refuses an empty agent list. - Use the AND-wrapped form
{"where":{"and":[{"id":[...]}]}}. A bare{"where":{"id":{"inq":[...]}}}returns HTTP 412 "column reference id is ambiguous" (the scan query joins tables; theand-wrap disambiguates). - Targeting is by Agent.id (not
deviceId, which is null right after enroll). scanTypeis a client-side UI enum, never sent. AV scans (Scan - AV Quick/Full) are policy-driven, not callable via this endpoint.- Sibling endpoints exist for broader scope:
POST /organizations/scan,/locations/scan,/locations/{id}/scan(same{where, options}shape).
options (EDR forensic toggles, all optional; empty {} is valid):
process, module, driver, memory, account, artifact, autostart, application, installed, hook, network, events, plus extensions:[{id,args,order}].
isolate is shape-correct but RUN-unverified (cuts the endpoint off-network) — confirm
the extension id via GET /Extensions (e.g. Host Isolation [Win/Linux]) and test on
an ACG-internal box first.
Deployment (not a REST call)
The agent installs by running the binary on the endpoint:
agent.exe --key <RegKey> --url https://<instance>.infocyte.com, or via the official
PowerShell wrapper one-liner. Pass the FULL -URL, not -InstanceName <cname> —
the install script's loose .com regex matches "zcom" inside azcomp4587, leaving
--url empty (a value is required for '--url'):
Install-EDR -URL "https://azcomp4587.infocyte.com" -RegKey <key>
To land an agent in a SPECIFIC group: POST /Targets (create group) → POST /agentKeys
(mint key, caller-supplied id) → install with that key. Pull existing keys from
GET /agentKeys (each {id:<key>, targetId}). Push via GuruRMM /rmm or any
remote-exec channel. Nothing ties install to Datto RMM.
Liftable client
github.com/KaseyaDEDR/PowershellTools — the InfocyteHUNTAPI module (Apache-2.0) is
a complete REST wrapper and the de-facto API spec. extension-docs (Lua 5.3)
documents the agent response actions (host isolation isolator:isolate(), process
kill, quarantine).
Webhooks (for the GuruRMM add-on)
Admin → Webhooks → Add Webhook: EDR POSTs full alert JSON over HTTPS on each detection, with custom auth headers supported. This is the recommended near-real-time feed for a GuruRMM "EDR add-on" dashboard, complemented by REST polling here. (Not wired in this skill yet — Feature 6 work.)