Files
claudetools/.claude/skills/datto-edr/references/api-reference.md
Howard Enos 27595d6475 sync: auto-sync from HOWARD-HOME at 2026-07-07 11:01:18
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:01:18
2026-07-07 11:01:49 -07:00

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 Authorization header. NO Bearer prefix, no Basic, no OAuth. Authorization: <token>. (Source: KaseyaDEDR PowershellTools requestHelpers.ps1; confirmed live — Bearer prefix 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.yaml field credentials.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}}.
  • where operators: {"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 where is REQUIRED. An absent/empty where scans 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; the and-wrap disambiguates).
  • Targeting is by Agent.id (not deviceId, which is null right after enroll).
  • scanType is 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.)