Files
claudetools/.claude/skills/datto-edr/references/api-reference.md
Howard Enos 1e80fb24db datto-edr: fix scan to verified Agents/scan endpoint + harden
- 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>
2026-06-25 13:55:22 -07:00

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 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.

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.)