10 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.
Alert Suppression Rules (verified live 2026-07-07)
Suppress a false-positive detection so it stops firing (and stops triggering any
attached auto-response like isolate-host). LoopBack models are API-reachable:
| Op | Method/path |
|---|---|
| List rules | GET /SuppressionRules ([id, alertId, name, versionCount, description, organizationId, locationId, active, deleted]) |
| Count | GET /SuppressionRules/count |
| Match criteria (per rule) | GET /SuppressionRules/{id}/versions → [{id, suppressionRuleId, name, description, metadata:{...}}] |
metadata is the full match-field map; each key is {value, active, display, dataType, operator}. A field participates in the match ONLY when active:true; all active fields are
AND-ed. Available fields (display): Alert Type, Item Type, Organization Name, Location
Name, Hostname, IP Address, Name, File Path, File SHA1, File SHA256, File Signature Issuer,
Process Command Line, AV Threat Name, Operating System, Threat Status, Severity, Rule
Name, EPP Type, Threat Category, Process Owner, Process Owner UID, AV Hits, Parent Process
Name, Grand Parent Process Name.
Scoping guidance (learned from the vwp-qbs RMM false-positive incident): the tightest
safe suppression matches on Process Command Line (a unique fingerprint of the exact
script) plus Grand Parent Process Name (the launching agent, e.g. gururmm-agent.exe).
That trusts one exact known-good automation without blinding a whole binary. NEVER suppress
on Name/File Path/File SHA* of a LOLBin (powershell/rundll32/etc.) alone — it disables
the rule for that binary everywhere. Add Rule Name to limit to one rule; add Hostname/
Organization Name/Location Name to limit scope (empty organizationId/locationId on the
rule = fleet-wide, gated by the metadata match). Do NOT blanket-whitelist an RMM agent as
grandparent by itself — the RMM is a SYSTEM-level RCE channel and the top MSP attack path;
whitelisting all of it blinds EDR exactly where it matters.
Create — CONSOLE-observed, API create RUN-unverified. Console flow: open the alert →
Create Suppression Rule → check the desired Match fields → Save. This POSTs a
SuppressionRule (carrying alertId, name, description, organizationId, locationId,
active) plus a version whose metadata has the chosen fields flipped to active:true. To
replicate via API, POST /SuppressionRules then the version to /SuppressionRules/{id}/ versions with that metadata shape — verify the exact envelope on the next real create (build
→ read back via the GET above → delete if wrong) before relying on it. Example on this tenant:
rule e4dd55bf-… (name "Exfiltration Over HTTP Protocol", alertId 5d5f39b1-…) matches
Process Command Line + Grand Parent = gururmm-agent.exe.
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.)