- 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>
141 lines
7.2 KiB
Markdown
141 lines
7.2 KiB
Markdown
# 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.)
|